github.com/containers/podman/v5@v5.1.0-rc1/hack/buildah-vendor-treadmill (about)

     1  #!/usr/bin/perl
     2  #
     3  # buildah-vendor-treadmill - daily vendor of latest-buildah onto latest-podman
     4  #
     5  package Podman::BuildahVendorTreadmill;
     6  
     7  use v5.14;
     8  use utf8;
     9  use open qw( :encoding(UTF-8) :std );
    10  
    11  use strict;
    12  use warnings;
    13  
    14  use File::Temp                  qw(tempfile);
    15  use JSON;
    16  use LWP::UserAgent;
    17  use POSIX                       qw(strftime);
    18  
    19  (our $ME = $0) =~ s|.*/||;
    20  our $VERSION = '0.3';
    21  
    22  # For debugging, show data structures using DumpTree($var)
    23  #use Data::TreeDumper; $Data::TreeDumper::Displayaddress = 0;
    24  
    25  ###############################################################################
    26  # BEGIN user-customizable section
    27  
    28  # Page describing this process in much more detail
    29  our $Docs_URL =
    30      'https://github.com/containers/podman/wiki/Buildah-Vendor-Treadmill';
    31  
    32  # github path to buildah
    33  our $Buildah = 'github.com/containers/buildah';
    34  
    35  # FIXME FIXME FIXME: add 'main'? I hope we never need this script for branches.
    36  our $Treadmill_PR_Title = 'DO NOT MERGE: buildah vendor treadmill';
    37  
    38  # Github API; this is where we query to find out the active treadmill PR
    39  our $API_URL = 'https://api.github.com/graphql';
    40  
    41  # Use colors if available and if stdout is a tty
    42  our $C_Highlight = '';
    43  our $C_Warning = '';
    44  our $C_Reset = '';
    45  eval '
    46      use Term::ANSIColor;
    47      if (-t 1) {
    48          $C_Highlight = color("green");
    49          $C_Warning   = color("bold red");
    50          $C_Reset     = color("reset");
    51  
    52      }
    53      $SIG{__WARN__} = sub { print STDERR $C_Warning, "@_", $C_Reset; };
    54  
    55  ';
    56  
    57  # END   user-customizable section
    58  ###############################################################################
    59  
    60  ###############################################################################
    61  # BEGIN boilerplate args checking, usage messages
    62  
    63  sub usage {
    64      print  <<"END_USAGE";
    65  Usage: $ME [OPTIONS] [--sync | --pick [PR] | --reset ]
    66  
    67  $ME is (2022-04-20) **EXPERIMENTAL**
    68  
    69  $ME is intended to solve the problem of vendoring
    70  buildah into podman.
    71  
    72  Call me with one of three options:
    73  
    74      --sync  The usual case. Mostly used by Ed. Called from a
    75              development branch, this just updates everything so
    76              we vendor in latest-buildah (main) on top of
    77              latest-podman (main). With a few sanity checks.
    78  
    79      --pick  Used for really-truly vendoring in a new buildah; will
    80              cherry-pick a commit on your buildah-vendor working branch.
    81              Optional PR arg is the ID of the treadmill PR on github.
    82  
    83      --reset Used after vendoring buildah into main, when there
    84              really aren't any buildah patches to keep rolling.
    85  
    86  For latest documentation and best practices, please see:
    87  
    88      $Docs_URL
    89  
    90  OPTIONS:
    91  
    92    --help         display this message
    93    --version      display program name and version
    94  END_USAGE
    95  
    96      exit;
    97  }
    98  
    99  # Command-line options.  Note that this operates directly on @ARGV !
   100  our %action;
   101  our $debug   = 0;
   102  our $force_old_main = 0;        # in --pick, proceeds even if main is old
   103  our $force_retry = 0;           # in --sync, continue despite saved checkpoint
   104  our $force_testing = 0;         # in --sync, test even no podman/buildah changes
   105  our $verbose = 0;
   106  our $NOT     = '';              # print "blahing the blah$NOT\n" if $debug
   107  sub handle_opts {
   108      use Getopt::Long;
   109      GetOptions(
   110          'sync'       => sub { $action{sync}++  },
   111          'pick'       => sub { $action{pick}++  },
   112          'reset'      => sub { $action{reset}++ },
   113  
   114          'force-old-main'  => \$force_old_main,
   115          'force-retry'     => \$force_retry,
   116          'force-testing'   => \$force_testing,
   117  
   118          'debug!'     => \$debug,
   119          'dry-run|n!' => sub { $NOT = ' [NOT]' },
   120          'verbose|v'  => \$verbose,
   121  
   122          help         => \&usage,
   123          version      => sub { print "$ME version $VERSION\n"; exit 0 },
   124      ) or die "Try `$ME --help' for help\n";
   125  }
   126  
   127  # END   boilerplate args checking, usage messages
   128  ###############################################################################
   129  
   130  ############################## CODE BEGINS HERE ###############################
   131  
   132  # The term is "modulino".
   133  __PACKAGE__->main()                                     unless caller();
   134  
   135  # Main code.
   136  sub main {
   137      # Note that we operate directly on @ARGV, not on function parameters.
   138      # This is deliberate: it's because Getopt::Long only operates on @ARGV
   139      # and there's no clean way to make it use @_.
   140      handle_opts();                      # will set package globals
   141  
   142      my @action = keys(%action);
   143      die "$ME: Please invoke me with one of --sync or --pick\n"
   144          if ! @action;
   145      die "$ME: Please invoke me with ONLY one of --sync or --pick\n"
   146          if @action > 1;
   147  
   148      my $handler = __PACKAGE__->can("do_@action")
   149          or die "$ME: No handler available for --@action\n";
   150  
   151      # We've validated the command-line args. Before running action, check
   152      # that repo is clean. None of our actions can be run on a dirty repo.
   153      assert_clean_repo();
   154  
   155      $handler->(@ARGV);
   156  }
   157  
   158  ###############################################################################
   159  # BEGIN sync and its helpers
   160  
   161  sub do_sync {
   162      die "$ME: --sync takes no arguments; try $ME --help\n" if @_;
   163  
   164      # Preserve current branch name, so we can come back after switching to main
   165      my $current_branch = git_current_branch();
   166  
   167      # Branch HEAD must be the treadmill commit.
   168      my $commit_message = git('log', '-1', '--format=%s', 'HEAD');
   169      print "[$commit_message]\n"         if $verbose;
   170      $commit_message =~ /buildah.*treadmill/
   171          or die "$ME: HEAD must be a 'buildah treadmill' commit.\n";
   172  
   173      # ...and previous commit must be a scratch buildah vendor
   174      $commit_message = git('log', '-1', '--format=%B', 'HEAD^');
   175      $commit_message =~ /DO NOT MERGE.* vendor in buildah.*JUNK COMMIT/s
   176          or die "$ME: HEAD^ must be a DO NOT MERGE / JUNK COMMIT commit\n";
   177      assert_buildah_vendor_commit('HEAD^');
   178  
   179      # Looks good so far.
   180      my $buildah_old = vendored_buildah();
   181      print "-> buildah old = $buildah_old\n";
   182  
   183      # Pull main, and pivot back to this branch
   184      pull_main();
   185      git('checkout', '-q', $current_branch);
   186  
   187      # Make a temporary copy of this branch
   188      my $temp_branch = strftime("__buildah-treadmill-checkpoint/%Y%m%d-%H%M%S", localtime);
   189      git('branch', $temp_branch, $current_branch);
   190      progress("Current branch preserved as $temp_branch");
   191  
   192      # Get the hash of the top (treadmill) commit, to cherry-pick later
   193      my $treadmill_commit = git('rev-parse', 'HEAD');
   194  
   195      #
   196      # Danger Will Robinson! This is where it gets scary: a failure here
   197      # can leave us in a state where we could lose the treadmill patches.
   198      # Proceed with extreme caution.
   199      #
   200      local $SIG{__DIE__} = sub {
   201          print STDERR $C_Warning, "@_", <<"END_FAIL_INSTRUCTIONS";
   202  
   203  This is not something I can recover from. Your human judgment is needed.
   204  
   205  You will need to recover from this manually. Your best option is to
   206  look at the source code for this script.
   207  
   208  Treadmill branch copy is preserved in $temp_branch
   209  
   210  To restore state to where you were before this sync:
   211      \$ git checkout main
   212      \$ git branch -f $current_branch $treadmill_commit
   213  END_FAIL_INSTRUCTIONS
   214  
   215          exit 1;
   216      };
   217  
   218      my $forkpoint = git_forkpoint();
   219      my $rebased;
   220  
   221      # Unlikely to fail
   222      git('reset', '--hard', 'HEAD^^');
   223  
   224      # Rebase branch. Also unlikely to fail
   225      my $main_commit = git('rev-parse', 'main');
   226      if ($forkpoint eq $main_commit) {
   227          progress("[Already rebased on podman main]");
   228      }
   229      else {
   230          progress("Rebasing on podman main...");
   231          git('rebase', '--empty=keep', 'main');
   232          $rebased = 1;
   233      }
   234  
   235      # This does have a high possibility of failing.
   236      progress("Vendoring in buildah...");
   237      system('go', 'mod', 'edit', '--require' => "${Buildah}\@main") == 0
   238          or die "$ME: go mod edit failed";
   239      system('make', 'vendor') == 0
   240          or die "$ME: make vendor failed";
   241      my $buildah_new = vendored_buildah();
   242      print "-> buildah new = $buildah_new\n";
   243  
   244      # Tweak .cirrus.yml so we run bud tests first in CI (to fail fast).
   245      tweak_cirrus_test_order();
   246  
   247      # 'make vendor' seems to git-add files under buildah itself, but not
   248      # under other changed modules. Add those now, otherwise we fail
   249      # the dirty-tree test in CI.
   250      if (my @v = git('status', '--porcelain', '--untracked=all', 'vendor')) {
   251          if (my @untracked = grep { /^\?\?\s/ } @v) {
   252              my %repos = map {
   253                  s!^.*?vendor/[^/]+/([^/]+/[^/]+)/.*$!$1!; $_ => 1;
   254              } @untracked;
   255              my $repos = join(', ', sort keys %repos);
   256              progress("Adding untracked files under $repos");
   257              git('add', 'vendor');
   258          }
   259      }
   260  
   261      # Commit everything.
   262      git_commit_buildah($buildah_new);
   263  
   264      # And, finally, this has the highest possibility of failing
   265      local $SIG{__DIE__} = sub {
   266          print STDERR $C_Warning, "@_", <<"END_FAIL_INSTRUCTIONS";
   267  
   268  This is not something I can recover from. Your human judgment is needed.
   269  
   270  Chances are, you might be able to run 'git status', look for
   271  merge conflicts, manually resolve those, 'git add', then
   272  'git cherry-pick --continue'. If that works, run this script
   273  again (you will probably need the --force-retry option).
   274  
   275  If that DOES NOT work, your only option is to look at the source code
   276  for this script. Sorry. There's only so much that can be done automatically.
   277  
   278  Treadmill branch copy is preserved in $temp_branch
   279  
   280  To restore state to where you were before this sync:
   281      \$ git checkout main
   282      \$ git branch -f $current_branch $treadmill_commit
   283  END_FAIL_INSTRUCTIONS
   284  
   285          exit 1;
   286      };
   287      progress('Reapplying treadmill patches');
   288      git('cherry-pick', '--allow-empty', $treadmill_commit);
   289  
   290      # It worked! Clean up: remove our local die() handler and the saved branch
   291      undef $SIG{__DIE__};
   292      git('branch', '-D', $temp_branch);
   293  
   294      # if buildah is unchanged, and we did not pull main, exit cleanly
   295      my $change_message = '';
   296      if ($buildah_new eq $buildah_old) {
   297          if (! $rebased) {
   298              $change_message = "Nothing has changed (same buildah, same podman).";
   299              if ($force_testing) {
   300                  $change_message .= " Testing anyway due to --force-testing.";
   301              }
   302              else {
   303                  progress($change_message);
   304                  progress("Not much point to testing this, but use --force-testing to continue.");
   305                  exit 0;
   306              }
   307          }
   308          else {
   309              $change_message = "Podman has bumped, but Buildah is unchanged. There's probably not much point to testing this.";
   310          }
   311      }
   312      else {
   313          my $samenew = ($rebased ? 'new' : 'same');
   314          $change_message = "New buildah, $samenew podman. Good candidate for pushing.";
   315      }
   316      progress($change_message);
   317  
   318      build_and_check_podman();
   319  
   320      progress("All OK. It's now up to you to 'git push --force'");
   321      progress(" --- Reminder: $change_message");
   322  
   323      # Kind of kludgy. If user had to retry a prior failed attempt, and
   324      # things are now successful, remind them to delete old checkpoints.
   325      # ($force_retry is a 'git branch -D' command string at this point.)
   326      if ($force_retry) {
   327          progress(" --- Retry worked! You may now $force_retry");
   328      }
   329  }
   330  
   331  ###############
   332  #  pull_main  #  Switch to main, and pull latest from github
   333  ###############
   334  sub pull_main {
   335      progress("Pulling podman main...");
   336      git('checkout', '-q', 'main');
   337      git('pull', '-r', git_upstream(), 'main');
   338  }
   339  
   340  #############################
   341  #  tweak_cirrus_test_order  #  Run bud tests first, to fail fast & early
   342  #############################
   343  sub tweak_cirrus_test_order {
   344      my $cirrus_yml = '.cirrus.yml';
   345      my $tmpfile = "$cirrus_yml.tmp.$$";
   346      unlink $tmpfile;
   347  
   348      progress("Tweaking test order in $cirrus_yml to run bud tests early");
   349      open my $in, '<', $cirrus_yml
   350          or do {
   351              warn "$ME: Cannot read $cirrus_yml: $!\n";
   352              warn "$ME: Will continue anyway\n";
   353              return;
   354          };
   355      open my $out, '>'. $tmpfile
   356          or die "$ME: Cannot create $tmpfile: $!\n";
   357      my $current_task = '';
   358      my $in_depend;
   359      while (my $line = <$in>) {
   360          chomp $line;
   361          if ($line =~ /^(\S+)_task:$/) {
   362              $current_task = $1;
   363              undef $in_depend;
   364          }
   365          elsif ($line =~ /^(\s+)depends_on:$/) {
   366              $in_depend = $1;
   367          }
   368          elsif ($in_depend && $line =~ /^($in_depend\s+-\s+)(\S+)/) {
   369              my $indent = $1;
   370  
   371              # Run the buildah-bud tests early: that's the entire point
   372              # of the treadmill PR. Here we switch Cirrus task dependencies
   373              # such that bud tests run as early as possible.
   374              if ($current_task =~ /buildah_bud_test/) {
   375                  # Buildah bud now depends only on validate...
   376                  $line = "${indent}validate";
   377              }
   378              elsif ($2 eq 'validate' && $current_task ne 'success') {
   379                  # ...and all other tests that relied on validate now rely on
   380                  # bud tests instead. The point of the treadmill PR is to
   381                  # run the bud tests and only then, if everything passes,
   382                  # run normal tests. (Reason: bud tests are the only ones
   383                  # likely to fail on a buildah revendor, and we want to see
   384                  # failures early).
   385                  $line = "${indent}buildah_bud_test";
   386              }
   387          }
   388          else {
   389              undef $in_depend;
   390  
   391              # FIXME THIS IS HORRIBLE!
   392              # Add rootless jobs to the buildah bud test matrix.
   393              # This is incredibly fragile; it relies on the fact
   394              # (true as of 2023-12-07) that the "matrix" yaml lines
   395              # are formatted just so and are followed immediately
   396              # by a "gce_instance" line.
   397              #
   398              # Since Ed is the only one who ever runs this script,
   399              # he is expected to notice if this ever changes, and
   400              # to fix it.
   401              if ($current_task eq 'buildah_bud_test') {
   402                  if ($line =~ /^(\s+)gce_instance:/) {
   403                      print { $out } <<'END_ROOTLESS_BUD';
   404          - env:
   405              PODBIN_NAME: podman
   406              PRIV_NAME: rootless
   407          - env:
   408              PODBIN_NAME: remote
   409              PRIV_NAME: rootless
   410  END_ROOTLESS_BUD
   411                  }
   412              }
   413          }
   414  
   415          print { $out } $line, "\n";
   416      }
   417      close $in;
   418      close $out
   419          or die "$ME: Error writing $tmpfile: $!\n";
   420      chmod 0644 => $tmpfile;
   421      rename $tmpfile => $cirrus_yml
   422          or die "$ME: Could not rename $tmpfile: $!\n";
   423  }
   424  
   425  ############################
   426  #  build_and_check_podman  #  Run quick (local) sanity checks before pushing
   427  ############################
   428  sub build_and_check_podman {
   429      my $errs = 0;
   430  
   431      # Confirm that we can still build podman
   432      progress("Running 'make' to confirm that podman builds cleanly...");
   433      system('make') == 0
   434          or die "$ME: 'make' failed with new buildah. Cannot continue.\n";
   435  
   436      # See if any new options need man pages. (C_Warning will highlight errs)
   437      progress('Cross-checking man pages...');
   438      print $C_Warning;
   439      $errs += system('hack/xref-helpmsgs-manpages');
   440      print $C_Reset;
   441  
   442      # Confirm that buildah-bud patches still apply. This requires knowing
   443      # the name of the directory created by the bud-tests script.
   444      progress("Confirming that buildah-bud-tests patches still apply...");
   445      system('rm -rf test-buildah-*');
   446      if (system('test/buildah-bud/run-buildah-bud-tests', '--no-test')) {
   447          # Error
   448          ++$errs;
   449          warn "$ME: Leaving test-buildah- directory for you to investigate\n";
   450      }
   451      else {
   452          # Patches apply cleanly. Clean up
   453          system('rm -rf test-buildah-*');
   454      }
   455  
   456      return if !$errs;
   457      warn <<"END_WARN";
   458  $ME: Errors found. I have to stop now for you to fix them.
   459      Your best bet now is:
   460        1) Find and fix whatever needs to be fixed; then
   461        2) git commit -am'fixme-fixme'; then
   462        3) git rebase -i main:
   463           a) you are now in an editor window
   464           b) move the new fixme-fixme commit up a line, to between the
   465              'buildah vendor treadmill' and 'vendor in buildah @ ...' lines
   466           c) change 'pick' to 'squash' (or just 's')
   467           d) save & quit to continue the rebase
   468           e) back to a new editor window
   469           f) change the commit message: remove fixme-fixme, add a description
   470              of what you actually fixed. If possible, reference the PR (buildah
   471              or podman) that introduced the failure
   472           g) save & quit to continue the rebase
   473  
   474      Now, for good measure, rerun this script.
   475  
   476      For full documentation, refer to
   477  
   478          $Docs_URL
   479  END_WARN
   480      exit 1;
   481  }
   482  
   483  # END   sync and its helpers
   484  ###############################################################################
   485  # BEGIN pick and its helpers
   486  #
   487  # This is what gets used on a real vendor-new-buildah PR
   488  
   489  sub do_pick {
   490      my $current_branch = git_current_branch();
   491  
   492      # Confirm that current branch is a buildah-vendor one
   493      assert_buildah_vendor_commit('HEAD');
   494      progress("HEAD is a buildah vendor commit. Good.");
   495  
   496      # Identify and pull the treadmill PR.
   497      my $treadmill_pr = shift || treadmill_pr();
   498  
   499      my $treadmill_branch = "$ME/pr$treadmill_pr/tmp$$";
   500      progress("Fetching treadmill PR $treadmill_pr into $treadmill_branch");
   501      git('fetch', '-q', git_upstream(), "pull/$treadmill_pr/head:$treadmill_branch");
   502  
   503      # Compare merge bases of our branch and the treadmill one
   504      progress("Checking merge bases");
   505      check_merge_bases($treadmill_pr, $treadmill_branch);
   506  
   507      # read buildah go.mod from it, and from current tree, and compare
   508      my $buildah_on_treadmill = vendored_buildah($treadmill_branch);
   509      my $buildah_here         = vendored_buildah();
   510      if ($buildah_on_treadmill ne $buildah_here) {
   511          warn "$ME: Warning: buildah version mismatch:\n";
   512          warn "$ME: on treadmill:   $buildah_on_treadmill\n";
   513          warn "$ME: on this branch: $buildah_here\n";
   514          # FIXME: should this require --force? A yes/no prompt?
   515          # FIXME: I think not, because usual case will be a true tagged version
   516          warn "$ME: Continuing anyway\n";
   517      }
   518  
   519      cherry_pick($treadmill_pr, $treadmill_branch);
   520  
   521      # Clean up
   522      git('branch', '-D', $treadmill_branch);
   523  
   524      build_and_check_podman();
   525  
   526      progress("Looks good! Please 'git commit --amend' and edit commit message before pushing.");
   527  }
   528  
   529  ##################
   530  #  treadmill_pr  #  Returns ID of open podman PR with the desired subject
   531  ##################
   532  sub treadmill_pr {
   533      # Github API (or maybe just the search endpoint???) is restricted.
   534      my $token = $ENV{GITHUB_TOKEN}
   535          or do {
   536              warn <<"END_NEED_PR";
   537  $ME: Cannot proceed without PR ID.
   538  
   539  If you have a github API token, please: export GITHUB_TOKEN=.......
   540  and re-run me.
   541  
   542  If you do not have a github API token, please go here:
   543  
   544     https://github.com/containers/podman/pulls?q=is%3Apr+is%3Aopen+%22buildah+vendor+treadmill%22
   545  
   546  ...then reinvoke me, adding that PR ID to the command line args.
   547  
   548  As of 2022-09-12 the treadmill PR is 13808, but that may change over time.
   549  END_NEED_PR
   550              exit 1;
   551          };
   552  
   553      my $query = <<'END_QUERY';
   554  {
   555    search(
   556      query: "buildah vendor treadmill repo:containers/podman",
   557      type: ISSUE,
   558      first: 10
   559    ) {
   560      edges { node { ... on PullRequest { number state title } } }
   561    }
   562  }
   563  END_QUERY
   564  
   565      my $ua = LWP::UserAgent->new;
   566      $ua->agent("$ME " . $ua->agent);              # Identify ourself
   567  
   568      my %headers = (
   569          'Authorization' => "bearer $token",
   570          'Accept'        => "application/vnd.github.antiope-preview+json",
   571          'Content-Type'  => "application/json",
   572      );
   573      $ua->default_header($_ => $headers{$_}) for keys %headers;
   574  
   575      # Massage the query: escape quotes, put it all in one line, collapse spaces
   576      $query =~ s/\"/\\"/g;
   577      $query =~ s/\n/\\n/g;
   578      $query =~ s/\s+/ /g;
   579      # ...and now one more massage
   580      my $postquery = qq/{ "query": \"$query\" }/;
   581  
   582      print $postquery, "\n"            if $debug;
   583      my $res = $ua->post($API_URL, Content => $postquery);
   584      if ((my $code = $res->code) != 200) {
   585          warn "$ME: GraphQL request failed on $API_URL:\n";
   586          print STDERR "  ", $code, " ", $res->message, "\n";
   587          warn "Cannot continue.\n";
   588          exit 1;
   589      }
   590  
   591      # Got something. Confirm that it has all our required fields
   592      my $content = decode_json($res->content);
   593      use Data::Dump; dd $content         if $debug;
   594      exists $content->{data}
   595          or die "$ME: No '{data}' section in response\n";
   596      exists $content->{data}{search}
   597          or die "$ME: No '{data}{search}' section in response\n";
   598      exists $content->{data}{search}{edges}
   599          or die "$ME: No '{data}{search}{edges}' section in response\n";
   600  
   601      # Confirm that there is exactly one such PR
   602      my @prs = @{ $content->{data}{search}{edges} };
   603      @prs > 0
   604          or die "$ME: WEIRD! No 'buildah vendor treadmill' PRs found!\n";
   605      @prs = grep { $_->{node}{title} eq $Treadmill_PR_Title } @prs
   606          or die "$ME: No PRs found with title '$Treadmill_PR_Title'\n";
   607      @prs = grep { $_->{node}{state} eq 'OPEN' } @prs
   608          or die "$ME: Found '$Treadmill_PR_Title' PRs, but none are OPEN\n";
   609      @prs == 1
   610          or die "$ME: Multiple OPEN '$Treadmill_PR_Title' PRs found!\n";
   611  
   612      # Yay. Found exactly one.
   613      return $prs[0]{node}{number};
   614  }
   615  
   616  #######################
   617  #  check_merge_bases  #  It's OK if our branch is newer than treadmill
   618  #######################
   619  sub check_merge_bases {
   620      my $treadmill_pr     = shift;       # e.g., 12345
   621      my $treadmill_branch = shift;       # e.g., b-v-p/pr12345/tmpNNN
   622  
   623      # Fetch latest main, for accurate comparison
   624      git('fetch', '-q', git_upstream(), 'main');
   625  
   626      my $forkpoint_cur       = git_forkpoint();
   627      my $forkpoint_treadmill = git_forkpoint($treadmill_branch);
   628  
   629      print "fork cur: $forkpoint_cur\nfork tm:  $forkpoint_treadmill\n"
   630          if $debug;
   631      if ($forkpoint_cur eq $forkpoint_treadmill) {
   632          progress("Nice. This branch is up-to-date wrt treadmill PR $treadmill_pr");
   633          return;
   634      }
   635  
   636      # They differ.
   637      if (git_is_ancestor($forkpoint_cur, $forkpoint_treadmill)) {
   638          warn <<"END_WARN";
   639  $ME: treadmill PR $treadmill_pr is based on
   640      a newer main than this branch. This means it might have
   641      more up-to-date patches.
   642  
   643  END_WARN
   644  
   645          if ($force_old_main) {
   646              warn "$ME: Proceeding due to --force-old-main\n";
   647              return;
   648          }
   649  
   650          # Cannot continue. Clean up side branch, and bail.
   651          git('branch', '-D', $treadmill_branch);
   652          warn "$ME: You might want to consider rebasing on latest main.\n";
   653          warn "$ME: Aborting. Use --force-old-main to continue without rebasing.\n";
   654          exit 1;
   655      }
   656      else {
   657          progress("Your branch is based on a newer main than treadmill PR $treadmill_pr. This is usually OK.");
   658      }
   659  }
   660  
   661  #################
   662  #  cherry_pick  #  cherry-pick a commit, updating its commit message
   663  #################
   664  sub cherry_pick {
   665      my $treadmill_pr     = shift;       # e.g., 12345
   666      my $treadmill_branch = shift;       # e.g., b-v-p/pr12345/tmpNNN
   667  
   668      progress("Cherry-picking from $treadmill_pr");
   669  
   670      # Create a temp script. Do so in /var/tmp because sometimes $TMPDIR
   671      # (e.g. /tmp) has noexec.
   672      my ($fh, $editor) = tempfile( "$ME.edit-commit-message.XXXXXXXX", DIR => "/var/tmp" );
   673      printf { $fh } <<'END_EDIT_SCRIPT', $ME, $VERSION, $treadmill_pr;
   674  #!/bin/bash
   675  
   676  if [[ -z "$1" ]]; then
   677      echo "FATAL: Did not get called with an arg" >&2
   678      exit 1
   679  fi
   680  
   681  msgfile=$1
   682  if [[ ! -e $msgfile ]]; then
   683      echo "FATAL: git-commit file does not exist: $msgfile" >&2
   684      exit 1
   685  fi
   686  
   687  tmpfile=$msgfile.tmp
   688  rm -f $tmpfile
   689  
   690  cat >$tmpfile <<EOF
   691  WIP: Fixes for vendoring Buildah
   692  
   693  This commit was automatically cherry-picked
   694  by %s v%s
   695  from the buildah vendor treadmill PR, #%s
   696  
   697  /vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
   698  > The git commit message from that PR is below. Please review it,
   699  > edit as necessary, then remove this comment block.
   700  \^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   701  
   702  EOF
   703  
   704  # Strip the "DO NOT MERGE" header from the treadmill PR, print only
   705  # the "Changes since YYYY-MM-DD" and subsequent lines
   706  sed -ne '/^Changes since /,$ p' <$msgfile >>$tmpfile
   707  mv $tmpfile $msgfile
   708  
   709  END_EDIT_SCRIPT
   710      close $fh
   711          or die "$ME: Error writing $editor: $!\n";
   712      chmod 0755 => $editor;
   713      local $ENV{EDITOR} = $editor;
   714      git('cherry-pick', '--allow-empty', '--edit', $treadmill_branch);
   715      unlink $editor;
   716  }
   717  
   718  # END   pick and its helpers
   719  ###############################################################################
   720  # BEGIN reset and its helpers
   721  
   722  sub do_reset {
   723      die "$ME: --sync takes no arguments; try $ME --help\n" if @_;
   724  
   725      my $current_branch = git_current_branch();
   726  
   727      # Make sure side branch == main (i.e., there are no commits on the branch)
   728      if (git('rev-parse', $current_branch) ne git('rev-parse', 'main')) {
   729          die "$ME: for --reset, $current_branch must == main\n";
   730      }
   731  
   732      # Pull main, and pivot back to this branch
   733      pull_main();
   734      git('checkout', '-q', $current_branch);
   735  
   736      git('rebase', '--empty=keep', 'main');
   737      git_commit_buildah('[none]');
   738  
   739      my $ymd = strftime("%Y-%m-%d", localtime);
   740      git('commit', '--allow-empty', '-s', '-m' => <<"END_COMMIT_MESSAGE");
   741  $Treadmill_PR_Title
   742  
   743  As you run --sync, please update this commit message with your
   744  actual changes.
   745  
   746  Changes since $ymd:
   747  END_COMMIT_MESSAGE
   748  
   749      progress("Done. You may now run --sync.\n");
   750  }
   751  
   752  # END   reset and its helpers
   753  ###############################################################################
   754  # BEGIN general-purpose helpers
   755  
   756  ##############
   757  #  progress  #  Progris riport Dr Strauss says I shud rite down what I think
   758  ##############
   759  sub progress {
   760      print $C_Highlight, "|\n+---> @_\n", $C_Reset;
   761  }
   762  
   763  #######################
   764  #  assert_clean_repo  #  Don't even think of running with local changes
   765  #######################
   766  sub assert_clean_repo {
   767      # During --sync we create a temporary copy of the treadmill branch,
   768      # in case something goes wrong. The branch is deleted on success.
   769      # If one exists, it means we may have lost work.
   770      my @relics = grep {
   771          m!^__buildah-treadmill-checkpoint/\d+-\d+$!
   772      } git('branch', '--list', '--format=%(refname:lstrip=2)');
   773      if (@relics) {
   774          if ($force_retry) {
   775              warn <<"END_WARN";
   776  $ME: WARNING: leftover checkpoint(s): @relics
   777  
   778     ...continuing due to --force-retry.
   779  
   780     If things work out, you can 'git branch -D @relics'
   781  END_WARN
   782  
   783              # OK, ugly override of a binary flag, but it's OK because
   784              # it helps with user-friendliness: offer a reminder upon
   785              # successful completion of the script.
   786              $force_retry = "git branch -D @relics";
   787          }
   788          else {
   789              warn <<"END_WARN";
   790  $ME: FATAL: leftover checkpoint: @relics
   791  
   792     This means that something went very wrong during an earlier sync run.
   793     Your git branch may be in an inconsistent state. Your work to date
   794     may be lost. This branch may be your only hope of recovering it.
   795  
   796     This is not something a script can resolve. You need to look at this
   797     branch, compare to your git HEAD, and manually reconcile any differences.
   798  
   799     If you really know what you're doing, i.e., if you've reconciled
   800     merge conflicts and have a pretty secure branch structure, try
   801     rerunning me with --force-retry. Or, if that checkpoint is a
   802     remnant from a past run, and you're ultra-certain that you don't
   803     need it, you can git branch -D @relics
   804  END_WARN
   805              exit 1;
   806          }
   807      }
   808  
   809      # OK so far. Now check for modified files.
   810      if (my @changed = git('status', '--porcelain', '--untracked=no')) {
   811          warn "$ME: Modified files in repo:\n";
   812          warn "    $_\n" for @changed;
   813          exit 1;
   814      }
   815  
   816      # ...and for untracked files under vendor/
   817      if (my @v = git('status', '--porcelain', '--untracked=all', 'vendor')) {
   818          warn "$ME: Untracked vendor files:\n";
   819          warn "    $_\n" for @v;
   820          exit 1;
   821      }
   822  }
   823  
   824  ########################
   825  #  git_current_branch  #  e.g., 'vendor_buildah'
   826  ########################
   827  sub git_current_branch() {
   828      my $b = git('rev-parse', '--abbrev-ref=strict', 'HEAD');
   829  
   830      # There is no circumstance in which we can ever be called from main
   831      die "$ME: must run from side branch, not main\n" if $b eq 'main';
   832      return $b;
   833  }
   834  
   835  ###################
   836  #  git_forkpoint  #  Hash at which branch (default: cur) branched from main
   837  ###################
   838  sub git_forkpoint {
   839      # '--fork-point vendor-branch' fails silently on Paul's git tree,
   840      # but plain merge-base works fine. My head hurts from trying to
   841      # understand the docs, so I give up. Just try fork-point first,
   842      # and if it fails, try without. #cargocult #gitishard
   843      my $forkpoint = eval { git('merge-base', '--fork-point', 'main', @_) };
   844      if ($@) {
   845          $forkpoint = git('merge-base', 'main', @_);
   846      }
   847      return $forkpoint;
   848  }
   849  
   850  #####################
   851  #  git_is_ancestor  #  Is hash1 an ancestor of hash2?
   852  #####################
   853  sub git_is_ancestor {
   854      # Use system(), not git(), because we don't want to abort on exit status
   855      my $rc = system('git', 'merge-base', '--is-ancestor', @_);
   856      die "$ME: Cannot continue\n"        if $? > 256; # e.g., Not a valid object
   857  
   858      # Translate shell 0/256 status to logical 1/0
   859      return !$rc;
   860  }
   861  
   862  ##################
   863  #  git_upstream  #  Name of true github upstream
   864  ##################
   865  sub git_upstream {
   866      for my $line (git('remote', '-v')) {
   867          my ($remote, $url, $type) = split(' ', $line);
   868          if ($url =~ m!github\.com.*containers/(podman|libpod)!) {
   869              if ($type =~ /fetch/) {
   870                  return $remote;
   871              }
   872          }
   873      }
   874  
   875      die "$ME: did not find a remote with 'github.com/containers/podman'\n";
   876  }
   877  
   878  ########################
   879  #  git_commit_buildah  #  Do the buildah commit
   880  ########################
   881  sub git_commit_buildah {
   882      my $buildah_version = shift;
   883  
   884      # When called by --reset, this can be empty
   885      git('commit', '-as', '--allow-empty', '-m', <<"END_COMMIT_MESSAGE");
   886  DO NOT MERGE: vendor in buildah \@ $buildah_version
   887  
   888  This is a JUNK COMMIT from $ME v$VERSION.
   889  
   890  DO NOT MERGE! This is just a way to keep the buildah-podman
   891  vendoring in sync. Refer to:
   892  
   893     $Docs_URL
   894  END_COMMIT_MESSAGE
   895  }
   896  
   897  #########
   898  #  git  #  Run a git command
   899  #########
   900  sub git {
   901      my @cmd = ('git', @_);
   902      print "\$ @cmd\n"                   if $verbose || $debug;
   903      open my $fh, '-|', @cmd
   904          or die "$ME: Cannot fork: $!\n";
   905      my @results;
   906      while (my $line = <$fh>) {
   907          chomp $line;
   908          push @results, $line;
   909      }
   910      close $fh
   911          or die "$ME: command failed: @cmd\n";
   912  
   913      return wantarray ? @results : join("\n", @results);
   914  }
   915  
   916  ##################################
   917  #  assert_buildah_vendor_commit  #  Fails if input arg is not a buildah vendor
   918  ##################################
   919  sub assert_buildah_vendor_commit {
   920      my $ref = shift;                    # in: probably HEAD or HEAD^
   921  
   922      my @deltas = git('diff', '--name-only', "$ref^", $ref);
   923  
   924      # It's OK if there are no deltas, e.g. immediately after a buildah vendor PR
   925      return if !@deltas;
   926  
   927      # It's OK if there are more modified files than just these.
   928      # It's not OK if any of these are missing.
   929      my @expect = qw(go.mod go.sum vendor/modules.txt);
   930      my @missing;
   931      for my $expect (@expect) {
   932          if (! grep { $_ eq $expect } @deltas) {
   933              push @missing, "$expect is unchanged";
   934          }
   935      }
   936  
   937      if (! grep { m!^vendor/\Q$Buildah\E/! } @deltas) {
   938          push @missing, "no changes under $Buildah";
   939      }
   940  
   941      return if !@missing;
   942  
   943      warn "$ME: $ref does not look like a buildah vendor commit:\n";
   944      warn "$ME:  - $_\n" for @missing;
   945      die "$ME: Cannot continue\n";
   946  }
   947  
   948  ######################
   949  #  vendored_buildah  #  Returns currently-vendored buildah
   950  ######################
   951  sub vendored_buildah {
   952      my $gomod_file = 'go.mod';
   953      my @gomod;
   954      if (@_) {
   955          # Called with a branch argument; fetch that version of go.mod
   956          $gomod_file = "@_:$gomod_file";
   957          @gomod = git('show', $gomod_file);
   958      }
   959      else {
   960          # No branch argument, read file
   961          open my $fh, '<', $gomod_file
   962            or die "$ME: Cannot read $gomod_file: $!\n";
   963          while (my $line = <$fh>) {
   964              chomp $line;
   965              push @gomod, $line;
   966          }
   967          close $fh;
   968      }
   969  
   970      for my $line (@gomod) {
   971          if ($line =~ m!^\s+\Q$Buildah\E\s+(\S+)!) {
   972              return $1;
   973          }
   974      }
   975  
   976      die "$ME: Could not find buildah in $gomod_file!\n";
   977  }
   978  
   979  # END   general-purpose helpers
   980  ###############################################################################