Loading...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 | #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0+ # # Copyright 2025 Canonical Ltd. # Written by Simon Glass <simon.glass@canonical.com> # """Entry point for pickman - parses arguments and dispatches to control.""" import argparse import os import sys import unittest # Allow 'from pickman import xxx' to work via symlink our_path = os.path.dirname(os.path.realpath(__file__)) sys.path.insert(0, os.path.join(our_path, '..')) # pylint: disable=wrong-import-position,import-error from pickman import control from pickman import ftest from u_boot_pylib import test_util def add_main_commands(subparsers): """Add main pickman commands to the argument parser Args: subparsers (ArgumentParser): ArgumentParser subparsers object """ add_source = subparsers.add_parser('add-source', help='Add a source branch to track') add_source.add_argument('source', help='Source branch name') apply_cmd = subparsers.add_parser('apply', help='Apply next commits using Claude') apply_cmd.add_argument('source', help='Source branch name') apply_cmd.add_argument('-b', '--branch', help='Branch name to create') apply_cmd.add_argument('-p', '--push', action='store_true', help='Push branch and create GitLab MR') apply_cmd.add_argument('-r', '--remote', default='ci', help='Git remote for push (default: ci)') apply_cmd.add_argument('-t', '--target', default='master', help='Target branch for MR (default: master)') check_cmd = subparsers.add_parser( 'check', help='Check current branch for cherry-picks with large deltas') check_cmd.add_argument('-t', '--threshold', type=float, default=0.2, help='Delta threshold as fraction (default: 0.2 = ' '20%%)') check_cmd.add_argument('-m', '--min-lines', type=int, default=10, help='Minimum lines changed to check delta ' '(default: 10)') check_cmd.add_argument('-v', '--verbose', action='store_true', help='Show detailed stats for all commits') check_cmd.add_argument('-d', '--diff', action='store_true', help='Show source code diff for problem commits') check_gl = subparsers.add_parser('check-gitlab', help='Check GitLab permissions') check_gl.add_argument('-r', '--remote', default='ci', help='Git remote (default: ci)') commit_src = subparsers.add_parser('commit-source', help='Update database with last commit') commit_src.add_argument('source', help='Source branch name') commit_src.add_argument('commit', help='Commit hash to record') subparsers.add_parser('compare', help='Compare branches') count_merges = subparsers.add_parser( 'count-merges', help='Count remaining merges to process') count_merges.add_argument('source', help='Source branch name') subparsers.add_parser('list-sources', help='List tracked source branches') next_merges = subparsers.add_parser( 'next-merges', help='Show next N merges to be applied') next_merges.add_argument('source', help='Source branch name') next_merges.add_argument('-c', '--count', type=int, default=10, help='Number of merges to show (default: 10)') next_set = subparsers.add_parser( 'next-set', help='Show next set of commits to cherry-pick') next_set.add_argument('source', help='Source branch name') pick_cmd = subparsers.add_parser('pick', help='Cherry-pick commits ad-hoc') pick_cmd.add_argument('commits', help='Commit range (a..b) or merge commit') pick_cmd.add_argument('-b', '--branch', help='Branch name to create') pick_cmd.add_argument('-p', '--push', action='store_true', help='Push branch and create GitLab MR') pick_cmd.add_argument('-r', '--remote', default='ci', help='Git remote for push (default: ci)') pick_cmd.add_argument('-t', '--target', default='master', help='Target branch for MR (default: master)') review_cmd = subparsers.add_parser( 'review', help='Check open MRs and handle comments') review_cmd.add_argument('-r', '--remote', default='ci', help='Git remote (default: ci)') rewind_cmd = subparsers.add_parser( 'rewind', help='Rewind source position back by N merges') rewind_cmd.add_argument('source', help='Source branch name') rewind_cmd.add_argument('-c', '--count', type=int, default=1, help='Number of merges to rewind (default: 1)') rewind_cmd.add_argument('-f', '--force', action='store_true', help='Actually execute (default is dry run)') rewind_cmd.add_argument('-r', '--remote', default='ci', help='Git remote for MR lookup (default: ci)') step_cmd = subparsers.add_parser('step', help='Create MR if none pending') step_cmd.add_argument('source', help='Source branch name') step_cmd.add_argument('-F', '--fix-retries', type=int, default=3, help='Max pipeline-fix attempts per MR ' '(0 to disable, default: 3)') step_cmd.add_argument('-m', '--max-mrs', type=int, default=5, help='Max open MRs allowed (default: 5)') step_cmd.add_argument('-r', '--remote', default='ci', help='Git remote (default: ci)') step_cmd.add_argument('-t', '--target', default='master', help='Target branch for MR (default: master)') poll_cmd = subparsers.add_parser('poll', help='Run step repeatedly until stopped') poll_cmd.add_argument('source', help='Source branch name') poll_cmd.add_argument('-F', '--fix-retries', type=int, default=3, help='Max pipeline-fix attempts per MR ' '(0 to disable, default: 3)') poll_cmd.add_argument('-i', '--interval', type=int, default=300, help='Interval between steps in seconds ' '(default: 300)') poll_cmd.add_argument('-m', '--max-mrs', type=int, default=5, help='Max open MRs allowed (default: 5)') poll_cmd.add_argument('-r', '--remote', default='ci', help='Git remote (default: ci)') poll_cmd.add_argument('-t', '--target', default='master', help='Target branch for MR (default: master)') push_cmd = subparsers.add_parser('push-branch', help='Push branch using GitLab API token') push_cmd.add_argument('branch', help='Branch name to push') push_cmd.add_argument('-r', '--remote', default='ci', help='Git remote (default: ci)') push_cmd.add_argument('-f', '--force', action='store_true', help='Force push (overwrite remote branch)') push_cmd.add_argument('--run-ci', action='store_true', help='Run CI pipeline (default: skip for new MRs)') def add_test_commands(subparsers): """Add test-related commands to the argument parser Args: subparsers (ArgumentParser): ArgumentParser subparsers object """ test_cmd = subparsers.add_parser('test', help='Run tests') test_cmd.add_argument('-P', '--processes', type=int, help='Number of processes to run tests ' '(default: all)') test_cmd.add_argument('-T', '--test-coverage', action='store_true', help='Run tests and check for 100%% coverage') test_cmd.add_argument('-v', '--verbosity', type=int, default=1, help='Verbosity level (0-4, default: 1)') test_cmd.add_argument('tests', nargs='*', help='Specific tests to run') def parse_args(argv): """Parse command line arguments. Args: argv (list): Command line arguments Returns: Namespace: Parsed arguments """ parser = argparse.ArgumentParser(description='Check commit differences') parser.add_argument('--no-colour', action='store_true', help='Disable colour output') subparsers = parser.add_subparsers(dest='cmd', required=True) add_main_commands(subparsers) add_test_commands(subparsers) return parser.parse_args(argv) def get_test_classes(): """Get all test classes from the ftest module. Returns: list: List of test class objects """ return [getattr(ftest, name) for name in dir(ftest) if name.startswith('Test') and isinstance(getattr(ftest, name), type) and issubclass(getattr(ftest, name), unittest.TestCase)] def run_tests(processes, verbosity, test_name): """Run the pickman test suite. Args: processes (int): Number of processes for concurrent tests verbosity (int): Verbosity level (0-4) test_name (str): Specific test to run, or None for all Returns: int: 0 if tests passed, 1 otherwise """ result = test_util.run_test_suites( 'pickman', False, verbosity, False, False, processes, test_name, None, get_test_classes()) return 0 if result.wasSuccessful() else 1 def run_test_coverage(args): """Run tests with coverage checking. Args: args (list): Specific tests to run, or None for all """ # agent.py and gitlab_api.py require external services (Claude, GitLab) # so they can't achieve 100% coverage in unit tests test_util.run_test_coverage( 'tools/pickman/pickman', None, ['*test*', '*__main__.py', 'tools/u_boot_pylib/*'], None, extra_args=None, args=args, allow_failures=['tools/pickman/agent.py', 'tools/pickman/gitlab_api.py', 'tools/pickman/control.py']) def main(argv=None): """Main function to parse args and run commands. Args: argv (list): Command line arguments (None for sys.argv[1:]) """ args = parse_args(argv) if args.cmd == 'test': if args.test_coverage: run_test_coverage(args.tests or None) return 0 test_name = args.tests[0] if args.tests else None return run_tests(args.processes, args.verbosity, test_name) return control.do_pickman(args) if __name__ == '__main__': sys.exit(main()) |