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