Merge branch 'mh/avoid-rewriting-packed-refs' into maint
[git] / contrib / fast-import / import-directories.perl
1 #!/usr/bin/perl
2 #
3 # Copyright 2008-2009 Peter Krefting <peter@softwolves.pp.se>
4 #
5 # ------------------------------------------------------------------------
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, see <http://www.gnu.org/licenses/>.
18 #
19 # ------------------------------------------------------------------------
20
21 =pod
22
23 =head1 NAME
24
25 import-directories - Import bits and pieces to Git.
26
27 =head1 SYNOPSIS
28
29 B<import-directories.perl> F<configfile> F<outputfile>
30
31 =head1 DESCRIPTION
32
33 Script to import arbitrary projects version controlled by the "copy the
34 source directory to a new location and edit it there"-version controlled
35 projects into version control. Handles projects with arbitrary branching
36 and version trees, taking a file describing the inputs and generating a
37 file compatible with the L<git-fast-import(1)> format.
38
39 =head1 CONFIGURATION FILE
40
41 =head2 Format
42
43 The configuration file is based on the standard I<.ini> format.
44
45  ; Comments start with semi-colons
46  [section]
47  key=value
48
49 Please see below for information on how to escape special characters.
50
51 =head2 Global configuration
52
53 Global configuration is done in the B<[config]> section, which should be
54 the first section in the file. Configuration can be changed by
55 repeating configuration sections later on.
56
57  [config]
58  ; configure conversion of CRLFs. "convert" means that all CRLFs
59  ; should be converted into LFs (suitable for the core.autocrlf
60  ; setting set to true in Git). "none" means that all data is
61  ; treated as binary.
62  crlf=convert
63
64 =head2 Revision configuration
65
66 Each revision that is to be imported is described in three
67 sections. Revisions should be defined in topological order, so
68 that a revision's parent has always been defined when a new revision
69 is introduced. All the sections for one revision must be defined
70 before defining the next revision.
71
72 Each revision is assigned a unique numerical identifier. The
73 numbers do not need to be consecutive, nor monotonically
74 increasing.
75
76 For instance, if your configuration file contains only the two
77 revisions 4711 and 42, where 4711 is the initial commit, the
78 only requirement is that 4711 is completely defined before 42.
79
80 =pod
81
82 =head3 Revision description section
83
84 A section whose section name is just an integer gives meta-data
85 about the revision.
86
87  [3]
88  ; author sets the author of the revisions
89  author=Peter Krefting <peter@softwolves.pp.se>
90  ; branch sets the branch that the revision should be committed to
91  branch=master
92  ; parent describes the revision that is the parent of this commit
93  ; (optional)
94  parent=1
95  ; merges describes a revision that is merged into this commit
96  ; (optional; can be repeated)
97  merges=2
98  ; selects one file to take the timestamp from
99  ; (optional; if unspecified, the most recent file from the .files
100  ;  section is used)
101  timestamp=3/source.c
102
103 =head3 Revision contents section
104
105 A section whose section name is an integer followed by B<.files>
106 describe all the files included in this revision. If a file that
107 was available previously is not included in this revision, it will
108 be removed.
109
110 If an on-disk revision is incomplete, you can point to files from
111 a previous revision. There are no restrictions on where the source
112 files are located, nor on their names.
113
114  [3.files]
115  ; the key is the path inside the repository, the value is the path
116  ; as seen from the importer script.
117  source.c=ver-3.00/source.c
118  source.h=ver-2.99/source.h
119  readme.txt=ver-3.00/introduction to the project.txt
120
121 File names are treated as byte strings (but please see below on
122 quoting rules), and should be stored in the configuration file in
123 the encoding that should be used in the generated repository.
124
125 =head3 Revision commit message section
126
127 A section whose section name is an integer followed by B<.message>
128 gives the commit message. This section is read verbatim, up until
129 the beginning of the next section. As such, a commit message may not
130 contain a line that begins with an opening square bracket ("[") and
131 ends with a closing square bracket ("]"), unless they are surrounded
132 by whitespace or other characters.
133
134  [3.message]
135  Implement foobar.
136  ; trailing blank lines are ignored.
137
138 =cut
139
140 # Globals
141 use strict;
142 use warnings;
143 use integer;
144 my $crlfmode = 0;
145 my @revs;
146 my (%revmap, %message, %files, %author, %branch, %parent, %merges, %time, %timesource);
147 my $sectiontype = 0;
148 my $rev = 0;
149 my $mark = 1;
150
151 # Check command line
152 if ($#ARGV < 1 || $ARGV[0] =~ /^--?h/)
153 {
154     exec('perldoc', $0);
155     exit 1;
156 }
157
158 # Open configuration
159 my $config = $ARGV[0];
160 open CFG, '<', $config or die "Cannot open configuration file \"$config\": ";
161
162 # Open output
163 my $output = $ARGV[1];
164 open OUT, '>', $output or die "Cannot create output file \"$output\": ";
165 binmode OUT;
166
167 LINE: while (my $line = <CFG>)
168 {
169         $line =~ s/\r?\n$//;
170         next LINE if $sectiontype != 4 && $line eq '';
171         next LINE if $line =~ /^;/;
172         my $oldsectiontype = $sectiontype;
173         my $oldrev = $rev;
174
175         # Sections
176         if ($line =~ m"^\[(config|(\d+)(|\.files|\.message))\]$")
177         {
178                 if ($1 eq 'config')
179                 {
180                         $sectiontype = 1;
181                 }
182                 elsif ($3 eq '')
183                 {
184                         $sectiontype = 2;
185                         $rev = $2;
186                         # Create a new revision
187                         die "Duplicate rev: $line\n " if defined $revmap{$rev};
188                         print "Reading revision $rev\n";
189                         push @revs, $rev;
190                         $revmap{$rev} = $mark ++;
191                         $time{$revmap{$rev}} = 0;
192                 }
193                 elsif ($3 eq '.files')
194                 {
195                         $sectiontype = 3;
196                         $rev = $2;
197                         die "Revision mismatch: $line\n " unless $rev == $oldrev;
198                 }
199                 elsif ($3 eq '.message')
200                 {
201                         $sectiontype = 4;
202                         $rev = $2;
203                         die "Revision mismatch: $line\n " unless $rev == $oldrev;
204                 }
205                 else
206                 {
207                         die "Internal parse error: $line\n ";
208                 }
209                 next LINE;
210         }
211
212         # Parse data
213         if ($sectiontype != 4)
214         {
215                 # Key and value
216                 if ($line =~ m"^\s*([^\s].*=.*[^\s])\s*$")
217                 {
218                         my ($key, $value) = &parsekeyvaluepair($1);
219                         # Global configuration
220                         if (1 == $sectiontype)
221                         {
222                                 if ($key eq 'crlf')
223                                 {
224                                         $crlfmode = 1, next LINE if $value eq 'convert';
225                                         $crlfmode = 0, next LINE if $value eq 'none';
226                                 }
227                                 die "Unknown configuration option: $line\n ";
228                         }
229                         # Revision specification
230                         if (2 == $sectiontype)
231                         {
232                                 my $current = $revmap{$rev};
233                                 $author{$current} = $value, next LINE if $key eq 'author';
234                                 $branch{$current} = $value, next LINE if $key eq 'branch';
235                                 $parent{$current} = $value, next LINE if $key eq 'parent';
236                                 $timesource{$current} = $value, next LINE if $key eq 'timestamp';
237                                 push(@{$merges{$current}}, $value), next LINE if $key eq 'merges';
238                                 die "Unknown revision option: $line\n ";
239                         }
240                         # Filespecs
241                         if (3 == $sectiontype)
242                         {
243                                 # Add the file and create a marker
244                                 die "File not found: $line\n " unless -f $value;
245                                 my $current = $revmap{$rev};
246                                 ${$files{$current}}{$key} = $mark;
247                                 my $time = &fileblob($value, $crlfmode, $mark ++);
248
249                                 # Update revision timestamp if more recent than other
250                                 # files seen, or if this is the file we have selected
251                                 # to take the time stamp from using the "timestamp"
252                                 # directive.
253                                 if ((defined $timesource{$current} && $timesource{$current} eq $value)
254                                     || $time > $time{$current})
255                                 {
256                                         $time{$current} = $time;
257                                 }
258                         }
259                 }
260                 else
261                 {
262                         die "Parse error: $line\n ";
263                 }
264         }
265         else
266         {
267                 # Commit message
268                 my $current = $revmap{$rev};
269                 if (defined $message{$current})
270                 {
271                         $message{$current} .= "\n";
272                 }
273                 $message{$current} .= $line;
274         }
275 }
276 close CFG;
277
278 # Start spewing out data for git-fast-import
279 foreach my $commit (@revs)
280 {
281         # Progress
282         print OUT "progress Creating revision $commit\n";
283
284         # Create commit header
285         my $mark = $revmap{$commit};
286
287         # Branch and commit id
288         print OUT "commit refs/heads/", $branch{$mark}, "\nmark :", $mark, "\n";
289
290         # Author and timestamp
291         die "No timestamp defined for $commit (no files?)\n" unless defined $time{$mark};
292         print OUT "committer ", $author{$mark}, " ", $time{$mark}, " +0100\n";
293
294         # Commit message
295         die "No message defined for $commit\n" unless defined $message{$mark};
296         my $message = $message{$mark};
297         $message =~ s/\n$//; # Kill trailing empty line
298         print OUT "data ", length($message), "\n", $message, "\n";
299
300         # Parent and any merges
301         print OUT "from :", $revmap{$parent{$mark}}, "\n" if defined $parent{$mark};
302         if (defined $merges{$mark})
303         {
304                 foreach my $merge (@{$merges{$mark}})
305                 {
306                         print OUT "merge :", $revmap{$merge}, "\n";
307                 }
308         }
309
310         # Output file marks
311         print OUT "deleteall\n"; # start from scratch
312         foreach my $file (sort keys %{$files{$mark}})
313         {
314                 print OUT "M 644 :", ${$files{$mark}}{$file}, " $file\n";
315         }
316         print OUT "\n";
317 }
318
319 # Create one file blob
320 sub fileblob
321 {
322         my ($filename, $crlfmode, $mark) = @_;
323
324         # Import the file
325         print OUT "progress Importing $filename\nblob\nmark :$mark\n";
326         open FILE, '<', $filename or die "Cannot read $filename\n ";
327         binmode FILE;
328         my ($size, $mtime) = (stat(FILE))[7,9];
329         my $file;
330         read FILE, $file, $size;
331         close FILE;
332         $file =~ s/\r\n/\n/g if $crlfmode;
333         print OUT "data ", length($file), "\n", $file, "\n";
334
335         return $mtime;
336 }
337
338 # Parse a key=value pair
339 sub parsekeyvaluepair
340 {
341 =pod
342
343 =head2 Escaping special characters
344
345 Key and value strings may be enclosed in quotes, in which case
346 whitespace inside the quotes is preserved. Additionally, an equal
347 sign may be included in the key by preceding it with a backslash.
348 For example:
349
350  "key1 "=value1
351  key2=" value2"
352  key\=3=value3
353  key4=value=4
354  "key5""=value5
355
356 Here the first key is "key1 " (note the trailing white-space) and the
357 second value is " value2" (note the leading white-space). The third
358 key contains an equal sign "key=3" and so does the fourth value, which
359 does not need to be escaped. The fifth key contains a trailing quote,
360 which does not need to be escaped since it is inside a surrounding
361 quote.
362
363 =cut
364         my $pair = shift;
365
366         # Separate key and value by the first non-quoted equal sign
367         my ($key, $value);
368         if ($pair =~ /^(.*[^\\])=(.*)$/)
369         {
370                 ($key, $value) = ($1, $2)
371         }
372         else
373         {
374                 die "Parse error: $pair\n ";
375         }
376
377         # Unquote and unescape the key and value separately
378         return (&unescape($key), &unescape($value));
379 }
380
381 # Unquote and unescape
382 sub unescape
383 {
384         my $string = shift;
385
386         # First remove enclosing quotes. Backslash before the trailing
387         # quote leaves both.
388         if ($string =~ /^"(.*[^\\])"$/)
389         {
390                 $string = $1;
391         }
392
393         # Second remove any backslashes inside the unquoted string.
394         # For later: Handle special sequences like \t ?
395         $string =~ s/\\(.)/$1/g;
396
397         return $string;
398 }
399
400 __END__
401
402 =pod
403
404 =head1 EXAMPLES
405
406 B<import-directories.perl> F<project.import>
407
408 =head1 AUTHOR
409
410 Copyright 2008-2009 Peter Krefting E<lt>peter@softwolves.pp.se>
411
412 This program is free software; you can redistribute it and/or modify
413 it under the terms of the GNU General Public License as published by
414 the Free Software Foundation.
415
416 =cut