• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  # FIXME(b/221489531): Remove  start_version & end_version
216  if 'version_range' in patch_dict:
217    start_version = patch_dict['version_range'].get('from', 0)
218    end_version = patch_dict['version_range'].get('until', None)
219  else:
220    start_version = patch_dict.get('start_version', 0)
221    end_version = patch_dict.get('end_version', None)
222  is_critical = patch_dict.get('is_critical', False)
223
224  return start_version, end_version, is_critical
225
226
227def ApplyPatch(src_path, patch_path):
228  """Attempts to apply the patch.
229
230  Args:
231    src_path: The absolute path to the unpacked sources of the package.
232    patch_path: The absolute path to the patch in $FILESDIR/
233
234  Returns:
235    A boolean where 'True' means that the patch applied fine or 'False' means
236    that the patch failed to apply.
237  """
238
239  if not os.path.isdir(src_path):
240    raise ValueError('Invalid src path provided: %s' % src_path)
241
242  if not os.path.isfile(patch_path):
243    raise ValueError('Invalid patch file provided: %s' % patch_path)
244
245  # Test the patch with '--dry-run' before actually applying the patch.
246  test_patch_cmd = [
247      'patch', '--dry-run', '-d', src_path, '-f', '-p1', '-E',
248      '--no-backup-if-mismatch', '-i', patch_path
249  ]
250
251  # Cmd to apply a patch in the src unpack path.
252  apply_patch_cmd = [
253      'patch', '-d', src_path, '-f', '-p1', '-E', '--no-backup-if-mismatch',
254      '-i', patch_path
255  ]
256
257  try:
258    check_output(test_patch_cmd)
259
260  # If the mode is 'continue', then catching the exception makes sure that
261  # the program does not exit on the first failed applicable patch.
262  except subprocess.CalledProcessError:
263    # Test run on the patch failed to apply.
264    return False
265
266  # Test run succeeded on the patch.
267  check_output(apply_patch_cmd)
268
269  return True
270
271
272def UpdatePatchMetadataFile(patch_metadata_file, patches):
273  """Updates the .json file with unchanged and at least one changed patch.
274
275  Args:
276    patch_metadata_file: The absolute path to the .json file that has all the
277    patches and its metadata.
278    patches: A list of patches whose metadata were or were not updated.
279
280  Raises:
281    ValueError: The patch metadata file does not have the correct extension.
282  """
283
284  if not patch_metadata_file.endswith('.json'):
285    raise ValueError('File does not end in ".json": %s' % patch_metadata_file)
286
287  with open(patch_metadata_file, 'w') as patch_file:
288    json.dump(patches, patch_file, indent=4, separators=(',', ': '))
289
290
291def GetCommitHashesForBisection(src_path, good_svn_version, bad_svn_version):
292  """Gets the good and bad commit hashes required by `git bisect start`."""
293
294  bad_commit_hash = get_llvm_hash.GetGitHashFrom(src_path, bad_svn_version)
295
296  good_commit_hash = get_llvm_hash.GetGitHashFrom(src_path, good_svn_version)
297
298  return good_commit_hash, bad_commit_hash
299
300
301def PerformBisection(src_path, good_commit, bad_commit, svn_version,
302                     patch_metadata_file, filesdir_path, num_patches):
303  """Performs bisection to determine where a patch stops applying."""
304
305  start_cmd = [
306      'git', '-C', src_path, 'bisect', 'start', bad_commit, good_commit
307  ]
308
309  check_output(start_cmd)
310
311  run_cmd = [
312      'git', '-C', src_path, 'bisect', 'run',
313      os.path.abspath(__file__), '--svn_version',
314      '%d' % svn_version, '--patch_metadata_file', patch_metadata_file,
315      '--filesdir_path', filesdir_path, '--src_path', src_path,
316      '--failure_mode', 'internal_bisection', '--num_patches_to_iterate',
317      '%d' % num_patches
318  ]
319
320  check_call(run_cmd)
321
322  # Successfully bisected the patch, so retrieve the SVN version from the
323  # commit message.
324  get_bad_commit_hash_cmd = [
325      'git', '-C', src_path, 'rev-parse', 'refs/bisect/bad'
326  ]
327
328  git_hash = check_output(get_bad_commit_hash_cmd)
329
330  end_cmd = ['git', '-C', src_path, 'bisect', 'reset']
331
332  check_output(end_cmd)
333
334  # `git bisect run` returns the bad commit hash and the commit message.
335  version = get_llvm_hash.GetVersionFrom(src_path, git_hash.rstrip())
336
337  return version
338
339
340def CleanSrcTree(src_path):
341  """Cleans the source tree of the changes made in 'src_path'."""
342
343  reset_src_tree_cmd = ['git', '-C', src_path, 'reset', 'HEAD', '--hard']
344
345  check_output(reset_src_tree_cmd)
346
347  clean_src_tree_cmd = ['git', '-C', src_path, 'clean', '-fd']
348
349  check_output(clean_src_tree_cmd)
350
351
352def SaveSrcTreeState(src_path):
353  """Stashes the changes made so far to the source tree."""
354
355  save_src_tree_cmd = ['git', '-C', src_path, 'stash', '-a']
356
357  check_output(save_src_tree_cmd)
358
359
360def RestoreSrcTreeState(src_path, bad_commit_hash):
361  """Restores the changes made to the source tree."""
362
363  checkout_cmd = ['git', '-C', src_path, 'checkout', bad_commit_hash]
364
365  check_output(checkout_cmd)
366
367  get_changes_cmd = ['git', '-C', src_path, 'stash', 'pop']
368
369  check_output(get_changes_cmd)
370
371
372def HandlePatches(svn_version,
373                  patch_metadata_file,
374                  filesdir_path,
375                  src_path,
376                  mode,
377                  good_svn_version=None,
378                  num_patches_to_iterate=None,
379                  continue_bisection=False):
380  """Handles the patches in the .json file for the package.
381
382  Args:
383    svn_version: The LLVM version to use for patch management.
384    patch_metadata_file: The absolute path to the .json file in '$FILESDIR/'
385    that has all the patches and their metadata.
386    filesdir_path: The absolute path to $FILESDIR.
387    src_path: The absolute path to the unpacked destination of the package.
388    mode: The action to take when an applicable patch failed to apply.
389      Ex: 'FailureModes.FAIL'
390    good_svn_version: Only used by 'bisect_patches' which tells
391    `git bisect start` the good version.
392    num_patches_to_iterate: The number of patches to iterate in the .JSON file
393    (internal use). Only used by `git bisect run`.
394    continue_bisection: Only used for 'bisect_patches' mode. If flag is set,
395    then bisection will continue to the next patch when successfully bisected a
396    patch.
397
398  Returns:
399    Depending on the mode, 'None' would be returned if everything went well or
400    the .json file was not updated. Otherwise, a list or multiple lists would
401    be returned that indicates what has changed.
402
403  Raises:
404    ValueError: The patch metadata file does not exist or does not end with
405    '.json' or the absolute path to $FILESDIR does not exist or the unpacked
406    path does not exist or if the mode is 'fail', then an applicable patch
407    failed to apply.
408  """
409
410  # A flag for whether the mode specified would possible modify the patches.
411  can_modify_patches = False
412
413  # 'fail' or 'continue' mode would not modify a patch's metadata, so the .json
414  # file would stay the same.
415  if mode != FailureModes.FAIL and mode != FailureModes.CONTINUE:
416    can_modify_patches = True
417
418  # A flag that determines whether at least one patch's metadata was
419  # updated due to the mode that is passed in.
420  updated_patch = False
421
422  # A list of patches that will be in the updated .json file.
423  applicable_patches = []
424
425  # A list of patches that successfully applied.
426  applied_patches = []
427
428  # A list of patches that were disabled.
429  disabled_patches = []
430
431  # A list of bisected patches.
432  bisected_patches = []
433
434  # A list of non applicable patches.
435  non_applicable_patches = []
436
437  # A list of patches that will not be included in the updated .json file
438  removed_patches = []
439
440  # Whether the patch metadata file was modified where 'None' means that the
441  # patch metadata file was not modified otherwise the absolute path to the
442  # patch metadata file is stored.
443  modified_metadata = None
444
445  # A list of patches that failed to apply.
446  failed_patches = []
447
448  with open(patch_metadata_file) as patch_file:
449    patch_file_contents = json.load(patch_file)
450
451    if mode == FailureModes.BISECT_PATCHES:
452      # A good and bad commit are required by `git bisect start`.
453      good_commit, bad_commit = GetCommitHashesForBisection(
454          src_path, good_svn_version, svn_version)
455
456    # Patch format:
457    # {
458    #   "rel_patch_path" : "[REL_PATCH_PATH_FROM_$FILESDIR]"
459    #   [PATCH_METADATA] if available.
460    # }
461    #
462    # For each patch, find the path to it in $FILESDIR and get its metadata if
463    # available, then check if the patch is applicable.
464    for patch_dict_index, cur_patch_dict in enumerate(patch_file_contents):
465      # Used by the internal bisection. All the patches in the interval [0, N]
466      # have been iterated.
467      if num_patches_to_iterate and \
468          (patch_dict_index + 1) > num_patches_to_iterate:
469        break
470
471      # Get the absolute path to the patch in $FILESDIR.
472      path_to_patch = GetPathToPatch(filesdir_path,
473                                     cur_patch_dict['rel_patch_path'])
474
475      # Get the patch's metadata.
476      #
477      # Index information of 'patch_metadata':
478      #   [0]: start_version
479      #   [1]: end_version
480      #   [2]: is_critical
481      patch_metadata = GetPatchMetadata(cur_patch_dict)
482
483      if not patch_metadata[1]:
484        # Patch does not have an 'end_version' value which implies 'end_version'
485        # == 'inf' ('svn_version' will always be less than 'end_version'), so
486        # the patch is applicable if 'svn_version' >= 'start_version'.
487        patch_applicable = svn_version >= patch_metadata[0]
488      else:
489        # Patch is applicable if 'svn_version' >= 'start_version' &&
490        # "svn_version" < "end_version".
491        patch_applicable = (svn_version >= patch_metadata[0] and \
492                            svn_version < patch_metadata[1])
493
494      if can_modify_patches:
495        # Add to the list only if the mode can potentially modify a patch.
496        #
497        # If the mode is 'remove_patches', then all patches that are
498        # applicable or are from the future will be added to the updated .json
499        # file and all patches that are not applicable will be added to the
500        # remove patches list which will not be included in the updated .json
501        # file.
502        if patch_applicable or svn_version < patch_metadata[0] or \
503            mode != FailureModes.REMOVE_PATCHES:
504          applicable_patches.append(cur_patch_dict)
505        elif mode == FailureModes.REMOVE_PATCHES:
506          removed_patches.append(path_to_patch)
507
508          if not modified_metadata:
509            # At least one patch will be removed from the .json file.
510            modified_metadata = patch_metadata_file
511
512      if not patch_applicable:
513        non_applicable_patches.append(os.path.basename(path_to_patch))
514
515      # There is no need to apply patches in 'remove_patches' mode because the
516      # mode removes patches that do not apply anymore based off of
517      # 'svn_version.'
518      if patch_applicable and mode != FailureModes.REMOVE_PATCHES:
519        patch_applied = ApplyPatch(src_path, path_to_patch)
520
521        if not patch_applied:  # Failed to apply patch.
522          failed_patches.append(os.path.basename(path_to_patch))
523
524          # Check the mode to determine what action to take on the failing
525          # patch.
526          if mode == FailureModes.DISABLE_PATCHES:
527            # Set the patch's 'end_version' to 'svn_version' so the patch
528            # would not be applicable anymore (i.e. the patch's 'end_version'
529            # would not be greater than 'svn_version').
530
531            # Last element in 'applicable_patches' is the current patch.
532            applicable_patches[-1]['end_version'] = svn_version
533
534            disabled_patches.append(os.path.basename(path_to_patch))
535
536            if not updated_patch:
537              # At least one patch has been modified, so the .json file
538              # will be updated with the new patch metadata.
539              updated_patch = True
540
541              modified_metadata = patch_metadata_file
542          elif mode == FailureModes.BISECT_PATCHES:
543            # Figure out where the patch's stops applying and set the patch's
544            # 'end_version' to that version.
545
546            # Do not want to overwrite the changes to the current progress of
547            # 'bisect_patches' on the source tree.
548            SaveSrcTreeState(src_path)
549
550            # Need a clean source tree for `git bisect run` to avoid unnecessary
551            # fails for patches.
552            CleanSrcTree(src_path)
553
554            print('\nStarting to bisect patch %s for SVN version %d:\n' %
555                  (os.path.basename(cur_patch_dict['rel_patch_path']),
556                   svn_version))
557
558            # Performs the bisection: calls `git bisect start` and
559            # `git bisect run`, where `git bisect run` is going to call this
560            # script as many times as needed with specific arguments.
561            bad_svn_version = PerformBisection(
562                src_path, good_commit, bad_commit, svn_version,
563                patch_metadata_file, filesdir_path, patch_dict_index + 1)
564
565            print('\nSuccessfully bisected patch %s, starts to fail to apply '
566                  'at %d\n' % (os.path.basename(
567                      cur_patch_dict['rel_patch_path']), bad_svn_version))
568
569            # Overwrite the .JSON file with the new 'end_version' for the
570            # current failed patch so that if there are other patches that
571            # fail to apply, then the 'end_version' for the current patch could
572            # be applicable when `git bisect run` is performed on the next
573            # failed patch because the same .JSON file is used for `git bisect
574            # run`.
575            patch_file_contents[patch_dict_index][
576                'end_version'] = bad_svn_version
577            UpdatePatchMetadataFile(patch_metadata_file, patch_file_contents)
578
579            # Clear the changes made to the source tree by `git bisect run`.
580            CleanSrcTree(src_path)
581
582            if not continue_bisection:
583              # Exiting program early because 'continue_bisection' is not set.
584              sys.exit(0)
585
586            bisected_patches.append(
587                '%s starts to fail to apply at %d' % (os.path.basename(
588                    cur_patch_dict['rel_patch_path']), bad_svn_version))
589
590            # Continue where 'bisect_patches' left off.
591            RestoreSrcTreeState(src_path, bad_commit)
592
593            if not modified_metadata:
594              # At least one patch's 'end_version' has been updated.
595              modified_metadata = patch_metadata_file
596
597          elif mode == FailureModes.FAIL:
598            if applied_patches:
599              print('The following patches applied successfully up to the '
600                    'failed patch:')
601              print('\n'.join(applied_patches))
602
603            # Throw an exception on the first patch that failed to apply.
604            raise ValueError(
605                'Failed to apply patch: %s' % os.path.basename(path_to_patch))
606          elif mode == FailureModes.INTERNAL_BISECTION:
607            # Determine the exit status for `git bisect run` because of the
608            # failed patch in the interval [0, N].
609            #
610            # NOTE: `git bisect run` exit codes are as follows:
611            #   130: Terminates the bisection.
612            #   1: Similar as `git bisect bad`.
613
614            # Some patch in the interval [0, N) failed, so terminate bisection
615            # (the patch stack is broken).
616            if (patch_dict_index + 1) != num_patches_to_iterate:
617              print('\nTerminating bisection due to patch %s failed to apply '
618                    'on SVN version %d.\n' % (os.path.basename(
619                        cur_patch_dict['rel_patch_path']), svn_version))
620
621              # Man page for `git bisect run` states that any value over 127
622              # terminates it.
623              sys.exit(130)
624
625            # Changes to the source tree need to be removed, otherwise some
626            # patches may fail when applying the patch to the source tree when
627            # `git bisect run` calls this script again.
628            CleanSrcTree(src_path)
629
630            # The last patch in the interval [0, N] failed to apply, so let
631            # `git bisect run` know that the last patch (the patch that failed
632            # originally which led to `git bisect run` to be invoked) is bad
633            # with exit code 1.
634            sys.exit(1)
635        else:  # Successfully applied patch
636          applied_patches.append(os.path.basename(path_to_patch))
637
638  # All patches in the interval [0, N] applied successfully, so let
639  # `git bisect run` know that the program exited with exit code 0 (good).
640  if mode == FailureModes.INTERNAL_BISECTION:
641    # Changes to the source tree need to be removed, otherwise some
642    # patches may fail when applying the patch to the source tree when
643    # `git bisect run` calls this script again.
644    #
645    # Also, if `git bisect run` will NOT call this script again (terminated) and
646    # if the source tree changes are not removed, `git bisect reset` will
647    # complain that the changes would need to be 'stashed' or 'removed' in
648    # order to reset HEAD back to the bad commit's git hash, so HEAD will remain
649    # on the last git hash used by `git bisect run`.
650    CleanSrcTree(src_path)
651
652    # NOTE: Exit code 0 is similar to `git bisect good`.
653    sys.exit(0)
654
655  # Create a namedtuple of the patch results.
656  PatchInfo = namedtuple('PatchInfo', [
657      'applied_patches', 'failed_patches', 'non_applicable_patches',
658      'disabled_patches', 'removed_patches', 'modified_metadata'
659  ])
660
661  patch_info = PatchInfo(
662      applied_patches=applied_patches,
663      failed_patches=failed_patches,
664      non_applicable_patches=non_applicable_patches,
665      disabled_patches=disabled_patches,
666      removed_patches=removed_patches,
667      modified_metadata=modified_metadata)
668
669  # Determine post actions after iterating through the patches.
670  if mode == FailureModes.REMOVE_PATCHES:
671    if removed_patches:
672      UpdatePatchMetadataFile(patch_metadata_file, applicable_patches)
673  elif mode == FailureModes.DISABLE_PATCHES:
674    if updated_patch:
675      UpdatePatchMetadataFile(patch_metadata_file, applicable_patches)
676  elif mode == FailureModes.BISECT_PATCHES:
677    PrintPatchResults(patch_info)
678    if modified_metadata:
679      print('\nThe following patches have been bisected:')
680      print('\n'.join(bisected_patches))
681
682    # Exiting early because 'bisect_patches' will not be called from other
683    # scripts, only this script uses 'bisect_patches'. The intent is to provide
684    # bisection information on the patches and aid in the bisection process.
685    sys.exit(0)
686
687  return patch_info
688
689
690def PrintPatchResults(patch_info):
691  """Prints the results of handling the patches of a package.
692
693  Args:
694    patch_info: A namedtuple that has information on the patches.
695  """
696
697  if patch_info.applied_patches:
698    print('\nThe following patches applied successfully:')
699    print('\n'.join(patch_info.applied_patches))
700
701  if patch_info.failed_patches:
702    print('\nThe following patches failed to apply:')
703    print('\n'.join(patch_info.failed_patches))
704
705  if patch_info.non_applicable_patches:
706    print('\nThe following patches were not applicable:')
707    print('\n'.join(patch_info.non_applicable_patches))
708
709  if patch_info.modified_metadata:
710    print('\nThe patch metadata file %s has been modified' % os.path.basename(
711        patch_info.modified_metadata))
712
713  if patch_info.disabled_patches:
714    print('\nThe following patches were disabled:')
715    print('\n'.join(patch_info.disabled_patches))
716
717  if patch_info.removed_patches:
718    print('\nThe following patches were removed from the patch metadata file:')
719    for cur_patch_path in patch_info.removed_patches:
720      print('%s' % os.path.basename(cur_patch_path))
721
722
723def main():
724  """Applies patches to the source tree and takes action on a failed patch."""
725
726  args_output = GetCommandLineArgs()
727
728  if args_output.failure_mode != FailureModes.INTERNAL_BISECTION.value:
729    # If the SVN version of HEAD is not the same as 'svn_version', then some
730    # patches that fail to apply could successfully apply if HEAD's SVN version
731    # was the same as 'svn_version'. In other words, HEAD's git hash should be
732    # what is being updated to (e.g. LLVM_NEXT_HASH).
733    if not args_output.use_src_head:
734      VerifyHEADIsTheSameAsSVNVersion(args_output.src_path,
735                                      args_output.svn_version)
736  else:
737    # `git bisect run` called this script.
738    #
739    # `git bisect run` moves HEAD each time it invokes this script, so set the
740    # 'svn_version' to be current HEAD's SVN version so that the previous
741    # SVN version is not used in determining whether a patch is applicable.
742    args_output.svn_version = GetHEADSVNVersion(args_output.src_path)
743
744  # Get the results of handling the patches of the package.
745  patch_info = HandlePatches(
746      args_output.svn_version, args_output.patch_metadata_file,
747      args_output.filesdir_path, args_output.src_path,
748      FailureModes(args_output.failure_mode), args_output.good_svn_version,
749      args_output.num_patches_to_iterate, args_output.continue_bisection)
750
751  PrintPatchResults(patch_info)
752
753
754if __name__ == '__main__':
755  main()
756