external import script should rebase away whitespace
[openafs.git] / src / external / import-external-git.pl
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5
6 use English;
7 use Getopt::Long;
8 use File::Basename;
9 use File::Temp;
10 use File::Path;
11 use IO::File;
12 use IO::Pipe;
13 use Pod::Usage;
14 use Cwd;
15
16 # Import an external git repository into the OpenAFS tree, taking the path
17 # to a local clone of that repository, a file containing a list of mappings
18 # between that repository and the location in the OpenAFS one, and optionally
19 # a commit-ish
20
21 my $help;
22 my $man;
23 my $externalDir;
24 my $nowhitespace;
25 my $result = GetOptions("help|?" => \$help,
26                         "nofixwhitespace" => \$nowhitespace,
27                         "man" => \$man,
28                         "externaldir=s" => \$externalDir);
29                 
30 pod2usage(1) if $help;
31 pod2usage(-existatus => 0, -verbose =>2) if $man;
32
33 my $module = shift;
34 my $clonePath = shift;
35 my $commitish = shift;
36
37 pod2usage(2) if !defined($module) || !defined($clonePath);
38
39 if (!$commitish) {
40   $commitish = "HEAD";
41 }
42
43 # Use the PROGRAM_NAME to work out where we should be importing to.
44 if (!$externalDir) {
45   $externalDir = dirname(Cwd::abs_path($PROGRAM_NAME));
46 }
47
48 # Read in our mapping file
49 my %mapping;
50 my $fh = IO::File->new("$externalDir/$module-files")
51   or die "Couldn't open mapping file : $!\n";
52 while (<$fh>) {
53   next if /^\s#/;
54   if (/^(\S+)\s+(\S+)$/) {
55     $mapping{$1} = $2;
56   } elsif (/\w+/) {
57     die "Unrecognised line in mapping file : $_\n";
58   }
59 }
60 undef $fh;
61
62 # Read in our last-sha1 file
63 my $last;
64
65 $fh = IO::File->new("$externalDir/$module-last");
66 if ($fh) {
67   $last = $fh->getline;
68   chomp $last;
69 }
70 undef $fh;
71
72 my $author;
73 $fh = IO::File->new("$externalDir/$module-author");
74 if ($fh) {
75   $author = $fh->getline;
76   chomp $author;
77 }
78 undef $fh;
79
80 # Create the external directory, if it doesn't exist.
81 mkdir "$externalDir/$module" if (! -d "$externalDir/$module");
82
83 # Make ourselves a temporary directory
84 my $tempdir = File::Temp::tempdir(CLEANUP => 1);
85
86 # Write a list of all of the files that we're going to want out of the other
87 # repository in a format we can use with tar.
88 $fh = IO::File->new($tempdir."/filelist", "w")
89   or die "Can't open temporary file list for writing\n";
90 foreach (sort keys(%mapping)) {
91   $fh->print("source/".$_."\n");
92 }
93 undef $fh;
94
95 # Change directory to the root of the source repository
96 chdir $clonePath
97   or die "Unable to change directory to $clonePath : $!\n";
98
99 # Figure out some better names for the commit object we're using
100 my $commitSha1 = `git rev-parse $commitish`;
101 my $commitDesc = `git describe $commitish`;
102 chomp $commitSha1;
103 chomp $commitDesc;
104
105 # If we know what our last import was, then get a list of all of the changes
106 # since that import
107 my $changes;
108 if ($last) {
109   my $filelist = join(' ', sort keys(%mapping));
110   $changes = `git shortlog $last..$commitish $filelist`;
111 }
112
113 # Populate our temporary directory with the originals of everything that was
114 # listed in the mapping file
115 system("git archive --format=tar --prefix=source/ $commitish".
116        "  | tar -x -C $tempdir -T $tempdir/filelist") == 0
117  or die "git archive and tar failed : $!\n";
118
119 # change our CWD to the module directory - git ls-files seems to require this
120 chdir "$externalDir/$module"
121   or die "Unable to change directory to $externalDir/$module : $!\n";
122
123 # Now we're about to start fiddling with local state. Make a note of where we
124 # were.
125
126 # Use git stash to preserve whatever state there may be in the current
127 # working tree. Sadly git stash returns a 0 exit status if there are no
128 # local changes, so we need to check for local changes first.
129
130 my $stashed;
131 if (system("git diff-index --quiet --cached HEAD --ignore-submodules") != 0 ||
132     system("git diff-files --quiet --ignore-submodules") != 0) {
133   if (system("git stash") != 0) {
134     die "git stash failed with : $!\n";
135   }
136   $stashed = 1;
137 }
138
139
140 eval {
141   my @addedFiles;
142   my @deletedFiles;
143
144   # Use git-ls-files to get the list of currently committed files for the module
145   my $lspipe = IO::Pipe->new();
146   $lspipe->reader(qw(git ls-files));
147
148   my %filesInTree;
149   while(<$lspipe>) {
150     chomp;
151     $filesInTree{$_}++;
152   }
153
154   foreach my $source (sort keys(%mapping)) {
155     if (-f "$tempdir/source/$source") {
156       File::Path::make_path(File::Basename::dirname($mapping{$source}));
157       if (!-f "$externalDir/$module/".$mapping{$source}) {
158          push @addedFiles, $mapping{$source};
159       }
160       system("cp $tempdir/source/$source ".
161              "   $externalDir/$module/".$mapping{$source}) == 0
162          or die "Copy failed with $!\n";
163       system("git add $externalDir/$module/".$mapping{$source}) == 0
164          or die "git add failed with $!\n";
165       delete $filesInTree{$mapping{$source}}
166     } else {
167       die "Couldn't find file $source in original tree\n";
168     }
169   }
170
171   # Use git rm to delete everything that's committed that we don't have a
172   # relacement for.
173   foreach my $missing (keys(%filesInTree)) {
174     system("git rm $missing") == 0
175       or die "Couldn't git rm $missing : $!\n";
176     push @deletedFiles, $missing;
177   }
178
179   if (system("git status") == 0) {
180     my $fh=IO::File->new("$externalDir/$module-last", "w");
181     $fh->print($commitSha1."\n");
182     undef $fh;
183     system("git add $externalDir/$module-last") == 0
184        or die "Git add of last file failed with $!\n";
185
186     $fh=IO::File->new("$tempdir/commit-msg", "w")
187       or die "Unable to write commit message\n";
188     $fh->print("Import of code from $module\n");
189     $fh->print("\n");
190     $fh->print("This commit updates the code imported from $module to\n");
191     $fh->print("$commitSha1 ($commitDesc)\n");
192     if ($changes) {
193         $fh->print("\n");
194         $fh->print("Upstream changes are:\n\n");
195         $fh->print($changes);
196     }
197     if (@addedFiles) {
198         $fh->print("\n");
199         $fh->print("New files are:\n");
200         $fh->print(join("\n", map { "\t".$_  } sort @addedFiles));
201         $fh->print("\n");
202     }
203     if (@deletedFiles) {
204         $fh->print("\n");
205         $fh->print("Deleted files are:\n");
206         $fh->print(join("\n", map { "\t".$_  } sort @deletedFiles));
207         $fh->print("\n");
208     }
209     undef $fh;
210     $author="--author '$author'" if ($author);
211     system("git commit -F $tempdir/commit-msg $author") == 0
212       or die "Commit failed : $!\n";
213     if ($nowhitespace) {
214         print STDERR "WARNING: not fixing whitespace errors.\n";
215     } else {
216         system("git rebase --whitespace=fix HEAD^") == 0
217             or print STDERR "WARNING: Fixing whitespace errors failed.\n";
218     }
219   }
220 };
221
222 my $code = 0;
223
224 if ($@) {
225   print STDERR "Import failed with $@\n";
226   print STDERR "Attempting to reset back to where we were ...\n";
227   system("git reset --hard HEAD") == 0
228     or die "Unable to reset, sorry. You'll need to pick up the pieces\n";
229   $code = 1;
230
231
232 if ($stashed) {
233   system("git stash pop") == 0
234     or die "git stash pop failed with : $!\n";
235 }
236
237 exit $code;
238
239 __END__
240
241 =head1 NAME
242
243 import-external-git - Import bits of an external git repo to OpenAFS
244
245 =head1 SYNOPSIS
246
247 import-external-git [options] <module> <repository> [<commitish>]
248
249   Options
250     --help              brief help message
251     --man               full documentation
252     --externalDir       exact path to import into
253     --nofixwhitespace   don't apply whitespace fixes
254
255 =head1 DESCRIPTION
256
257 import-external-git imports selected files from an external git repository
258 into the OpenAFS src/external tree. For a given <module> it assumes that
259 src/external/<module>-files already exists, and contains a space separated
260 list of source and destination file names. <repository> should point to a
261 local clone of the external project's git repository, and <commitish> points
262 to an object within that tree. If <commitish> isn't specified, the current
263 branch HEAD of that repository is used.
264
265 =cut