xref: /rk3399_rockchip-uboot/tools/patman/gitutil.py (revision 2a9e2c6a0962cb04a12d833cc5c1c0920fa3b4f5)
1# Copyright (c) 2011 The Chromium OS Authors.
2#
3# SPDX-License-Identifier:	GPL-2.0+
4#
5
6import command
7import re
8import os
9import series
10import subprocess
11import sys
12import terminal
13
14import checkpatch
15import settings
16
17# True to use --no-decorate - we check this in Setup()
18use_no_decorate = True
19
20def LogCmd(commit_range, git_dir=None, oneline=False, reverse=False,
21           count=None):
22    """Create a command to perform a 'git log'
23
24    Args:
25        commit_range: Range expression to use for log, None for none
26        git_dir: Path to git repositiory (None to use default)
27        oneline: True to use --oneline, else False
28        reverse: True to reverse the log (--reverse)
29        count: Number of commits to list, or None for no limit
30    Return:
31        List containing command and arguments to run
32    """
33    cmd = ['git']
34    if git_dir:
35        cmd += ['--git-dir', git_dir]
36    cmd += ['--no-pager', 'log', '--no-color']
37    if oneline:
38        cmd.append('--oneline')
39    if use_no_decorate:
40        cmd.append('--no-decorate')
41    if reverse:
42        cmd.append('--reverse')
43    if count is not None:
44        cmd.append('-n%d' % count)
45    if commit_range:
46        cmd.append(commit_range)
47    return cmd
48
49def CountCommitsToBranch():
50    """Returns number of commits between HEAD and the tracking branch.
51
52    This looks back to the tracking branch and works out the number of commits
53    since then.
54
55    Return:
56        Number of patches that exist on top of the branch
57    """
58    pipe = [LogCmd('@{upstream}..', oneline=True),
59            ['wc', '-l']]
60    stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
61    patch_count = int(stdout)
62    return patch_count
63
64def NameRevision(commit_hash):
65    """Gets the revision name for a commit
66
67    Args:
68        commit_hash: Commit hash to look up
69
70    Return:
71        Name of revision, if any, else None
72    """
73    pipe = ['git', 'name-rev', commit_hash]
74    stdout = command.RunPipe([pipe], capture=True, oneline=True).stdout
75
76    # We expect a commit, a space, then a revision name
77    name = stdout.split(' ')[1].strip()
78    return name
79
80def GuessUpstream(git_dir, branch):
81    """Tries to guess the upstream for a branch
82
83    This lists out top commits on a branch and tries to find a suitable
84    upstream. It does this by looking for the first commit where
85    'git name-rev' returns a plain branch name, with no ! or ^ modifiers.
86
87    Args:
88        git_dir: Git directory containing repo
89        branch: Name of branch
90
91    Returns:
92        Tuple:
93            Name of upstream branch (e.g. 'upstream/master') or None if none
94            Warning/error message, or None if none
95    """
96    pipe = [LogCmd(branch, git_dir=git_dir, oneline=True, count=100)]
97    result = command.RunPipe(pipe, capture=True, capture_stderr=True,
98                             raise_on_error=False)
99    if result.return_code:
100        return None, "Branch '%s' not found" % branch
101    for line in result.stdout.splitlines()[1:]:
102        commit_hash = line.split(' ')[0]
103        name = NameRevision(commit_hash)
104        if '~' not in name and '^' not in name:
105            if name.startswith('remotes/'):
106                name = name[8:]
107            return name, "Guessing upstream as '%s'" % name
108    return None, "Cannot find a suitable upstream for branch '%s'" % branch
109
110def GetUpstream(git_dir, branch):
111    """Returns the name of the upstream for a branch
112
113    Args:
114        git_dir: Git directory containing repo
115        branch: Name of branch
116
117    Returns:
118        Tuple:
119            Name of upstream branch (e.g. 'upstream/master') or None if none
120            Warning/error message, or None if none
121    """
122    try:
123        remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
124                                       'branch.%s.remote' % branch)
125        merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
126                                      'branch.%s.merge' % branch)
127    except:
128        upstream, msg = GuessUpstream(git_dir, branch)
129        return upstream, msg
130
131    if remote == '.':
132        return merge
133    elif remote and merge:
134        leaf = merge.split('/')[-1]
135        return '%s/%s' % (remote, leaf), None
136    else:
137        raise ValueError, ("Cannot determine upstream branch for branch "
138                "'%s' remote='%s', merge='%s'" % (branch, remote, merge))
139
140
141def GetRangeInBranch(git_dir, branch, include_upstream=False):
142    """Returns an expression for the commits in the given branch.
143
144    Args:
145        git_dir: Directory containing git repo
146        branch: Name of branch
147    Return:
148        Expression in the form 'upstream..branch' which can be used to
149        access the commits. If the branch does not exist, returns None.
150    """
151    upstream, msg = GetUpstream(git_dir, branch)
152    if not upstream:
153        return None, msg
154    rstr = '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)
155    return rstr, msg
156
157def CountCommitsInBranch(git_dir, branch, include_upstream=False):
158    """Returns the number of commits in the given branch.
159
160    Args:
161        git_dir: Directory containing git repo
162        branch: Name of branch
163    Return:
164        Number of patches that exist on top of the branch, or None if the
165        branch does not exist.
166    """
167    range_expr, msg = GetRangeInBranch(git_dir, branch, include_upstream)
168    if not range_expr:
169        return None, msg
170    pipe = [LogCmd(range_expr, git_dir=git_dir, oneline=True),
171            ['wc', '-l']]
172    result = command.RunPipe(pipe, capture=True, oneline=True)
173    patch_count = int(result.stdout)
174    return patch_count, msg
175
176def CountCommits(commit_range):
177    """Returns the number of commits in the given range.
178
179    Args:
180        commit_range: Range of commits to count (e.g. 'HEAD..base')
181    Return:
182        Number of patches that exist on top of the branch
183    """
184    pipe = [LogCmd(commit_range, oneline=True),
185            ['wc', '-l']]
186    stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
187    patch_count = int(stdout)
188    return patch_count
189
190def Checkout(commit_hash, git_dir=None, work_tree=None, force=False):
191    """Checkout the selected commit for this build
192
193    Args:
194        commit_hash: Commit hash to check out
195    """
196    pipe = ['git']
197    if git_dir:
198        pipe.extend(['--git-dir', git_dir])
199    if work_tree:
200        pipe.extend(['--work-tree', work_tree])
201    pipe.append('checkout')
202    if force:
203        pipe.append('-f')
204    pipe.append(commit_hash)
205    result = command.RunPipe([pipe], capture=True, raise_on_error=False,
206                             capture_stderr=True)
207    if result.return_code != 0:
208        raise OSError, 'git checkout (%s): %s' % (pipe, result.stderr)
209
210def Clone(git_dir, output_dir):
211    """Checkout the selected commit for this build
212
213    Args:
214        commit_hash: Commit hash to check out
215    """
216    pipe = ['git', 'clone', git_dir, '.']
217    result = command.RunPipe([pipe], capture=True, cwd=output_dir,
218                             capture_stderr=True)
219    if result.return_code != 0:
220        raise OSError, 'git clone: %s' % result.stderr
221
222def Fetch(git_dir=None, work_tree=None):
223    """Fetch from the origin repo
224
225    Args:
226        commit_hash: Commit hash to check out
227    """
228    pipe = ['git']
229    if git_dir:
230        pipe.extend(['--git-dir', git_dir])
231    if work_tree:
232        pipe.extend(['--work-tree', work_tree])
233    pipe.append('fetch')
234    result = command.RunPipe([pipe], capture=True, capture_stderr=True)
235    if result.return_code != 0:
236        raise OSError, 'git fetch: %s' % result.stderr
237
238def CreatePatches(start, count, series):
239    """Create a series of patches from the top of the current branch.
240
241    The patch files are written to the current directory using
242    git format-patch.
243
244    Args:
245        start: Commit to start from: 0=HEAD, 1=next one, etc.
246        count: number of commits to include
247    Return:
248        Filename of cover letter
249        List of filenames of patch files
250    """
251    if series.get('version'):
252        version = '%s ' % series['version']
253    cmd = ['git', 'format-patch', '-M', '--signoff']
254    if series.get('cover'):
255        cmd.append('--cover-letter')
256    prefix = series.GetPatchPrefix()
257    if prefix:
258        cmd += ['--subject-prefix=%s' % prefix]
259    cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)]
260
261    stdout = command.RunList(cmd)
262    files = stdout.splitlines()
263
264    # We have an extra file if there is a cover letter
265    if series.get('cover'):
266       return files[0], files[1:]
267    else:
268       return None, files
269
270def BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True):
271    """Build a list of email addresses based on an input list.
272
273    Takes a list of email addresses and aliases, and turns this into a list
274    of only email address, by resolving any aliases that are present.
275
276    If the tag is given, then each email address is prepended with this
277    tag and a space. If the tag starts with a minus sign (indicating a
278    command line parameter) then the email address is quoted.
279
280    Args:
281        in_list:        List of aliases/email addresses
282        tag:            Text to put before each address
283        alias:          Alias dictionary
284        raise_on_error: True to raise an error when an alias fails to match,
285                False to just print a message.
286
287    Returns:
288        List of email addresses
289
290    >>> alias = {}
291    >>> alias['fred'] = ['f.bloggs@napier.co.nz']
292    >>> alias['john'] = ['j.bloggs@napier.co.nz']
293    >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
294    >>> alias['boys'] = ['fred', ' john']
295    >>> alias['all'] = ['fred ', 'john', '   mary   ']
296    >>> BuildEmailList(['john', 'mary'], None, alias)
297    ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
298    >>> BuildEmailList(['john', 'mary'], '--to', alias)
299    ['--to "j.bloggs@napier.co.nz"', \
300'--to "Mary Poppins <m.poppins@cloud.net>"']
301    >>> BuildEmailList(['john', 'mary'], 'Cc', alias)
302    ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
303    """
304    quote = '"' if tag and tag[0] == '-' else ''
305    raw = []
306    for item in in_list:
307        raw += LookupEmail(item, alias, raise_on_error=raise_on_error)
308    result = []
309    for item in raw:
310        if not item in result:
311            result.append(item)
312    if tag:
313        return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
314    return result
315
316def EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname,
317        self_only=False, alias=None, in_reply_to=None):
318    """Email a patch series.
319
320    Args:
321        series: Series object containing destination info
322        cover_fname: filename of cover letter
323        args: list of filenames of patch files
324        dry_run: Just return the command that would be run
325        raise_on_error: True to raise an error when an alias fails to match,
326                False to just print a message.
327        cc_fname: Filename of Cc file for per-commit Cc
328        self_only: True to just email to yourself as a test
329        in_reply_to: If set we'll pass this to git as --in-reply-to.
330            Should be a message ID that this is in reply to.
331
332    Returns:
333        Git command that was/would be run
334
335    # For the duration of this doctest pretend that we ran patman with ./patman
336    >>> _old_argv0 = sys.argv[0]
337    >>> sys.argv[0] = './patman'
338
339    >>> alias = {}
340    >>> alias['fred'] = ['f.bloggs@napier.co.nz']
341    >>> alias['john'] = ['j.bloggs@napier.co.nz']
342    >>> alias['mary'] = ['m.poppins@cloud.net']
343    >>> alias['boys'] = ['fred', ' john']
344    >>> alias['all'] = ['fred ', 'john', '   mary   ']
345    >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
346    >>> series = series.Series()
347    >>> series.to = ['fred']
348    >>> series.cc = ['mary']
349    >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
350            False, alias)
351    'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
352"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
353    >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \
354            alias)
355    'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
356"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'
357    >>> series.cc = ['all']
358    >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
359            True, alias)
360    'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
361--cc-cmd cc-fname" cover p1 p2'
362    >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
363            False, alias)
364    'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
365"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
366"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
367
368    # Restore argv[0] since we clobbered it.
369    >>> sys.argv[0] = _old_argv0
370    """
371    to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error)
372    if not to:
373        git_config_to = command.Output('git', 'config', 'sendemail.to')
374        if not git_config_to:
375            print ("No recipient.\n"
376                   "Please add something like this to a commit\n"
377                   "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n"
378                   "Or do something like this\n"
379                   "git config sendemail.to u-boot@lists.denx.de")
380            return
381    cc = BuildEmailList(series.get('cc'), '--cc', alias, raise_on_error)
382    if self_only:
383        to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error)
384        cc = []
385    cmd = ['git', 'send-email', '--annotate']
386    if in_reply_to:
387        cmd.append('--in-reply-to="%s"' % in_reply_to)
388
389    cmd += to
390    cmd += cc
391    cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
392    if cover_fname:
393        cmd.append(cover_fname)
394    cmd += args
395    str = ' '.join(cmd)
396    if not dry_run:
397        os.system(str)
398    return str
399
400
401def LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0):
402    """If an email address is an alias, look it up and return the full name
403
404    TODO: Why not just use git's own alias feature?
405
406    Args:
407        lookup_name: Alias or email address to look up
408        alias: Dictionary containing aliases (None to use settings default)
409        raise_on_error: True to raise an error when an alias fails to match,
410                False to just print a message.
411
412    Returns:
413        tuple:
414            list containing a list of email addresses
415
416    Raises:
417        OSError if a recursive alias reference was found
418        ValueError if an alias was not found
419
420    >>> alias = {}
421    >>> alias['fred'] = ['f.bloggs@napier.co.nz']
422    >>> alias['john'] = ['j.bloggs@napier.co.nz']
423    >>> alias['mary'] = ['m.poppins@cloud.net']
424    >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
425    >>> alias['all'] = ['fred ', 'john', '   mary   ']
426    >>> alias['loop'] = ['other', 'john', '   mary   ']
427    >>> alias['other'] = ['loop', 'john', '   mary   ']
428    >>> LookupEmail('mary', alias)
429    ['m.poppins@cloud.net']
430    >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
431    ['arthur.wellesley@howe.ro.uk']
432    >>> LookupEmail('boys', alias)
433    ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
434    >>> LookupEmail('all', alias)
435    ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
436    >>> LookupEmail('odd', alias)
437    Traceback (most recent call last):
438    ...
439    ValueError: Alias 'odd' not found
440    >>> LookupEmail('loop', alias)
441    Traceback (most recent call last):
442    ...
443    OSError: Recursive email alias at 'other'
444    >>> LookupEmail('odd', alias, raise_on_error=False)
445    Alias 'odd' not found
446    []
447    >>> # In this case the loop part will effectively be ignored.
448    >>> LookupEmail('loop', alias, raise_on_error=False)
449    Recursive email alias at 'other'
450    Recursive email alias at 'john'
451    Recursive email alias at 'mary'
452    ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
453    """
454    if not alias:
455        alias = settings.alias
456    lookup_name = lookup_name.strip()
457    if '@' in lookup_name: # Perhaps a real email address
458        return [lookup_name]
459
460    lookup_name = lookup_name.lower()
461    col = terminal.Color()
462
463    out_list = []
464    if level > 10:
465        msg = "Recursive email alias at '%s'" % lookup_name
466        if raise_on_error:
467            raise OSError, msg
468        else:
469            print col.Color(col.RED, msg)
470            return out_list
471
472    if lookup_name:
473        if not lookup_name in alias:
474            msg = "Alias '%s' not found" % lookup_name
475            if raise_on_error:
476                raise ValueError, msg
477            else:
478                print col.Color(col.RED, msg)
479                return out_list
480        for item in alias[lookup_name]:
481            todo = LookupEmail(item, alias, raise_on_error, level + 1)
482            for new_item in todo:
483                if not new_item in out_list:
484                    out_list.append(new_item)
485
486    #print "No match for alias '%s'" % lookup_name
487    return out_list
488
489def GetTopLevel():
490    """Return name of top-level directory for this git repo.
491
492    Returns:
493        Full path to git top-level directory
494
495    This test makes sure that we are running tests in the right subdir
496
497    >>> os.path.realpath(os.path.dirname(__file__)) == \
498            os.path.join(GetTopLevel(), 'tools', 'patman')
499    True
500    """
501    return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
502
503def GetAliasFile():
504    """Gets the name of the git alias file.
505
506    Returns:
507        Filename of git alias file, or None if none
508    """
509    fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile',
510            raise_on_error=False)
511    if fname:
512        fname = os.path.join(GetTopLevel(), fname.strip())
513    return fname
514
515def GetDefaultUserName():
516    """Gets the user.name from .gitconfig file.
517
518    Returns:
519        User name found in .gitconfig file, or None if none
520    """
521    uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
522    return uname
523
524def GetDefaultUserEmail():
525    """Gets the user.email from the global .gitconfig file.
526
527    Returns:
528        User's email found in .gitconfig file, or None if none
529    """
530    uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
531    return uemail
532
533def Setup():
534    """Set up git utils, by reading the alias files."""
535    # Check for a git alias file also
536    global use_no_decorate
537
538    alias_fname = GetAliasFile()
539    if alias_fname:
540        settings.ReadGitAliases(alias_fname)
541    cmd = LogCmd(None, count=0)
542    use_no_decorate = (command.RunPipe([cmd], raise_on_error=False)
543                       .return_code == 0)
544
545def GetHead():
546    """Get the hash of the current HEAD
547
548    Returns:
549        Hash of HEAD
550    """
551    return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H')
552
553if __name__ == "__main__":
554    import doctest
555
556    doctest.testmod()
557