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