Documentation/git-gc: explain --auto in description
[gitweb.git] / git-svn.perl
index 1130a0949ad77b0e15579ff1bf4d75ba6644f8cd..22bb47b34d030ec4d1b3dc7cda05465559e0a09d 100755 (executable)
@@ -9,6 +9,11 @@
 $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';
 $ENV{TZ} = 'UTC';
 $| = 1; # unbuffer STDOUT
 
-sub fatal (@) { print STDERR @_; exit 1 }
+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)\n";
+       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';
@@ -123,6 +128,16 @@ BEGIN
        '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
                        } ],
@@ -357,7 +372,7 @@ sub cmd_set_tree {
                } elsif (scalar @tmp > 1) {
                        push @revs, reverse(command('rev-list',@tmp));
                } else {
-                       fatal "Failed to rev-parse $c\n";
+                       fatal "Failed to rev-parse $c";
                }
        }
        my $gs = Git::SVN->new;
@@ -367,7 +382,7 @@ sub cmd_set_tree {
                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}\n";
+                     " current: $gs->{last_rev}";
        }
        $gs->set_tree($_) foreach @revs;
        print "Done committing ",scalar @revs," revisions to SVN\n";
@@ -396,7 +411,7 @@ sub cmd_dcommit {
                        (undef, $last_rev, undef) = cmt_metadata("$d~1");
                        unless (defined $last_rev) {
                                fatal "Unable to extract revision information ",
-                                     "from commit $d~1\n";
+                                     "from commit $d~1";
                        }
                }
                if ($_dry_run) {
@@ -488,7 +503,100 @@ 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->traverse_ignore(\*STDOUT, $gs->{path}, $r);
+       $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_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);
+       });
+}
+
+# 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)
+       # 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;
+
+       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 {
@@ -537,7 +645,7 @@ sub cmd_multi_fetch {
 sub cmd_commit_diff {
        my ($ta, $tb, $url) = @_;
        my $usage = "Usage: $0 commit-diff -r<revision> ".
-                   "<tree-ish> <tree-ish> [<URL>]\n";
+                   "<tree-ish> <tree-ish> [<URL>]";
        fatal($usage) if (!defined $ta || !defined $tb);
        my $svn_path;
        if (!defined $url) {
@@ -555,7 +663,7 @@ sub cmd_commit_diff {
        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\n");
+                     "I have no idea what you mean");
        }
        if (defined $_file) {
                $_message = file_to_s($_file);
@@ -618,7 +726,7 @@ sub complete_svn_url {
        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\n");
+                             "and a separate URL is not specified");
                }
                return ($url, $path);
        }
@@ -639,7 +747,7 @@ sub complete_url_ls_init {
                $repo_path =~ s#^/+##;
                unless ($ra) {
                        fatal("E: '$repo_path' is not a complete URL ",
-                             "and a separate URL is not specified\n");
+                             "and a separate URL is not specified");
                }
        }
        my $url = $ra->{url};
@@ -1480,28 +1588,45 @@ sub rel_path {
        $url;
 }
 
-sub traverse_ignore {
-       my ($self, $fh, $path, $r) = @_;
-       $path =~ s#^/+##g;
-       my $ra = $self->ra;
-       my ($dirent, undef, $props) = $ra->get_dir($path, $r);
+# 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;
-       $p =~ s#^\Q$self->{path}\E(/|$)##;
-       print $fh length $p ? "\n# $p\n" : "\n# /\n";
-       if (my $s = $props->{'svn:ignore'}) {
-               $s =~ s/[\r\n]+/\n/g;
-               chomp $s;
-               if (length $p == 0) {
-                       $s =~ s#\n#\n/$p#g;
-                       print $fh "/$s\n";
-               } else {
-                       $s =~ s#\n#\n/$p/#g;
-                       print $fh "/$p/$s\n";
-               }
-       }
+       # 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->traverse_ignore($fh, "$path/$_", $r);
+               $self->prop_walk($path . '/' . $_, $rev, $sub);
        }
 }
 
@@ -1628,7 +1753,7 @@ sub assert_index_clean {
                $x = command_oneline('write-tree');
                if ($y ne $x) {
                        ::fatal "trees ($treeish) $y != $x\n",
-                               "Something is seriously wrong...\n";
+                               "Something is seriously wrong...";
                }
        });
 }
@@ -2054,7 +2179,7 @@ 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\n");
+               fatal("Must have an existing revision to commit");
        }
        my %ed_opts = ( r => $self->{last_rev},
                        log => $log_entry->{log},
@@ -3003,7 +3128,7 @@ sub apply_diff {
                if (defined $o{$f}) {
                        $self->$f($m);
                } else {
-                       fatal("Invalid change type: $f\n");
+                       fatal("Invalid change type: $f");
                }
        }
        $self->rmdirs if $_rmdir;
@@ -3634,15 +3759,15 @@ sub config_pager {
 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: $!\n";
+       defined(my $pid = fork) or ::fatal "Can't fork: $!";
        if (!$pid) {
                open STDOUT, '>&', $wfd or
-                                    ::fatal "Can't redirect to stdout: $!\n";
+                                    ::fatal "Can't redirect to stdout: $!";
                return;
        }
-       open STDIN, '<&', $rfd or ::fatal "Can't redirect stdin: $!\n";
+       open STDIN, '<&', $rfd or ::fatal "Can't redirect stdin: $!";
        $ENV{LESS} ||= 'FRSX';
-       exec $pager or ::fatal "Can't run pager: $! ($pager)\n";
+       exec $pager or ::fatal "Can't run pager: $! ($pager)";
 }
 
 sub tz_to_s_offset {
@@ -3778,7 +3903,7 @@ sub cmd_show_log {
                        $r_min = $r_max = $::_revision;
                } else {
                        ::fatal "-r$::_revision is not supported, use ",
-                               "standard \'git log\' arguments instead\n";
+                               "standard 'git log' arguments instead";
                }
        }