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
10use warnings;
11use strict;
12use Getopt::Std;
13
14sub usage() {
15 print <<EOT;
16$0 [-f] [-n] <source> <destination>
17$0 [-f] [-n] [-k] <source> ... <destination directory>
18EOT
19 exit(1);
20}
21
22our ($opt_n, $opt_f, $opt_h, $opt_k, $opt_v);
23getopts("hnfkv") || usage;
24usage() if $opt_h;
25@ARGV >= 1 or usage;
26
27my $GIT_DIR = `git rev-parse --git-dir`;
28exit 1 if $?; # rev-parse would have given "not a git dir" message.
29chomp($GIT_DIR);
30
31my (@srcArgs, @dstArgs, @srcs, @dsts);
32my ($src, $dst, $base, $dstDir);
33
34# remove any trailing slash in arguments
35for (@ARGV) { s/\/*$//; }
36
37my $argCount = scalar @ARGV;
38if (-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}
49else {
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"
67for (@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
75my (@allfiles,@srcfiles,@dstfiles);
76my $safesrc;
77my (%overwritten, %srcForDst);
78
79$/ = "\0";
80open(F, 'git-ls-files -z |')
81 or die "Failed to open pipe from git-ls-files: " . $!;
82
83@allfiles = map { chomp; $_; } <F>;
84close(F);
85
86
87my ($i, $bad);
88while(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
162my (@deletedfiles,@addedfiles,@changedfiles);
163$bad = "";
164while(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
201if ($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}
212else {
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
239if ($bad ne "") {
240 print "Error: $bad\n";
241 exit(1);
242}