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