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