diff options
Diffstat (limited to 'contrib')
| -rw-r--r-- | contrib/emacs/git.el | 121 | ||||
| -rw-r--r-- | contrib/emacs/vc-git.el | 135 | ||||
| -rwxr-xr-x | contrib/git-svn/git-svn.perl | 282 | ||||
| -rw-r--r-- | contrib/git-svn/git-svn.txt | 52 | ||||
| -rw-r--r-- | contrib/git-svn/t/t0000-contrib-git-svn.sh | 36 | ||||
| -rwxr-xr-x | contrib/gitview/gitview | 7 |
6 files changed, 480 insertions, 153 deletions
diff --git a/contrib/emacs/git.el b/contrib/emacs/git.el index 8f234772b0..5135e361be 100644 --- a/contrib/emacs/git.el +++ b/contrib/emacs/git.el @@ -39,13 +39,11 @@ ;; - hook into file save (after-save-hook) ;; - diff against other branch ;; - renaming files from the status buffer -;; - support for appending signed-off-by ;; - creating tags ;; - fetch/pull ;; - switching branches ;; - revlist browser ;; - git-show-branch browser -;; - customize support ;; - menus ;; @@ -53,65 +51,92 @@ (require 'ewoc) -;;;; Faces +;;;; Customizations ;;;; ------------------------------------------------------------ +(defgroup git nil + "Git user interface") + +(defcustom git-committer-name nil + "User name to use for commits. +The default is to fall back to `add-log-full-name' and then `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 `add-log-mailing-address' and then `user-mail-address'." + :group 'git + :type '(choice (const :tag "Default" nil) + (string :tag "Email"))) + +(defcustom git-commits-coding-system 'utf-8 + "Default coding system for the log message of git commits." + :group 'git + :type '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-per-dir-ignore-file ".gitignore" + "Name of the per-directory ignore file." + :group 'git + :type 'string) + (defface git-status-face '((((class color) (background light)) (:foreground "purple"))) - "Git mode face used to highlight added and modified files.") + "Git mode face used to highlight added and modified files." + :group 'git) (defface git-unmerged-face '((((class color) (background light)) (:foreground "red" :bold t))) - "Git mode face used to highlight unmerged files.") + "Git mode face used to highlight unmerged files." + :group 'git) (defface git-unknown-face '((((class color) (background light)) (:foreground "goldenrod" :bold t))) - "Git mode face used to highlight unknown files.") + "Git mode face used to highlight unknown files." + :group 'git) (defface git-uptodate-face '((((class color) (background light)) (:foreground "grey60"))) - "Git mode face used to highlight up-to-date files.") + "Git mode face used to highlight up-to-date files." + :group 'git) (defface git-ignored-face '((((class color) (background light)) (:foreground "grey60"))) - "Git mode face used to highlight ignored files.") + "Git mode face used to highlight ignored files." + :group 'git) (defface git-mark-face '((((class color) (background light)) (:foreground "red" :bold t))) - "Git mode face used for the file marks.") + "Git mode face used for the file marks." + :group 'git) (defface git-header-face '((((class color) (background light)) (:foreground "blue"))) - "Git mode face used for commit headers.") + "Git mode face used for commit headers." + :group 'git) (defface git-separator-face '((((class color) (background light)) (:foreground "brown"))) - "Git mode face used for commit separator.") + "Git mode face used for commit separator." + :group 'git) (defface git-permission-face '((((class color) (background light)) (:foreground "green" :bold t))) - "Git mode face used for permission changes.") - -(defvar git-committer-name nil - "*User name to use for commits. -If not set, fall back to `add-log-full-name' and then `user-full-name'.") - -(defvar git-committer-email nil - "*Email address to use for commits. -If not set, fall back to `add-log-mailing-address' and then `user-mail-address'.") - -(defvar git-commits-coding-system 'utf-8 - "Default coding system for git commits.") - -(defconst git-log-msg-separator "--- log message follows this line ---") - -(defconst git-per-dir-ignore-file ".gitignore" - "Name of the per-directory ignore file.") + "Git mode face used for permission changes." + :group 'git) ;;;; Utilities ;;;; ------------------------------------------------------------ +(defconst git-log-msg-separator "--- log message follows this line ---") + (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)) @@ -213,14 +238,19 @@ If not set, fall back to `add-log-mailing-address' and then `user-mail-address'. "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))) + (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 (expand-file-name git-per-dir-ignore-file dir))) + (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)))) + (save-buffer)) + (when created + (git-run-command nil nil "update-index" "--info-only" "--add" "--" (file-relative-name ignore-name))) + (git-add-status-file (if created 'added 'modified) (file-relative-name ignore-name)))) ;;;; Wrappers for basic git commands @@ -272,7 +302,7 @@ If not set, fall back to `add-log-mailing-address' and then `user-mail-address'. (with-current-buffer buffer (goto-char (point-min)) (if - (setq log-start (re-search-forward (concat "^" git-log-msg-separator "\n") nil t)) + (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)) @@ -388,9 +418,9 @@ If not set, fall back to `add-log-mailing-address' and then `user-mail-address'. (propertize (if (or (not old-perm) (not new-perm) - (eq 0 (logand #O111 (logxor old-perm new-perm)))) + (eq 0 (logand ?\111 (logxor old-perm new-perm)))) " " - (if (eq 0 (logand #O111 old-perm)) "+x" "-x")) + (if (eq 0 (logand ?\111 old-perm)) "+x" "-x")) 'face 'git-permission-face)) (defun git-fileinfo-prettyprint (info) @@ -787,7 +817,8 @@ If not set, fall back to `add-log-mailing-address' and then `user-mail-address'. (unless git-status (error "Not in git-status buffer.")) (let ((buffer (get-buffer-create "*git-commit*")) (merge-heads (git-get-merge-heads)) - (dir default-directory)) + (dir default-directory) + (sign-off git-append-signed-off-by)) (with-current-buffer buffer (when (eq 0 (buffer-size)) (cd dir) @@ -804,9 +835,18 @@ If not set, fall back to `add-log-mailing-address' and then `user-mail-address'. 'face 'git-header-face) (propertize git-log-msg-separator 'face 'git-separator-face) "\n") - (when (and merge-heads (file-readable-p ".git/MERGE_MSG")) - (insert-file-contents ".git/MERGE_MSG")))) - (log-edit #'git-do-commit nil #'git-log-edit-files buffer))) + (cond ((and merge-heads (file-readable-p ".git/MERGE_MSG")) + (insert-file-contents ".git/MERGE_MSG")) + (sign-off + (insert (format "\n\nSigned-off-by: %s <%s>\n" + (git-get-committer-name) (git-get-committer-email))))))) + (let ((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))))) + (log-edit #'git-do-commit nil #'git-log-edit-files buffer)))) (defun git-find-file () "Visit the current file in its own buffer." @@ -891,7 +931,7 @@ If not set, fall back to `add-log-mailing-address' and then `user-mail-address'. (define-key map "d" diff-map) (define-key map "=" 'git-diff-file) (define-key map "f" 'git-find-file) - (define-key map [RET] 'git-find-file) + (define-key map "\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) @@ -937,6 +977,7 @@ Commands: (erase-buffer) (let ((status (ewoc-create 'git-fileinfo-prettyprint "" ""))) (set (make-local-variable 'git-status) status)) + (set (make-local-variable 'list-buffers-directory) default-directory) (run-hooks 'git-status-mode-hook))) (defun git-status (dir) @@ -946,8 +987,8 @@ Commands: (if (file-directory-p (concat (file-name-as-directory dir) ".git")) (let ((buffer (create-file-buffer (expand-file-name "*git-status*" dir)))) (switch-to-buffer buffer) - (git-status-mode) (cd dir) + (git-status-mode) (git-refresh-status) (goto-char (point-min))) (message "%s is not a git working tree." dir))) diff --git a/contrib/emacs/vc-git.el b/contrib/emacs/vc-git.el new file mode 100644 index 0000000000..2453cdcfae --- /dev/null +++ b/contrib/emacs/vc-git.el @@ -0,0 +1,135 @@ +;;; 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'. +;; +;; TODO +;; - changelog generation +;; - working with revisions other than HEAD +;; + +(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))) + (when dir (cd dir)) + (and (eq 0 (call-process "git" nil '(t nil) nil "ls-files" "-c" "-z" "--" name)) + (let ((str (buffer-string))) + (and (> (length str) (length name)) + (string= (substring str 0 (1+ (length name))) (concat name "\0")))))))) + +(defun vc-git-state (file) + "git-specific version of `vc-state'." + (let ((diff (vc-git--run-command-string file "diff-index" "-z" "HEAD" "--"))) + (if (and diff (string-match ":[0-7]\\{6\\} [0-7]\\{6\\} [0-9a-f]\\{40\\} [0-9a-f]\\{40\\} [ADMU]\0[^\0]+\0" diff)) + 'edited + 'up-to-date))) + +(defun vc-git-workfile-version (file) + "git-specific version of `vc-workfile-version'." + (let ((str (with-output-to-string + (with-current-buffer standard-output + (call-process "git" nil '(t nil) nil "symbolic-ref" "HEAD"))))) + (if (string-match "^\\(refs/heads/\\)?\\(.+\\)$" str) + (match-string 2 str) + str))) + +(defun vc-git-revert (file &optional contents-done) + "Revert FILE to the version stored in the git repository." + (if contents-done + (vc-git--run-command file "update-index" "--") + (vc-git--run-command file "checkout" "HEAD"))) + +(defun vc-git-checkout-model (file) + 'implicit) + +(defun vc-git-workfile-unchanged-p (file) + (let ((sha1 (vc-git--run-command-string file "hash-object" "--")) + (head (vc-git--run-command-string file "ls-tree" "-z" "HEAD" "--"))) + (and head + (string-match "[0-7]\\{6\\} blob \\([0-9a-f]\\{40\\}\\)\t[^\0]+\0" head) + (string= (car (split-string sha1 "\n")) (match-string 1 head))))) + +(defun vc-git-register (file &optional rev comment) + "Register FILE into the git version-control system." + (vc-git--run-command file "update-index" "--add" "--")) + +(defun vc-git-print-log (file) + (let ((name (file-relative-name file)) + (coding-system-for-read git-commits-coding-system)) + (vc-do-command nil 'async "git" name "rev-list" "--pretty" "HEAD" "--"))) + +(defun vc-git-diff (file &optional rev1 rev2) + (let ((name (file-relative-name file))) + (if (and rev1 rev2) + (vc-do-command "*vc-diff*" 0 "git" name "diff-tree" "-p" rev1 rev2 "--") + (vc-do-command "*vc-diff*" 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) + (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 "annotate" name))) + +(defun vc-git-annotate-time () + (and (re-search-forward "[0-9a-f]+\t(.*\t\\([0-9]+\\)-\\([0-9]+\\)-\\([0-9]+\\) \\([0-9]+\\):\\([0-9]+\\):\\([0-9]+\\) \\([-+0-9]+\\)\t[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/git-svn/git-svn.perl b/contrib/git-svn/git-svn.perl index 0b7416526d..3c860e458c 100755 --- a/contrib/git-svn/git-svn.perl +++ b/contrib/git-svn/git-svn.perl @@ -4,19 +4,12 @@ use warnings; use strict; use vars qw/ $AUTHOR $VERSION - $SVN_URL $SVN_INFO $SVN_WC + $SVN_URL $SVN_INFO $SVN_WC $SVN_UUID $GIT_SVN_INDEX $GIT_SVN $GIT_DIR $REV_DIR/; $AUTHOR = 'Eric Wong <normalperson@yhbt.net>'; $VERSION = '0.10.0'; $GIT_DIR = $ENV{GIT_DIR} || "$ENV{PWD}/.git"; -$GIT_SVN = $ENV{GIT_SVN_ID} || 'git-svn'; -$GIT_SVN_INDEX = "$GIT_DIR/$GIT_SVN/index"; -$ENV{GIT_DIR} ||= $GIT_DIR; -$SVN_URL = undef; -$REV_DIR = "$GIT_DIR/$GIT_SVN/revs"; -$SVN_WC = "$GIT_DIR/$GIT_SVN/tree"; - # make sure the svn binary gives consistent output between locales and TZs: $ENV{TZ} = 'UTC'; $ENV{LC_ALL} = 'C'; @@ -24,6 +17,7 @@ $ENV{LC_ALL} = 'C'; # If SVN:: library support is added, please make the dependencies # optional and preserve the capability to use the command-line client. # use eval { require SVN::... } to make it lazy load +# We don't use any modules not in the standard Perl distribution: use Carp qw/croak/; use IO::File qw//; use File::Basename qw/dirname basename/; @@ -32,27 +26,30 @@ use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/; use File::Spec qw//; use POSIX qw/strftime/; my $sha1 = qr/[a-f\d]{40}/; -my $sha1_short = qr/[a-f\d]{6,40}/; +my $sha1_short = qr/[a-f\d]{4,40}/; my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit, - $_find_copies_harder, $_l, $_version); - -GetOptions( 'revision|r=s' => \$_revision, - 'no-ignore-externals' => \$_no_ignore_ext, - 'stdin|' => \$_stdin, - 'edit|e' => \$_edit, - 'rmdir' => \$_rmdir, - 'help|H|h' => \$_help, - 'find-copies-harder' => \$_find_copies_harder, - 'l=i' => \$_l, - 'version|V' => \$_version, - 'no-stop-on-copy' => \$_no_stop_copy ); + $_find_copies_harder, $_l, $_version, $_upgrade, $_authors); +my (@_branch_from, %tree_map, %users); + +my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext, + 'branch|b=s' => \@_branch_from, + 'authors-file|A=s' => \$_authors ); my %cmd = ( - fetch => [ \&fetch, "Download new revisions from SVN" ], - init => [ \&init, "Initialize and fetch (import)"], - commit => [ \&commit, "Commit git revisions to SVN" ], - 'show-ignore' => [ \&show_ignore, "Show svn:ignore listings" ], - rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)" ], - help => [ \&usage, "Show help" ], + fetch => [ \&fetch, "Download new revisions from SVN", + { 'revision|r=s' => \$_revision, %fc_opts } ], + init => [ \&init, "Initialize and fetch (import)", { } ], + commit => [ \&commit, "Commit git revisions to SVN", + { 'stdin|' => \$_stdin, + 'edit|e' => \$_edit, + 'rmdir' => \$_rmdir, + 'find-copies-harder' => \$_find_copies_harder, + 'l=i' => \$_l, + %fc_opts, + } ], + 'show-ignore' => [ \&show_ignore, "Show svn:ignore listings", { } ], + rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)", + { 'no-ignore-externals' => \$_no_ignore_ext, + 'upgrade' => \$_upgrade } ], ); my $cmd; for (my $i = 0; $i < @ARGV; $i++) { @@ -63,16 +60,23 @@ for (my $i = 0; $i < @ARGV; $i++) { } }; -# we may be called as git-svn-(command), or git-svn(command). -foreach (keys %cmd) { - if (/git\-svn\-?($_)(?:\.\w+)?$/) { - $cmd = $1; - last; - } -} +my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd); + +GetOptions(%opts, 'help|H|h' => \$_help, + 'version|V' => \$_version, + 'id|i=s' => \$GIT_SVN) or exit 1; + +$GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn'; +$GIT_SVN_INDEX = "$GIT_DIR/$GIT_SVN/index"; +$ENV{GIT_DIR} ||= $GIT_DIR; +$SVN_URL = undef; +$REV_DIR = "$GIT_DIR/$GIT_SVN/revs"; +$SVN_WC = "$GIT_DIR/$GIT_SVN/tree"; + usage(0) if $_help; version() if $_version; -usage(1) unless (defined $cmd); +usage(1) unless defined $cmd; +load_authors() if $_authors; svn_check_ignore_externals(); $cmd{$cmd}->[0]->(@ARGV); exit 0; @@ -84,15 +88,25 @@ sub usage { print $fd <<""; git-svn - bidirectional operations between a single Subversion tree and git Usage: $0 <command> [options] [arguments]\n -Available commands: + + print $fd "Available commands:\n" unless $cmd; foreach (sort keys %cmd) { - print $fd ' ',pack('A10',$_),$cmd{$_}->[1],"\n"; + next if $cmd && $cmd ne $_; + print $fd ' ',pack('A13',$_),$cmd{$_}->[1],"\n"; + foreach (keys %{$cmd{$_}->[2]}) { + # prints out arguments as they should be passed: + my $x = s#=s$## ? '<arg>' : s#=i$## ? '<num>' : ''; + print $fd ' ' x 17, join(', ', map { length $_ > 1 ? + "--$_" : "-$_" } + split /\|/,$_)," $x\n"; + } } print $fd <<""; -\nGIT_SVN_ID may be set in the environment to an arbitrary identifier if -you're tracking multiple SVN branches/repositories in one git repository -and want to keep them separate. +\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; } @@ -104,15 +118,19 @@ sub version { sub rebuild { $SVN_URL = shift or undef; - my $repo_uuid; my $newest_rev = 0; + if ($_upgrade) { + sys('git-update-ref',"refs/remotes/$GIT_SVN","$GIT_SVN-HEAD"); + } else { + check_upgrade_needed(); + } my $pid = open(my $rev_list,'-|'); defined $pid or croak $!; if ($pid == 0) { - exec("git-rev-list","$GIT_SVN-HEAD") or croak $!; + exec("git-rev-list","refs/remotes/$GIT_SVN") or croak $!; } - my $first; + my $latest; while (<$rev_list>) { chomp; my $c = $_; @@ -132,18 +150,20 @@ sub rebuild { "$c, $id\n"; } } + + # if we merged or otherwise started elsewhere, this is + # how we break out of it + next if (defined $SVN_UUID && ($uuid ne $SVN_UUID)); + next if (defined $SVN_URL && ($url ne $SVN_URL)); + print "r$rev = $c\n"; - unless (defined $first) { + unless (defined $latest) { if (!$SVN_URL && !$url) { croak "SVN repository location required: $url\n"; } $SVN_URL ||= $url; - $repo_uuid = setup_git_svn(); - $first = $rev; - } - if ($uuid ne $repo_uuid) { - croak "Repository UUIDs do not match!\ngot: $uuid\n", - "expected: $repo_uuid\n"; + $SVN_UUID ||= setup_git_svn(); + $latest = $rev; } assert_revision_eq_or_unknown($rev, $c); sys('git-update-ref',"$GIT_SVN/revs/$rev",$c); @@ -151,7 +171,7 @@ sub rebuild { } close $rev_list or croak $?; if (!chdir $SVN_WC) { - my @svn_co = ('svn','co',"-r$first"); + my @svn_co = ('svn','co',"-r$latest"); push @svn_co, '--ignore-externals' unless $_no_ignore_ext; sys(@svn_co, $SVN_URL, $SVN_WC); chdir $SVN_WC or croak $!; @@ -168,6 +188,13 @@ sub rebuild { exec('git-write-tree'); } waitpid $pid, 0; + + if ($_upgrade) { + print STDERR <<""; +Keeping deprecated refs/head/$GIT_SVN-HEAD for now. Please remove it +when you have upgraded your tools and habits to use refs/remotes/$GIT_SVN + + } } sub init { @@ -180,6 +207,7 @@ sub init { sub fetch { my (@parents) = @_; + check_upgrade_needed(); $SVN_URL ||= file_to_s("$GIT_DIR/$GIT_SVN/info/url"); my @log_args = -d $SVN_WC ? ($SVN_WC) : ($SVN_URL); unless ($_revision) { @@ -199,9 +227,6 @@ sub fetch { sys(@svn_co, $SVN_URL, $SVN_WC); chdir $SVN_WC or croak $!; $last_commit = git_commit($base, @parents); - unless (-f "$GIT_DIR/refs/heads/master") { - sys(qw(git-update-ref refs/heads/master),$last_commit); - } assert_svn_wc_clean($base->{revision}, $last_commit); } else { chdir $SVN_WC or croak $!; @@ -217,16 +242,20 @@ sub fetch { $last_commit = git_commit($log_msg, $last_commit, @parents); } assert_svn_wc_clean($last_rev, $last_commit); + unless (-e "$GIT_DIR/refs/heads/master") { + sys(qw(git-update-ref refs/heads/master),$last_commit); + } return pop @$svn_log; } sub commit { my (@commits) = @_; + check_upgrade_needed(); if ($_stdin || !@commits) { print "Reading from stdin...\n"; @commits = (); while (<STDIN>) { - if (/\b([a-f\d]{6,40})\b/) { + if (/\b($sha1_short)\b/o) { unshift @commits, $1; } } @@ -248,7 +277,6 @@ sub commit { chdir $SVN_WC or croak $!; my $svn_current_rev = svn_info('.')->{'Last Changed Rev'}; foreach my $c (@revs) { - print "Committing $c\n"; my $mods = svn_checkout_tree($svn_current_rev, $c); if (scalar @$mods == 0) { print "Skipping, no changes detected\n"; @@ -295,24 +323,31 @@ sub setup_git_svn { mkpath(["$GIT_DIR/$GIT_SVN/info"]); mkpath([$REV_DIR]); s_to_file($SVN_URL,"$GIT_DIR/$GIT_SVN/info/url"); - my $uuid = svn_info($SVN_URL)->{'Repository UUID'} or + $SVN_UUID = svn_info($SVN_URL)->{'Repository UUID'} or croak "Repository UUID unreadable\n"; - s_to_file($uuid,"$GIT_DIR/$GIT_SVN/info/uuid"); + s_to_file($SVN_UUID,"$GIT_DIR/$GIT_SVN/info/uuid"); open my $fd, '>>', "$GIT_DIR/$GIT_SVN/info/exclude" or croak $!; print $fd '.svn',"\n"; close $fd or croak $!; - return $uuid; + return $SVN_UUID; } sub assert_svn_wc_clean { my ($svn_rev, $treeish) = @_; croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/); croak "$treeish is not a sha1!\n" unless ($treeish =~ /^$sha1$/o); - my $svn_info = svn_info('.'); - if ($svn_rev != $svn_info->{'Last Changed Rev'}) { - croak "Expected r$svn_rev, got r", - $svn_info->{'Last Changed Rev'},"\n"; + my $lcr = svn_info('.')->{'Last Changed Rev'}; + if ($svn_rev != $lcr) { + print STDERR "Checking for copy-tree ... "; + # use + my @diff = grep(/^Index: /,(safe_qx(qw(svn diff), + "-r$lcr:$svn_rev"))); + if (@diff) { + croak "Nope! Expected r$svn_rev, got r$lcr\n"; + } else { + print STDERR "OK!\n"; + } } my @status = grep(!/^Performing status on external/,(`svn status`)); @status = grep(!/^\s*$/,@status); @@ -495,7 +530,7 @@ sub svn_checkout_tree { my ($svn_rev, $treeish) = @_; my $from = file_to_s("$REV_DIR/$svn_rev"); assert_svn_wc_clean($svn_rev,$from); - print "diff-tree '$from' '$treeish'\n"; + print "diff-tree $from $treeish\n"; my $pid = open my $diff_fh, '-|'; defined $pid or croak $!; if ($pid == 0) { @@ -506,7 +541,7 @@ sub svn_checkout_tree { } my $mods = parse_diff_tree($diff_fh); unless (@$mods) { - # git can do empty commits, SVN doesn't allow it... + # git can do empty commits, but SVN doesn't allow it... return $mods; } my ($rm, $add) = precommit_check($mods); @@ -593,7 +628,7 @@ sub svn_commit_tree { my ($svn_rev, $commit) = @_; my $commit_msg = "$GIT_DIR/$GIT_SVN/.svn-commit.tmp.$$"; my %log_msg = ( msg => '' ); - open my $msg, '>', $commit_msg or croak $!; + open my $msg, '>', $commit_msg or croak $!; chomp(my $type = `git-cat-file -t $commit`); if ($type eq 'commit') { @@ -607,8 +642,10 @@ sub svn_commit_tree { while (<$msg_fh>) { if (!$in_msg) { $in_msg = 1 if (/^\s*$/); + } elsif (/^git-svn-id: /) { + # skip this, we regenerate the correct one + # on re-fetch anyways } else { - $log_msg{msg} .= $_; print $msg $_ or croak $!; } } @@ -620,6 +657,15 @@ sub svn_commit_tree { my $editor = $ENV{VISUAL} || $ENV{EDITOR} || 'vi'; system($editor, $commit_msg); } + + # file_to_s removes all trailing newlines, so just use chomp() here: + open $msg, '<', $commit_msg or croak $!; + { local $/; chomp($log_msg{msg} = <$msg>); } + close $msg or croak $!; + + my ($oneline) = ($log_msg{msg} =~ /([^\n\r]+)/); + print "Committing $commit: $oneline\n"; + my @ci_output = safe_qx(qw(svn commit -F),$commit_msg); my ($committed) = grep(/^Committed revision \d+\./,@ci_output); unlink $commit_msg; @@ -711,6 +757,10 @@ sub svn_log_raw { author => $author, lines => $lines, msg => '' ); + if (defined $_authors && ! defined $users{$author}) { + die "Author: $author not defined in ", + "$_authors file\n"; + } push @svn_log, \%log_msg; $state = 'msg_start'; next; @@ -810,9 +860,9 @@ sub git_commit { my ($log_msg, @parents) = @_; assert_revision_unknown($log_msg->{revision}); my $out_fh = IO::File->new_tmpfile or croak $!; - my $info = svn_info('.'); - my $uuid = $info->{'Repository UUID'}; - defined $uuid or croak "Unable to get Repository UUID\n"; + $SVN_UUID ||= svn_info('.')->{'Repository UUID'}; + + map_tree_joins() if (@_branch_from && !%tree_map); # commit parents can be conditionally bound to a particular # svn revision via: "svn_revno=commit_sha1", filter them out here: @@ -835,19 +885,26 @@ sub git_commit { git_addremove(); chomp(my $tree = `git-write-tree`); croak if $?; + if (exists $tree_map{$tree}) { + my %seen_parent = map { $_ => 1 } @exec_parents; + foreach (@{$tree_map{$tree}}) { + # MAXPARENT is defined to 16 in commit-tree.c: + if ($seen_parent{$_} || @exec_parents > 16) { + next; + } + push @exec_parents, $_; + $seen_parent{$_} = 1; + } + } my $msg_fh = IO::File->new_tmpfile or croak $!; print $msg_fh $log_msg->{msg}, "\ngit-svn-id: ", "$SVN_URL\@$log_msg->{revision}", - " $uuid\n" or croak $!; + " $SVN_UUID\n" or croak $!; $msg_fh->flush == 0 or croak $!; seek $msg_fh, 0, 0 or croak $!; - $ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} = - $log_msg->{author}; - $ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} = - $log_msg->{author}."\@$uuid"; - $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = - $log_msg->{date}; + set_commit_env($log_msg); + my @exec = ('git-commit-tree',$tree); push @exec, '-p', $_ foreach @exec_parents; open STDIN, '<&', $msg_fh or croak $!; @@ -863,7 +920,7 @@ sub git_commit { if ($commit !~ /^$sha1$/o) { croak "Failed to commit, invalid sha1: $commit\n"; } - my @update_ref = ('git-update-ref',"refs/heads/$GIT_SVN-HEAD",$commit); + my @update_ref = ('git-update-ref',"refs/remotes/$GIT_SVN",$commit); if (my $primary_parent = shift @exec_parents) { push @update_ref, $primary_parent; } @@ -873,6 +930,16 @@ sub git_commit { return $commit; } +sub set_commit_env { + my ($log_msg) = @_; + my $author = $log_msg->{author}; + my ($name,$email) = defined $users{$author} ? @{$users{$author}} + : ($author,"$author\@$SVN_UUID"); + $ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} = $name; + $ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} = $email; + $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_msg->{date}; +} + sub apply_mod_line_blob { my $m = shift; if ($m->{mode_b} =~ /^120/) { @@ -936,6 +1003,63 @@ sub svn_check_ignore_externals { $_no_ignore_ext = 1; } } + +sub check_upgrade_needed { + my $old = eval { + my $pid = open my $child, '-|'; + defined $pid or croak $!; + if ($pid == 0) { + close STDERR; + exec('git-rev-parse',"$GIT_SVN-HEAD") or croak $?; + } + my @ret = (<$child>); + close $child or croak $?; + die $? if $?; # just in case close didn't error out + return wantarray ? @ret : join('',@ret); + }; + return unless $old; + my $head = eval { safe_qx('git-rev-parse',"refs/remotes/$GIT_SVN") }; + if ($@ || !$head) { + print STDERR "Please run: $0 rebuild --upgrade\n"; + exit 1; + } +} + +# fills %tree_map with a reverse mapping of trees to commits. Useful +# for finding parents to commit on. +sub map_tree_joins { + foreach my $br (@_branch_from) { + my $pid = open my $pipe, '-|'; + defined $pid or croak $!; + if ($pid == 0) { + exec(qw(git-rev-list --pretty=raw), $br) or croak $?; + } + while (<$pipe>) { + if (/^commit ($sha1)$/o) { + my $commit = $1; + my ($tree) = (<$pipe> =~ /^tree ($sha1)$/o); + unless (defined $tree) { + die "Failed to parse commit $commit\n"; + } + push @{$tree_map{$tree}}, $commit; + } + } + close $pipe or croak $?; + } +} + +# '<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"; + while (<$authors>) { + chomp; + next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/; + my ($user, $name, $email) = ($1, $2, $3); + $users{$user} = [$name, $email]; + } + close $authors or croak $!; +} + __END__ Data structures: @@ -960,7 +1084,7 @@ diff-index line ($m hash) mode_a => first column of diff-index output, no leading ':', mode_b => second column of diff-index output, sha1_b => sha1sum of the final blob, - chg => change type [MCRAD], + 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) } diff --git a/contrib/git-svn/git-svn.txt b/contrib/git-svn/git-svn.txt index b29073997c..8e9a971a85 100644 --- a/contrib/git-svn/git-svn.txt +++ b/contrib/git-svn/git-svn.txt @@ -27,7 +27,7 @@ For importing svn, git-svnimport is potentially more powerful when operating on repositories organized under the recommended trunk/branch/tags structure, and should be faster, too. -git-svn completely ignores the very limited view of branching that +git-svn mostly ignores the very limited view of branching that Subversion has. This allows git-svn to be much easier to use, especially on repositories that are not organized in a manner that git-svnimport is designed for. @@ -41,12 +41,12 @@ init:: fetch:: Fetch unfetched revisions from the SVN_URL we are tracking. - refs/heads/git-svn-HEAD will be updated to the latest revision. + refs/heads/remotes/git-svn will be updated to the latest revision. - Note: You should never attempt to modify the git-svn-HEAD branch - outside of git-svn. Instead, create a branch from git-svn-HEAD + Note: You should never attempt to modify the remotes/git-svn branch + outside of git-svn. Instead, create a branch from remotes/git-svn and work on that branch. Use the 'commit' command (see below) - to write git commits back to git-svn-HEAD. + to write git commits back to remotes/git-svn. commit:: Commit specified commit or tree objects to SVN. This relies on @@ -116,8 +116,38 @@ OPTIONS They are both passed directly to git-diff-tree see git-diff-tree(1) for more information. +ADVANCED OPTIONS +---------------- +-b<refname>:: +--branch <refname>:: + Used with 'fetch' or 'commit'. + + This can be used to join arbitrary git branches to remotes/git-svn + on new commits where the tree object is equivalent. + + When used with different GIT_SVN_ID values, tags and branches in + SVN can be tracked this way, as can some merges where the heads + end up having completely equivalent content. This can even be + used to track branches across multiple SVN _repositories_. + + This option may be specified multiple times, once for each + branch. + +-i<GIT_SVN_ID>:: +--id <GIT_SVN_ID>:: + This sets GIT_SVN_ID (instead of using the environment). See + the section on "Tracking Multiple Repositories or Branches" for + more information on using GIT_SVN_ID. + COMPATIBILITY OPTIONS --------------------- +--upgrade:: + Only used with the 'rebuild' command. + + Run this if you used an old version of git-svn that used + 'git-svn-HEAD' instead of 'remotes/git-svn' as the branch + for tracking the remote. + --no-ignore-externals:: Only used with the 'fetch' and 'rebuild' command. @@ -155,14 +185,14 @@ Tracking and contributing to an Subversion managed-project: # Fetch remote revisions:: git-svn fetch # Create your own branch to hack on:: - git checkout -b my-branch git-svn-HEAD + git checkout -b my-branch remotes/git-svn # Commit only the git commits you want to SVN:: git-svn commit <tree-ish> [<tree-ish_2> ...] # Commit all the git commits from my-branch that don't exist in SVN:: - git-svn commit git-svn-HEAD..my-branch + git-svn commit remotes/git-svn..my-branch # Something is committed to SVN, pull the latest into your branch:: - git-svn fetch && git pull . git-svn-HEAD -# Append svn:ignore settings to the default git exclude file: + git-svn fetch && git pull . remotes/git-svn +# Append svn:ignore settings to the default git exclude file:: git-svn show-ignore >> .git/info/exclude DESIGN PHILOSOPHY @@ -184,8 +214,8 @@ SVN repositories via one git repository. Simply set the GIT_SVN_ID environment variable to a name other other than "git-svn" (the default) and git-svn will ignore the contents of the $GIT_DIR/git-svn directory and instead do all of its work in $GIT_DIR/$GIT_SVN_ID for that -invocation. The interface branch will be $GIT_SVN_ID-HEAD, instead of -git-svn-HEAD. Any $GIT_SVN_ID-HEAD branch should never be modified +invocation. The interface branch will be remotes/$GIT_SVN_ID, instead of +remotes/git-svn. Any remotes/$GIT_SVN_ID branch should never be modified by the user outside of git-svn commands. ADDITIONAL FETCH ARGUMENTS diff --git a/contrib/git-svn/t/t0000-contrib-git-svn.sh b/contrib/git-svn/t/t0000-contrib-git-svn.sh index 181dfe008b..80ad3573db 100644 --- a/contrib/git-svn/t/t0000-contrib-git-svn.sh +++ b/contrib/git-svn/t/t0000-contrib-git-svn.sh @@ -71,14 +71,14 @@ test_expect_success \ name='try a deep --rmdir with a commit' -git checkout -b mybranch git-svn-HEAD +git checkout -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" test_expect_success "$name" \ - "git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch && + "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch && test -d $SVN_TREE/dir && test ! -d $SVN_TREE/dir/a" @@ -91,13 +91,13 @@ git update-index --add dir/file/file git commit -m "$name" test_expect_code 1 "$name" \ - 'git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch' \ + 'git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch' \ || true name='detect node change from directory to file #1' rm -rf dir $GIT_DIR/index -git checkout -b mybranch2 git-svn-HEAD +git checkout -b mybranch2 remotes/git-svn mv bar/zzz zzz rm -rf bar mv zzz bar @@ -106,13 +106,13 @@ git update-index --add -- bar git commit -m "$name" test_expect_code 1 "$name" \ - 'git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch2' \ + 'git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch2' \ || true name='detect node change from file to directory #2' rm -f $GIT_DIR/index -git checkout -b mybranch3 git-svn-HEAD +git checkout -b mybranch3 remotes/git-svn rm bar/zzz git-update-index --remove bar/zzz mkdir bar/zzz @@ -121,13 +121,13 @@ git-update-index --add bar/zzz/yyy git commit -m "$name" test_expect_code 1 "$name" \ - 'git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch3' \ + 'git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch3' \ || true name='detect node change from directory to file #2' rm -f $GIT_DIR/index -git checkout -b mybranch4 git-svn-HEAD +git checkout -b mybranch4 remotes/git-svn rm -rf dir git update-index --remove -- dir/file touch dir @@ -136,19 +136,19 @@ git update-index --add -- dir git commit -m "$name" test_expect_code 1 "$name" \ - 'git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch4' \ + 'git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch4' \ || true name='remove executable bit from a file' rm -f $GIT_DIR/index -git checkout -b mybranch5 git-svn-HEAD +git checkout -b mybranch5 remotes/git-svn chmod -x exec.sh git update-index exec.sh git commit -m "$name" test_expect_success "$name" \ - "git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch5 && + "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 && test ! -x $SVN_TREE/exec.sh" @@ -158,7 +158,7 @@ git update-index exec.sh git commit -m "$name" test_expect_success "$name" \ - "git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch5 && + "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 && test -x $SVN_TREE/exec.sh" @@ -170,7 +170,7 @@ git update-index exec.sh git commit -m "$name" test_expect_success "$name" \ - "git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch5 && + "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 && test -L $SVN_TREE/exec.sh" @@ -182,7 +182,7 @@ git update-index --add bar/zzz exec-2.sh git commit -m "$name" test_expect_success "$name" \ - "git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch5 && + "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 && test -x $SVN_TREE/bar/zzz && test -L $SVN_TREE/exec-2.sh" @@ -196,7 +196,7 @@ git update-index exec-2.sh git commit -m "$name" test_expect_success "$name" \ - "git-svn commit --find-copies-harder --rmdir git-svn-HEAD..mybranch5 && + "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 && test -f $SVN_TREE/exec-2.sh && test ! -L $SVN_TREE/exec-2.sh && diff -u help $SVN_TREE/exec-2.sh" @@ -207,9 +207,9 @@ name='test fetch functionality (svn => git) with alternate GIT_SVN_ID' GIT_SVN_ID=alt export GIT_SVN_ID test_expect_success "$name" \ - "git-svn init $svnrepo && git-svn fetch -v && - git-rev-list --pretty=raw git-svn-HEAD | grep ^tree | uniq > a && - git-rev-list --pretty=raw alt-HEAD | grep ^tree | uniq > b && + "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 && diff -u a b" test_done diff --git a/contrib/gitview/gitview b/contrib/gitview/gitview index ea05cd4240..781badbc5b 100755 --- a/contrib/gitview/gitview +++ b/contrib/gitview/gitview @@ -513,7 +513,7 @@ class GitView: scrollwin = gtk.ScrolledWindow() - scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + 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() @@ -526,9 +526,6 @@ class GitView: self.treeview.show() cell = CellRendererGraph() - # Set the default width to 265 - # This make sure that we have nice display with large tag names - cell.set_property("width", 265) column = gtk.TreeViewColumn() column.set_resizable(True) column.pack_start(cell, expand=True) @@ -801,7 +798,7 @@ class GitView: button.set_relief(gtk.RELIEF_NONE) button.set_sensitive(True) button.connect("clicked", self._show_clicked_cb, - child_id, commit.commit_sha1) + child_id, commit.commit_sha1, self.encoding) hbox.pack_start(button, expand=False, fill=True) button.show() |
