Merge branch 'lt/rev-list' into next
[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 # normalize paths, needed to compare against versioned files and update-index
66 # also, this is nicer to end-users by doing ".//a/./b/.//./c" ==> "a/b/c"
67 for (@srcArgs, @dstArgs) {
68     s|^\./||;
69     s|/\./|/| while (m|/\./|);
70     s|//+|/|g;
71     # Also "a/b/../c" ==> "a/c"
72     1 while (s,(^|/)[^/]+/\.\./,$1,);
73 }
74
75 my (@allfiles,@srcfiles,@dstfiles);
76 my $safesrc;
77 my (%overwritten, %srcForDst);
78
79 $/ = "\0";
80 open(F, 'git-ls-files -z |')
81         or die "Failed to open pipe from git-ls-files: " . $!;
82
83 @allfiles = map { chomp; $_; } <F>;
84 close(F);
85
86
87 my ($i, $bad);
88 while(scalar @srcArgs > 0) {
89     $src = shift @srcArgs;
90     $dst = shift @dstArgs;
91     $bad = "";
92
93     for ($src, $dst) {
94         # Be nicer to end-users by doing ".//a/./b/.//./c" ==> "a/b/c"
95         s|^\./||;
96         s|/\./|/| while (m|/\./|);
97         s|//+|/|g;
98         # Also "a/b/../c" ==> "a/c"
99         1 while (s,(^|/)[^/]+/\.\./,$1,);
100     }
101
102     if ($opt_v) {
103         print "Checking rename of '$src' to '$dst'\n";
104     }
105
106     unless (-f $src || -l $src || -d $src) {
107         $bad = "bad source '$src'";
108     }
109
110     $safesrc = quotemeta($src);
111     @srcfiles = grep /^$safesrc(\/|$)/, @allfiles;
112
113     $overwritten{$dst} = 0;
114     if (($bad eq "") && -e $dst) {
115         $bad = "destination '$dst' already exists";
116         if ($opt_f) {
117             # only files can overwrite each other: check both source and destination
118             if (-f $dst && (scalar @srcfiles == 1)) {
119                 print "Warning: $bad; will overwrite!\n";
120                 $bad = "";
121                 $overwritten{$dst} = 1;
122             }
123             else {
124                 $bad = "Can not overwrite '$src' with '$dst'";
125             }
126         }
127     }
128     
129     if (($bad eq "") && ($dst =~ /^$safesrc\//)) {
130         $bad = "can not move directory '$src' into itself";
131     }
132
133     if ($bad eq "") {
134         if (scalar @srcfiles == 0) {
135             $bad = "'$src' not under version control";
136         }
137     }
138
139     if ($bad eq "") {
140        if (defined $srcForDst{$dst}) {
141            $bad = "can not move '$src' to '$dst'; already target of ";
142            $bad .= "'".$srcForDst{$dst}."'";
143        }
144        else {
145            $srcForDst{$dst} = $src;
146        }
147     }
148
149     if ($bad ne "") {
150         if ($opt_k) {
151             print "Warning: $bad; skipping\n";
152             next;
153         }
154         print "Error: $bad\n";
155         exit(1);
156     }
157     push @srcs, $src;
158     push @dsts, $dst;
159 }
160
161 # Final pass: rename/move
162 my (@deletedfiles,@addedfiles,@changedfiles);
163 $bad = "";
164 while(scalar @srcs > 0) {
165     $src = shift @srcs;
166     $dst = shift @dsts;
167
168     if ($opt_n || $opt_v) { print "Renaming $src to $dst\n"; }
169     if (!$opt_n) {
170         if (!rename($src,$dst)) {
171             $bad = "renaming '$src' failed: $!";
172             if ($opt_k) {
173                 print "Warning: skipped: $bad\n";
174                 $bad = "";
175                 next;
176             }
177             last;
178         }
179     }
180
181     $safesrc = quotemeta($src);
182     @srcfiles = grep /^$safesrc(\/|$)/, @allfiles;
183     @dstfiles = @srcfiles;
184     s/^$safesrc(\/|$)/$dst$1/ for @dstfiles;
185
186     push @deletedfiles, @srcfiles;
187     if (scalar @srcfiles == 1) {
188         # $dst can be a directory with 1 file inside
189         if ($overwritten{$dst} ==1) {
190             push @changedfiles, $dstfiles[0];
191
192         } else {
193             push @addedfiles, $dstfiles[0];
194         }
195     }
196     else {
197         push @addedfiles, @dstfiles;
198     }
199 }
200
201 if ($opt_n) {
202     if (@changedfiles) {
203         print "Changed  : ". join(", ", @changedfiles) ."\n";
204     }
205     if (@addedfiles) {
206         print "Adding   : ". join(", ", @addedfiles) ."\n";
207     }
208     if (@deletedfiles) {
209         print "Deleting : ". join(", ", @deletedfiles) ."\n";
210     }
211 }
212 else {
213     if (@changedfiles) {
214         open(H, "| git-update-index -z --stdin")
215                 or die "git-update-index failed to update changed files with code $!\n";
216         foreach my $fileName (@changedfiles) {
217                 print H "$fileName\0";
218         }
219         close(H);
220     }
221     if (@addedfiles) {
222         open(H, "| git-update-index --add -z --stdin")
223                 or die "git-update-index failed to add new names with code $!\n";
224         foreach my $fileName (@addedfiles) {
225                 print H "$fileName\0";
226         }
227         close(H);
228     }
229     if (@deletedfiles) {
230         open(H, "| git-update-index --remove -z --stdin")
231                 or die "git-update-index failed to remove old names with code $!\n";
232         foreach my $fileName (@deletedfiles) {
233                 print H "$fileName\0";
234         }
235         close(H);
236     }
237 }
238
239 if ($bad ne "") {
240     print "Error: $bad\n";
241     exit(1);
242 }