read-tree --aggressive: remove deleted entry from the working tree.
[git.git] / git-mv.perl
blobf3e859ae48af05d0b982a159c2197bab7e5da996
1 #!/usr/bin/perl
3 # Copyright 2005, Ryan Anderson <ryan@michonline.com>
4 # Josef Weidendorfer <Josef.Weidendorfer@gmx.de>
6 # This file is licensed under the GPL v2, or a later version
7 # at the discretion of Linus Torvalds.
10 use warnings;
11 use strict;
12 use Getopt::Std;
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);
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;
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);
31 my (@srcArgs, @dstArgs, @srcs, @dsts);
32 my ($src, $dst, $base, $dstDir);
34 # remove any trailing slash in arguments
35 for (@ARGV) { s/\/*$//; }
37 my $argCount = scalar @ARGV;
38 if (-d $ARGV[$argCount-1]) {
39 $dstDir = $ARGV[$argCount-1];
40 @srcArgs = @ARGV[0..$argCount-2];
42 foreach $src (@srcArgs) {
43 $base = $src;
44 $base =~ s/^.*\///;
45 $dst = "$dstDir/". $base;
46 push @dstArgs, $dst;
49 else {
50 if ($argCount < 2) {
51 print "Error: need at least two arguments\n";
52 exit(1);
54 if ($argCount > 2) {
55 print "Error: moving to directory '"
56 . $ARGV[$argCount-1]
57 . "' not possible; not existing\n";
58 exit(1);
60 @srcArgs = ($ARGV[0]);
61 @dstArgs = ($ARGV[1]);
62 $dstDir = "";
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,);
75 my (@allfiles,@srcfiles,@dstfiles);
76 my $safesrc;
77 my (%overwritten, %srcForDst);
79 $/ = "\0";
80 open(F, 'git-ls-files -z |')
81 or die "Failed to open pipe from git-ls-files: " . $!;
83 @allfiles = map { chomp; $_; } <F>;
84 close(F);
87 my ($i, $bad);
88 while(scalar @srcArgs > 0) {
89 $src = shift @srcArgs;
90 $dst = shift @dstArgs;
91 $bad = "";
93 if ($opt_v) {
94 print "Checking rename of '$src' to '$dst'\n";
97 unless (-f $src || -l $src || -d $src) {
98 $bad = "bad source '$src'";
101 $safesrc = quotemeta($src);
102 @srcfiles = grep /^$safesrc(\/|$)/, @allfiles;
104 $overwritten{$dst} = 0;
105 if (($bad eq "") && -e $dst) {
106 $bad = "destination '$dst' already exists";
107 if ($opt_f) {
108 # only files can overwrite each other: check both source and destination
109 if (-f $dst && (scalar @srcfiles == 1)) {
110 print "Warning: $bad; will overwrite!\n";
111 $bad = "";
112 $overwritten{$dst} = 1;
114 else {
115 $bad = "Can not overwrite '$src' with '$dst'";
120 if (($bad eq "") && ($dst =~ /^$safesrc\//)) {
121 $bad = "can not move directory '$src' into itself";
124 if ($bad eq "") {
125 if (scalar @srcfiles == 0) {
126 $bad = "'$src' not under version control";
130 if ($bad eq "") {
131 if (defined $srcForDst{$dst}) {
132 $bad = "can not move '$src' to '$dst'; already target of ";
133 $bad .= "'".$srcForDst{$dst}."'";
135 else {
136 $srcForDst{$dst} = $src;
140 if ($bad ne "") {
141 if ($opt_k) {
142 print "Warning: $bad; skipping\n";
143 next;
145 print "Error: $bad\n";
146 exit(1);
148 push @srcs, $src;
149 push @dsts, $dst;
152 # Final pass: rename/move
153 my (@deletedfiles,@addedfiles,@changedfiles);
154 $bad = "";
155 while(scalar @srcs > 0) {
156 $src = shift @srcs;
157 $dst = shift @dsts;
159 if ($opt_n || $opt_v) { print "Renaming $src to $dst\n"; }
160 if (!$opt_n) {
161 if (!rename($src,$dst)) {
162 $bad = "renaming '$src' failed: $!";
163 if ($opt_k) {
164 print "Warning: skipped: $bad\n";
165 $bad = "";
166 next;
168 last;
172 $safesrc = quotemeta($src);
173 @srcfiles = grep /^$safesrc(\/|$)/, @allfiles;
174 @dstfiles = @srcfiles;
175 s/^$safesrc(\/|$)/$dst$1/ for @dstfiles;
177 push @deletedfiles, @srcfiles;
178 if (scalar @srcfiles == 1) {
179 # $dst can be a directory with 1 file inside
180 if ($overwritten{$dst} ==1) {
181 push @changedfiles, $dstfiles[0];
183 } else {
184 push @addedfiles, $dstfiles[0];
187 else {
188 push @addedfiles, @dstfiles;
192 if ($opt_n) {
193 if (@changedfiles) {
194 print "Changed : ". join(", ", @changedfiles) ."\n";
196 if (@addedfiles) {
197 print "Adding : ". join(", ", @addedfiles) ."\n";
199 if (@deletedfiles) {
200 print "Deleting : ". join(", ", @deletedfiles) ."\n";
203 else {
204 if (@changedfiles) {
205 open(H, "| git-update-index -z --stdin")
206 or die "git-update-index failed to update changed files with code $!\n";
207 foreach my $fileName (@changedfiles) {
208 print H "$fileName\0";
210 close(H);
212 if (@addedfiles) {
213 open(H, "| git-update-index --add -z --stdin")
214 or die "git-update-index failed to add new names with code $!\n";
215 foreach my $fileName (@addedfiles) {
216 print H "$fileName\0";
218 close(H);
220 if (@deletedfiles) {
221 open(H, "| git-update-index --remove -z --stdin")
222 or die "git-update-index failed to remove old names with code $!\n";
223 foreach my $fileName (@deletedfiles) {
224 print H "$fileName\0";
226 close(H);
230 if ($bad ne "") {
231 print "Error: $bad\n";
232 exit(1);