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.107';
3 2     2   118873 use strict;
  2         4  
  2         49  
4 2     2   6 use warnings;
  2         2  
  2         49  
5 2     2   6 use Cwd qw( cwd );
  2         2  
  2         69  
6 2     2   8 use Carp;
  2         1  
  2         103  
7 2     2   8 use Scalar::Util qw( blessed );
  2         1  
  2         77  
8 2     2   6 use List::Util qw( first );
  2         2  
  2         115  
9 2     2   8 use File::Basename qw( basename );
  2         2  
  2         113  
10 2     2   671 use Git::FastExport;
  2         3  
  2         2994  
11              
12             sub new {
13 48     48 1 77016627 my ( $class, $options, @args ) = @_;
14              
15             # create the object
16 48         575 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         118 for my $key (qw( select )) {
33 48 100       247 $self->{$key} = $options->{$key} if exists $options->{$key};
34             }
35             croak "Invalid value for 'select' option: '$self->{select}'"
36 48 100       589 if $self->{select} !~ /^(?:first|last|random)$/;
37              
38             # process the remaining args
39 47         143 $self->stitch( splice @args, 0, 2 ) while @args;
40              
41 46         121 return $self;
42             }
43              
44             # add a new repo to stich in
45             sub stitch {
46 66     66 1 2248 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     498 : eval { # assume a path
52 45         52 my $r;
53 45         72212 my $orig = cwd;
54              
55             # chdir and create a Git::Repository object there
56 45 50       319 if ( defined $repo ) {
57 45 100       1102 chdir $repo or croak "Can't chdir to $repo: $!";
58 44         688 $r = Git::Repository->new();
59 44 50       1152321 chdir $orig or croak "Can't chdir back to $orig: $!";
60             }
61 0         0 else { die "Undefined repository path" }
62 44         111 $r;
63             };
64 66 100       354 $@ =~ s/ at .*\z//s, croak $@ if !$export;
65              
66             # do not stich a repo with itself
67 65         332 $repo = $export->git_dir;
68 65 100       717 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         1200 my @parts = File::Spec->splitdir( ( File::Spec->splitpath( $repo, 1 ) )[1] );
73 63         152 my $name = pop @parts;
74 63 50       268 $name = pop @parts if $name eq '.git';
75 63         128 $name =~ s/\.git$//;
76 63         137 $name =~ y/-A-Za-z0-9_/-/cs;
77 63         252 $name =~ s/^-|-$//g;
78 63 50       160 $dir = $name if not defined $dir;
79              
80             # check if the name is not used already and pick a replacement if it is
81 63 50       188 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       2648 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         382933 my $stream =
93             $export->command(qw( fast-export --progress=1 --all --date-order ))
94             ->stdout;
95              
96             # set up the internal structures
97 63         331616 $self->{repo}{$repo}{repo} = $repo;
98 63         314 $self->{repo}{$repo}{dir} = $dir;
99 63         129 $self->{repo}{$repo}{git} = $export;
100 63         594 $self->{repo}{$repo}{parser} = Git::FastExport->new($stream);
101 63         247 $self->{repo}{$repo}{name} = $name;
102 63         293 $self->{repo}{$repo}{block} = $self->{repo}{$repo}{parser}->next_block();
103 63         390 $self->_translate_block($repo);
104              
105 63         283 return $self;
106             }
107              
108             # return the next block in the stitched stream
109             sub next_block {
110 752     752 1 171688 my ($self) = @_;
111 752         777 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         1170 delete $repo->{$_} for grep { !defined $repo->{$_}{block} } keys %$repo;
  1390         2967  
116              
117             # no repo left, we're done
118 752 100       4166 return if ! keys %$repo;
119              
120             # return any non-commit block directly
121 720 100       2684 if ( my $next
122 1136     1136   1883 = first { $repo->{$_}{block}{type} ne 'commit' } keys %$repo )
123             {
124 456         482 my $block = $repo->{$next}{block};
125 456         924 $repo->{$next}{block} = $repo->{$next}{parser}->next_block();
126 456         807 $self->_translate_block( $next );
127 456         799 return $block;
128             }
129              
130             # select the oldest available commit
131 264         564 my ($next) = keys %$repo;
132             $next
133             = $repo->{$next}{block}{committer_date} < $repo->{$_}{block}{committer_date} ? $next : $_
134 264 100       1159 for keys %$repo;
135 264         311 my $commit = $repo->{$next}{block};
136              
137             # fetch the next block
138 264         531 $repo->{$next}{block} = $repo->{$next}{parser}->next_block();
139 264         375 $self->_translate_block( $next );
140              
141             # prepare the attachement algorithm
142 264         264 $repo = $repo->{$next};
143 264         249 my $commits = $self->{commits};
144              
145             # first commit in the old repo linked to latest commit in new repo
146 264 100 100     873 if ( $self->{last} && !$commit->{from} ) {
147 30         81 $commit->{from} = ["from :$self->{last}"];
148             }
149              
150             # update historical information
151 264         866 my ($id) = $commit->{mark}[0] =~ /:(\d+)/g;
152 264         390 $self->{last} = $id; # last commit applied
153 264         621 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         1571 };
162              
163             # mark our original source
164 264         1087 $commit->{header} =~ s/$/-$repo->{name}/;
165              
166             # this commit's parents
167 274 100       946 my @parents = map {/:(\d+)/g} @{ $commit->{from} || [] },
  264         648  
168 264 100       247 @{ $commit->{merge} || [] };
  264         782  
169              
170             # get the reference parent list used by _last_alien_child()
171 264         312 my $parents = {};
172 264         350 for my $parent (@parents) {
173 274 100       486 if ( $commits->{$parent}{repo} eq $node->{repo} ) {
174 244         178 push @{ $parents->{ $node->{repo} } }, $parent;
  244         653  
175             }
176             else { # record the parents from the other repositories
177 30         47 for my $repo ( grep $_ ne $node->{repo},
178 30         121 keys %{ $commits->{$parent}{parents} } )
179             {
180 8         17 push @{ $parents->{$repo} },
181 8 50       13 @{ $commits->{$parent}{parents}{$repo} || [] };
  8         32  
182             }
183             }
184             }
185              
186             # map each parent to its last "alien" commit
187             my %parent_map = map {
188 264         317 $_ => $self->_last_alien_child( $commits->{$_}, $ref, $parents )->{name}
189 274         499 } @parents;
190              
191             # map parent marks
192 264 100       251 for ( @{ $commit->{from} || [] }, @{ $commit->{merge} || [] } ) {
  264 100       777  
  264         764  
193 274         1287 s/:(\d+)/:$parent_map{$1}/g;
194             }
195              
196             # update the parents information
197 264         332 for my $parent ( map { $commits->{ $parent_map{$_} } } @parents ) {
  274         485  
198 274         198 push @{ $parent->{children} }, $node->{name};
  274         365  
199 274         181 push @{ $node->{parents}{ $parent->{repo} } }, $parent->{name};
  274         643  
200             }
201              
202             # dump the commit
203 264         756 return $commit;
204             }
205              
206             sub _translate_block {
207 783     783   788 my ( $self, $repo ) = @_;
208 783         700 my $mark_map = $self->{mark_map};
209 783         731 my $block = $self->{repo}{$repo}{block};
210              
211             # nothing to do
212 783 100       1120 return if !defined $block;
213              
214             # mark our original source
215             $block->{header} =~ s/$/-$self->{repo}{$repo}{name}/
216 721 100       2634 if $block->{type} =~ /^(?:reset|tag)$/;
217              
218             # map to the new mark
219 721 100       574 for ( @{ $block->{mark} || [] } ) {
  721         2560  
220 264         1269 s/:(\d+)/:$self->{mark}/;
221 264         1039 $mark_map->{$repo}{$1} = $self->{mark}++;
222             }
223              
224             # update marks in from & merge
225 721 100       593 for ( @{ $block->{from} || [] }, @{ $block->{merge} || [] } ) {
  721 100       1870  
  721         2256  
226 312         1293 s/:(\d+)/:$mark_map->{$repo}{$1}/g;
227             }
228              
229             # update marks & dir in files
230 721         679 for ( @{ $block->{files} } ) {
  721         1720  
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   269 my ( $self, $node, $ref, $parents ) = @_;
253 274         240 my $commits = $self->{commits};
254              
255 274         224 my $from = $node->{name};
256 274         195 my $repo = $node->{repo};
257              
258 274         183 while (1) {
259              
260             # no children nodes
261 457 100       271 return $node if ( !@{ $node->{children} } );
  457         1234  
262              
263             # some children nodes are local
264             return $node
265 235 100       218 if grep { $commits->{$_}{repo} eq $repo } @{ $node->{children} };
  295         846  
  235         257  
266              
267             # all children are alien to us
268 183         145 my @valid;
269 183         136 for my $id ( @{ $node->{children} } ) {
  183         255  
270              
271 237         248 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         180 my %pparents;
276 237 100       174 @{pparents}{ @{ $peer->{parents}{ $peer->{repo} } || [] } } = ();
  237         810  
277             next
278             if grep !exists $pparents{$_},
279 237 50       188 @{ $parents->{ $peer->{repo} } };
  237         508  
280              
281             # this child node has a valid parent list
282 237         379 push @valid, $id;
283             }
284              
285             # compute the commit to attach to, using the requested algorithm
286 183 50       295 if (@valid) {
287             my $node_id = $self->{cache}{"$from $node->{name}"} ||=
288             $self->{select} eq 'last' ? $valid[-1]
289 183 50 66     946 : $self->{select} eq 'first' ? $valid[0]
    100          
290             : $valid[ rand @valid ];
291 183         253 $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__