File Coverage

blib/lib/Git/FastExport/Stitch.pm
Criterion Covered Total %
statement 157 162 96.9
branch 59 70 84.2
condition 8 12 66.6
subroutine 14 14 100.0
pod 3 3 100.0
total 241 261 92.3


line stmt bran cond sub pod time code
1             package Git::FastExport::Stitch;
2             $Git::FastExport::Stitch::VERSION = '0.106';
3 3     3   90958 use strict;
  3         8  
  3         89  
4 3     3   12 use warnings;
  3         6  
  3         94  
5 3     3   13 use Cwd qw( cwd );
  3         3  
  3         178  
6 3     3   14 use Carp;
  3         5  
  3         174  
7 3     3   13 use Scalar::Util qw( blessed );
  3         5  
  3         143  
8 3     3   13 use List::Util qw( first );
  3         4  
  3         190  
9 3     3   14 use File::Basename qw( basename );
  3         3  
  3         198  
10 3     3   875 use Git::FastExport;
  3         5  
  3         7444  
11              
12             sub new {
13 46     46 1 48662220 my ( $class, $options, @args ) = @_;
14              
15             # create the object
16 46         725 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 46         252 for my $key (qw( select )) {
33 46 100       269 $self->{$key} = $options->{$key} if exists $options->{$key};
34             }
35             croak "Invalid value for 'select' option: '$self->{select}'"
36 46 100       813 if $self->{select} !~ /^(?:first|last|random)$/;
37              
38             # process the remaining args
39 45         180 $self->stitch( splice @args, 0, 2 ) while @args;
40              
41 44         162 return $self;
42             }
43              
44             # add a new repo to stich in
45             sub stitch {
46 62     62 1 3268 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 62 100 66     717 : eval { # assume a path
52 42         85 my $r;
53 42         160640 my $orig = cwd;
54              
55             # chdir and create a Git::Repository object there
56 42 50       310 if ( defined $repo ) {
57 42 100       1939 chdir $repo or croak "Can't chdir to $repo: $!";
58 41         1220 $r = Git::Repository->new();
59 41 50       1565407 chdir $orig or croak "Can't chdir back to $orig: $!";
60             }
61 0         0 else { die "Undefined repository path" }
62 41         152 $r;
63             };
64 62 100       392 $@ =~ s/ at .*\z//s, croak $@ if !$export;
65              
66             # do not stich a repo with itself
67 61         511 $repo = $export->git_dir;
68 61 100       1068 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 59         2043 my @parts = File::Spec->splitdir( ( File::Spec->splitpath( $repo, 1 ) )[1] );
73 59         212 my $name = pop @parts;
74 59 50       239 $name = pop @parts if $name eq '.git';
75 59         132 $name =~ s/\.git$//;
76 59         263 $name =~ y/-A-Za-z0-9_/-/cs;
77 59         693 $name =~ s/^-|-$//g;
78 59 50       224 $dir = $name if not defined $dir;
79              
80             # check if the name is not used already and pick a replacement if it is
81 59 50       241 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             # initiate the Git::FastExport stream
88 59         581 my $stream =
89             $export->command(qw( fast-export --progress=1 --all --date-order ))
90             ->stdout;
91              
92             # set up the internal structures
93 59         574277 $self->{repo}{$repo}{repo} = $repo;
94 59         384 $self->{repo}{$repo}{dir} = $dir;
95 59         250 $self->{repo}{$repo}{git} = $export;
96 59         1126 $self->{repo}{$repo}{parser} = Git::FastExport->new($stream);
97 59         316 $self->{repo}{$repo}{name} = $name;
98 59         528 $self->{repo}{$repo}{block} = $self->{repo}{$repo}{parser}->next_block();
99 59         497 $self->_translate_block($repo);
100              
101 59         573 return $self;
102             }
103              
104             # return the next block in the stitched stream
105             sub next_block {
106 1134     1134 1 324810 my ($self) = @_;
107 1134         1600 my $repo = $self->{repo};
108              
109             # keep a list of next blocks (per repo)
110             # any undef block means the stream is finished
111 1134         2706 delete $repo->{$_} for grep { !defined $repo->{$_}{block} } keys %$repo;
  2170         7128  
112              
113             # no repo left, we're done
114 1134 100       16128 return if ! keys %$repo;
115              
116             # return any non-commit block directly
117 1104 100       6736 if ( my $next
118 1724     1724   8794 = first { $repo->{$_}{block}{type} ne 'commit' } keys %$repo )
119             {
120 854         1003 my $block = $repo->{$next}{block};
121 854         4327 $repo->{$next}{block} = $repo->{$next}{parser}->next_block();
122 854         2210 $self->_translate_block( $next );
123 854         2434 return $block;
124             }
125              
126             # select the oldest available commit
127 250         782 my ($next) = keys %$repo;
128             $next
129             = $repo->{$next}{block}{committer_date} < $repo->{$_}{block}{committer_date} ? $next : $_
130 250 100       1746 for keys %$repo;
131 250         409 my $commit = $repo->{$next}{block};
132              
133             # fetch the next block
134 250         854 $repo->{$next}{block} = $repo->{$next}{parser}->next_block();
135 250         765 $self->_translate_block( $next );
136              
137             # prepare the attachement algorithm
138 250         326 $repo = $repo->{$next};
139 250         311 my $commits = $self->{commits};
140              
141             # first commit in the old repo linked to latest commit in new repo
142 250 100 100     1136 if ( $self->{last} && !$commit->{from} ) {
143 28         135 $commit->{from} = ["from :$self->{last}"];
144             }
145              
146             # update historical information
147 250         1528 my ($id) = $commit->{mark}[0] =~ /:(\d+)/g;
148 250         486 $self->{last} = $id; # last commit applied
149 250         809 my $ref = ( split / /, $commit->{header} )[1];
150             my $node = $commits->{$id} = {
151             name => $id,
152             repo => $repo->{repo},
153             ref => $ref,
154             children => [],
155             parents => {},
156             merge => exists $commit->{merge},
157 250         2615 };
158              
159             # mark our original source
160 250         1667 $commit->{header} =~ s/$/-$repo->{name}/;
161              
162             # this commit's parents
163 262 100       1335 my @parents = map {/:(\d+)/g} @{ $commit->{from} || [] },
  250         803  
164 250 100       329 @{ $commit->{merge} || [] };
  250         1174  
165              
166             # get the reference parent list used by _last_alien_child()
167 250         459 my $parents = {};
168 250         541 for my $parent (@parents) {
169 262 100       713 if ( $commits->{$parent}{repo} eq $node->{repo} ) {
170 234         218 push @{ $parents->{ $node->{repo} } }, $parent;
  234         970  
171             }
172             else { # record the parents from the other repositories
173 28         111 for my $repo ( grep $_ ne $node->{repo},
174 28         178 keys %{ $commits->{$parent}{parents} } )
175             {
176 8         25 push @{ $parents->{$repo} },
177 8 50       30 @{ $commits->{$parent}{parents}{$repo} || [] };
  8         60  
178             }
179             }
180             }
181              
182             # map each parent to its last "alien" commit
183             my %parent_map = map {
184 250         361 $_ => $self->_last_alien_child( $commits->{$_}, $ref, $parents )->{name}
185 262         719 } @parents;
186              
187             # map parent marks
188 250 100       300 for ( @{ $commit->{from} || [] }, @{ $commit->{merge} || [] } ) {
  250 100       729  
  250         1199  
189 262         2213 s/:(\d+)/:$parent_map{$1}/g;
190             }
191              
192             # update the parents information
193 250         479 for my $parent ( map { $commits->{ $parent_map{$_} } } @parents ) {
  262         955  
194 262         361 push @{ $parent->{children} }, $node->{name};
  262         614  
195 262         457 push @{ $node->{parents}{ $parent->{repo} } }, $parent->{name};
  262         974  
196             }
197              
198             # dump the commit
199 250         1280 return $commit;
200             }
201              
202             sub _translate_block {
203 1163     1163   2871 my ( $self, $repo ) = @_;
204 1163         2569 my $mark_map = $self->{mark_map};
205 1163         1978 my $block = $self->{repo}{$repo}{block};
206              
207             # nothing to do
208 1163 100       2571 return if !defined $block;
209              
210             # mark our original source
211             $block->{header} =~ s/$/-$self->{repo}{$repo}{name}/
212 1105 100       4758 if $block->{type} =~ /^(?:reset|tag)$/;
213              
214             # map to the new mark
215 1105 100       1281 for ( @{ $block->{mark} || [] } ) {
  1105         6181  
216 463         3985 s/:(\d+)/:$self->{mark}/;
217 463         3048 $mark_map->{$repo}{$1} = $self->{mark}++;
218             }
219              
220             # update marks in from & merge
221 1105 100       1195 for ( @{ $block->{from} || [] }, @{ $block->{merge} || [] } ) {
  1105 100       5184  
  1105         4448  
222 298         2078 s/:(\d+)/:$mark_map->{$repo}{$1}/g;
223             }
224              
225             # update marks & dir in files
226 1105         1214 for ( @{ $block->{files} } ) {
  1105         3715  
227 212         1858 s/^M (\d+) :(\d+)/M $1 :$mark_map->{$repo}{$2}/;
228 212         426 my $dir = $self->{repo}{$repo}{dir};
229 212 50 33     1210 if ( defined $dir && $dir ne '' ) {
230 212         2049 s!^(M \d+ :\d+) (\"?)(.*)!$1 $2$dir/$3!; # filemodify
231 212         326 s!^D (\"?)(.*)!D $1$dir/$2!; # filedelete
232              
233             # /!\ quotes may happen - die and fix if needed
234 212 50       692 die "Choked on quoted paths in $repo! Culprit:\n$_\n"
235             if /^[CR] \S+ \S+ /;
236              
237             # filecopy | filerename
238 212         895 s!^([CR]) (\"?)(\S+) (\"?)(\S+)!$1 $2$dir/$3 $4$dir/$5!;
239             }
240             }
241             }
242              
243             # find the last child of this node
244             # that has either no child
245             # or a child in our repo
246             # or an alien child that has the same parent list
247             sub _last_alien_child {
248 262     262   379 my ( $self, $node, $ref, $parents ) = @_;
249 262         379 my $commits = $self->{commits};
250              
251 262         382 my $from = $node->{name};
252 262         294 my $repo = $node->{repo};
253              
254 262         269 while (1) {
255              
256             # no children nodes
257 437 100       541 return $node if ( !@{ $node->{children} } );
  437         2016  
258              
259             # some children nodes are local
260             return $node
261 225 100       266 if grep { $commits->{$_}{repo} eq $repo } @{ $node->{children} };
  285         1313  
  225         397  
262              
263             # all children are alien to us
264 175         196 my @valid;
265 175         170 for my $id ( @{ $node->{children} } ) {
  175         394  
266              
267 229         338 my $peer = $commits->{$id};
268              
269             # parents of $peer in $peer's repo contains
270             # all parents from $parent in $peer's repo
271 229         210 my %pparents;
272 229 100       235 @{pparents}{ @{ $peer->{parents}{ $peer->{repo} } || [] } } = ();
  229         1099  
273             next
274             if grep !exists $pparents{$_},
275 229 50       360 @{ $parents->{ $peer->{repo} } };
  229         783  
276              
277             # this child node has a valid parent list
278 229         583 push @valid, $id;
279             }
280              
281             # compute the commit to attach to, using the requested algorithm
282 175 50       379 if (@valid) {
283             my $node_id = $self->{cache}{"$from $node->{name}"} ||=
284             $self->{select} eq 'last' ? $valid[-1]
285 175 50 66     1433 : $self->{select} eq 'first' ? $valid[0]
    100          
286             : $valid[ rand @valid ];
287 175         365 $node = $commits->{$node_id};
288             }
289             }
290              
291             # return last valid child
292 0           return $node;
293             }
294              
295             'progress 1 objects';
296              
297             __END__