gitutil.py 23 KB

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