2f2ca78d1b7c9ae91f1d1b4011e82ff173c95644
[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 eval {
138   # Use git-ls-files to get the list of currently committed files for the module
139   my $lspipe = IO::Pipe->new();
140   $lspipe->reader(qw(git ls-files));
141
142   my %filesInTree;
143   while(<$lspipe>) {
144     chomp;
145     $filesInTree{$_}++;
146   }
147
148   foreach my $source (sort keys(%mapping)) {
149     if (-f "$tempdir/source/$source") {
150       File::Path::make_path(File::Basename::dirname($mapping{$source}));
151       system("cp $tempdir/source/$source ".
152              "   $externalDir/$module/".$mapping{$source}) == 0
153          or die "Copy failed with $!\n";
154       system("git add $externalDir/$module/".$mapping{$source}) == 0
155          or die "git add failed with $!\n";
156       delete $filesInTree{$mapping{$source}}
157     } else {
158       die "Couldn't find file $source in original tree\n";
159     }
160   }
161
162   # Use git rm to delete everything that's committed that we don't have a
163   # relacement for.
164   foreach my $missing (keys(%filesInTree)) {
165     system("git rm $missing") == 0
166       or die "Couldn't git rm $missing : $!\n";
167   }
168
169   if (system("git status") == 0) {
170     my $fh=IO::File->new("$externalDir/$module-last", "w");
171     $fh->print($commitSha1."\n");
172     undef $fh;
173     system("git add $externalDir/$module-last") == 0
174        or die "Git add of last file failed with $!\n";
175
176     $fh=IO::File->new("$tempdir/commit-msg", "w")
177       or die "Unable to write commit message\n";
178     $fh->print("Import of code from $module\n");
179     $fh->print("\n");
180     $fh->print("This commit updates the code imported from $module to\n");
181     $fh->print("$commitSha1 ($commitDesc)\n");
182     if ($changes) {
183         $fh->print("\n");
184         $fh->print("Upstream changes are:\n\n");
185         $fh->print($changes);
186     }
187     undef $fh;
188     $author="--author '$author'" if ($author);
189     system("git commit -F $tempdir/commit-msg $author") == 0
190       or die "Commit failed : $!\n";
191   }
192 };
193
194 my $code = 0;
195
196 if ($@) {
197   print STDERR "Import failed with $@\n";
198   print STDERR "Attempting to reset back to where we were ...\n";
199   system("git reset --hard HEAD") == 0
200     or die "Unable to reset, sorry. You'll need to pick up the pieces\n";
201   $code = 1;
202
203
204 if ($stashed) {
205   system("git stash pop") == 0
206     or die "git stash pop failed with : $!\n";
207 }
208
209 exit $code;
210
211 __END__
212
213 =head1 NAME
214
215 import-external-git - Import bits of an external git repo to OpenAFS
216
217 =head1 SYNOPSIS
218
219 import-external-git [options] <module> <repository> [<commitish>]
220
221   Options
222     --help              brief help message
223     --man               full documentation
224     --externalDir       exact path to import into
225
226 =head1 DESCRIPTION
227
228 import-external-git imports selected files from an external git repository
229 into the OpenAFS src/external tree. For a given <module> it assumes that
230 src/external/<module>-files already exists, and contains a space separated
231 list of source and destination file names. <repository> should point to a
232 local clone of the external project's git repository, and <commitish> points
233 to an object within that tree. If <commitish> isn't specified, the current
234 branch HEAD of that repository is used.
235
236 =cut