chiark / gitweb /
Rename pwx to pwx_local as a backup.
[elogind.git] / pwx_local / migrate_tree.pl
1 #!/usr/bin/perl -w
2
3 # ================================================================
4 # ===        ==> --------     HISTORY      -------- <==        ===
5 # ================================================================
6 #
7 # Version  Date        Maintainer      Changes, Additions, Fixes
8 # 0.0.1    2018-05-02  sed, PrydeWorX  First basic design.
9 # 0.0.2    2018-05-07  sed, PrydeWorX  Work flow integrated up to creating the formatted patches.
10 # 0.0.3    2018-05-13  sed, PrydeWorX  Reworking of the formatted patches added.
11 # 0.1.0    2018-05-14  sed, PrydeWorX  Application of the reworked patches added.
12 # 0.2.0    2018-05-15  sed, PrydeWorX  First working version.
13 # 0.2.1                                Fixed usage of Try::Tiny.
14 # 0.2.2    2018-05-16  sed, PrydeWorX  Made sure that the commit file is always written on exit,
15 #                                        but only if a potential commits file was finished reading.
16 #
17 # ========================
18 # === Little TODO list ===
19 # ========================
20 #
21 use strict;
22 use warnings;
23 use Cwd qw(getcwd abs_path);
24 use File::Basename;
25 use File::Find;
26 use Git::Wrapper;
27 use Readonly;
28 use Try::Tiny;
29
30 # ================================================================
31 # ===        ==> ------ Help Text and Version ----- <==        ===
32 # ================================================================
33 Readonly my $VERSION     => "0.2.2"; # Please keep this current!
34 Readonly my $VERSMIN     => "-" x length($VERSION);
35 Readonly my $PROGDIR     => dirname($0);
36 Readonly my $PROGNAME    => basename($0);
37 Readonly my $WORKDIR     => getcwd();
38 Readonly my $CHECK_TREE  => abs_path( $PROGDIR . "/check_tree.pl" );
39 Readonly my $COMMIT_FILE => abs_path( $PROGDIR . "/last_mutual_commits.csv" );
40 Readonly my $USAGE_SHORT => "$PROGNAME <--help|[OPTIONS] <upstream path> <refid>>";
41 Readonly my $USAGE_LONG  => qq#
42 elogind git tree migration V$VERSION
43 ----------------------------$VERSMIN
44
45   Reset the git tree in <upstream path> to the <refid>. The latter can be any
46   commit, branch or tag. Then search its history since the last mutual commit
47   for any commit that touches at least one file in any subdirectory of the
48   directory this script was called from.
49   
50   Please note that this program was written especially for elogind. It is very
51   unlikely that it can be used in any other project.
52
53 USAGE:
54   $USAGE_SHORT
55
56 OPTIONS:
57      --advance       : Use the last upstream commit that has been written
58                        into "$COMMIT_FILE" as the last
59                        mutual commit to use. This is useful for continued
60                        migration of branches. Incompatible with -c|--commit.
61   -c|--commit <hash> : The mutual commit to use. If this option is not used,
62                        the script looks into "$COMMIT_FILE"
63                        and uses the commit noted for <refid>. Incompatible
64                        with --advance.
65   -h|--help            Show this help and exit.
66   -o|--output <path> : Path to where to write the patches. The default is to
67                        write into "$PROGDIR/patches".
68
69 Notes:
70   - The upstream tree is reset and put back into the current state after the
71     script finishes.
72   - When the script succeeds, it adds a line to "$COMMIT_FILE"
73     of the form:
74     <tag>-last <newest found commit>. You can use that line for the next
75     <refid> you wish to migrate to.
76 #;
77
78 # ================================================================
79 # ===        ==> -------- Global variables -------- <==        ===
80 # ================================================================
81
82 my $commit_count   = 0;   # It is easiest to count the relevant commits globally.
83 my $commits_read   = 0;   # Set to one once the commit file is completely read.
84 my $do_advance     = 0;   # If set by --advance, use src-<hash> as last commit.
85 my %hSrcCommits    = ();  # Record here which patch file is which commit.
86 my %hDirectories   = ();  # Filled when searching relevant files, used to validate new files.
87 my @lCommits       = ();  # List of all relevant commits that have been found, in topological order.
88 my @lCreated       = ();  # List of all files that were created using the migrated commits.
89 my @lPatches       = ();  # List of the formatted patches build from @lCommits.
90 my $main_result    = 1;   # Used for parse_args() only, as simple $result is local everywhere.
91 my $mutual_commit  = "";  # The last mutual commit to use. Will be read from csv if not set by args.
92 my $output_path    = "";
93 my $previous_refid = "";  # Store current upstream state, so we can revert afterwards.
94 my $prg_line       = "";  # Current line when showing progress
95 my $show_help      = 0;
96 my @source_files   = ();  # Final file list to process, generated in in generate_file_list().
97 my $upstream_path  = "";
98 my $wanted_refid   = "";  # The refid to reset the upstream tree to.
99
100 # ================================================================
101 # ===        ==> ------- MAIN DATA STRUCTURES ------ <==       ===
102 # ================================================================
103 my %hCommits = ();  # Hash of all upstream commits that touch at least one file:
104                     # ( refid : count of files touched )
105 my %hFiles   = ();  # List of all source files as %hFile structures with a simple
106                     # ( tgt : $hFile ) mapping.
107 my $hFile    = {};  # Simple description of one file consisting of:
108                     # Note: The store is %hFiles, this is used as a global pointer.
109                     #       Further although the target is the key in %hFiles, we
110                     #       store it here, too, so we always no the $hFile's key.
111                     # ( patch: Full path to the patch that check_tree.pl would generate
112                     #   src  : The potential relative upstream path with 'elogind' substituted by 'systemd'
113                     #   tgt  : The found relative path in the local tree
114                     # )
115 my %hMutuals = ();  # Mapping of the $COMMIT_FILE, that works as follows:
116                     # CSV lines are structured as:
117                     # <refid> <hash> src-<hash> tgt-<hash>
118                     # They map as follows:
119                     # ( <path to upstream tree> {
120                     #       <refid> : {
121                     #           mutual : <hash> | This is the last mutual commit
122                     #           src    : <hash> | This is the last individual commit in the upstream tree (*)
123                     #           tgt    : <hash> | This is the last individual commit in the local tree    (*)
124                     #       } } )
125                     # (*) When this entry was written. This means that src-<hash> can be used as
126                     #     the next last mutual commit, when this migration run is finished. To make
127                     #     this automatic, the --advance option triggers exactly that.
128
129 # ================================================================
130 # ===        ==> --------  Function list   -------- <==        ===
131 # ================================================================
132
133 sub apply_patches;       # Apply a reworked patch.
134 sub build_hCommits;      # Build a hash of commits for the current hFile.
135 sub build_hFile;         # Add an entry to hFiles for a specific target file.
136 sub build_lCommits;      # Build the topological list of all relevant commits.
137 sub build_lPatches;      # Fill $output_path with formatted patches from @lCommits.
138 sub check_tree;          # Use check_tree.pl on the given commit and file.
139 sub checkout_tree;       # Checkout the given refid on the given path.
140 sub generate_file_list;  # Find all relevant files and store them in @wanted_files
141 sub get_last_mutual;     # Find or read the last mutual refid between this and the upstream tree.
142 sub handle_sig;          # Signal handler so we don't break without writing a new commit file.
143 sub parse_args;          # Parse ARGV for the options we support
144 sub rework_patch;        # Use check_tree.pl to generate valid diffs on all valid files within the patch.
145 sub set_last_mutual;     # Write back %hMutuals to $COMMIT_FILE
146 sub shorten_refid;       # Take DIR and REFID and return the shortest possible REFID in DIR.
147 sub show_prg;            # Helper to show a progress line that is not permanent.
148 sub wanted;              # Callback function for File::Find
149
150 # set signal-handlers
151 local $SIG{'INT'}  = \&handle_sig;
152 local $SIG{'QUIT'} = \&handle_sig;
153 local $SIG{'TERM'} = \&handle_sig;
154
155 # ================================================================
156 # ===        ==> --------    Prechecks     -------- <==        ==
157 # ================================================================
158
159 -x $CHECK_TREE or die("$CHECK_TREE not found!");
160
161 $output_path = abs_path("$PROGDIR/patches");
162 $main_result = parse_args(@ARGV);
163 (
164         ( !$main_result )  ## Note: Error or --help given, then exit.
165           or ( $show_help and print "$USAGE_LONG" ) ) and exit( !$main_result );
166 get_last_mutual and generate_file_list
167   or exit 1;
168 checkout_tree($upstream_path, $wanted_refid, 1)
169   or exit 1;
170
171 # ================================================================
172 # ===        ==> -------- = MAIN PROGRAM = -------- <==        ===
173 # ================================================================
174
175 # -----------------------------------------------------------------
176 # --- 1) Go through all files and generate a list of all source ---
177 # ---    commits that touch the file.                           ---
178 # -----------------------------------------------------------------
179 print "Searching relevant commits ...";
180 for my $file_part (@source_files) {
181         build_hFile($file_part) or next;
182         build_hCommits or next;
183 }
184 printf( " %d commits found\n", $commit_count );
185
186 # -----------------------------------------------------------------
187 # --- 2) Get a list of all commits and build @lCommits, checking --
188 # ---    against the found hashes in $hCommits. This will build ---
189 # ---    a list that has the correct order the commits must be  ---
190 # ---    applied.                                               ---
191 # -----------------------------------------------------------------
192 build_lCommits or exit 1;
193
194 # -----------------------------------------------------------------
195 # --- 3) Go through the relevant commits and create formatted   ---
196 # ---    patches for them.                                      ---
197 # -----------------------------------------------------------------
198 build_lPatches or exit 1;
199
200 # -----------------------------------------------------------------
201 # --- 4) Go through the patches and rewrite them. We only want  ---
202 # ---    them to touch files of relevance, and need them to     ---
203 # ---    contain only diffs that are valid for us. We'll use    ---
204 # ---    check_tree.pl to achieve the latter.                   ---
205 # -----------------------------------------------------------------
206 for ( my $i = 0 ; $i < $commit_count ; ++$i ) {
207         my $fmt = sprintf( "%04d-*.patch", $i + 1 );
208         my @lFiles = glob qq("${output_path}/${fmt}");
209
210         # Be sure this is solid!
211         # ----------------------------------------------------------
212         if ( scalar @lFiles > 1 ) {
213                 print "\nERROR: $fmt results in more than one patch!\n";
214                 exit 1;
215         } elsif ( 1 > scalar @lFiles ) {
216                 print "\nERROR: No patches found for $fmt!";
217                 exit 1;
218         }
219
220         show_prg( sprintf("Reworking %s", basename( $lFiles[0] ) ) );
221         rework_patch( $lFiles[0] ) or exit 1;
222
223         # If the patch was eventually empty, rework_patch() has deleted it.
224         -f $lFiles[0] or next;
225
226         # -------------------------------------------------------------
227         # --- 5) Reworked patches must be applied directly.         ---
228         # ---    Otherwise we'll screw up if a newly created file   ---
229         # ---    gets patched later.                                ---
230         # -------------------------------------------------------------
231         show_prg( sprintf("Applying  %s", basename( $lFiles[0] ) ) );
232         apply_patch( $lFiles[0] ) or exit 1;
233         
234         # The patch file is no longer needed. Keeping it would lead to confusion.
235         unlink($lFiles[0]);
236 } ## end for ( my $i = 0 ; $i < ...)
237 show_prg("");
238
239
240 # ===========================
241 # === END OF MAIN PROGRAM ===
242 # ===========================
243
244 # ================================================================
245 # ===        ==> --------     Cleanup      -------- <==        ===
246 # ================================================================
247
248 END {
249         set_last_mutual;
250         length($previous_refid) and checkout_tree($upstream_path, $previous_refid, 0);
251 }
252
253
254 # ================================================================
255 # ===        ==> ---- Function Implementations ---- <==        ===
256 # ================================================================
257
258 # --------------------------------------------------------------
259 # --- Apply a reworked patch                                 ---
260 # --------------------------------------------------------------
261 sub apply_patch {
262         my ($pFile)     = @_;
263         my $git         = Git::Wrapper->new($WORKDIR);
264         my @lGitRes     = ();
265         my $patch_lines = "";
266
267         # --- 1) Read the patch, we have to use it directly via STDIN ---
268         # ---------------------------------------------------------------
269         if ( open( my $fIn, "<", $pFile ) ) {
270                 my @lLines = <$fIn>;
271                 close($fIn);
272                 chomp(@lLines);
273                 $patch_lines = join( "\n", @lLines ) . "\n";
274         } else {
275                 print "\nERROR: $pFile could not be opened for reading!\n$!\n";
276                 return 0;
277         }
278
279         # --- 2) Try to apply the patch as is                         ---
280         # ---------------------------------------------------------------
281         my $result = 1;
282         try {
283                 @lGitRes = $git->am( {
284                                 "3"    => 1,
285                                 -STDIN => $patch_lines
286                         } );
287         } catch {
288                 # We try again without 3-way-merging
289                 $git->am( { "abort" => 1 } );
290                 show_prg( sprintf("Applying  %s (2nd try)", basename($pFile) ) );
291                 $result = 0;
292         };
293
294         if (0 == $result) {
295                 # --- 3) Try to apply the patch without 3-way-merging         ---
296                 # ---------------------------------------------------------------
297         
298                 try {
299                         @lGitRes = $git->am( {
300                                         -STDIN => $patch_lines
301                                 } );
302                         $result = 1;
303                 } catch {
304                         $git->am( { "abort" => 1 } );
305                         print "\nERROR: Couldn't apply $pFile\n";
306                         print "Exit Code : " . $_->status . "\n";
307                         print "Message   : " . $_->error . "\n";
308                 };
309                 $result or return $result; ## Give up and exit
310         }
311
312         # --- 4) Get the new commit id, so we can update %hMutuals ---
313         # ---------------------------------------------------------------
314         $hMutuals{$upstream_path}{$wanted_refid}{tgt} = shorten_refid($WORKDIR, "HEAD");
315         length($hMutuals{$upstream_path}{$wanted_refid}{tgt}) or return 0; # Give up and exit
316         
317         # The commit of the just applied patch file becomes the last mutual commit.
318         $hMutuals{$upstream_path}{$wanted_refid}{mutual}
319                 = shorten_refid($upstream_path, $hSrcCommits{$pFile});
320         length($hMutuals{$upstream_path}{$wanted_refid}{mutual}) or return 0; # Give up and exit
321
322         return $result;
323 } ## end sub apply_patch
324
325 # ------------------------------------------------------
326 # --- Build a hash of commits for the current hFile. ---
327 # ------------------------------------------------------
328 sub build_hCommits {
329         my $git = Git::Wrapper->new($upstream_path);
330         my @lRev = $git->rev_list( {
331                 topo_order => 1,
332                 "reverse" => 1,
333                 oneline => 1
334         }, "${mutual_commit}..${wanted_refid}", $hFile->{src} );
335
336         for my $line (@lRev) {
337                 if ( $line =~ m/^(\S+)\s+/ ) {
338                         defined( $hCommits{$1} )
339                           or ++$commit_count and $hCommits{$1} = 0;
340                         ++$hCommits{$1};
341                 }
342         } ## end for my $line (@lRev)
343
344         return 1;
345 } ## end sub build_hCommits
346
347 # ------------------------------------------------------------------
348 # --- Build a list of the relevant commits in topological order. ---
349 # ------------------------------------------------------------------
350 sub build_lCommits {
351         my $git = Git::Wrapper->new($upstream_path);
352
353         my @lRev = $git->rev_list( {
354                 topo_order => 1,
355                 "reverse" => 1,
356                 oneline => 1
357                  }, "${mutual_commit}..${wanted_refid}" );
358
359         for my $line (@lRev) {
360                 if ( $line =~ m/^(\S+)\s+/ ) {
361                         defined( $hCommits{$1} )
362                           and show_prg("Noting down $1")
363                           and push @lCommits, "$1";
364                 }
365         } ## end for my $line (@lRev)
366         show_prg("");
367
368         return 1;
369 } ## end sub build_lCommits
370
371 # ----------------------------------------------------------
372 # --- Add an entry to hFiles for a specific target file. ---
373 # ----------------------------------------------------------
374 sub build_hFile {
375         my ($tgt) = @_;
376
377         defined($tgt) and length($tgt) or print("ERROR\n") and die("build_hfile: tgt is empty ???");
378
379         # We only prefixed './' to unify things. Now it is no longer needed:
380         $tgt =~ s,^\./,,;
381
382         # Check the target file
383         my $src = "$tgt";
384         $src =~ s/elogind/systemd/g;
385         $src =~ s/\.pwx$//;
386         -f "$upstream_path/$src" or return 0;
387
388         # Build the patch name
389         my $patch = $tgt;
390         $patch =~ s/\//_/g;
391
392         # Build the central data structure.
393         $hFiles{$tgt} = {
394                 patch => $output_path . "/" . $patch . ".patch",
395                 src   => $src,
396                 tgt   => $tgt
397         };
398
399         # This is now our current hFile
400         $hFile = $hFiles{$tgt};
401
402         return 1;
403 } ## end sub build_hFile
404
405 # ----------------------------------------------------------------
406 # --- Fill $output_path with formatted patches from @lCommits. ---
407 # ----------------------------------------------------------------
408 sub build_lPatches {
409         my $git    = Git::Wrapper->new($upstream_path);
410         my $cnt    = 0;
411         my @lRev   = ();
412         my @lPath  = ();
413         my $result = 1;
414
415         for my $refid (@lCommits) {
416                 @lRev = $git->rev_list( { "1" => 1, oneline => 1 }, $refid );
417
418                 show_prg( sprintf( "Building %03d: %s", ++$cnt, $lRev[0] ) );
419
420                 try {
421                         @lPath = $git->format_patch(
422                                 {
423                                         "1"                  => 1,
424                                         "find-copies"        => 1,
425                                         "find-copies-harder" => 1,
426                                         "numbered"           => 1,
427                                         "output-directory"   => $output_path
428                                 },
429                                 "--start-number=$cnt",
430                                 $refid
431                         );
432                 } catch {
433                         print "\nERROR: Couldn't format-patch $refid\n";
434                         print "Exit Code : " . $_->status . "\n";
435                         print "Message   : " . $_->error . "\n";
436                         $result = 0;
437                 };
438                 $result or return $result;
439         } ## end for my $refid (@lCommits)
440         $cnt and show_prg("") and print("$cnt patches built\n");
441
442         # Just a safe guard, that is almost guaranteed to never catch.
443         if ( $cnt != $commit_count ) {
444                 print "ERROR: $commit_count patches expected, but only $cnt patches generated!\n";
445                 return 0;
446         }
447
448         return 1;
449 } ## end sub build_lPatches
450
451 # -------------------------------------------------------
452 # --- Use check_tree.pl on the given commit and file. ---
453 # -------------------------------------------------------
454 sub check_tree {
455         my ( $commit, $file, $isNew ) = @_;
456         my $stNew = "";
457
458         # If this is the creation of a new file, the hFile must be built.
459         if ($isNew) {
460                 my $tgt_file = basename($file);
461                 my $tgt_dir  = dirname($file);
462                 $tgt_file =~ s/systemd/elogind/g;
463
464                 defined( $hDirectories{$tgt_dir} )
465                   or $tgt_dir =~ s/systemd/elogind/g;
466
467                 my $tgt = "$tgt_dir/$tgt_file";
468
469                 # Build the patch name
470                 my $patch = $tgt;
471                 $patch =~ s/\//_/g;
472                 $hFiles{$file} = {
473                         patch => $output_path . "/" . $patch . ".patch",
474                         src   => $file,
475                         tgt   => $tgt
476                 };
477                 $stNew = "--create ";
478         } ## end if ($isNew)
479         my $path = $hFiles{$file}{patch};
480
481         # Now get the patch built
482         my @lResult = `$CHECK_TREE --stay -c $commit ${stNew}-f $file $upstream_path 2>&1`;
483         my $res     = $?;
484         my $err     = $!;
485
486         if ($res) {
487                 print "\n$CHECK_TREE died!\n";
488
489                 if ( $res == -1 ) {
490                         print "failed to execute: $err\n";
491                 } elsif ( $? & 127 ) {
492                         printf "Signal %d, %s coredump\n", ( $? & 127 ), ( $? & 128 ) ? 'with' : 'without';
493                 } else {
494                         printf "child exited with value %d\n", $? >> 8;
495                 }
496                 print "-----\n" . join( "", @lResult ) . "-----\n";
497                 return "";
498         } ## end if ($res)
499
500         # If check_tree found no diff or cleaned up all hunks, no patch is created.
501         for my $line (@lResult) {
502                 chomp $line;
503                 if ( $line =~ m/${file}:\s+(clean|same)/ ) {
504                         return "none";
505                 }
506         } ## end for my $line (@lResult)
507
508         return $path;
509 } ## end sub check_tree
510
511 # -----------------------------------------------------------------------
512 # --- Checkout the given refid on $upstream_path                      ---
513 # --- Param 1 is the path where to do the checkout
514 # --- Param 2 is the refid to check out.                              ---
515 # --- Param 3 can be set to 1, if mutuals{src} and previous_refid     ---
516 # ---         shall be stored.                                        ---
517 # --- Returns 1 on success, 0 otherwise.                              ---
518 # -----------------------------------------------------------------------
519 sub checkout_tree {
520         my ($path, $commit, $do_store) = @_;
521
522         # It is completely in order to not wanting to checkout a specific commit.
523         defined($commit) and length($commit) or return 1;
524
525         my $git        = Git::Wrapper->new($path);
526         my $new_commit = "";
527         my $old_commit = shorten_refid($path, "HEAD");;
528
529         # The current commit must be valid:
530         length($old_commit) or return 0;
531
532         # Get the shortened commit hash of $commit
533         $new_commit = shorten_refid($path, $commit);
534         length($new_commit) or return 0;
535
536         # Now check it out, unless we are already there:
537         if ( $old_commit ne $new_commit ) {
538                 my $result     = 1;
539                 print "Checking out $new_commit in ${path}...";
540                 try {
541                         $git->checkout($new_commit);
542                 } catch {
543                         print "\nERROR: Couldn't checkout \"new_commit\" in $path\n";
544                         print "Exit Code : " . $_->status . "\n";
545                         print "Message   : " . $_->error . "\n";
546                         $result = 0;
547                 };
548                 $result or return $result;
549                 print " done\n";
550         } ## end if ( $previous_refid ne...)
551         
552         # Save the commit hash of the wanted refid and the previous commit if wanted
553         if ($do_store) {
554                 $hMutuals{$path}{$wanted_refid}{src} = $new_commit;
555                 $previous_refid                      = $old_commit;
556         }
557
558         return 1;
559 } ## end sub checkout_upstream
560
561 # -----------------------------------------------------------------------
562 # --- Finds all relevant files and store them in @wanted_files        ---
563 # --- Returns 1 on success, 0 otherwise.                              ---
564 # -----------------------------------------------------------------------
565 sub generate_file_list {
566
567         # Do some cleanup first. Just to be sure.
568         print "Cleaning up...";
569         `rm -rf build`;
570         `find -iname '*.orig' -or -iname '*.bak' -or -iname '*.rej' -or -iname '*~' -or -iname '*.gc??' | xargs rm -f`;
571         print " done\n";
572
573         # The idea now is, that we use File::Find to search for files
574         # in all legal directories this program allows.
575         print "Find relevant files...";
576         for my $xDir ( "docs", "factory", "m4", "man", "shell-completion", "src", "tools" ) {
577                 if ( -d "$xDir" ) {
578                         find( \&wanted, "$xDir" );
579                 }
580         }
581
582         # There are also root files we need to check. Thanks to the usage of
583         # check_tree.pl to generate the later commit diffs, these can be safely
584         # added to our file list as well.
585         for my $xFile ( "Makefile", "Makefile.am", "TODO", "CODING_STYLE", "configure", ".mailmap", "LICENSE.GPL2", "meson_options.txt", "NEWS", "meson.build", "configure.ac", ".gitignore" ) {
586                 -f "$xFile" and push @source_files, "./$xFile";
587         }
588         print " done - " . ( scalar @source_files ) . " files found\n";
589
590         # Just to be sure...
591         scalar @source_files
592           or print("ERROR: No source files found? Where the hell are we?\n")
593           and return 0;
594
595         # Eventually we can add each directory to %hDirectories
596         for my $xFile (@source_files) {
597                 my $xDir = dirname($xFile);
598                 $xDir =~ s,^\./,,;
599                 if ( length($xDir) > 1 ) {
600                         defined( $hDirectories{$xDir} ) or $hDirectories{$xDir} = 1;
601                 }
602         } ## end for my $xFile (@source_files)
603
604         return 1;
605 } ## end sub generate_file_list
606
607 # ------------------------------------------------------------------------------
608 # --- Find or read the last mutual refid between this and the upstream tree. ---
609 # ------------------------------------------------------------------------------
610 sub get_last_mutual {
611
612         # No matter whether the commit is set or not, we need to read the
613         # commit file now, and write it back later if we have changes.
614         if ( -f $COMMIT_FILE ) {
615                 if ( open my $fIn, "<", $COMMIT_FILE ) {
616                         my @lLines = <$fIn>;
617                         close $fIn;
618                         chomp(@lLines);
619
620                         for my $line (@lLines) {
621                                 # Skip comments
622                                 $line =~ m/^\s*#/ and next;
623
624                                 # Skip empty lines
625                                 $line =~ m/^\s*$/ and next;
626
627                                 if ( $line =~ m/^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*$/ ) {
628                                         my $usp = $1;
629                                         my $ref = $2;
630                                         my $mut = $3;
631                                         my $src = $4;
632                                         my $tgt = $5;
633                                         
634                                         # We mast be in the right branch or right tag!
635                                         checkout_tree($usp, $ref, 0) or return 0;
636                                         
637                                         $hMutuals{$usp}{$ref} = {
638                                                 mutual => shorten_refid($usp, $mut),
639                                                 src    => undef,
640                                                 tgt    => undef
641                                         };
642                                         $src =~ m/^src-(\S+)$/ and $hMutuals{$usp}{$ref}{src} = shorten_refid($usp, $1);
643                                         $tgt =~ m/^tgt-(\S+)$/ and $hMutuals{$usp}{$ref}{tgt} = shorten_refid($WORKDIR, $1);
644                                 } ## end if ( $line =~ m/^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*$/)
645                         } ## end for my $line (@lLines)
646                 } else {
647                         print("ERROR: $COMMIT_FILE can not be read!\n$!\n");
648                         return 0;
649                 }
650                 
651                 # Make sure we are back at the wanted place in the upstream tree
652                 checkout_tree($upstream_path, $wanted_refid, 0);
653         } ## end if ( -f $COMMIT_FILE )
654
655         # Note down that reading of any file is done.
656         $commits_read = 1;
657
658         # If this is already set, we are fine.
659         if ( length($mutual_commit) ) {
660                 $hMutuals{$upstream_path}{$wanted_refid}{mutual} = shorten_refid($upstream_path, $mutual_commit);
661                 length($hMutuals{$upstream_path}{$wanted_refid}{mutual}) or return 0;
662         }
663
664         # Now check against --advance and then set $mutual_commit accordingly.
665         if ( defined( $hMutuals{$upstream_path}{$wanted_refid} ) ) {
666                 if ($do_advance) {
667                         defined( $hMutuals{$upstream_path}{$wanted_refid}{src} )
668                                 and $hMutuals{$upstream_path}{$wanted_refid}{mutual}
669                                         = $hMutuals{$upstream_path}{$wanted_refid}{src}
670                                  or print "ERROR: --advance set, but no source hash found!\n" and return 0;
671                 }
672                 $mutual_commit = $hMutuals{$upstream_path}{$wanted_refid}{mutual};
673                 return 1;
674         } ## end if ( defined( $hMutuals...))
675
676         print "ERROR: There is no last mutual commit known for refid \"$wanted_refid\"!\n";
677
678         return 0;
679 } ## end sub get_last_mutual
680
681
682 # ---------------------------------------------------------------------------
683 # --- Signal handler so we don't break without writing a new commit file. ---
684 # ---------------------------------------------------------------------------
685 sub handle_sig {
686         my($sig) = @_;
687         print "\nCaught SIG${sig}!\n";
688         exit 1;
689 }
690
691
692 # -----------------------------------------------------------------------
693 # --- parse the given list for arguments.                             ---
694 # --- returns 1 on success, 0 otherwise.                              ---
695 # --- sets global $show_help to 1 if the long help should be printed. ---
696 # -----------------------------------------------------------------------
697 sub parse_args {
698         my @args   = @_;
699         my $result = 1;
700
701         for ( my $i = 0 ; $i < @args ; ++$i ) {
702
703                 # Check --advance option
704                 if ( $args[$i] =~ m/^--advance$/ ) {
705                         $do_advance = 1;
706                 }
707
708                 # Check for -c|--commit option
709                 # -------------------------------------------------------------------------------
710                 elsif ( $args[$i] =~ m/^-(?:c|-commit)$/ ) {
711                         if (   ( ( $i + 1 ) >= @args )
712                                 || ( $args[ $i + 1 ] =~ m,^[-/.], ) )
713                         {
714                                 print "ERROR: Option $args[$i] needs a refid as argument!\n\nUsage: $USAGE_SHORT\n";
715                                 $result = 0;
716                                 next;
717                         } ## end if ( ( ( $i + 1 ) >= @args...))
718                         $mutual_commit = $args[ ++$i ];
719                 } ## end elsif ( $args[$i] =~ m/^-(?:c|-commit)$/)
720
721                 # Check for -h|--help option
722                 # -------------------------------------------------------------------------------
723                 elsif ( $args[$i] =~ m/^-(?:h|-help)$/ ) {
724                         $show_help = 1;
725                 }
726
727                 # Check for -o|--output option
728                 # -------------------------------------------------------------------------------
729                 elsif ( $args[$i] =~ m/^-(?:o|-output)$/ ) {
730                         if (   ( ( $i + 1 ) >= @args )
731                                 || ( $args[ $i + 1 ] =~ m,^[-/.], ) )
732                         {
733                                 print "ERROR: Option $args[$i] needs a path as argument!\n\nUsage: $USAGE_SHORT\n";
734                                 $result = 0;
735                                 next;
736                         } ## end if ( ( ( $i + 1 ) >= @args...))
737                         $output_path = abs_path( $args[ ++$i ] );
738                 } ## end elsif ( $args[$i] =~ m/^-(?:o|-output)$/)
739
740                 # Check for unknown options:
741                 # -------------------------------------------------------------------------------
742                 elsif ( $args[$i] =~ m/^-/ ) {
743                         print "ERROR: Unknown option \"$args[$i]\" encountered!\n\nUsage: $USAGE_SHORT\n";
744                         $result = 0;
745                 }
746
747                 # Everything else is considered to the path to upstream first and refid second
748                 # -------------------------------------------------------------------------------
749                 else {
750                         # But only if they are not set, yet:
751                         if ( length($upstream_path) && length($wanted_refid) ) {
752                                 print "ERROR: Superfluous argument \"$args[$i]\" found!\n\nUsage: $USAGE_SHORT\n";
753                                 $result = 0;
754                                 next;
755                         }
756                         if ( length($upstream_path) ) {
757                                 $wanted_refid = "$args[$i]";
758                         } else {
759                                 if ( !-d "$args[$i]" ) {
760                                         print "ERROR: Upstream path \"$args[$i]\" does not exist!\n\nUsage: $USAGE_SHORT\n";
761                                         $result = 0;
762                                         next;
763                                 }
764                                 $upstream_path = $args[$i];
765                         } ## end else [ if ( length($upstream_path...))]
766                 } ## end else [ if ( $args[$i] =~ m/^--advance$/)]
767         }  ## End looping arguments
768
769         # If we have no refid now, show short help.
770         if ( $result && !$show_help && !length($wanted_refid) ) {
771                 print "ERROR: Please provide a path to upstream and a refid!\n\nUsage: $USAGE_SHORT\n";
772                 $result = 0;
773         }
774
775         # If both --advance and --commit were used, we can not tell what the
776         # user really wants. So better be safe here, or we might screw the tree!
777         if ( $do_advance && length($mutual_commit) ) {
778                 print "ERROR: You have used both --advance and --commit.\n";
779                 print "       Which one is the one you really want?\n\n";
780                 print "Usage: $USAGE_SHORT\n";
781                 $result = 0;
782         } ## end if ( $do_advance && length...)
783
784         return $result;
785 }  ## parse_srgs() end
786
787 # --------------------------------------------------------------
788 # --- Use check_tree.pl to generate valid diffs on all valid ---
789 # --- files within the patch with the given number.          ---
790 # --------------------------------------------------------------
791 sub rework_patch {
792         my ($pFile) = @_;
793         my @lLines = ();
794
795         if ( open( my $fIn, "<", $pFile ) ) {
796                 @lLines = <$fIn>;
797                 close($fIn);
798                 chomp(@lLines);
799         } else {
800                 print "\nERROR: $pFile could not be opened for reading!\n$!\n";
801                 return 0;
802         }
803
804         # Copy the header, ended by either '---' or 'diff '
805         # ----------------------------------------------------------
806         my @lOut   = ();
807         my $lCnt   = scalar @lLines;
808         my $commit = "";
809
810         while ( $lCnt-- > 0 ) {
811
812                 # Can not be done in while(), or empty lines would break the loop.
813                 my $line = shift @lLines;
814
815                 # We break this once we have found a file summary line
816                 if ( $line =~ m/^\s+(\S+)\s+\|\s+\d+/ ) {
817                         unshift @lLines, $line;  ## Still needed
818                         ++$lCnt;                 ## Yeah, put it up again!
819                         last;
820                 }
821
822                 # Before transfering the line, see if this is the commit info
823                 $line =~ m/^From (\S+)\s\w{3}\s\w{3}\s\d{2}/ and $commit = $1;
824
825                 push @lOut, $line;
826         } ## end while ( $lCnt-- > 0 )
827
828         # There is something wrong if we have no commit hash now
829         if ( 0 == length($commit) ) {
830                 print "\nERROR: No 'From <hash>' line found!\n";
831                 return 0;
832         }
833
834         # There is something wrong if the next part is not a file summary line.
835         # ----------------------------------------------------------------------
836         if ( !defined( $lLines[0] ) || !( $lLines[0] =~ m/^\s+(\S+)\s+\|\s+\d+/ ) ) {
837                 print "\nERROR: No file summary block found!\n";
838                 print "The line currently looked at is:\n";
839                 print "|" . ( defined( $lLines[0] ) ? $lLines[0] : "UNDEF" ) . "|\n";
840                 print "We still have $lCnt lines to go.\n";
841                 print "\nlOut so far:\n" . join( "\n", @lOut ) . "\n---------- END\n";
842                 return 0;
843         } ## end if ( !defined( $lLines...))
844
845         my @lFixedPatches = ();
846         while ( $lCnt-- > 0 ) {
847                 my $isNew = 0;             ## 1 if we hit a creation summary line
848                 my $line  = shift @lLines;
849                 my $real  = "";            ## The real file to work with
850                 my $src   = "";            ## Source in upstream
851                 my $tgt   = "";            ## Target in downstream
852
853                 # This ends on the first empty line.
854                 $line =~ m/^\s*$/ and push( @lOut, "" ) and last;
855
856                 # This is either a line modification information, or a
857                 # creation line of a new file. These look like this...
858                 #   src/shared/meson.build                      |   1 +
859                 $line =~ m/^\s+(\S+)\s+\|.*/ and $src = $1;
860
861                 # ...or that:
862                 #   create mode 100644 src/network/netdev/wireguard.c
863                 $line =~ m/^\s+create\s+mode\s+\d+\s+(\S+)\s*$/
864                   and $src   = $1
865                   and $isNew = 1;
866
867                 # Otherwise it is the modification summary line
868                 length($src) or push( @lOut, $line ) and next;
869
870                 $tgt = $src;
871                 $tgt =~ s/systemd/elogind/g;
872
873                 # The determination what is valid is different for whether this is
874                 # the modification of an existing or the creation of a new file
875                 if ($isNew) {
876                         defined( $hDirectories{ dirname($tgt) } ) and $real = $tgt or
877                         defined( $hDirectories{ dirname($src) } ) and $real = $src;
878                 } else {
879                         # Try the renamed first, then the non-renamed
880                         defined( $hFiles{$tgt} ) and $real = $tgt
881                           or defined( $hFiles{$src} )
882                           and $real = $src;
883                 } ## end else [ if ($isNew) ]
884
885                 # We neither need diffs on invalid files, nor new files in invalid directories.
886                 length($real) or next;
887
888                 # Now use $real to get the patch needed, if it is set.
889                 my $pNew = check_tree( $commit, $real, $isNew );
890
891                 # If no patch was generated, the file is either "same" or "clean".
892                 $pNew eq "none" and next;
893
894                 # However, an empty $pNew indicates an error. (check_tree() has it reported already.)
895                 length($pNew) and push @lFixedPatches, $pNew or return 0;
896
897                 # If we are here, transfer the file line. It is useful.
898                 $line =~ s/$src/$real/;
899                 push @lOut, $line;
900         }  ## End of scanning lines
901
902         if ( 0 == scalar @lFixedPatches) {
903                 unlink $pFile; ## Empty patch...
904                 return 1;
905         }
906
907         # Load all generated patches and add them to lOut
908         # ----------------------------------------------------------
909         for my $pNew (@lFixedPatches) {
910                 if ( open my $fIn, "<", $pNew ) {
911                         my @lNew = <$fIn>;
912                         close($fIn);
913                         chomp(@lNew);
914                         push @lOut, @lNew;
915                         unlink $pNew;
916                 } else {
917                         print "\nERROR: Can't open $pNew for reading!\n$!\n";
918                         return 0;
919                 }
920         } ## end for my $pNew (@lFixedPatches)
921
922         # Store the patch commit for later reference
923         # ----------------------------------------------------------
924         $hSrcCommits{$pFile} = $commit;
925
926         # Eventually overwrite $pFile with @lOut
927         # ----------------------------------------------------------
928         if ( open( my $fOut, ">", $pFile ) ) {
929                 print $fOut join( "\n", @lOut ) . "\n";
930                 close($fOut);
931         } else {
932                 print "\nERROR: Can not opne $pFile for writing!\n$!\n";
933                 return 0;
934         }
935
936         return 1;
937 } ## end sub rework_patch
938
939
940 # --------------------------------------------
941 # --- Write back %hMutuals to $COMMIT_FILE ---
942 # --------------------------------------------
943 sub set_last_mutual {
944
945         # Don't do anything if we haven't finished reading the commit file:
946         $commits_read or return 1;
947
948         my $out_text = "# Automatically generated commit information\n"
949                      . "# Only edit if you know what these do!\n\n";
950         my ($pLen, $rLen, $mLen, $sLen) = (0, 0, 0, 0); # Length for the fmt
951         
952         # First we need a length to set all fields to.
953         # ---------------------------------------------------------------
954         # (And build a shortcut while at it so we do ...
955         for my $path (sort keys %hMutuals) {
956                 length($path) > $pLen and $pLen = length($path);
957                 for my $refid (sort keys %{$hMutuals{$path}}) {
958                         my $hM = $hMutuals{$path}{$refid}; # Shortcut!
959                         length($refid)        > $rLen and $rLen = length($refid);
960                         length($hM->{mutual}) > $mLen and $mLen = length($hM->{mutual});
961                         defined($hM->{src}) and (length($hM->{src}) > 4)
962                                 and $hM->{src} = "src-" . $hM->{src}
963                                  or $hM->{src} = "x";
964                         length($hM->{src})    > $sLen and $sLen = length($hM->{src});
965                         defined($hM->{tgt}) and (length($hM->{tgt}) > 4)
966                                 and $hM->{tgt} = "tgt-" . $hM->{tgt}
967                                  or $hM->{tgt} = "x";
968                 }
969         }
970         
971         # Now we can build the fmt
972         my $out_fmt  = sprintf("%%-%ds %%-%ds %%-%ds %%-%ds %%s\n", $pLen, $rLen, $mLen, $sLen);
973         
974         # Second we build the out text
975         # ---------------------------------------------------------------
976         for my $path (sort keys %hMutuals) {
977                 for my $refid (sort keys %{$hMutuals{$path}}) {
978                         my $hM = $hMutuals{$path}{$refid}; # Shortcut!
979                         $out_text .= sprintf($out_fmt,
980                                 $path,
981                                 $refid,
982                                 $hM->{mutual},
983                                 $hM->{src},
984                                 $hM->{tgt}
985                         );
986                 }
987         }
988         
989         # Third, write a new $COMMIT_FILE
990         # ---------------------------------------------------------------
991         if (open(my $fOut, ">", $COMMIT_FILE)) {
992                 print $fOut $out_text;
993                 close($fOut);
994         } else {
995                 print "ERROR: Can not open $COMMIT_FILE for writing!\n$!\n";
996                 print "The content would have been:\n" . ('-' x 24) . "\n$out_text" . ('-' x 24) . "\n";
997                 exit 1;
998         }
999
1000         return 1;
1001 }
1002
1003
1004 # -------------------------------------------------------------------------
1005 # --- Take DIR and REFID and return the shortest possible REFID in DIR. ---
1006 # -------------------------------------------------------------------------
1007 sub shorten_refid {
1008         my ($p, $r) = @_;
1009
1010         defined($p) and length($p) or die("shorten_refid() called with undef path!");
1011         defined($r) and length($r) or die("shorten_refid() called with undef refid!");
1012
1013         my $git        = Git::Wrapper->new($p);
1014         my @lOut       = ();
1015         my $result     = 1;
1016
1017         # Get the shortest possible $r (REFID)
1018         try {
1019                 @lOut = $git->rev_parse( { short => 1 }, "$r" );
1020         } catch {
1021                 print "ERROR: Couldn't rev-parse ${p}::${r}\n";
1022                 print "Exit Code : " . $_->status . "\n";
1023                 print "Message   : " . $_->error . "\n";
1024                 $result = 0;
1025         };
1026         $result and return $lOut[0];
1027         return "";
1028 }
1029
1030 # Helper to show the argument as a non permanent progress line.
1031 sub show_prg {
1032         my ($msg) = @_;
1033         my $len = length($prg_line);
1034
1035         $len and print "\r" . ( ' ' x $len ) . "\r";
1036
1037         $prg_line = $msg;
1038
1039         if ( length($prg_line) ) {
1040                 local $| = 1;
1041                 print $msg;
1042         }
1043
1044         return 1;
1045 } ## end sub show_prg
1046
1047 # Callback function for File::Find
1048 sub wanted {
1049         my $f = $File::Find::name;
1050
1051         $f =~ m,^\./, or $f = "./$f";
1052
1053         -f $_
1054           and ( !( $_ =~ m/\.pwx$/ ) )
1055           and push @source_files, $File::Find::name;
1056
1057         return 1;
1058 } ## end sub wanted