Merge branch 'tl/anno'
[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 # remove any trailing slash in arguments
35 for (@ARGV) { s/\/*$//; }
36
37 my $argCount = scalar @ARGV;
38 if (-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 }
49 else {
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
65 my $subdir_prefix = `git rev-parse --show-prefix`;
66 chomp($subdir_prefix);
67
68 # run in git base directory, so that git-ls-files lists all revisioned files
69 chdir "$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"
73 for (@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
83 my (@allfiles,@srcfiles,@dstfiles);
84 my $safesrc;
85 my (%overwritten, %srcForDst);
86
87 $/ = "\0";
88 open(F, 'git-ls-files -z |')
89         or die "Failed to open pipe from git-ls-files: " . $!;
90
91 @allfiles = map { chomp; $_; } <F>;
92 close(F);
93
94
95 my ($i, $bad);
96 while(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
170 my (@deletedfiles,@addedfiles,@changedfiles);
171 $bad = "";
172 while(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
209 if ($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 }
220 else {
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
247 if ($bad ne "") {
248     print "Error: $bad\n";
249     exit(1);
250 }