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