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
34my $argCount = scalar @ARGV;
35if (-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}
48else {
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
64my (@allfiles,@srcfiles,@dstfiles);
65my $safesrc;
66my (%overwritten, %srcForDst);
67
68$/ = "\0";
69open(F, 'git-ls-files -z |')
70 or die "Failed to open pipe from git-ls-files: " . $!;
71
72@allfiles = map { chomp; $_; } <F>;
73close(F);
74
75
76my ($i, $bad);
77while(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
151my (@deletedfiles,@addedfiles,@changedfiles);
152$bad = "";
153while(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
190if ($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}
201else {
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
228if ($bad ne "") {
229 print "Error: $bad\n";
230 exit(1);
231}