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
9BEGIN {
10 unless (exists $ENV{'RUNNING_GIT_TESTS'}) {
11 unshift @INC, '@@INSTLIBDIR@@';
12 }
13}
14use warnings;
15use strict;
16use Getopt::Std;
17use Git;
18
19sub usage() {
20 print <<EOT;
21$0 [-f] [-n] <source> <destination>
22$0 [-f] [-n] [-k] <source> ... <destination directory>
23EOT
24 exit(1);
25}
26
27our ($opt_n, $opt_f, $opt_h, $opt_k, $opt_v);
28getopts("hnfkv") || usage;
29usage() if $opt_h;
30@ARGV >= 1 or usage;
31
32my $repo = Git->repository();
33
34my (@srcArgs, @dstArgs, @srcs, @dsts);
35my ($src, $dst, $base, $dstDir);
36
37# remove any trailing slash in arguments
38for (@ARGV) { s/\/*$//; }
39
40my $argCount = scalar @ARGV;
41if (-d $ARGV[$argCount-1]) {
42 $dstDir = $ARGV[$argCount-1];
43 @srcArgs = @ARGV[0..$argCount-2];
44
45 foreach $src (@srcArgs) {
46 $base = $src;
47 $base =~ s/^.*\///;
48 $dst = "$dstDir/". $base;
49 push @dstArgs, $dst;
50 }
51}
52else {
53 if ($argCount < 2) {
54 print "Error: need at least two arguments\n";
55 exit(1);
56 }
57 if ($argCount > 2) {
58 print "Error: moving to directory '"
59 . $ARGV[$argCount-1]
60 . "' not possible; not existing\n";
61 exit(1);
62 }
63 @srcArgs = ($ARGV[0]);
64 @dstArgs = ($ARGV[1]);
65 $dstDir = "";
66}
67
68my $subdir_prefix = $repo->wc_subdir();
69
70# run in git base directory, so that git-ls-files lists all revisioned files
71chdir $repo->wc_path();
72$repo->wc_chdir('');
73
74# normalize paths, needed to compare against versioned files and update-index
75# also, this is nicer to end-users by doing ".//a/./b/.//./c" ==> "a/b/c"
76for (@srcArgs, @dstArgs) {
77 # prepend git prefix as we run from base directory
78 $_ = $subdir_prefix.$_;
79 s|^\./||;
80 s|/\./|/| while (m|/\./|);
81 s|//+|/|g;
82 # Also "a/b/../c" ==> "a/c"
83 1 while (s,(^|/)[^/]+/\.\./,$1,);
84}
85
86my (@allfiles,@srcfiles,@dstfiles);
87my $safesrc;
88my (%overwritten, %srcForDst);
89
90{
91 local $/ = "\0";
92 @allfiles = $repo->command('ls-files', '-z');
93}
94
95
96my ($i, $bad);
97while(scalar @srcArgs > 0) {
98 $src = shift @srcArgs;
99 $dst = shift @dstArgs;
100 $bad = "";
101
102 for ($src, $dst) {
103 # Be nicer to end-users by doing ".//a/./b/.//./c" ==> "a/b/c"
104 s|^\./||;
105 s|/\./|/| while (m|/\./|);
106 s|//+|/|g;
107 # Also "a/b/../c" ==> "a/c"
108 1 while (s,(^|/)[^/]+/\.\./,$1,);
109 }
110
111 if ($opt_v) {
112 print "Checking rename of '$src' to '$dst'\n";
113 }
114
115 unless (-f $src || -l $src || -d $src) {
116 $bad = "bad source '$src'";
117 }
118
119 $safesrc = quotemeta($src);
120 @srcfiles = grep /^$safesrc(\/|$)/, @allfiles;
121
122 $overwritten{$dst} = 0;
123 if (($bad eq "") && -e $dst) {
124 $bad = "destination '$dst' already exists";
125 if ($opt_f) {
126 # only files can overwrite each other: check both source and destination
127 if (-f $dst && (scalar @srcfiles == 1)) {
128 print "Warning: $bad; will overwrite!\n";
129 $bad = "";
130 $overwritten{$dst} = 1;
131 }
132 else {
133 $bad = "Can not overwrite '$src' with '$dst'";
134 }
135 }
136 }
137
138 if (($bad eq "") && ($dst =~ /^$safesrc\//)) {
139 $bad = "can not move directory '$src' into itself";
140 }
141
142 if ($bad eq "") {
143 if (scalar @srcfiles == 0) {
144 $bad = "'$src' not under version control";
145 }
146 }
147
148 if ($bad eq "") {
149 if (defined $srcForDst{$dst}) {
150 $bad = "can not move '$src' to '$dst'; already target of ";
151 $bad .= "'".$srcForDst{$dst}."'";
152 }
153 else {
154 $srcForDst{$dst} = $src;
155 }
156 }
157
158 if ($bad ne "") {
159 if ($opt_k) {
160 print "Warning: $bad; skipping\n";
161 next;
162 }
163 print "Error: $bad\n";
164 exit(1);
165 }
166 push @srcs, $src;
167 push @dsts, $dst;
168}
169
170# Final pass: rename/move
171my (@deletedfiles,@addedfiles,@changedfiles);
172$bad = "";
173while(scalar @srcs > 0) {
174 $src = shift @srcs;
175 $dst = shift @dsts;
176
177 if ($opt_n || $opt_v) { print "Renaming $src to $dst\n"; }
178 if (!$opt_n) {
179 if (!rename($src,$dst)) {
180 $bad = "renaming '$src' failed: $!";
181 if ($opt_k) {
182 print "Warning: skipped: $bad\n";
183 $bad = "";
184 next;
185 }
186 last;
187 }
188 }
189
190 $safesrc = quotemeta($src);
191 @srcfiles = grep /^$safesrc(\/|$)/, @allfiles;
192 @dstfiles = @srcfiles;
193 s/^$safesrc(\/|$)/$dst$1/ for @dstfiles;
194
195 push @deletedfiles, @srcfiles;
196 if (scalar @srcfiles == 1) {
197 # $dst can be a directory with 1 file inside
198 if ($overwritten{$dst} ==1) {
199 push @changedfiles, $dstfiles[0];
200
201 } else {
202 push @addedfiles, $dstfiles[0];
203 }
204 }
205 else {
206 push @addedfiles, @dstfiles;
207 }
208}
209
210if ($opt_n) {
211 if (@changedfiles) {
212 print "Changed : ". join(", ", @changedfiles) ."\n";
213 }
214 if (@addedfiles) {
215 print "Adding : ". join(", ", @addedfiles) ."\n";
216 }
217 if (@deletedfiles) {
218 print "Deleting : ". join(", ", @deletedfiles) ."\n";
219 }
220}
221else {
222 if (@changedfiles) {
223 my ($fd, $ctx) = $repo->command_input_pipe('update-index', '-z', '--stdin');
224 foreach my $fileName (@changedfiles) {
225 print $fd "$fileName\0";
226 }
227 git_cmd_try { $repo->command_close_pipe($fd, $ctx); }
228 'git-update-index failed to update changed files with code %d';
229 }
230 if (@addedfiles) {
231 my ($fd, $ctx) = $repo->command_input_pipe('update-index', '--add', '-z', '--stdin');
232 foreach my $fileName (@addedfiles) {
233 print $fd "$fileName\0";
234 }
235 git_cmd_try { $repo->command_close_pipe($fd, $ctx); }
236 'git-update-index failed to add new files with code %d';
237 }
238 if (@deletedfiles) {
239 my ($fd, $ctx) = $repo->command_input_pipe('update-index', '--remove', '-z', '--stdin');
240 foreach my $fileName (@deletedfiles) {
241 print $fd "$fileName\0";
242 }
243 git_cmd_try { $repo->command_close_pipe($fd, $ctx); }
244 'git-update-index failed to remove old files with code %d';
245 }
246}
247
248if ($bad ne "") {
249 print "Error: $bad\n";
250 exit(1);
251}