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