git-mv.perlon commit git-svn: bugfix and optimize the 'log' command (c0d4822)
   1#!/usr/bin/perl
   2#
   3# Copyright 2005, Ryan Anderson <ryan@michonline.com>
   4#                 Josef Weidendorfer <Josef.Weidendorfer@gmx.de>
   5#
   6# This file is licensed under the GPL v2, or a later version
   7# at the discretion of Linus Torvalds.
   8
   9
  10use warnings;
  11use strict;
  12use Getopt::Std;
  13
  14sub usage() {
  15        print <<EOT;
  16$0 [-f] [-n] <source> <destination>
  17$0 [-f] [-n] [-k] <source> ... <destination directory>
  18EOT
  19        exit(1);
  20}
  21
  22our ($opt_n, $opt_f, $opt_h, $opt_k, $opt_v);
  23getopts("hnfkv") || usage;
  24usage() if $opt_h;
  25@ARGV >= 1 or usage;
  26
  27my $GIT_DIR = `git rev-parse --git-dir`;
  28exit 1 if $?; # rev-parse would have given "not a git dir" message.
  29chomp($GIT_DIR);
  30
  31my (@srcArgs, @dstArgs, @srcs, @dsts);
  32my ($src, $dst, $base, $dstDir);
  33
  34# remove any trailing slash in arguments
  35for (@ARGV) { s/\/*$//; }
  36
  37my $argCount = scalar @ARGV;
  38if (-d $ARGV[$argCount-1]) {
  39        $dstDir = $ARGV[$argCount-1];
  40        @srcArgs = @ARGV[0..$argCount-2];
  41
  42        foreach $src (@srcArgs) {
  43                $base = $src;
  44                $base =~ s/^.*\///;
  45                $dst = "$dstDir/". $base;
  46                push @dstArgs, $dst;
  47        }
  48}
  49else {
  50    if ($argCount < 2) {
  51        print "Error: need at least two arguments\n";
  52        exit(1);
  53    }
  54    if ($argCount > 2) {
  55        print "Error: moving to directory '"
  56            . $ARGV[$argCount-1]
  57            . "' not possible; not existing\n";
  58        exit(1);
  59    }
  60    @srcArgs = ($ARGV[0]);
  61    @dstArgs = ($ARGV[1]);
  62    $dstDir = "";
  63}
  64
  65my $subdir_prefix = `git rev-parse --show-prefix`;
  66chomp($subdir_prefix);
  67
  68# run in git base directory, so that git-ls-files lists all revisioned files
  69chdir "$GIT_DIR/..";
  70
  71# normalize paths, needed to compare against versioned files and update-index
  72# also, this is nicer to end-users by doing ".//a/./b/.//./c" ==> "a/b/c"
  73for (@srcArgs, @dstArgs) {
  74    # prepend git prefix as we run from base directory
  75    $_ = $subdir_prefix.$_;
  76    s|^\./||;
  77    s|/\./|/| while (m|/\./|);
  78    s|//+|/|g;
  79    # Also "a/b/../c" ==> "a/c"
  80    1 while (s,(^|/)[^/]+/\.\./,$1,);
  81}
  82
  83my (@allfiles,@srcfiles,@dstfiles);
  84my $safesrc;
  85my (%overwritten, %srcForDst);
  86
  87$/ = "\0";
  88open(F, 'git-ls-files -z |')
  89        or die "Failed to open pipe from git-ls-files: " . $!;
  90
  91@allfiles = map { chomp; $_; } <F>;
  92close(F);
  93
  94
  95my ($i, $bad);
  96while(scalar @srcArgs > 0) {
  97    $src = shift @srcArgs;
  98    $dst = shift @dstArgs;
  99    $bad = "";
 100
 101    for ($src, $dst) {
 102        # Be nicer to end-users by doing ".//a/./b/.//./c" ==> "a/b/c"
 103        s|^\./||;
 104        s|/\./|/| while (m|/\./|);
 105        s|//+|/|g;
 106        # Also "a/b/../c" ==> "a/c"
 107        1 while (s,(^|/)[^/]+/\.\./,$1,);
 108    }
 109
 110    if ($opt_v) {
 111        print "Checking rename of '$src' to '$dst'\n";
 112    }
 113
 114    unless (-f $src || -l $src || -d $src) {
 115        $bad = "bad source '$src'";
 116    }
 117
 118    $safesrc = quotemeta($src);
 119    @srcfiles = grep /^$safesrc(\/|$)/, @allfiles;
 120
 121    $overwritten{$dst} = 0;
 122    if (($bad eq "") && -e $dst) {
 123        $bad = "destination '$dst' already exists";
 124        if ($opt_f) {
 125            # only files can overwrite each other: check both source and destination
 126            if (-f $dst && (scalar @srcfiles == 1)) {
 127                print "Warning: $bad; will overwrite!\n";
 128                $bad = "";
 129                $overwritten{$dst} = 1;
 130            }
 131            else {
 132                $bad = "Can not overwrite '$src' with '$dst'";
 133            }
 134        }
 135    }
 136    
 137    if (($bad eq "") && ($dst =~ /^$safesrc\//)) {
 138        $bad = "can not move directory '$src' into itself";
 139    }
 140
 141    if ($bad eq "") {
 142        if (scalar @srcfiles == 0) {
 143            $bad = "'$src' not under version control";
 144        }
 145    }
 146
 147    if ($bad eq "") {
 148       if (defined $srcForDst{$dst}) {
 149           $bad = "can not move '$src' to '$dst'; already target of ";
 150           $bad .= "'".$srcForDst{$dst}."'";
 151       }
 152       else {
 153           $srcForDst{$dst} = $src;
 154       }
 155    }
 156
 157    if ($bad ne "") {
 158        if ($opt_k) {
 159            print "Warning: $bad; skipping\n";
 160            next;
 161        }
 162        print "Error: $bad\n";
 163        exit(1);
 164    }
 165    push @srcs, $src;
 166    push @dsts, $dst;
 167}
 168
 169# Final pass: rename/move
 170my (@deletedfiles,@addedfiles,@changedfiles);
 171$bad = "";
 172while(scalar @srcs > 0) {
 173    $src = shift @srcs;
 174    $dst = shift @dsts;
 175
 176    if ($opt_n || $opt_v) { print "Renaming $src to $dst\n"; }
 177    if (!$opt_n) {
 178        if (!rename($src,$dst)) {
 179            $bad = "renaming '$src' failed: $!";
 180            if ($opt_k) {
 181                print "Warning: skipped: $bad\n";
 182                $bad = "";
 183                next;
 184            }
 185            last;
 186        }
 187    }
 188
 189    $safesrc = quotemeta($src);
 190    @srcfiles = grep /^$safesrc(\/|$)/, @allfiles;
 191    @dstfiles = @srcfiles;
 192    s/^$safesrc(\/|$)/$dst$1/ for @dstfiles;
 193
 194    push @deletedfiles, @srcfiles;
 195    if (scalar @srcfiles == 1) {
 196        # $dst can be a directory with 1 file inside
 197        if ($overwritten{$dst} ==1) {
 198            push @changedfiles, $dstfiles[0];
 199
 200        } else {
 201            push @addedfiles, $dstfiles[0];
 202        }
 203    }
 204    else {
 205        push @addedfiles, @dstfiles;
 206    }
 207}
 208
 209if ($opt_n) {
 210    if (@changedfiles) {
 211        print "Changed  : ". join(", ", @changedfiles) ."\n";
 212    }
 213    if (@addedfiles) {
 214        print "Adding   : ". join(", ", @addedfiles) ."\n";
 215    }
 216    if (@deletedfiles) {
 217        print "Deleting : ". join(", ", @deletedfiles) ."\n";
 218    }
 219}
 220else {
 221    if (@changedfiles) {
 222        open(H, "| git-update-index -z --stdin")
 223                or die "git-update-index failed to update changed files with code $!\n";
 224        foreach my $fileName (@changedfiles) {
 225                print H "$fileName\0";
 226        }
 227        close(H);
 228    }
 229    if (@addedfiles) {
 230        open(H, "| git-update-index --add -z --stdin")
 231                or die "git-update-index failed to add new names with code $!\n";
 232        foreach my $fileName (@addedfiles) {
 233                print H "$fileName\0";
 234        }
 235        close(H);
 236    }
 237    if (@deletedfiles) {
 238        open(H, "| git-update-index --remove -z --stdin")
 239                or die "git-update-index failed to remove old names with code $!\n";
 240        foreach my $fileName (@deletedfiles) {
 241                print H "$fileName\0";
 242        }
 243        close(H);
 244    }
 245}
 246
 247if ($bad ne "") {
 248    print "Error: $bad\n";
 249    exit(1);
 250}