git-commit-vandalism/git-mv.perl
Josef Weidendorfer 9e7c73de0b git-mv: fixes for path handling
Moving a directory ending in a slash was not working as the
destination was not calculated correctly.
E.g. in the git repo,

 git-mv t/ Documentation

gave the error

 Error: destination 'Documentation' already exists

To get rid of this problem, strip trailing slashes from all arguments.
The comment in cg-mv made me curious about this issue; Pasky, thanks!
As result, the workaround in cg-mv is not needed any more.

Also, another bug was shown by cg-mv. When moving files outside of
a subdirectory, it typically calls git-mv with something like

 git-mv Documentation/git.txt Documentation/../git-mv.txt

which triggers the following error from git-update-index:

 Ignoring path Documentation/../git-mv.txt

The result is a moved file, removed from git revisioning, but not
added again. To fix this, the paths have to be normalized not have ".."
in the middle. This was already done in git-mv, but only for
a better visual appearance :(

Signed-off-by: Josef Weidendorfer <Josef.Weidendorfer@gmx.de>
Signed-off-by: Junio C Hamano <junkio@cox.net>
2006-03-01 12:13:46 -08:00

234 lines
5.1 KiB
Perl
Executable File

#!/usr/bin/perl
#
# Copyright 2005, Ryan Anderson <ryan@michonline.com>
# Josef Weidendorfer <Josef.Weidendorfer@gmx.de>
#
# This file is licensed under the GPL v2, or a later version
# at the discretion of Linus Torvalds.
use warnings;
use strict;
use Getopt::Std;
sub usage() {
print <<EOT;
$0 [-f] [-n] <source> <destination>
$0 [-f] [-n] [-k] <source> ... <destination directory>
EOT
exit(1);
}
our ($opt_n, $opt_f, $opt_h, $opt_k, $opt_v);
getopts("hnfkv") || usage;
usage() if $opt_h;
@ARGV >= 1 or usage;
my $GIT_DIR = `git rev-parse --git-dir`;
exit 1 if $?; # rev-parse would have given "not a git dir" message.
chomp($GIT_DIR);
my (@srcArgs, @dstArgs, @srcs, @dsts);
my ($src, $dst, $base, $dstDir);
# remove any trailing slash in arguments
for (@ARGV) { s/\/*$//; }
my $argCount = scalar @ARGV;
if (-d $ARGV[$argCount-1]) {
$dstDir = $ARGV[$argCount-1];
@srcArgs = @ARGV[0..$argCount-2];
foreach $src (@srcArgs) {
$base = $src;
$base =~ s/^.*\///;
$dst = "$dstDir/". $base;
push @dstArgs, $dst;
}
}
else {
if ($argCount < 2) {
print "Error: need at least two arguments\n";
exit(1);
}
if ($argCount > 2) {
print "Error: moving to directory '"
. $ARGV[$argCount-1]
. "' not possible; not existing\n";
exit(1);
}
@srcArgs = ($ARGV[0]);
@dstArgs = ($ARGV[1]);
$dstDir = "";
}
# normalize paths, needed to compare against versioned files and update-index
# also, this is nicer to end-users by doing ".//a/./b/.//./c" ==> "a/b/c"
for (@srcArgs, @dstArgs) {
s|^\./||;
s|/\./|/| while (m|/\./|);
s|//+|/|g;
# Also "a/b/../c" ==> "a/c"
1 while (s,(^|/)[^/]+/\.\./,$1,);
}
my (@allfiles,@srcfiles,@dstfiles);
my $safesrc;
my (%overwritten, %srcForDst);
$/ = "\0";
open(F, 'git-ls-files -z |')
or die "Failed to open pipe from git-ls-files: " . $!;
@allfiles = map { chomp; $_; } <F>;
close(F);
my ($i, $bad);
while(scalar @srcArgs > 0) {
$src = shift @srcArgs;
$dst = shift @dstArgs;
$bad = "";
if ($opt_v) {
print "Checking rename of '$src' to '$dst'\n";
}
unless (-f $src || -l $src || -d $src) {
$bad = "bad source '$src'";
}
$safesrc = quotemeta($src);
@srcfiles = grep /^$safesrc(\/|$)/, @allfiles;
$overwritten{$dst} = 0;
if (($bad eq "") && -e $dst) {
$bad = "destination '$dst' already exists";
if ($opt_f) {
# only files can overwrite each other: check both source and destination
if (-f $dst && (scalar @srcfiles == 1)) {
print "Warning: $bad; will overwrite!\n";
$bad = "";
$overwritten{$dst} = 1;
}
else {
$bad = "Can not overwrite '$src' with '$dst'";
}
}
}
if (($bad eq "") && ($dst =~ /^$safesrc\//)) {
$bad = "can not move directory '$src' into itself";
}
if ($bad eq "") {
if (scalar @srcfiles == 0) {
$bad = "'$src' not under version control";
}
}
if ($bad eq "") {
if (defined $srcForDst{$dst}) {
$bad = "can not move '$src' to '$dst'; already target of ";
$bad .= "'".$srcForDst{$dst}."'";
}
else {
$srcForDst{$dst} = $src;
}
}
if ($bad ne "") {
if ($opt_k) {
print "Warning: $bad; skipping\n";
next;
}
print "Error: $bad\n";
exit(1);
}
push @srcs, $src;
push @dsts, $dst;
}
# Final pass: rename/move
my (@deletedfiles,@addedfiles,@changedfiles);
$bad = "";
while(scalar @srcs > 0) {
$src = shift @srcs;
$dst = shift @dsts;
if ($opt_n || $opt_v) { print "Renaming $src to $dst\n"; }
if (!$opt_n) {
if (!rename($src,$dst)) {
$bad = "renaming '$src' failed: $!";
if ($opt_k) {
print "Warning: skipped: $bad\n";
$bad = "";
next;
}
last;
}
}
$safesrc = quotemeta($src);
@srcfiles = grep /^$safesrc(\/|$)/, @allfiles;
@dstfiles = @srcfiles;
s/^$safesrc(\/|$)/$dst$1/ for @dstfiles;
push @deletedfiles, @srcfiles;
if (scalar @srcfiles == 1) {
# $dst can be a directory with 1 file inside
if ($overwritten{$dst} ==1) {
push @changedfiles, $dstfiles[0];
} else {
push @addedfiles, $dstfiles[0];
}
}
else {
push @addedfiles, @dstfiles;
}
}
if ($opt_n) {
if (@changedfiles) {
print "Changed : ". join(", ", @changedfiles) ."\n";
}
if (@addedfiles) {
print "Adding : ". join(", ", @addedfiles) ."\n";
}
if (@deletedfiles) {
print "Deleting : ". join(", ", @deletedfiles) ."\n";
}
}
else {
if (@changedfiles) {
open(H, "| git-update-index -z --stdin")
or die "git-update-index failed to update changed files with code $!\n";
foreach my $fileName (@changedfiles) {
print H "$fileName\0";
}
close(H);
}
if (@addedfiles) {
open(H, "| git-update-index --add -z --stdin")
or die "git-update-index failed to add new names with code $!\n";
foreach my $fileName (@addedfiles) {
print H "$fileName\0";
}
close(H);
}
if (@deletedfiles) {
open(H, "| git-update-index --remove -z --stdin")
or die "git-update-index failed to remove old names with code $!\n";
foreach my $fileName (@deletedfiles) {
print H "$fileName\0";
}
close(H);
}
}
if ($bad ne "") {
print "Error: $bad\n";
exit(1);
}