File Coverage

blib/lib/Git/FastExport/Stitch.pm
Criterion Covered Total %
statement 151 164 92.0
branch 58 72 80.5
condition 7 12 58.3
subroutine 14 14 100.0
pod 3 3 100.0
total 233 265 87.9


line stmt bran cond sub pod time code
1             package Git::FastExport::Stitch;
2             $Git::FastExport::Stitch::VERSION = '0.108';
3 2     2   259998 use strict;
  2         12  
  2         58  
4 2     2   11 use warnings;
  2         3  
  2         55  
5 2     2   11 use Cwd qw( cwd );
  2         4  
  2         89  
6 2     2   11 use Carp;
  2         3  
  2         110  
7 2     2   14 use Scalar::Util qw( blessed );
  2         4  
  2         102  
8 2     2   12 use List::Util qw( first );
  2         4  
  2         123  
9 2     2   14 use File::Basename qw( basename );
  2         4  
  2         191  
10 2     2   844 use Git::FastExport;
  2         6  
  2         4015  
11              
12             sub new {
13 48     48 1 9713813 my ( $class, $options, @args ) = @_;
14              
15             # create the object
16 48         1230 my $self = bless {
17              
18             # internal structures
19             mark => 1_000_000, # mark counter in the new repo
20             mark_map => {},
21             commits => {},
22             repo => {},
23             name => {},
24             cache => {},
25              
26             # default options
27             select => 'last',
28              
29             }, $class;
30              
31             # set the options
32 48         398 for my $key (qw( select )) {
33 48 100       326 $self->{$key} = $options->{$key} if exists $options->{$key};
34             }
35             croak "Invalid value for 'select' option: '$self->{select}'"
36 48 100       976 if $self->{select} !~ /^(?:first|last|random)$/;
37              
38             # process the remaining args
39 47         242 $self->stitch( splice @args, 0, 2 ) while @args;
40              
41 46         181 return $self;
42             }
43              
44             # add a new repo to stich in
45             sub stitch {
46 66     66 1 4730 my ( $self, $repo, $dir ) = @_;
47              
48             # $repo is either a Git::Repository object or a valid path
49             my $export = blessed($repo) && $repo->isa('Git::Repository')
50             ? $repo # a Git::Repository object
51 66 100 66     944 : eval { # assume a path
52 45         114 my $r;
53 45         115035 my $orig = cwd;
54              
55             # chdir and create a Git::Repository object there
56 45 50       685 if ( defined $repo ) {
57 45 100       1420 chdir $repo or croak "Can't chdir to $repo: $!";
58 44         2073 $r = Git::Repository->new();
59 44 50       1987128 chdir $orig or croak "Can't chdir back to $orig: $!";
60             }
61 0         0 else { die "Undefined repository path" }
62 44         248 $r;
63             };
64 66 100       501 $@ =~ s/ at .*\z//s, croak $@ if !$export;
65              
66             # do not stich a repo with itself
67 65         934 $repo = $export->git_dir;
68 65 100       1351 croak "Already stitching repository $repo" if exists $self->{repo}{$repo};
69              
70             # pick the refs suffix:
71             # use base directory without the .git extension or non ASCII characters
72 63         2375 my @parts = File::Spec->splitdir( ( File::Spec->splitpath( $repo, 1 ) )[1] );
73 63         381 my $name = pop @parts;
74 63 50       291 $name = pop @parts if $name eq '.git';
75 63         217 $name =~ s/\.git$//;
76 63 50       452 $dir = $name if not defined $dir;
77 63         289 $name =~ y/-A-Za-z0-9_/-/cs;
78 63         669 $name =~ s/^-|-$//g;
79              
80             # check if the name is not used already and pick a replacement if it is
81 63 50       354 if ( exists $self->{name}{$name} ) {
82 0         0 my $suffix = "A";
83 0         0 $suffix++ while ( exists $self->{name}{"$name-$suffix"} );
84 0         0 $name .= "-$suffix";
85             }
86              
87             # git fast-export appeared in git 1.5.4
88 63 50       5268 croak "stitch() requires a git version greater or equal to 1.5.4, this is only version ${\$export->version}"
  0         0  
89             if $export->version_lt('1.5.4');
90              
91             # initiate the Git::FastExport stream
92 63         671799 my $stream =
93             $export->command(qw( fast-export --progress=1 --all --date-order ))
94             ->stdout;
95              
96             # set up the internal structures
97 63         627355 $self->{repo}{$repo}{repo} = $repo;
98 63         664 $self->{repo}{$repo}{dir} = $dir;
99 63         699 $self->{repo}{$repo}{git} = $export;
100 63         1559 $self->{repo}{$repo}{parser} = Git::FastExport->new($stream);
101 63         579 $self->{repo}{$repo}{name} = $name;
102 63         654 $self->{repo}{$repo}{block} = $self->{repo}{$repo}{parser}->next_block();
103 63         1185 $self->_translate_block($repo);
104              
105 63         646 return $self;
106             }
107              
108             # return the next block in the stitched stream
109             sub next_block {
110 752     752 1 299091 my ($self) = @_;
111 752         1503 my $repo = $self->{repo};
112              
113             # keep a list of next blocks (per repo)
114             # any undef block means the stream is finished
115 752         2053 delete $repo->{$_} for grep { !defined $repo->{$_}{block} } keys %$repo;
  1390         4940  
116              
117             # no repo left, we're done
118 752 100       8147 return if ! keys %$repo;
119              
120             # return any non-commit block directly
121 720 100       4350 if ( my $next
122 1153     1153   3144 = first { $repo->{$_}{block}{type} ne 'commit' } keys %$repo )
123             {
124 456         751 my $block = $repo->{$next}{block};
125 456         1551 $repo->{$next}{block} = $repo->{$next}{parser}->next_block();
126 456         1509 $self->_translate_block( $next );
127 456         1479 return $block;
128             }
129              
130             # select the oldest available commit
131 264         841 my ($next) = keys %$repo;
132             $next
133             = $repo->{$next}{block}{committer_date} < $repo->{$_}{block}{committer_date} ? $next : $_
134 264 100       1465 for keys %$repo;
135 264         515 my $commit = $repo->{$next}{block};
136              
137             # fetch the next block
138 264         713 $repo->{$next}{block} = $repo->{$next}{parser}->next_block();
139 264         676 $self->_translate_block( $next );
140              
141             # prepare the attachement algorithm
142 264         496 $repo = $repo->{$next};
143 264         424 my $commits = $self->{commits};
144              
145             # first commit in the old repo linked to latest commit in new repo
146 264 100 100     1244 if ( $self->{last} && !$commit->{from} ) {
147 30         177 $commit->{from} = ["from :$self->{last}"];
148             }
149              
150             # update historical information
151 264         1339 my ($id) = $commit->{mark}[0] =~ /:(\d+)/g;
152 264         792 $self->{last} = $id; # last commit applied
153 264         906 my $ref = ( split / /, $commit->{header} )[1];
154             my $node = $commits->{$id} = {
155             name => $id,
156             repo => $repo->{repo},
157             ref => $ref,
158             children => [],
159             parents => {},
160             merge => exists $commit->{merge},
161 264         3210 };
162              
163             # mark our original source
164 264         1835 $commit->{header} =~ s/$/-$repo->{name}/;
165              
166             # this commit's parents
167 274 100       1307 my @parents = map {/:(\d+)/g} @{ $commit->{from} || [] },
  264         926  
168 264 100       539 @{ $commit->{merge} || [] };
  264         1213  
169              
170             # get the reference parent list used by _last_alien_child()
171 264         626 my $parents = {};
172 264         506 for my $parent (@parents) {
173 274 100       707 if ( $commits->{$parent}{repo} eq $node->{repo} ) {
174 244         341 push @{ $parents->{ $node->{repo} } }, $parent;
  244         808  
175             }
176             else { # record the parents from the other repositories
177 30         75 for my $repo ( grep $_ ne $node->{repo},
178 30         166 keys %{ $commits->{$parent}{parents} } )
179             {
180 8         23 push @{ $parents->{$repo} },
181 8 50       19 @{ $commits->{$parent}{parents}{$repo} || [] };
  8         40  
182             }
183             }
184             }
185              
186             # map each parent to its last "alien" commit
187             my %parent_map = map {
188 264         444 $_ => $self->_last_alien_child( $commits->{$_}, $ref, $parents )->{name}
189 274         686 } @parents;
190              
191             # map parent marks
192 264 100       465 for ( @{ $commit->{from} || [] }, @{ $commit->{merge} || [] } ) {
  264 100       893  
  264         1043  
193 274         1781 s/:(\d+)/:$parent_map{$1}/g;
194             }
195              
196             # update the parents information
197 264         669 for my $parent ( map { $commits->{ $parent_map{$_} } } @parents ) {
  274         679  
198 274         421 push @{ $parent->{children} }, $node->{name};
  274         594  
199 274         413 push @{ $node->{parents}{ $parent->{repo} } }, $parent->{name};
  274         863  
200             }
201              
202             # dump the commit
203 264         1092 return $commit;
204             }
205              
206             sub _translate_block {
207 783     783   1590 my ( $self, $repo ) = @_;
208 783         1605 my $mark_map = $self->{mark_map};
209 783         1349 my $block = $self->{repo}{$repo}{block};
210              
211             # nothing to do
212 783 100       1729 return if !defined $block;
213              
214             # mark our original source
215             $block->{header} =~ s/$/-$self->{repo}{$repo}{name}/
216 721 100       4268 if $block->{type} =~ /^(?:reset|tag)$/;
217              
218             # map to the new mark
219 721 100       1209 for ( @{ $block->{mark} || [] } ) {
  721         3346  
220 264         1718 s/:(\d+)/:$self->{mark}/;
221 264         2004 $mark_map->{$repo}{$1} = $self->{mark}++;
222             }
223              
224             # update marks in from & merge
225 721 100       1390 for ( @{ $block->{from} || [] }, @{ $block->{merge} || [] } ) {
  721 100       2349  
  721         2525  
226 312         1807 s/:(\d+)/:$mark_map->{$repo}{$1}/g;
227             }
228              
229             # update marks & dir in files
230 721         1167 for ( @{ $block->{files} } ) {
  721         2587  
231 0         0 s/^M (\d+) :(\d+)/M $1 :$mark_map->{$repo}{$2}/;
232 0         0 my $dir = $self->{repo}{$repo}{dir};
233 0 0 0     0 if ( defined $dir && $dir ne '' ) {
234 0         0 s!^(M \d+ :\d+) (\"?)(.*)!$1 $2$dir/$3!; # filemodify
235 0         0 s!^D (\"?)(.*)!D $1$dir/$2!; # filedelete
236              
237             # /!\ quotes may happen - die and fix if needed
238 0 0       0 die "Choked on quoted paths in $repo! Culprit:\n$_\n"
239             if /^[CR] \S+ \S+ /;
240              
241             # filecopy | filerename
242 0         0 s!^([CR]) (\"?)(\S+) (\"?)(\S+)!$1 $2$dir/$3 $4$dir/$5!;
243             }
244             }
245             }
246              
247             # find the last child of this node
248             # that has either no child
249             # or a child in our repo
250             # or an alien child that has the same parent list
251             sub _last_alien_child {
252 274     274   632 my ( $self, $node, $ref, $parents ) = @_;
253 274         471 my $commits = $self->{commits};
254              
255 274         448 my $from = $node->{name};
256 274         406 my $repo = $node->{repo};
257              
258 274         371 while (1) {
259              
260             # no children nodes
261 457 100       578 return $node if ( !@{ $node->{children} } );
  457         1622  
262              
263             # some children nodes are local
264             return $node
265 235 100       401 if grep { $commits->{$_}{repo} eq $repo } @{ $node->{children} };
  295         1047  
  235         440  
266              
267             # all children are alien to us
268 183         292 my @valid;
269 183         261 for my $id ( @{ $node->{children} } ) {
  183         438  
270              
271 237         430 my $peer = $commits->{$id};
272              
273             # parents of $peer in $peer's repo contains
274             # all parents from $parent in $peer's repo
275 237         330 my %pparents;
276 237 100       334 @{pparents}{ @{ $peer->{parents}{ $peer->{repo} } || [] } } = ();
  237         1027  
277             next
278             if grep !exists $pparents{$_},
279 237 50       404 @{ $parents->{ $peer->{repo} } };
  237         632  
280              
281             # this child node has a valid parent list
282 237         634 push @valid, $id;
283             }
284              
285             # compute the commit to attach to, using the requested algorithm
286 183 50       413 if (@valid) {
287             my $node_id = $self->{cache}{"$from $node->{name}"} ||=
288             $self->{select} eq 'last' ? $valid[-1]
289 183 50 66     1649 : $self->{select} eq 'first' ? $valid[0]
    100          
290             : $valid[ rand @valid ];
291 183         440 $node = $commits->{$node_id};
292             }
293             }
294              
295             # return last valid child
296 0           return $node;
297             }
298              
299             'progress 1 objects';
300              
301             __END__