1# SPDX-License-Identifier: GPL-2.0+ 2# Copyright (c) 2011 The Chromium OS Authors. 3# 4 5"""Basic utilities for running the git command-line tool from Python""" 6 7import os 8import sys 9 10from u_boot_pylib import command 11from u_boot_pylib import terminal 12 13# True to use --no-decorate - we check this in setup() 14USE_NO_DECORATE = True 15 16 17def log_cmd(commit_range, git_dir=None, oneline=False, reverse=False, 18 count=None, decorate=False): 19 """Create a command to perform a 'git log' 20 21 Args: 22 commit_range (str): Range expression to use for log, None for none 23 git_dir (str): Path to git repository (None to use default) 24 oneline (bool): True to use --oneline, else False 25 reverse (bool): True to reverse the log (--reverse) 26 count (int or None): Number of commits to list, or None for no limit 27 decorate (bool): True to use --decorate 28 29 Return: 30 List containing command and arguments to run 31 """ 32 cmd = ['git'] 33 if git_dir: 34 cmd += ['--git-dir', git_dir] 35 cmd += ['--no-pager', 'log', '--no-color'] 36 if oneline: 37 cmd.append('--oneline') 38 if USE_NO_DECORATE and not decorate: 39 cmd.append('--no-decorate') 40 if decorate: 41 cmd.append('--decorate') 42 if reverse: 43 cmd.append('--reverse') 44 if count is not None: 45 cmd.append(f'-n{count}') 46 if commit_range: 47 cmd.append(commit_range) 48 49 # Add this in case we have a branch with the same name as a directory. 50 # This avoids messages like this, for example: 51 # fatal: ambiguous argument 'test': both revision and filename 52 cmd.append('--') 53 return cmd 54 55 56def count_commits_to_branch(branch, git_dir=None, end=None): 57 """Returns number of commits between HEAD and the tracking branch. 58 59 This looks back to the tracking branch and works out the number of commits 60 since then. 61 62 Args: 63 branch (str or None): Branch to count from (None for current branch) 64 git_dir (str): Path to git repository (None to use default) 65 end (str): End commit to stop before 66 67 Return: 68 Number of patches that exist on top of the branch 69 """ 70 if end: 71 rev_range = f'{end}..{branch}' 72 elif branch: 73 us, msg = get_upstream(git_dir or '.git', branch) 74 if not us: 75 raise ValueError(msg) 76 rev_range = f'{us}..{branch}' 77 else: 78 rev_range = '@{upstream}..' 79 cmd = log_cmd(rev_range, git_dir=git_dir, oneline=True) 80 result = command.run_one(*cmd, capture=True, capture_stderr=True, 81 oneline=True, raise_on_error=False) 82 if result.return_code: 83 raise ValueError( 84 f'Failed to determine upstream: {result.stderr.strip()}') 85 patch_count = len(result.stdout.splitlines()) 86 return patch_count 87 88 89def name_revision(commit_hash): 90 """Gets the revision name for a commit 91 92 Args: 93 commit_hash (str): Commit hash to look up 94 95 Return: 96 Name of revision, if any, else None 97 """ 98 stdout = command.output_one_line('git', 'name-rev', commit_hash) 99 if not stdout: 100 return None 101 102 # We expect a commit, a space, then a revision name 103 name = stdout.split()[1].strip() 104 return name 105 106 107def guess_upstream(git_dir, branch): 108 """Tries to guess the upstream for a branch 109 110 This lists out top commits on a branch and tries to find a suitable 111 upstream. It does this by looking for the first commit where 112 'git name-rev' returns a plain branch name, with no ! or ^ modifiers. 113 114 Args: 115 git_dir (str): Git directory containing repo 116 branch (str): Name of branch 117 118 Returns: 119 Tuple: 120 Name of upstream branch (e.g. 'upstream/master') or None if none 121 Warning/error message, or None if none 122 """ 123 cmd = log_cmd(branch, git_dir=git_dir, oneline=True, count=100, 124 decorate=True) 125 result = command.run_one(*cmd, capture=True, capture_stderr=True, 126 raise_on_error=False) 127 if result.return_code: 128 return None, f"Branch '{branch}' not found" 129 for line in result.stdout.splitlines()[1:]: 130 parts = line.split(maxsplit=1) 131 if len(parts) >= 2 and parts[1].startswith('('): 132 commit_hash = parts[0] 133 name = name_revision(commit_hash) 134 if '~' not in name and '^' not in name: 135 if name.startswith('remotes/'): 136 name = name[8:] 137 return name, f"Guessing upstream as '{name}'" 138 return None, f"Cannot find a suitable upstream for branch '{branch}'" 139 140 141def get_upstream(git_dir, branch): 142 """Returns the name of the upstream for a branch 143 144 Args: 145 git_dir (str): Git directory containing repo 146 branch (str): Name of branch 147 148 Returns: 149 Tuple: 150 Name of upstream branch (e.g. 'upstream/master') or None if none 151 Warning/error message, or None if none 152 """ 153 try: 154 remote = command.output_one_line('git', '--git-dir', git_dir, 'config', 155 f'branch.{branch}.remote') 156 merge = command.output_one_line('git', '--git-dir', git_dir, 'config', 157 f'branch.{branch}.merge') 158 except command.CommandExc: 159 upstream, msg = guess_upstream(git_dir, branch) 160 return upstream, msg 161 162 if remote == '.': 163 return merge, None 164 if remote and merge: 165 # Drop the initial refs/heads from merge 166 leaf = merge.split('/', maxsplit=2)[2:] 167 return f'{remote}/{"/".join(leaf)}', None 168 raise ValueError("Cannot determine upstream branch for branch " 169 f"'{branch}' remote='{remote}', merge='{merge}'") 170 171 172def get_range_in_branch(git_dir, branch, include_upstream=False): 173 """Returns an expression for the commits in the given branch. 174 175 Args: 176 git_dir (str): Directory containing git repo 177 branch (str): Name of branch 178 include_upstream (bool): Include the upstream commit as well 179 Return: 180 Expression in the form 'upstream..branch' which can be used to 181 access the commits. If the branch does not exist, returns None. 182 """ 183 upstream, msg = get_upstream(git_dir, branch) 184 if not upstream: 185 return None, msg 186 rstr = f"{upstream}{'~' if include_upstream else ''}..{branch}" 187 return rstr, msg 188 189 190def count_commits_in_range(git_dir, range_expr): 191 """Returns the number of commits in the given range. 192 193 Args: 194 git_dir (str): Directory containing git repo 195 range_expr (str): Range to check 196 Return: 197 Number of patches that exist in the supplied range or None if none 198 were found 199 """ 200 cmd = log_cmd(range_expr, git_dir=git_dir, oneline=True) 201 result = command.run_one(*cmd, capture=True, capture_stderr=True, 202 raise_on_error=False) 203 if result.return_code: 204 return None, f"Range '{range_expr}' not found or is invalid" 205 patch_count = len(result.stdout.splitlines()) 206 return patch_count, None 207 208 209def count_commits_in_branch(git_dir, branch, include_upstream=False): 210 """Returns the number of commits in the given branch. 211 212 Args: 213 git_dir (str): Directory containing git repo 214 branch (str): Name of branch 215 include_upstream (bool): Include the upstream commit as well 216 Return: 217 Number of patches that exist on top of the branch, or None if the 218 branch does not exist. 219 """ 220 range_expr, msg = get_range_in_branch(git_dir, branch, include_upstream) 221 if not range_expr: 222 return None, msg 223 return count_commits_in_range(git_dir, range_expr) 224 225 226def count_commits(commit_range): 227 """Returns the number of commits in the given range. 228 229 Args: 230 commit_range (str): Range of commits to count (e.g. 'HEAD..base') 231 Return: 232 Number of patches that exist on top of the branch 233 """ 234 pipe = [log_cmd(commit_range, oneline=True), 235 ['wc', '-l']] 236 stdout = command.run_pipe(pipe, capture=True, oneline=True).stdout 237 patch_count = int(stdout) 238 return patch_count 239 240 241def checkout(commit_hash, git_dir=None, work_tree=None, force=False): 242 """Checkout the selected commit for this build 243 244 Args: 245 commit_hash (str): Commit hash to check out 246 git_dir (str): Directory containing git repo, or None for current dir 247 work_tree (str): Git worktree to use, or None if none 248 force (bool): True to force the checkout (git checkout -f) 249 """ 250 pipe = ['git'] 251 if git_dir: 252 pipe.extend(['--git-dir', git_dir]) 253 if work_tree: 254 pipe.extend(['--work-tree', work_tree]) 255 pipe.append('checkout') 256 if force: 257 pipe.append('-f') 258 pipe.append(commit_hash) 259 result = command.run_pipe([pipe], capture=True, raise_on_error=False, 260 capture_stderr=True) 261 if result.return_code != 0: 262 raise OSError(f'git checkout ({pipe}): {result.stderr}') 263 264 265def clone(repo, output_dir): 266 """Clone a repo 267 268 Args: 269 repo (str): Repo to clone (e.g. web address) 270 output_dir (str): Directory to close into 271 """ 272 result = command.run_one('git', 'clone', repo, '.', capture=True, 273 cwd=output_dir, capture_stderr=True) 274 if result.return_code != 0: 275 raise OSError(f'git clone: {result.stderr}') 276 277 278def fetch(git_dir=None, work_tree=None): 279 """Fetch from the origin repo 280 281 Args: 282 git_dir (str): Directory containing git repo, or None for current dir 283 work_tree (str or None): Git worktree to use, or None if none 284 """ 285 cmd = ['git'] 286 if git_dir: 287 cmd.extend(['--git-dir', git_dir]) 288 if work_tree: 289 cmd.extend(['--work-tree', work_tree]) 290 cmd.append('fetch') 291 result = command.run_one(*cmd, capture=True, capture_stderr=True) 292 if result.return_code != 0: 293 raise OSError(f'git fetch: {result.stderr}') 294 295 296def check_worktree_is_available(git_dir): 297 """Check if git-worktree functionality is available 298 299 Args: 300 git_dir (str): The repository to test in 301 302 Returns: 303 True if git-worktree commands will work, False otherwise. 304 """ 305 result = command.run_one('git', '--git-dir', git_dir, 'worktree', 'list', 306 capture=True, capture_stderr=True, 307 raise_on_error=False) 308 return result.return_code == 0 309 310 311def add_worktree(git_dir, output_dir, commit_hash=None): 312 """Create and checkout a new git worktree for this build 313 314 Args: 315 git_dir (str): The repository to checkout the worktree from 316 output_dir (str): Path for the new worktree 317 commit_hash (str): Commit hash to checkout 318 """ 319 # We need to pass --detach to avoid creating a new branch 320 cmd = ['git', '--git-dir', git_dir, 'worktree', 'add', '.', '--detach'] 321 if commit_hash: 322 cmd.append(commit_hash) 323 result = command.run_one(*cmd, capture=True, cwd=output_dir, 324 capture_stderr=True) 325 if result.return_code != 0: 326 raise OSError(f'git worktree add: {result.stderr}') 327 328 329def prune_worktrees(git_dir): 330 """Remove administrative files for deleted worktrees 331 332 Args: 333 git_dir (str): The repository whose deleted worktrees should be pruned 334 """ 335 result = command.run_one('git', '--git-dir', git_dir, 'worktree', 'prune', 336 capture=True, capture_stderr=True) 337 if result.return_code != 0: 338 raise OSError(f'git worktree prune: {result.stderr}') 339 340 341def create_patches(branch, start, count, ignore_binary, series, signoff=True, 342 git_dir=None, cwd=None): 343 """Create a series of patches from the top of the current branch. 344 345 The patch files are written to the current directory using 346 git format-patch. 347 348 Args: 349 branch (str): Branch to create patches from (None for current branch) 350 start (int): Commit to start from: 0=HEAD, 1=next one, etc. 351 count (int): number of commits to include 352 ignore_binary (bool): Don't generate patches for binary files 353 series (Series): Series object for this series (set of patches) 354 signoff (bool): True to add signoff lines automatically 355 git_dir (str): Path to git repository (None to use default) 356 cwd (str): Path to use for git operations 357 Return: 358 Filename of cover letter (None if none) 359 List of filenames of patch files 360 """ 361 cmd = ['git'] 362 if git_dir: 363 cmd += ['--git-dir', git_dir] 364 cmd += ['format-patch', '-M'] 365 if signoff: 366 cmd.append('--signoff') 367 if ignore_binary: 368 cmd.append('--no-binary') 369 if series.get('cover'): 370 cmd.append('--cover-letter') 371 prefix = series.GetPatchPrefix() 372 if prefix: 373 cmd += [f'--subject-prefix={prefix}'] 374 brname = branch or 'HEAD' 375 cmd += [f'{brname}~{start + count}..{brname}~{start}'] 376 377 stdout = command.run_list(cmd, cwd=cwd) 378 files = stdout.splitlines() 379 380 # We have an extra file if there is a cover letter 381 if series.get('cover'): 382 return files[0], files[1:] 383 return None, files 384 385 386def build_email_list(in_list, alias, tag=None, warn_on_error=True): 387 """Build a list of email addresses based on an input list. 388 389 Takes a list of email addresses and aliases, and turns this into a list 390 of only email address, by resolving any aliases that are present. 391 392 If the tag is given, then each email address is prepended with this 393 tag and a space. If the tag starts with a minus sign (indicating a 394 command line parameter) then the email address is quoted. 395 396 Args: 397 in_list (list of str): List of aliases/email addresses 398 alias (dict): Alias dictionary: 399 key: alias 400 value: list of aliases or email addresses 401 tag (str): Text to put before each address 402 warn_on_error (bool): True to raise an error when an alias fails to 403 match, False to just print a message. 404 405 Returns: 406 List of email addresses 407 408 >>> alias = {} 409 >>> alias['fred'] = ['f.bloggs@napier.co.nz'] 410 >>> alias['john'] = ['j.bloggs@napier.co.nz'] 411 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>'] 412 >>> alias['boys'] = ['fred', ' john'] 413 >>> alias['all'] = ['fred ', 'john', ' mary '] 414 >>> build_email_list(['john', 'mary'], alias, None) 415 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>'] 416 >>> build_email_list(['john', 'mary'], alias, '--to') 417 ['--to "j.bloggs@napier.co.nz"', \ 418'--to "Mary Poppins <m.poppins@cloud.net>"'] 419 >>> build_email_list(['john', 'mary'], alias, 'Cc') 420 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>'] 421 """ 422 raw = [] 423 for item in in_list: 424 raw += lookup_email(item, alias, warn_on_error=warn_on_error) 425 result = [] 426 for item in raw: 427 if item not in result: 428 result.append(item) 429 if tag: 430 return [x for email in result for x in (tag, email)] 431 return result 432 433 434def check_suppress_cc_config(): 435 """Check if sendemail.suppresscc is configured correctly. 436 437 Returns: 438 bool: True if the option is configured correctly, False otherwise. 439 """ 440 suppresscc = command.output_one_line( 441 'git', 'config', 'sendemail.suppresscc', raise_on_error=False) 442 443 # Other settings should be fine. 444 if suppresscc in ('all', 'cccmd'): 445 col = terminal.Color() 446 447 print(col.build(col.RED, 'error') + 448 f': git config sendemail.suppresscc set to {suppresscc}\n' + 449 ' patman needs --cc-cmd to be run to set the cc list.\n' + 450 ' Please run:\n' + 451 ' git config --unset sendemail.suppresscc\n' + 452 ' Or read the man page:\n' + 453 ' git send-email --help\n' + 454 ' and set an option that runs --cc-cmd\n') 455 return False 456 457 return True 458 459 460def email_patches(series, cover_fname, args, dry_run, warn_on_error, cc_fname, 461 alias, self_only=False, in_reply_to=None, thread=False, 462 smtp_server=None, cwd=None): 463 """Email a patch series. 464 465 Args: 466 series (Series): Series object containing destination info 467 cover_fname (str or None): filename of cover letter 468 args (list of str): list of filenames of patch files 469 dry_run (bool): Just return the command that would be run 470 warn_on_error (bool): True to print a warning when an alias fails to 471 match, False to ignore it. 472 cc_fname (str): Filename of Cc file for per-commit Cc 473 alias (dict): Alias dictionary: 474 key: alias 475 value: list of aliases or email addresses 476 self_only (bool): True to just email to yourself as a test 477 in_reply_to (str or None): If set we'll pass this to git as 478 --in-reply-to - should be a message ID that this is in reply to. 479 thread (bool): True to add --thread to git send-email (make 480 all patches reply to cover-letter or first patch in series) 481 smtp_server (str or None): SMTP server to use to send patches 482 cwd (str): Path to use for patch files (None to use current dir) 483 484 Returns: 485 Git command that was/would be run 486 487 # For the duration of this doctest pretend that we ran patman with ./patman 488 >>> _old_argv0 = sys.argv[0] 489 >>> sys.argv[0] = './patman' 490 491 >>> alias = {} 492 >>> alias['fred'] = ['f.bloggs@napier.co.nz'] 493 >>> alias['john'] = ['j.bloggs@napier.co.nz'] 494 >>> alias['mary'] = ['m.poppins@cloud.net'] 495 >>> alias['boys'] = ['fred', ' john'] 496 >>> alias['all'] = ['fred ', 'john', ' mary '] 497 >>> alias[os.getenv('USER')] = ['this-is-me@me.com'] 498 >>> series = {} 499 >>> series['to'] = ['fred'] 500 >>> series['cc'] = ['mary'] 501 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \ 502 False, alias) 503 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \ 504"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2' 505 >>> email_patches(series, None, ['p1'], True, True, 'cc-fname', False, \ 506 alias) 507 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \ 508"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" p1' 509 >>> series['cc'] = ['all'] 510 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \ 511 True, alias) 512 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \ 513send --cc-cmd cc-fname" cover p1 p2' 514 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \ 515 False, alias) 516 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \ 517"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \ 518"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2' 519 520 # Restore argv[0] since we clobbered it. 521 >>> sys.argv[0] = _old_argv0 522 """ 523 to = build_email_list(series.get('to'), alias, '--to', warn_on_error) 524 if not to: 525 if not command.output('git', 'config', 'sendemail.to', 526 raise_on_error=False): 527 print("No recipient.\n" 528 "Please add something like this to a commit\n" 529 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n" 530 "Or do something like this\n" 531 "git config sendemail.to u-boot@lists.denx.de") 532 return None 533 cc = build_email_list(list(set(series.get('cc')) - set(series.get('to'))), 534 alias, '--cc', warn_on_error) 535 if self_only: 536 to = build_email_list([os.getenv('USER')], '--to', alias, 537 warn_on_error) 538 cc = [] 539 cmd = ['git', 'send-email', '--annotate'] 540 if smtp_server: 541 cmd.append(f'--smtp-server={smtp_server}') 542 if in_reply_to: 543 cmd.append(f'--in-reply-to="{in_reply_to}"') 544 if thread: 545 cmd.append('--thread') 546 547 cmd += to 548 cmd += cc 549 cmd += ['--cc-cmd', f'{sys.argv[0]} send --cc-cmd {cc_fname}'] 550 if cover_fname: 551 cmd.append(cover_fname) 552 cmd += args 553 if not dry_run: 554 command.run(*cmd, capture=False, capture_stderr=False, cwd=cwd) 555 return' '.join([f'"{x}"' if ' ' in x and '"' not in x else x 556 for x in cmd]) 557 558 559def lookup_email(lookup_name, alias, warn_on_error=True, level=0): 560 """If an email address is an alias, look it up and return the full name 561 562 TODO: Why not just use git's own alias feature? 563 564 Args: 565 lookup_name (str): Alias or email address to look up 566 alias (dict): Alias dictionary 567 key: alias 568 value: list of aliases or email addresses 569 warn_on_error (bool): True to print a warning when an alias fails to 570 match, False to ignore it. 571 level (int): Depth of alias stack, used to detect recusion/loops 572 573 Returns: 574 tuple: 575 list containing a list of email addresses 576 577 Raises: 578 OSError if a recursive alias reference was found 579 ValueError if an alias was not found 580 581 >>> alias = {} 582 >>> alias['fred'] = ['f.bloggs@napier.co.nz'] 583 >>> alias['john'] = ['j.bloggs@napier.co.nz'] 584 >>> alias['mary'] = ['m.poppins@cloud.net'] 585 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz'] 586 >>> alias['all'] = ['fred ', 'john', ' mary '] 587 >>> alias['loop'] = ['other', 'john', ' mary '] 588 >>> alias['other'] = ['loop', 'john', ' mary '] 589 >>> lookup_email('mary', alias) 590 ['m.poppins@cloud.net'] 591 >>> lookup_email('arthur.wellesley@howe.ro.uk', alias) 592 ['arthur.wellesley@howe.ro.uk'] 593 >>> lookup_email('boys', alias) 594 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz'] 595 >>> lookup_email('all', alias) 596 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net'] 597 >>> lookup_email('odd', alias) 598 Alias 'odd' not found 599 [] 600 >>> lookup_email('loop', alias) 601 Traceback (most recent call last): 602 ... 603 OSError: Recursive email alias at 'other' 604 >>> lookup_email('odd', alias, warn_on_error=False) 605 [] 606 >>> # In this case the loop part will effectively be ignored. 607 >>> lookup_email('loop', alias, warn_on_error=False) 608 Recursive email alias at 'other' 609 Recursive email alias at 'john' 610 Recursive email alias at 'mary' 611 ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net'] 612 """ 613 lookup_name = lookup_name.strip() 614 if '@' in lookup_name: # Perhaps a real email address 615 return [lookup_name] 616 617 lookup_name = lookup_name.lower() 618 col = terminal.Color() 619 620 out_list = [] 621 if level > 10: 622 msg = f"Recursive email alias at '{lookup_name}'" 623 if warn_on_error: 624 raise OSError(msg) 625 print(col.build(col.RED, msg)) 626 return out_list 627 628 if lookup_name: 629 if lookup_name not in alias: 630 msg = f"Alias '{lookup_name}' not found" 631 if warn_on_error: 632 print(col.build(col.RED, msg)) 633 return out_list 634 for item in alias[lookup_name]: 635 todo = lookup_email(item, alias, warn_on_error, level + 1) 636 for new_item in todo: 637 if new_item not in out_list: 638 out_list.append(new_item) 639 640 return out_list 641 642 643def get_top_level(): 644 """Return name of top-level directory for this git repo. 645 646 Returns: 647 str: Full path to git top-level directory, or None if not found 648 649 This test makes sure that we are running tests in the right subdir 650 651 >>> os.path.realpath(os.path.dirname(__file__)) == \ 652 os.path.join(get_top_level(), 'tools', 'patman') 653 True 654 """ 655 result = command.run_one( 656 'git', 'rev-parse', '--show-toplevel', oneline=True, capture=True, 657 capture_stderr=True, raise_on_error=False) 658 if result.return_code: 659 return None 660 return result.stdout.strip() 661 662 663def get_alias_file(): 664 """Gets the name of the git alias file. 665 666 Returns: 667 str: Filename of git alias file, or None if none 668 """ 669 fname = command.output_one_line('git', 'config', 'sendemail.aliasesfile', 670 raise_on_error=False) 671 if not fname: 672 return None 673 674 fname = os.path.expanduser(fname.strip()) 675 if os.path.isabs(fname): 676 return fname 677 678 return os.path.join(get_top_level() or '', fname) 679 680 681def get_default_user_name(): 682 """Gets the user.name from .gitconfig file. 683 684 Returns: 685 User name found in .gitconfig file, or None if none 686 """ 687 uname = command.output_one_line('git', 'config', '--global', '--includes', 688 'user.name') 689 return uname 690 691 692def get_default_user_email(): 693 """Gets the user.email from the global .gitconfig file. 694 695 Returns: 696 User's email found in .gitconfig file, or None if none 697 """ 698 uemail = command.output_one_line('git', 'config', '--global', '--includes', 699 'user.email') 700 return uemail 701 702 703def get_default_subject_prefix(): 704 """Gets the format.subjectprefix from local .git/config file. 705 706 Returns: 707 Subject prefix found in local .git/config file, or None if none 708 """ 709 sub_prefix = command.output_one_line( 710 'git', 'config', 'format.subjectprefix', raise_on_error=False) 711 712 return sub_prefix 713 714 715def setup(): 716 """setup() - Set up git utils, by reading the alias files.""" 717 # Check for a git alias file also 718 global USE_NO_DECORATE 719 720 cmd = log_cmd(None, count=0) 721 USE_NO_DECORATE = (command.run_one(*cmd, raise_on_error=False) 722 .return_code == 0) 723 724 725def get_hash(spec, git_dir=None): 726 """Get the hash of a commit 727 728 Args: 729 spec (str): Git commit to show, e.g. 'my-branch~12' 730 git_dir (str): Path to git repository (None to use default) 731 732 Returns: 733 str: Hash of commit 734 """ 735 cmd = ['git'] 736 if git_dir: 737 cmd += ['--git-dir', git_dir] 738 cmd += ['show', '-s', '--pretty=format:%H', spec] 739 return command.output_one_line(*cmd) 740 741 742def get_head(): 743 """Get the hash of the current HEAD 744 745 Returns: 746 Hash of HEAD 747 """ 748 return get_hash('HEAD') 749 750 751def get_branch(git_dir=None): 752 """Get the branch we are currently on 753 754 Return: 755 str: branch name, or None if none 756 git_dir (str): Path to git repository (None to use default) 757 """ 758 cmd = ['git'] 759 if git_dir: 760 cmd += ['--git-dir', git_dir] 761 cmd += ['rev-parse', '--abbrev-ref', 'HEAD'] 762 out = command.output_one_line(*cmd, raise_on_error=False) 763 if out == 'HEAD': 764 return None 765 return out 766 767 768def check_dirty(git_dir=None, work_tree=None): 769 """Check if the tree is dirty 770 771 Args: 772 git_dir (str): Path to git repository (None to use default) 773 work_tree (str): Git worktree to use, or None if none 774 775 Return: 776 str: List of dirty filenames and state 777 """ 778 cmd = ['git'] 779 if git_dir: 780 cmd += ['--git-dir', git_dir] 781 if work_tree: 782 cmd += ['--work-tree', work_tree] 783 cmd += ['status', '--porcelain', '--untracked-files=no'] 784 return command.output(*cmd).splitlines() 785 786 787def check_branch(name, git_dir=None): 788 """Check if a branch exists 789 790 Args: 791 name (str): Name of the branch to check 792 git_dir (str): Path to git repository (None to use default) 793 """ 794 cmd = ['git'] 795 if git_dir: 796 cmd += ['--git-dir', git_dir] 797 cmd += ['branch', '--list', name] 798 799 # This produces ' <name>' or '* <name>' 800 out = command.output(*cmd).rstrip() 801 return out[2:] == name 802 803 804def rename_branch(old_name, name, git_dir=None): 805 """Check if a branch exists 806 807 Args: 808 old_name (str): Name of the branch to rename 809 name (str): New name for the branch 810 git_dir (str): Path to git repository (None to use default) 811 812 Return: 813 str: Output from command 814 """ 815 cmd = ['git'] 816 if git_dir: 817 cmd += ['--git-dir', git_dir] 818 cmd += ['branch', '--move', old_name, name] 819 820 # This produces ' <name>' or '* <name>' 821 return command.output(*cmd).rstrip() 822 823 824def get_commit_message(commit, git_dir=None): 825 """Gets the commit message for a commit 826 827 Args: 828 commit (str): commit to check 829 git_dir (str): Path to git repository (None to use default) 830 831 Return: 832 list of str: Lines from the commit message 833 """ 834 cmd = ['git'] 835 if git_dir: 836 cmd += ['--git-dir', git_dir] 837 cmd += ['show', '--quiet', commit] 838 839 out = command.output(*cmd) 840 # the header is followed by a blank line 841 lines = out.splitlines() 842 empty = lines.index('') 843 msg = lines[empty + 1:] 844 unindented = [line[4:] for line in msg] 845 846 return unindented 847 848 849def show_commit(commit, msg=True, diffstat=False, patch=False, colour=True, 850 git_dir=None): 851 """Runs 'git show' and returns the output 852 853 Args: 854 commit (str): commit to check 855 msg (bool): Show the commit message 856 diffstat (bool): True to include the diffstat 857 patch (bool): True to include the patch 858 colour (bool): True to force use of colour 859 git_dir (str): Path to git repository (None to use default) 860 861 Return: 862 list of str: Lines from the commit message 863 """ 864 cmd = ['git'] 865 if git_dir: 866 cmd += ['--git-dir', git_dir] 867 cmd += ['show'] 868 if colour: 869 cmd.append('--color') 870 if not msg: 871 cmd.append('--oneline') 872 if diffstat: 873 cmd.append('--stat') 874 else: 875 cmd.append('--quiet') 876 if patch: 877 cmd.append('--patch') 878 cmd.append(commit) 879 880 return command.output(*cmd) 881 882 883if __name__ == "__main__": 884 import doctest 885 886 doctest.testmod() 887