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