1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# Copyright 2019 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""A manager for patches."""
8
9from __future__ import print_function
10
11import argparse
12import json
13import os
14import subprocess
15import sys
16from collections import namedtuple
17
18import get_llvm_hash
19from failure_modes import FailureModes
20from subprocess_helpers import check_call
21from subprocess_helpers import check_output
22
23
24def is_directory(dir_path):
25  """Validates that the argument passed into 'argparse' is a directory."""
26
27  if not os.path.isdir(dir_path):
28    raise ValueError('Path is not a directory: %s' % dir_path)
29
30  return dir_path
31
32
33def is_patch_metadata_file(patch_metadata_file):
34  """Valides the argument into 'argparse' is a patch file."""
35
36  if not os.path.isfile(patch_metadata_file):
37    raise ValueError(
38        'Invalid patch metadata file provided: %s' % patch_metadata_file)
39
40  if not patch_metadata_file.endswith('.json'):
41    raise ValueError(
42        'Patch metadata file does not end in ".json": %s' % patch_metadata_file)
43
44  return patch_metadata_file
45
46
47def is_valid_failure_mode(failure_mode):
48  """Validates that the failure mode passed in is correct."""
49
50  cur_failure_modes = [mode.value for mode in FailureModes]
51
52  if failure_mode not in cur_failure_modes:
53    raise ValueError('Invalid failure mode provided: %s' % failure_mode)
54
55  return failure_mode
56
57
58def EnsureBisectModeAndSvnVersionAreSpecifiedTogether(failure_mode,
59                                                      good_svn_version):
60  """Validates that 'good_svn_version' is passed in only for bisection."""
61
62  if failure_mode != FailureModes.BISECT_PATCHES.value and good_svn_version:
63    raise ValueError('"good_svn_version" is only available for bisection.')
64  elif failure_mode == FailureModes.BISECT_PATCHES.value and \
65      not good_svn_version:
66    raise ValueError('A good SVN version is required for bisection (used by'
67                     '"git bisect start".')
68
69
70def GetCommandLineArgs():
71  """Get the required arguments from the command line."""
72
73  # Create parser and add optional command-line arguments.
74  parser = argparse.ArgumentParser(description='A manager for patches.')
75
76  # Add argument for the last good SVN version which is required by
77  # `git bisect start` (only valid for bisection mode).
78  parser.add_argument(
79      '--good_svn_version',
80      type=int,
81      help='INTERNAL USE ONLY... (used for bisection.)')
82
83  # Add argument for the number of patches it iterate. Only used when performing
84  # `git bisect run`.
85  parser.add_argument(
86      '--num_patches_to_iterate', type=int, help=argparse.SUPPRESS)
87
88  # Add argument for whether bisection should continue. Only used for
89  # 'bisect_patches.'
90  parser.add_argument(
91      '--continue_bisection',
92      type=bool,
93      default=False,
94      help='Determines whether bisection should continue after successfully '
95      'bisecting a patch (default: %(default)s) - only used for '
96      '"bisect_patches"')
97
98  # Trust src_path HEAD and svn_version.
99  parser.add_argument(
100      '--use_src_head',
101      action='store_true',
102      help='Use the HEAD of src_path directory as is, not necessarily the same '
103      'as the svn_version of upstream.')
104
105  # Add argument for the LLVM version to use for patch management.
106  parser.add_argument(
107      '--svn_version',
108      type=int,
109      required=True,
110      help='the LLVM svn version to use for patch management (determines '
111      'whether a patch is applicable)')
112
113  # Add argument for the patch metadata file that is in $FILESDIR.
114  parser.add_argument(
115      '--patch_metadata_file',
116      required=True,
117      type=is_patch_metadata_file,
118      help='the absolute path to the .json file in "$FILESDIR/" of the '
119      'package which has all the patches and their metadata if applicable')
120
121  # Add argument for the absolute path to the ebuild's $FILESDIR path.
122  # Example: '.../sys-devel/llvm/files/'.
123  parser.add_argument(
124      '--filesdir_path',
125      required=True,
126      type=is_directory,
127      help='the absolute path to the ebuild "files/" directory')
128
129  # Add argument for the absolute path to the unpacked sources.
130  parser.add_argument(
131      '--src_path',
132      required=True,
133      type=is_directory,
134      help='the absolute path to the unpacked LLVM sources')
135
136  # Add argument for the mode of the patch manager when handling failing
137  # applicable patches.
138  parser.add_argument(
139      '--failure_mode',
140      default=FailureModes.FAIL.value,
141      type=is_valid_failure_mode,
142      help='the mode of the patch manager when handling failed patches ' \
143          '(default: %(default)s)')
144
145  # Parse the command line.
146  args_output = parser.parse_args()
147
148  EnsureBisectModeAndSvnVersionAreSpecifiedTogether(
149      args_output.failure_mode, args_output.good_svn_version)
150
151  return args_output
152
153
154def GetHEADSVNVersion(src_path):
155  """Gets the SVN version of HEAD in the src tree."""
156
157  cmd = ['git', '-C', src_path, 'rev-parse', 'HEAD']
158
159  git_hash = check_output(cmd)
160
161  version = get_llvm_hash.GetVersionFrom(src_path, git_hash.rstrip())
162
163  return version
164
165
166def VerifyHEADIsTheSameAsSVNVersion(src_path, svn_version):
167  """Verifies that HEAD's SVN version matches 'svn_version'."""
168
169  head_svn_version = GetHEADSVNVersion(src_path)
170
171  if head_svn_version != svn_version:
172    raise ValueError('HEAD\'s SVN version %d does not match "svn_version"'
173                     ' %d, please move HEAD to "svn_version"s\' git hash.' %
174                     (head_svn_version, svn_version))
175
176
177def GetPathToPatch(filesdir_path, rel_patch_path):
178  """Gets the absolute path to a patch in $FILESDIR.
179
180  Args:
181    filesdir_path: The absolute path to $FILESDIR.
182    rel_patch_path: The relative path to the patch in '$FILESDIR/'.
183
184  Returns:
185    The absolute path to the patch in $FILESDIR.
186
187  Raises:
188    ValueError: Unable to find the path to the patch in $FILESDIR.
189  """
190
191  if not os.path.isdir(filesdir_path):
192    raise ValueError('Invalid path to $FILESDIR provided: %s' % filesdir_path)
193
194  # Combine $FILESDIR + relative path of patch to $FILESDIR.
195  patch_path = os.path.join(filesdir_path, rel_patch_path)
196
197  if not os.path.isfile(patch_path):
198    raise ValueError('The absolute path %s to the patch %s does not exist' %
199                     (patch_path, rel_patch_path))
200
201  return patch_path
202
203
204def GetPatchMetadata(patch_dict):
205  """Gets the patch's metadata.
206
207  Args:
208    patch_dict: A dictionary that has the patch metadata.
209
210  Returns:
211    A tuple that contains the metadata values.
212  """
213
214  # Get the metadata values of a patch if possible.
215  start_version = patch_dict.get('start_version', 0)
216  end_version = patch_dict.get('end_version', None)
217  is_critical = patch_dict.get('is_critical', False)
218
219  return start_version, end_version, is_critical
220
221
222def ApplyPatch(src_path, patch_path):
223  """Attempts to apply the patch.
224
225  Args:
226    src_path: The absolute path to the unpacked sources of the package.
227    patch_path: The absolute path to the patch in $FILESDIR/
228
229  Returns:
230    A boolean where 'True' means that the patch applied fine or 'False' means
231    that the patch failed to apply.
232  """
233
234  if not os.path.isdir(src_path):
235    raise ValueError('Invalid src path provided: %s' % src_path)
236
237  if not os.path.isfile(patch_path):
238    raise ValueError('Invalid patch file provided: %s' % patch_path)
239
240  # Test the patch with '--dry-run' before actually applying the patch.
241  test_patch_cmd = [
242      'patch', '--dry-run', '-d', src_path, '-f', '-p1', '-E',
243      '--no-backup-if-mismatch', '-i', patch_path
244  ]
245
246  # Cmd to apply a patch in the src unpack path.
247  apply_patch_cmd = [
248      'patch', '-d', src_path, '-f', '-p1', '-E', '--no-backup-if-mismatch',
249      '-i', patch_path
250  ]
251
252  try:
253    check_output(test_patch_cmd)
254
255  # If the mode is 'continue', then catching the exception makes sure that
256  # the program does not exit on the first failed applicable patch.
257  except subprocess.CalledProcessError:
258    # Test run on the patch failed to apply.
259    return False
260
261  # Test run succeeded on the patch.
262  check_output(apply_patch_cmd)
263
264  return True
265
266
267def UpdatePatchMetadataFile(patch_metadata_file, patches):
268  """Updates the .json file with unchanged and at least one changed patch.
269
270  Args:
271    patch_metadata_file: The absolute path to the .json file that has all the
272    patches and its metadata.
273    patches: A list of patches whose metadata were or were not updated.
274
275  Raises:
276    ValueError: The patch metadata file does not have the correct extension.
277  """
278
279  if not patch_metadata_file.endswith('.json'):
280    raise ValueError('File does not end in ".json": %s' % patch_metadata_file)
281
282  with open(patch_metadata_file, 'w') as patch_file:
283    json.dump(patches, patch_file, indent=4, separators=(',', ': '))
284
285
286def GetCommitHashesForBisection(src_path, good_svn_version, bad_svn_version):
287  """Gets the good and bad commit hashes required by `git bisect start`."""
288
289  bad_commit_hash = get_llvm_hash.GetGitHashFrom(src_path, bad_svn_version)
290
291  good_commit_hash = get_llvm_hash.GetGitHashFrom(src_path, good_svn_version)
292
293  return good_commit_hash, bad_commit_hash
294
295
296def PerformBisection(src_path, good_commit, bad_commit, svn_version,
297                     patch_metadata_file, filesdir_path, num_patches):
298  """Performs bisection to determine where a patch stops applying."""
299
300  start_cmd = [
301      'git', '-C', src_path, 'bisect', 'start', bad_commit, good_commit
302  ]
303
304  check_output(start_cmd)
305
306  run_cmd = [
307      'git', '-C', src_path, 'bisect', 'run',
308      os.path.abspath(__file__), '--svn_version',
309      '%d' % svn_version, '--patch_metadata_file', patch_metadata_file,
310      '--filesdir_path', filesdir_path, '--src_path', src_path,
311      '--failure_mode', 'internal_bisection', '--num_patches_to_iterate',
312      '%d' % num_patches
313  ]
314
315  check_call(run_cmd)
316
317  # Successfully bisected the patch, so retrieve the SVN version from the
318  # commit message.
319  get_bad_commit_hash_cmd = [
320      'git', '-C', src_path, 'rev-parse', 'refs/bisect/bad'
321  ]
322
323  git_hash = check_output(get_bad_commit_hash_cmd)
324
325  end_cmd = ['git', '-C', src_path, 'bisect', 'reset']
326
327  check_output(end_cmd)
328
329  # `git bisect run` returns the bad commit hash and the commit message.
330  version = get_llvm_hash.GetVersionFrom(src_path, git_hash.rstrip())
331
332  return version
333
334
335def CleanSrcTree(src_path):
336  """Cleans the source tree of the changes made in 'src_path'."""
337
338  reset_src_tree_cmd = ['git', '-C', src_path, 'reset', 'HEAD', '--hard']
339
340  check_output(reset_src_tree_cmd)
341
342  clean_src_tree_cmd = ['git', '-C', src_path, 'clean', '-fd']
343
344  check_output(clean_src_tree_cmd)
345
346
347def SaveSrcTreeState(src_path):
348  """Stashes the changes made so far to the source tree."""
349
350  save_src_tree_cmd = ['git', '-C', src_path, 'stash', '-a']
351
352  check_output(save_src_tree_cmd)
353
354
355def RestoreSrcTreeState(src_path, bad_commit_hash):
356  """Restores the changes made to the source tree."""
357
358  checkout_cmd = ['git', '-C', src_path, 'checkout', bad_commit_hash]
359
360  check_output(checkout_cmd)
361
362  get_changes_cmd = ['git', '-C', src_path, 'stash', 'pop']
363
364  check_output(get_changes_cmd)
365
366
367def HandlePatches(svn_version,
368                  patch_metadata_file,
369                  filesdir_path,
370                  src_path,
371                  mode,
372                  good_svn_version=None,
373                  num_patches_to_iterate=None,
374                  continue_bisection=False):
375  """Handles the patches in the .json file for the package.
376
377  Args:
378    svn_version: The LLVM version to use for patch management.
379    patch_metadata_file: The absolute path to the .json file in '$FILESDIR/'
380    that has all the patches and their metadata.
381    filesdir_path: The absolute path to $FILESDIR.
382    src_path: The absolute path to the unpacked destination of the package.
383    mode: The action to take when an applicable patch failed to apply.
384      Ex: 'FailureModes.FAIL'
385    good_svn_version: Only used by 'bisect_patches' which tells
386    `git bisect start` the good version.
387    num_patches_to_iterate: The number of patches to iterate in the .JSON file
388    (internal use). Only used by `git bisect run`.
389    continue_bisection: Only used for 'bisect_patches' mode. If flag is set,
390    then bisection will continue to the next patch when successfully bisected a
391    patch.
392
393  Returns:
394    Depending on the mode, 'None' would be returned if everything went well or
395    the .json file was not updated. Otherwise, a list or multiple lists would
396    be returned that indicates what has changed.
397
398  Raises:
399    ValueError: The patch metadata file does not exist or does not end with
400    '.json' or the absolute path to $FILESDIR does not exist or the unpacked
401    path does not exist or if the mode is 'fail', then an applicable patch
402    failed to apply.
403  """
404
405  # A flag for whether the mode specified would possible modify the patches.
406  can_modify_patches = False
407
408  # 'fail' or 'continue' mode would not modify a patch's metadata, so the .json
409  # file would stay the same.
410  if mode != FailureModes.FAIL and mode != FailureModes.CONTINUE:
411    can_modify_patches = True
412
413  # A flag that determines whether at least one patch's metadata was
414  # updated due to the mode that is passed in.
415  updated_patch = False
416
417  # A list of patches that will be in the updated .json file.
418  applicable_patches = []
419
420  # A list of patches that successfully applied.
421  applied_patches = []
422
423  # A list of patches that were disabled.
424  disabled_patches = []
425
426  # A list of bisected patches.
427  bisected_patches = []
428
429  # A list of non applicable patches.
430  non_applicable_patches = []
431
432  # A list of patches that will not be included in the updated .json file
433  removed_patches = []
434
435  # Whether the patch metadata file was modified where 'None' means that the
436  # patch metadata file was not modified otherwise the absolute path to the
437  # patch metadata file is stored.
438  modified_metadata = None
439
440  # A list of patches that failed to apply.
441  failed_patches = []
442
443  with open(patch_metadata_file) as patch_file:
444    patch_file_contents = json.load(patch_file)
445
446    if mode == FailureModes.BISECT_PATCHES:
447      # A good and bad commit are required by `git bisect start`.
448      good_commit, bad_commit = GetCommitHashesForBisection(
449          src_path, good_svn_version, svn_version)
450
451    # Patch format:
452    # {
453    #   "rel_patch_path" : "[REL_PATCH_PATH_FROM_$FILESDIR]"
454    #   [PATCH_METADATA] if available.
455    # }
456    #
457    # For each patch, find the path to it in $FILESDIR and get its metadata if
458    # available, then check if the patch is applicable.
459    for patch_dict_index, cur_patch_dict in enumerate(patch_file_contents):
460      # Used by the internal bisection. All the patches in the interval [0, N]
461      # have been iterated.
462      if num_patches_to_iterate and \
463          (patch_dict_index + 1) > num_patches_to_iterate:
464        break
465
466      # Get the absolute path to the patch in $FILESDIR.
467      path_to_patch = GetPathToPatch(filesdir_path,
468                                     cur_patch_dict['rel_patch_path'])
469
470      # Get the patch's metadata.
471      #
472      # Index information of 'patch_metadata':
473      #   [0]: start_version
474      #   [1]: end_version
475      #   [2]: is_critical
476      patch_metadata = GetPatchMetadata(cur_patch_dict)
477
478      if not patch_metadata[1]:
479        # Patch does not have an 'end_version' value which implies 'end_version'
480        # == 'inf' ('svn_version' will always be less than 'end_version'), so
481        # the patch is applicable if 'svn_version' >= 'start_version'.
482        patch_applicable = svn_version >= patch_metadata[0]
483      else:
484        # Patch is applicable if 'svn_version' >= 'start_version' &&
485        # "svn_version" < "end_version".
486        patch_applicable = (svn_version >= patch_metadata[0] and \
487                            svn_version < patch_metadata[1])
488
489      if can_modify_patches:
490        # Add to the list only if the mode can potentially modify a patch.
491        #
492        # If the mode is 'remove_patches', then all patches that are
493        # applicable or are from the future will be added to the updated .json
494        # file and all patches that are not applicable will be added to the
495        # remove patches list which will not be included in the updated .json
496        # file.
497        if patch_applicable or svn_version < patch_metadata[0] or \
498            mode != FailureModes.REMOVE_PATCHES:
499          applicable_patches.append(cur_patch_dict)
500        elif mode == FailureModes.REMOVE_PATCHES:
501          removed_patches.append(path_to_patch)
502
503          if not modified_metadata:
504            # At least one patch will be removed from the .json file.
505            modified_metadata = patch_metadata_file
506
507      if not patch_applicable:
508        non_applicable_patches.append(os.path.basename(path_to_patch))
509
510      # There is no need to apply patches in 'remove_patches' mode because the
511      # mode removes patches that do not apply anymore based off of
512      # 'svn_version.'
513      if patch_applicable and mode != FailureModes.REMOVE_PATCHES:
514        patch_applied = ApplyPatch(src_path, path_to_patch)
515
516        if not patch_applied:  # Failed to apply patch.
517          failed_patches.append(os.path.basename(path_to_patch))
518
519          # Check the mode to determine what action to take on the failing
520          # patch.
521          if mode == FailureModes.DISABLE_PATCHES:
522            # Set the patch's 'end_version' to 'svn_version' so the patch
523            # would not be applicable anymore (i.e. the patch's 'end_version'
524            # would not be greater than 'svn_version').
525
526            # Last element in 'applicable_patches' is the current patch.
527            applicable_patches[-1]['end_version'] = svn_version
528
529            disabled_patches.append(os.path.basename(path_to_patch))
530
531            if not updated_patch:
532              # At least one patch has been modified, so the .json file
533              # will be updated with the new patch metadata.
534              updated_patch = True
535
536              modified_metadata = patch_metadata_file
537          elif mode == FailureModes.BISECT_PATCHES:
538            # Figure out where the patch's stops applying and set the patch's
539            # 'end_version' to that version.
540
541            # Do not want to overwrite the changes to the current progress of
542            # 'bisect_patches' on the source tree.
543            SaveSrcTreeState(src_path)
544
545            # Need a clean source tree for `git bisect run` to avoid unnecessary
546            # fails for patches.
547            CleanSrcTree(src_path)
548
549            print('\nStarting to bisect patch %s for SVN version %d:\n' %
550                  (os.path.basename(cur_patch_dict['rel_patch_path']),
551                   svn_version))
552
553            # Performs the bisection: calls `git bisect start` and
554            # `git bisect run`, where `git bisect run` is going to call this
555            # script as many times as needed with specific arguments.
556            bad_svn_version = PerformBisection(
557                src_path, good_commit, bad_commit, svn_version,
558                patch_metadata_file, filesdir_path, patch_dict_index + 1)
559
560            print('\nSuccessfully bisected patch %s, starts to fail to apply '
561                  'at %d\n' % (os.path.basename(
562                      cur_patch_dict['rel_patch_path']), bad_svn_version))
563
564            # Overwrite the .JSON file with the new 'end_version' for the
565            # current failed patch so that if there are other patches that
566            # fail to apply, then the 'end_version' for the current patch could
567            # be applicable when `git bisect run` is performed on the next
568            # failed patch because the same .JSON file is used for `git bisect
569            # run`.
570            patch_file_contents[patch_dict_index][
571                'end_version'] = bad_svn_version
572            UpdatePatchMetadataFile(patch_metadata_file, patch_file_contents)
573
574            # Clear the changes made to the source tree by `git bisect run`.
575            CleanSrcTree(src_path)
576
577            if not continue_bisection:
578              # Exiting program early because 'continue_bisection' is not set.
579              sys.exit(0)
580
581            bisected_patches.append(
582                '%s starts to fail to apply at %d' % (os.path.basename(
583                    cur_patch_dict['rel_patch_path']), bad_svn_version))
584
585            # Continue where 'bisect_patches' left off.
586            RestoreSrcTreeState(src_path, bad_commit)
587
588            if not modified_metadata:
589              # At least one patch's 'end_version' has been updated.
590              modified_metadata = patch_metadata_file
591
592          elif mode == FailureModes.FAIL:
593            if applied_patches:
594              print('The following patches applied successfully up to the '
595                    'failed patch:')
596              print('\n'.join(applied_patches))
597
598            # Throw an exception on the first patch that failed to apply.
599            raise ValueError(
600                'Failed to apply patch: %s' % os.path.basename(path_to_patch))
601          elif mode == FailureModes.INTERNAL_BISECTION:
602            # Determine the exit status for `git bisect run` because of the
603            # failed patch in the interval [0, N].
604            #
605            # NOTE: `git bisect run` exit codes are as follows:
606            #   130: Terminates the bisection.
607            #   1: Similar as `git bisect bad`.
608
609            # Some patch in the interval [0, N) failed, so terminate bisection
610            # (the patch stack is broken).
611            if (patch_dict_index + 1) != num_patches_to_iterate:
612              print('\nTerminating bisection due to patch %s failed to apply '
613                    'on SVN version %d.\n' % (os.path.basename(
614                        cur_patch_dict['rel_patch_path']), svn_version))
615
616              # Man page for `git bisect run` states that any value over 127
617              # terminates it.
618              sys.exit(130)
619
620            # Changes to the source tree need to be removed, otherwise some
621            # patches may fail when applying the patch to the source tree when
622            # `git bisect run` calls this script again.
623            CleanSrcTree(src_path)
624
625            # The last patch in the interval [0, N] failed to apply, so let
626            # `git bisect run` know that the last patch (the patch that failed
627            # originally which led to `git bisect run` to be invoked) is bad
628            # with exit code 1.
629            sys.exit(1)
630        else:  # Successfully applied patch
631          applied_patches.append(os.path.basename(path_to_patch))
632
633  # All patches in the interval [0, N] applied successfully, so let
634  # `git bisect run` know that the program exited with exit code 0 (good).
635  if mode == FailureModes.INTERNAL_BISECTION:
636    # Changes to the source tree need to be removed, otherwise some
637    # patches may fail when applying the patch to the source tree when
638    # `git bisect run` calls this script again.
639    #
640    # Also, if `git bisect run` will NOT call this script again (terminated) and
641    # if the source tree changes are not removed, `git bisect reset` will
642    # complain that the changes would need to be 'stashed' or 'removed' in
643    # order to reset HEAD back to the bad commit's git hash, so HEAD will remain
644    # on the last git hash used by `git bisect run`.
645    CleanSrcTree(src_path)
646
647    # NOTE: Exit code 0 is similar to `git bisect good`.
648    sys.exit(0)
649
650  # Create a namedtuple of the patch results.
651  PatchInfo = namedtuple('PatchInfo', [
652      'applied_patches', 'failed_patches', 'non_applicable_patches',
653      'disabled_patches', 'removed_patches', 'modified_metadata'
654  ])
655
656  patch_info = PatchInfo(
657      applied_patches=applied_patches,
658      failed_patches=failed_patches,
659      non_applicable_patches=non_applicable_patches,
660      disabled_patches=disabled_patches,
661      removed_patches=removed_patches,
662      modified_metadata=modified_metadata)
663
664  # Determine post actions after iterating through the patches.
665  if mode == FailureModes.REMOVE_PATCHES:
666    if removed_patches:
667      UpdatePatchMetadataFile(patch_metadata_file, applicable_patches)
668  elif mode == FailureModes.DISABLE_PATCHES:
669    if updated_patch:
670      UpdatePatchMetadataFile(patch_metadata_file, applicable_patches)
671  elif mode == FailureModes.BISECT_PATCHES:
672    PrintPatchResults(patch_info)
673    if modified_metadata:
674      print('\nThe following patches have been bisected:')
675      print('\n'.join(bisected_patches))
676
677    # Exiting early because 'bisect_patches' will not be called from other
678    # scripts, only this script uses 'bisect_patches'. The intent is to provide
679    # bisection information on the patches and aid in the bisection process.
680    sys.exit(0)
681
682  return patch_info
683
684
685def PrintPatchResults(patch_info):
686  """Prints the results of handling the patches of a package.
687
688  Args:
689    patch_info: A namedtuple that has information on the patches.
690  """
691
692  if patch_info.applied_patches:
693    print('\nThe following patches applied successfully:')
694    print('\n'.join(patch_info.applied_patches))
695
696  if patch_info.failed_patches:
697    print('\nThe following patches failed to apply:')
698    print('\n'.join(patch_info.failed_patches))
699
700  if patch_info.non_applicable_patches:
701    print('\nThe following patches were not applicable:')
702    print('\n'.join(patch_info.non_applicable_patches))
703
704  if patch_info.modified_metadata:
705    print('\nThe patch metadata file %s has been modified' % os.path.basename(
706        patch_info.modified_metadata))
707
708  if patch_info.disabled_patches:
709    print('\nThe following patches were disabled:')
710    print('\n'.join(patch_info.disabled_patches))
711
712  if patch_info.removed_patches:
713    print('\nThe following patches were removed from the patch metadata file:')
714    for cur_patch_path in patch_info.removed_patches:
715      print('%s' % os.path.basename(cur_patch_path))
716
717
718def main():
719  """Applies patches to the source tree and takes action on a failed patch."""
720
721  args_output = GetCommandLineArgs()
722
723  if args_output.failure_mode != FailureModes.INTERNAL_BISECTION.value:
724    # If the SVN version of HEAD is not the same as 'svn_version', then some
725    # patches that fail to apply could successfully apply if HEAD's SVN version
726    # was the same as 'svn_version'. In other words, HEAD's git hash should be
727    # what is being updated to (e.g. LLVM_NEXT_HASH).
728    if not args_output.use_src_head:
729      VerifyHEADIsTheSameAsSVNVersion(args_output.src_path,
730                                      args_output.svn_version)
731  else:
732    # `git bisect run` called this script.
733    #
734    # `git bisect run` moves HEAD each time it invokes this script, so set the
735    # 'svn_version' to be current HEAD's SVN version so that the previous
736    # SVN version is not used in determining whether a patch is applicable.
737    args_output.svn_version = GetHEADSVNVersion(args_output.src_path)
738
739  # Get the results of handling the patches of the package.
740  patch_info = HandlePatches(
741      args_output.svn_version, args_output.patch_metadata_file,
742      args_output.filesdir_path, args_output.src_path,
743      FailureModes(args_output.failure_mode), args_output.good_svn_version,
744      args_output.num_patches_to_iterate, args_output.continue_bisection)
745
746  PrintPatchResults(patch_info)
747
748
749if __name__ == '__main__':
750  main()
751