git_cl_test.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958
  1. #!/usr/bin/env python
  2. # Copyright (c) 2012 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. """Unit tests for git_cl.py."""
  6. import os
  7. import StringIO
  8. import stat
  9. import sys
  10. import unittest
  11. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  12. from testing_support.auto_stub import TestCase
  13. import git_cl
  14. import git_common
  15. import subprocess2
  16. class PresubmitMock(object):
  17. def __init__(self, *args, **kwargs):
  18. self.reviewers = []
  19. @staticmethod
  20. def should_continue():
  21. return True
  22. class RietveldMock(object):
  23. def __init__(self, *args, **kwargs):
  24. pass
  25. @staticmethod
  26. def get_description(issue):
  27. return 'Issue: %d' % issue
  28. @staticmethod
  29. def get_issue_properties(_issue, _messages):
  30. return {
  31. 'reviewers': ['joe@chromium.org', 'john@chromium.org'],
  32. 'messages': [
  33. {
  34. 'approval': True,
  35. 'sender': 'john@chromium.org',
  36. },
  37. ],
  38. }
  39. class WatchlistsMock(object):
  40. def __init__(self, _):
  41. pass
  42. @staticmethod
  43. def GetWatchersForPaths(_):
  44. return ['joe@example.com']
  45. class CodereviewSettingsFileMock(object):
  46. def __init__(self):
  47. pass
  48. # pylint: disable=R0201
  49. def read(self):
  50. return ("CODE_REVIEW_SERVER: gerrit.chromium.org\n" +
  51. "GERRIT_HOST: True\n")
  52. class AuthenticatorMock(object):
  53. def __init__(self, *_args):
  54. pass
  55. def has_cached_credentials(self):
  56. return True
  57. class TestGitCl(TestCase):
  58. def setUp(self):
  59. super(TestGitCl, self).setUp()
  60. self.calls = []
  61. self._calls_done = 0
  62. self.mock(subprocess2, 'call', self._mocked_call)
  63. self.mock(subprocess2, 'check_call', self._mocked_call)
  64. self.mock(subprocess2, 'check_output', self._mocked_call)
  65. self.mock(subprocess2, 'communicate', self._mocked_call)
  66. self.mock(git_common, 'is_dirty_git_tree', lambda x: False)
  67. self.mock(git_common, 'get_or_create_merge_base',
  68. lambda *a: (
  69. self._mocked_call(['get_or_create_merge_base']+list(a))))
  70. self.mock(git_cl, 'BranchExists', lambda _: True)
  71. self.mock(git_cl, 'FindCodereviewSettingsFile', lambda: '')
  72. self.mock(git_cl, 'ask_for_data', self._mocked_call)
  73. self.mock(git_cl.presubmit_support, 'DoPresubmitChecks', PresubmitMock)
  74. self.mock(git_cl.rietveld, 'Rietveld', RietveldMock)
  75. self.mock(git_cl.rietveld, 'CachingRietveld', RietveldMock)
  76. self.mock(git_cl.upload, 'RealMain', self.fail)
  77. self.mock(git_cl.watchlists, 'Watchlists', WatchlistsMock)
  78. self.mock(git_cl.auth, 'get_authenticator_for_host', AuthenticatorMock)
  79. # It's important to reset settings to not have inter-tests interference.
  80. git_cl.settings = None
  81. def tearDown(self):
  82. try:
  83. if not self.has_failed():
  84. self.assertEquals([], self.calls)
  85. finally:
  86. super(TestGitCl, self).tearDown()
  87. def _mocked_call(self, *args, **_kwargs):
  88. self.assertTrue(
  89. self.calls,
  90. '@%d Expected: <Missing> Actual: %r' % (self._calls_done, args))
  91. top = self.calls.pop(0)
  92. if len(top) > 2 and top[2]:
  93. raise top[2]
  94. expected_args, result = top
  95. # Also logs otherwise it could get caught in a try/finally and be hard to
  96. # diagnose.
  97. if expected_args != args:
  98. msg = '@%d Expected: %r Actual: %r' % (
  99. self._calls_done, expected_args, args)
  100. git_cl.logging.error(msg)
  101. self.fail(msg)
  102. self._calls_done += 1
  103. return result
  104. @classmethod
  105. def _upload_calls(cls, similarity, find_copies, private):
  106. return (cls._git_base_calls(similarity, find_copies) +
  107. cls._git_upload_calls(private))
  108. @classmethod
  109. def _upload_no_rev_calls(cls, similarity, find_copies):
  110. return (cls._git_base_calls(similarity, find_copies) +
  111. cls._git_upload_no_rev_calls())
  112. @classmethod
  113. def _git_base_calls(cls, similarity, find_copies):
  114. if similarity is None:
  115. similarity = '50'
  116. similarity_call = ((['git', 'config', '--int', '--get',
  117. 'branch.master.git-cl-similarity'],), '')
  118. else:
  119. similarity_call = ((['git', 'config', '--int',
  120. 'branch.master.git-cl-similarity', similarity],), '')
  121. if find_copies is None:
  122. find_copies = True
  123. find_copies_call = ((['git', 'config', '--int', '--get',
  124. 'branch.master.git-find-copies'],), '')
  125. else:
  126. val = str(int(find_copies))
  127. find_copies_call = ((['git', 'config', '--int',
  128. 'branch.master.git-find-copies', val],), '')
  129. if find_copies:
  130. stat_call = ((['git', 'diff', '--no-ext-diff', '--stat',
  131. '--find-copies-harder', '-l100000', '-C'+similarity,
  132. 'fake_ancestor_sha', 'HEAD'],), '+dat')
  133. else:
  134. stat_call = ((['git', 'diff', '--no-ext-diff', '--stat',
  135. '-M'+similarity, 'fake_ancestor_sha', 'HEAD'],), '+dat')
  136. return [
  137. ((['git', 'config', 'rietveld.autoupdate'],), ''),
  138. ((['git', 'config', 'rietveld.server'],),
  139. 'codereview.example.com'),
  140. ((['git', 'symbolic-ref', 'HEAD'],), 'master'),
  141. similarity_call,
  142. ((['git', 'symbolic-ref', 'HEAD'],), 'master'),
  143. find_copies_call,
  144. ((['git', 'symbolic-ref', 'HEAD'],), 'master'),
  145. ((['git', 'config', 'branch.master.merge'],), 'master'),
  146. ((['git', 'config', 'branch.master.remote'],), 'origin'),
  147. ((['get_or_create_merge_base', 'master', 'master'],),
  148. 'fake_ancestor_sha'),
  149. ((['git', 'config', 'gerrit.host'],), ''),
  150. ((['git', 'config', 'branch.master.rietveldissue'],), ''),
  151. ] + cls._git_sanity_checks('fake_ancestor_sha', 'master') + [
  152. ((['git', 'rev-parse', '--show-cdup'],), ''),
  153. ((['git', 'rev-parse', 'HEAD'],), '12345'),
  154. ((['git', 'diff', '--name-status', '--no-renames', '-r',
  155. 'fake_ancestor_sha...', '.'],),
  156. 'M\t.gitignore\n'),
  157. ((['git', 'config', 'branch.master.rietveldpatchset'],),
  158. ''),
  159. ((['git', 'log', '--pretty=format:%s%n%n%b',
  160. 'fake_ancestor_sha...'],),
  161. 'foo'),
  162. ((['git', 'config', 'user.email'],), 'me@example.com'),
  163. stat_call,
  164. ((['git', 'log', '--pretty=format:%s\n\n%b',
  165. 'fake_ancestor_sha..HEAD'],),
  166. 'desc\n'),
  167. ((['git', 'config', 'rietveld.bug-prefix'],), ''),
  168. ]
  169. @classmethod
  170. def _git_upload_no_rev_calls(cls):
  171. return [
  172. ((['git', 'config', 'core.editor'],), ''),
  173. ]
  174. @classmethod
  175. def _git_upload_calls(cls, private):
  176. if private:
  177. cc_call = []
  178. private_call = []
  179. else:
  180. cc_call = [((['git', 'config', 'rietveld.cc'],), '')]
  181. private_call = [
  182. ((['git', 'config', 'rietveld.private'],), '')]
  183. return [
  184. ((['git', 'config', 'core.editor'],), ''),
  185. ] + cc_call + private_call + [
  186. ((['git', 'config', 'branch.master.base-url'],), ''),
  187. ((['git', 'config', 'rietveld.pending-ref-prefix'],), ''),
  188. ((['git',
  189. 'config', '--local', '--get-regexp', '^svn-remote\\.'],),
  190. (('', None), 0)),
  191. ((['git', 'rev-parse', '--show-cdup'],), ''),
  192. ((['git', 'svn', 'info'],), ''),
  193. ((['git', 'config', 'rietveld.project'],), ''),
  194. ((['git',
  195. 'config', 'branch.master.rietveldissue', '1'],), ''),
  196. ((['git', 'config', 'branch.master.rietveldserver',
  197. 'https://codereview.example.com'],), ''),
  198. ((['git',
  199. 'config', 'branch.master.rietveldpatchset', '2'],), ''),
  200. ((['git', 'rev-parse', 'HEAD'],), 'hash'),
  201. ((['git', 'symbolic-ref', 'HEAD'],), 'hash'),
  202. ((['git',
  203. 'config', 'branch.hash.last-upload-hash', 'hash'],), ''),
  204. ((['git', 'config', 'rietveld.run-post-upload-hook'],), ''),
  205. ]
  206. @staticmethod
  207. def _git_sanity_checks(diff_base, working_branch):
  208. fake_ancestor = 'fake_ancestor'
  209. fake_cl = 'fake_cl_for_patch'
  210. return [
  211. # Calls to verify branch point is ancestor
  212. ((['git',
  213. 'rev-parse', '--verify', diff_base],), fake_ancestor),
  214. ((['git',
  215. 'merge-base', fake_ancestor, 'HEAD'],), fake_ancestor),
  216. ((['git',
  217. 'rev-list', '^' + fake_ancestor, 'HEAD'],), fake_cl),
  218. # Mock a config miss (error code 1)
  219. ((['git',
  220. 'config', 'gitcl.remotebranch'],), (('', None), 1)),
  221. # Call to GetRemoteBranch()
  222. ((['git',
  223. 'config', 'branch.%s.merge' % working_branch],),
  224. 'refs/heads/master'),
  225. ((['git',
  226. 'config', 'branch.%s.remote' % working_branch],), 'origin'),
  227. ((['git', 'rev-list', '^' + fake_ancestor,
  228. 'refs/remotes/origin/master'],), ''),
  229. ]
  230. @classmethod
  231. def _dcommit_calls_1(cls):
  232. return [
  233. ((['git', 'config', 'rietveld.autoupdate'],),
  234. ''),
  235. ((['git', 'config', 'rietveld.pending-ref-prefix'],),
  236. ''),
  237. ((['git',
  238. 'config', '--local', '--get-regexp', '^svn-remote\\.'],),
  239. ((('svn-remote.svn.url svn://svn.chromium.org/chrome\n'
  240. 'svn-remote.svn.fetch trunk/src:refs/remotes/origin/master'),
  241. None),
  242. 0)),
  243. ((['git',
  244. 'config', 'rietveld.server'],), 'codereview.example.com'),
  245. ((['git', 'symbolic-ref', 'HEAD'],), 'refs/heads/working'),
  246. ((['git', 'config', '--int', '--get',
  247. 'branch.working.git-cl-similarity'],), ''),
  248. ((['git', 'symbolic-ref', 'HEAD'],), 'refs/heads/working'),
  249. ((['git', 'config', '--int', '--get',
  250. 'branch.working.git-find-copies'],), ''),
  251. ((['git', 'symbolic-ref', 'HEAD'],), 'refs/heads/working'),
  252. ((['git',
  253. 'config', 'branch.working.merge'],), 'refs/heads/master'),
  254. ((['git', 'config', 'branch.working.remote'],), 'origin'),
  255. ((['git', 'config', 'branch.working.merge'],),
  256. 'refs/heads/master'),
  257. ((['git', 'config', 'branch.working.remote'],), 'origin'),
  258. ((['git', 'rev-list', '--merges',
  259. '--grep=^SVN changes up to revision [0-9]*$',
  260. 'refs/remotes/origin/master^!'],), ''),
  261. ((['git', 'rev-list', '^refs/heads/working',
  262. 'refs/remotes/origin/master'],),
  263. ''),
  264. ((['git',
  265. 'log', '--grep=^git-svn-id:', '-1', '--pretty=format:%H'],),
  266. '3fc18b62c4966193eb435baabe2d18a3810ec82e'),
  267. ((['git',
  268. 'rev-list', '^3fc18b62c4966193eb435baabe2d18a3810ec82e',
  269. 'refs/remotes/origin/master'],), ''),
  270. ((['git',
  271. 'merge-base', 'refs/remotes/origin/master', 'HEAD'],),
  272. 'fake_ancestor_sha'),
  273. ]
  274. @classmethod
  275. def _dcommit_calls_normal(cls):
  276. return [
  277. ((['git', 'rev-parse', '--show-cdup'],), ''),
  278. ((['git', 'rev-parse', 'HEAD'],),
  279. '00ff397798ea57439712ed7e04ab96e13969ef40'),
  280. ((['git',
  281. 'diff', '--name-status', '--no-renames', '-r', 'fake_ancestor_sha...',
  282. '.'],),
  283. 'M\tPRESUBMIT.py'),
  284. ((['git',
  285. 'config', 'branch.working.rietveldissue'],), '12345'),
  286. ((['git',
  287. 'config', 'branch.working.rietveldpatchset'],), '31137'),
  288. ((['git', 'config', 'branch.working.rietveldserver'],),
  289. 'codereview.example.com'),
  290. ((['git', 'config', 'user.email'],), 'author@example.com'),
  291. ((['git', 'config', 'rietveld.tree-status-url'],), ''),
  292. ]
  293. @classmethod
  294. def _dcommit_calls_bypassed(cls):
  295. return [
  296. ((['git',
  297. 'config', 'branch.working.rietveldissue'],), '12345'),
  298. ((['git', 'config', 'branch.working.rietveldserver'],),
  299. 'codereview.example.com'),
  300. ]
  301. @classmethod
  302. def _dcommit_calls_3(cls):
  303. return [
  304. ((['git',
  305. 'diff', '--no-ext-diff', '--stat', '--find-copies-harder',
  306. '-l100000', '-C50', 'fake_ancestor_sha',
  307. 'refs/heads/working'],),
  308. (' PRESUBMIT.py | 2 +-\n'
  309. ' 1 files changed, 1 insertions(+), 1 deletions(-)\n')),
  310. ((['git', 'show-ref', '--quiet', '--verify',
  311. 'refs/heads/git-cl-commit'],),
  312. (('', None), 0)),
  313. ((['git', 'branch', '-D', 'git-cl-commit'],), ''),
  314. ((['git', 'show-ref', '--quiet', '--verify',
  315. 'refs/heads/git-cl-cherry-pick'],), ''),
  316. ((['git', 'rev-parse', '--show-cdup'],), '\n'),
  317. ((['git', 'checkout', '-q', '-b', 'git-cl-commit'],), ''),
  318. ((['git', 'reset', '--soft', 'fake_ancestor_sha'],), ''),
  319. ((['git', 'commit', '-m',
  320. 'Issue: 12345\n\nR=john@chromium.org\n\n'
  321. 'Review URL: https://codereview.example.com/12345 .'],),
  322. ''),
  323. ((['git', 'config', 'rietveld.force-https-commit-url'],), ''),
  324. ((['git',
  325. 'svn', 'dcommit', '-C50', '--no-rebase', '--rmdir'],),
  326. (('', None), 0)),
  327. ((['git', 'checkout', '-q', 'working'],), ''),
  328. ((['git', 'branch', '-D', 'git-cl-commit'],), ''),
  329. ]
  330. @staticmethod
  331. def _cmd_line(description, args, similarity, find_copies, private):
  332. """Returns the upload command line passed to upload.RealMain()."""
  333. return [
  334. 'upload', '--assume_yes', '--server',
  335. 'https://codereview.example.com',
  336. '--message', description
  337. ] + args + [
  338. '--cc', 'joe@example.com',
  339. ] + (['--private'] if private else []) + [
  340. '--git_similarity', similarity or '50'
  341. ] + (['--git_no_find_copies'] if find_copies == False else []) + [
  342. 'fake_ancestor_sha', 'HEAD'
  343. ]
  344. def _run_reviewer_test(
  345. self,
  346. upload_args,
  347. expected_description,
  348. returned_description,
  349. final_description,
  350. reviewers,
  351. private=False):
  352. """Generic reviewer test framework."""
  353. try:
  354. similarity = upload_args[upload_args.index('--similarity')+1]
  355. except ValueError:
  356. similarity = None
  357. if '--find-copies' in upload_args:
  358. find_copies = True
  359. elif '--no-find-copies' in upload_args:
  360. find_copies = False
  361. else:
  362. find_copies = None
  363. private = '--private' in upload_args
  364. self.calls = self._upload_calls(similarity, find_copies, private)
  365. def RunEditor(desc, _, **kwargs):
  366. self.assertEquals(
  367. '# Enter a description of the change.\n'
  368. '# This will be displayed on the codereview site.\n'
  369. '# The first line will also be used as the subject of the review.\n'
  370. '#--------------------This line is 72 characters long'
  371. '--------------------\n' +
  372. expected_description,
  373. desc)
  374. return returned_description
  375. self.mock(git_cl.gclient_utils, 'RunEditor', RunEditor)
  376. def check_upload(args):
  377. cmd_line = self._cmd_line(final_description, reviewers, similarity,
  378. find_copies, private)
  379. self.assertEquals(cmd_line, args)
  380. return 1, 2
  381. self.mock(git_cl.upload, 'RealMain', check_upload)
  382. git_cl.main(['upload'] + upload_args)
  383. def test_no_reviewer(self):
  384. self._run_reviewer_test(
  385. [],
  386. 'desc\n\nBUG=',
  387. '# Blah blah comment.\ndesc\n\nBUG=',
  388. 'desc\n\nBUG=',
  389. [])
  390. def test_keep_similarity(self):
  391. self._run_reviewer_test(
  392. ['--similarity', '70'],
  393. 'desc\n\nBUG=',
  394. '# Blah blah comment.\ndesc\n\nBUG=',
  395. 'desc\n\nBUG=',
  396. [])
  397. def test_keep_find_copies(self):
  398. self._run_reviewer_test(
  399. ['--no-find-copies'],
  400. 'desc\n\nBUG=',
  401. '# Blah blah comment.\ndesc\n\nBUG=\n',
  402. 'desc\n\nBUG=',
  403. [])
  404. def test_private(self):
  405. self._run_reviewer_test(
  406. ['--private'],
  407. 'desc\n\nBUG=',
  408. '# Blah blah comment.\ndesc\n\nBUG=\n',
  409. 'desc\n\nBUG=',
  410. [])
  411. def test_reviewers_cmd_line(self):
  412. # Reviewer is passed as-is
  413. description = 'desc\n\nR=foo@example.com\nBUG='
  414. self._run_reviewer_test(
  415. ['-r' 'foo@example.com'],
  416. description,
  417. '\n%s\n' % description,
  418. description,
  419. ['--reviewers=foo@example.com'])
  420. def test_reviewer_tbr_overriden(self):
  421. # Reviewer is overriden with TBR
  422. # Also verifies the regexp work without a trailing LF
  423. description = 'Foo Bar\n\nTBR=reviewer@example.com'
  424. self._run_reviewer_test(
  425. ['-r' 'foo@example.com'],
  426. 'desc\n\nR=foo@example.com\nBUG=',
  427. description.strip('\n'),
  428. description,
  429. ['--reviewers=reviewer@example.com'])
  430. def test_reviewer_multiple(self):
  431. # Handles multiple R= or TBR= lines.
  432. description = (
  433. 'Foo Bar\nTBR=reviewer@example.com\nBUG=\nR=another@example.com')
  434. self._run_reviewer_test(
  435. [],
  436. 'desc\n\nBUG=',
  437. description,
  438. description,
  439. ['--reviewers=another@example.com,reviewer@example.com'])
  440. def test_reviewer_send_mail(self):
  441. # --send-mail can be used without -r if R= is used
  442. description = 'Foo Bar\nR=reviewer@example.com'
  443. self._run_reviewer_test(
  444. ['--send-mail'],
  445. 'desc\n\nBUG=',
  446. description.strip('\n'),
  447. description,
  448. ['--reviewers=reviewer@example.com', '--send_mail'])
  449. def test_reviewer_send_mail_no_rev(self):
  450. # Fails without a reviewer.
  451. stdout = StringIO.StringIO()
  452. stderr = StringIO.StringIO()
  453. try:
  454. self.calls = self._upload_no_rev_calls(None, None)
  455. def RunEditor(desc, _, **kwargs):
  456. return desc
  457. self.mock(git_cl.gclient_utils, 'RunEditor', RunEditor)
  458. self.mock(sys, 'stdout', stdout)
  459. self.mock(sys, 'stderr', stderr)
  460. git_cl.main(['upload', '--send-mail'])
  461. self.fail()
  462. except SystemExit:
  463. self.assertEqual(
  464. 'Using 50% similarity for rename/copy detection. Override with '
  465. '--similarity.\n',
  466. stdout.getvalue())
  467. self.assertEqual(
  468. 'Must specify reviewers to send email.\n', stderr.getvalue())
  469. def test_dcommit(self):
  470. self.calls = (
  471. self._dcommit_calls_1() +
  472. self._git_sanity_checks('fake_ancestor_sha', 'working') +
  473. self._dcommit_calls_normal() +
  474. self._dcommit_calls_3())
  475. git_cl.main(['dcommit'])
  476. def test_dcommit_bypass_hooks(self):
  477. self.calls = (
  478. self._dcommit_calls_1() +
  479. self._dcommit_calls_bypassed() +
  480. self._dcommit_calls_3())
  481. git_cl.main(['dcommit', '--bypass-hooks'])
  482. @classmethod
  483. def _gerrit_base_calls(cls):
  484. return [
  485. ((['git', 'config', 'rietveld.autoupdate'],),
  486. ''),
  487. ((['git',
  488. 'config', 'rietveld.server'],), 'codereview.example.com'),
  489. ((['git', 'symbolic-ref', 'HEAD'],), 'master'),
  490. ((['git', 'config', '--int', '--get',
  491. 'branch.master.git-cl-similarity'],), ''),
  492. ((['git', 'symbolic-ref', 'HEAD'],), 'master'),
  493. ((['git', 'config', '--int', '--get',
  494. 'branch.master.git-find-copies'],), ''),
  495. ((['git', 'symbolic-ref', 'HEAD'],), 'master'),
  496. ((['git', 'config', 'branch.master.merge'],), 'master'),
  497. ((['git', 'config', 'branch.master.remote'],), 'origin'),
  498. ((['get_or_create_merge_base', 'master', 'master'],),
  499. 'fake_ancestor_sha'),
  500. ((['git', 'config', 'gerrit.host'],), 'True'),
  501. ] + cls._git_sanity_checks('fake_ancestor_sha', 'master') + [
  502. ((['git', 'rev-parse', '--show-cdup'],), ''),
  503. ((['git', 'rev-parse', 'HEAD'],), '12345'),
  504. ((['git',
  505. 'diff', '--name-status', '--no-renames', '-r',
  506. 'fake_ancestor_sha...', '.'],),
  507. 'M\t.gitignore\n'),
  508. ((['git', 'config', 'branch.master.rietveldissue'],), ''),
  509. ((['git',
  510. 'config', 'branch.master.rietveldpatchset'],), ''),
  511. ((['git',
  512. 'log', '--pretty=format:%s%n%n%b', 'fake_ancestor_sha...'],),
  513. 'foo'),
  514. ((['git', 'config', 'user.email'],), 'me@example.com'),
  515. ((['git',
  516. 'diff', '--no-ext-diff', '--stat', '--find-copies-harder',
  517. '-l100000', '-C50', 'fake_ancestor_sha', 'HEAD'],),
  518. '+dat'),
  519. ]
  520. @staticmethod
  521. def _gerrit_upload_calls(description, reviewers, squash,
  522. expected_upstream_ref='origin/refs/heads/master'):
  523. calls = [
  524. ((['git', 'config', '--bool', 'gerrit.squash-uploads'],), 'false'),
  525. ((['git', 'log', '--pretty=format:%s\n\n%b',
  526. 'fake_ancestor_sha..HEAD'],),
  527. description)
  528. ]
  529. if git_cl.CHANGE_ID not in description:
  530. calls += [
  531. ((['git', 'log', '--pretty=format:%s\n\n%b',
  532. 'fake_ancestor_sha..HEAD'],),
  533. description),
  534. ((['git', 'commit', '--amend', '-m', description],),
  535. ''),
  536. ((['git', 'log', '--pretty=format:%s\n\n%b',
  537. 'fake_ancestor_sha..HEAD'],),
  538. description)
  539. ]
  540. if squash:
  541. ref_to_push = 'abcdef0123456789'
  542. calls += [
  543. ((['git', 'show', '--format=%s\n\n%b', '-s',
  544. 'refs/heads/git_cl_uploads/master'],),
  545. (description, 0)),
  546. ((['git', 'config', 'branch.master.merge'],),
  547. 'refs/heads/master'),
  548. ((['git', 'config', 'branch.master.remote'],),
  549. 'origin'),
  550. ((['get_or_create_merge_base', 'master', 'master'],),
  551. 'origin/master'),
  552. ((['git', 'rev-parse', 'HEAD:'],),
  553. '0123456789abcdef'),
  554. ((['git', 'commit-tree', '0123456789abcdef', '-p',
  555. 'origin/master', '-m', 'd'],),
  556. ref_to_push),
  557. ]
  558. else:
  559. ref_to_push = 'HEAD'
  560. calls += [
  561. ((['git', 'rev-list',
  562. expected_upstream_ref + '..' + ref_to_push],), ''),
  563. ((['git', 'config', 'rietveld.cc'],), '')
  564. ]
  565. receive_pack = '--receive-pack=git receive-pack '
  566. receive_pack += '--cc=joe@example.com' # from watch list
  567. if reviewers:
  568. receive_pack += ' '
  569. receive_pack += ' '.join(
  570. '--reviewer=' + email for email in sorted(reviewers))
  571. receive_pack += ''
  572. calls += [
  573. ((['git',
  574. 'push', receive_pack, 'origin',
  575. ref_to_push + ':refs/for/refs/heads/master'],),
  576. '')
  577. ]
  578. if squash:
  579. calls += [
  580. ((['git', 'rev-parse', 'HEAD'],), 'abcdef0123456789'),
  581. ((['git', 'update-ref', '-m', 'Uploaded abcdef0123456789',
  582. 'refs/heads/git_cl_uploads/master', 'abcdef0123456789'],),
  583. '')
  584. ]
  585. return calls
  586. def _run_gerrit_upload_test(
  587. self,
  588. upload_args,
  589. description,
  590. reviewers,
  591. squash=False,
  592. expected_upstream_ref='origin/refs/heads/master'):
  593. """Generic gerrit upload test framework."""
  594. self.calls = self._gerrit_base_calls()
  595. self.calls += self._gerrit_upload_calls(
  596. description, reviewers, squash,
  597. expected_upstream_ref=expected_upstream_ref)
  598. git_cl.main(['upload'] + upload_args)
  599. def test_gerrit_upload_without_change_id(self):
  600. self._run_gerrit_upload_test(
  601. [],
  602. 'desc\n\nBUG=\n',
  603. [])
  604. def test_gerrit_no_reviewer(self):
  605. self._run_gerrit_upload_test(
  606. [],
  607. 'desc\n\nBUG=\nChange-Id:123456789\n',
  608. [])
  609. def test_gerrit_reviewers_cmd_line(self):
  610. self._run_gerrit_upload_test(
  611. ['-r', 'foo@example.com'],
  612. 'desc\n\nBUG=\nChange-Id:123456789',
  613. ['foo@example.com'])
  614. def test_gerrit_reviewer_multiple(self):
  615. self._run_gerrit_upload_test(
  616. [],
  617. 'desc\nTBR=reviewer@example.com\nBUG=\nR=another@example.com\n'
  618. 'Change-Id:123456789\n',
  619. ['reviewer@example.com', 'another@example.com'])
  620. def test_gerrit_upload_squash(self):
  621. self._run_gerrit_upload_test(
  622. ['--squash'],
  623. 'desc\n\nBUG=\nChange-Id:123456789\n',
  624. [],
  625. squash=True,
  626. expected_upstream_ref='origin/master')
  627. def test_upload_branch_deps(self):
  628. def mock_run_git(*args, **_kwargs):
  629. if args[0] == ['for-each-ref',
  630. '--format=%(refname:short) %(upstream:short)',
  631. 'refs/heads']:
  632. # Create a local branch dependency tree that looks like this:
  633. # test1 -> test2 -> test3 -> test4 -> test5
  634. # -> test3.1
  635. # test6 -> test0
  636. branch_deps = [
  637. 'test2 test1', # test1 -> test2
  638. 'test3 test2', # test2 -> test3
  639. 'test3.1 test2', # test2 -> test3.1
  640. 'test4 test3', # test3 -> test4
  641. 'test5 test4', # test4 -> test5
  642. 'test6 test0', # test0 -> test6
  643. 'test7', # test7
  644. ]
  645. return '\n'.join(branch_deps)
  646. self.mock(git_cl, 'RunGit', mock_run_git)
  647. git_cl.settings = git_cl.Settings()
  648. self.mock(git_cl.settings, 'GetIsGerrit', lambda: False)
  649. class RecordCalls:
  650. times_called = 0
  651. record_calls = RecordCalls()
  652. def mock_CMDupload(*args, **_kwargs):
  653. record_calls.times_called += 1
  654. return 0
  655. self.mock(git_cl, 'CMDupload', mock_CMDupload)
  656. self.calls = [
  657. (('[Press enter to continue or ctrl-C to quit]',), ''),
  658. ]
  659. class MockChangelist():
  660. def __init__(self):
  661. pass
  662. def GetBranch(self):
  663. return 'test1'
  664. def GetIssue(self):
  665. return '123'
  666. def GetPatchset(self):
  667. return '1001'
  668. ret = git_cl.upload_branch_deps(MockChangelist(), [])
  669. # CMDupload should have been called 5 times because of 5 dependent branches.
  670. self.assertEquals(5, record_calls.times_called)
  671. self.assertEquals(0, ret)
  672. def test_config_gerrit_download_hook(self):
  673. self.mock(git_cl, 'FindCodereviewSettingsFile', CodereviewSettingsFileMock)
  674. def ParseCodereviewSettingsContent(content):
  675. keyvals = {}
  676. keyvals['CODE_REVIEW_SERVER'] = 'gerrit.chromium.org'
  677. keyvals['GERRIT_HOST'] = 'True'
  678. return keyvals
  679. self.mock(git_cl.gclient_utils, 'ParseCodereviewSettingsContent',
  680. ParseCodereviewSettingsContent)
  681. self.mock(git_cl.os, 'access', self._mocked_call)
  682. self.mock(git_cl.os, 'chmod', self._mocked_call)
  683. src_dir = os.path.join(os.path.sep, 'usr', 'local', 'src')
  684. def AbsPath(path):
  685. if not path.startswith(os.path.sep):
  686. return os.path.join(src_dir, path)
  687. return path
  688. self.mock(git_cl.os.path, 'abspath', AbsPath)
  689. commit_msg_path = os.path.join(src_dir, '.git', 'hooks', 'commit-msg')
  690. def Exists(path):
  691. if path == commit_msg_path:
  692. return False
  693. # others paths, such as /usr/share/locale/....
  694. return True
  695. self.mock(git_cl.os.path, 'exists', Exists)
  696. self.mock(git_cl, 'urlretrieve', self._mocked_call)
  697. self.mock(git_cl, 'hasSheBang', self._mocked_call)
  698. self.calls = [
  699. ((['git', 'config', 'rietveld.autoupdate'],),
  700. ''),
  701. ((['git', 'config', 'rietveld.server',
  702. 'gerrit.chromium.org'],), ''),
  703. ((['git', 'config', '--unset-all', 'rietveld.cc'],), ''),
  704. ((['git', 'config', '--unset-all',
  705. 'rietveld.private'],), ''),
  706. ((['git', 'config', '--unset-all',
  707. 'rietveld.tree-status-url'],), ''),
  708. ((['git', 'config', '--unset-all',
  709. 'rietveld.viewvc-url'],), ''),
  710. ((['git', 'config', '--unset-all',
  711. 'rietveld.bug-prefix'],), ''),
  712. ((['git', 'config', '--unset-all',
  713. 'rietveld.cpplint-regex'],), ''),
  714. ((['git', 'config', '--unset-all',
  715. 'rietveld.force-https-commit-url'],), ''),
  716. ((['git', 'config', '--unset-all',
  717. 'rietveld.cpplint-ignore-regex'],), ''),
  718. ((['git', 'config', '--unset-all',
  719. 'rietveld.project'],), ''),
  720. ((['git', 'config', '--unset-all',
  721. 'rietveld.pending-ref-prefix'],), ''),
  722. ((['git', 'config', '--unset-all',
  723. 'rietveld.run-post-upload-hook'],), ''),
  724. ((['git', 'config', 'gerrit.host', 'True'],), ''),
  725. # DownloadHooks(False)
  726. ((['git', 'config', 'gerrit.host'],), 'True'),
  727. ((['git', 'rev-parse', '--show-cdup'],), ''),
  728. ((commit_msg_path, os.X_OK,), False),
  729. (('https://gerrit-review.googlesource.com/tools/hooks/commit-msg',
  730. commit_msg_path,), ''),
  731. ((commit_msg_path,), True),
  732. ((commit_msg_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR,), ''),
  733. # GetCodereviewSettingsInteractively
  734. ((['git', 'config', 'rietveld.server'],),
  735. 'gerrit.chromium.org'),
  736. (('Rietveld server (host[:port]) [https://gerrit.chromium.org]:',),
  737. ''),
  738. ((['git', 'config', 'rietveld.cc'],), ''),
  739. (('CC list:',), ''),
  740. ((['git', 'config', 'rietveld.private'],), ''),
  741. (('Private flag (rietveld only):',), ''),
  742. ((['git', 'config', 'rietveld.tree-status-url'],), ''),
  743. (('Tree status URL:',), ''),
  744. ((['git', 'config', 'rietveld.viewvc-url'],), ''),
  745. (('ViewVC URL:',), ''),
  746. # DownloadHooks(True)
  747. ((['git', 'config', 'rietveld.bug-prefix'],), ''),
  748. (('Bug Prefix:',), ''),
  749. ((['git', 'config', 'rietveld.run-post-upload-hook'],), ''),
  750. (('Run Post Upload Hook:',), ''),
  751. ((commit_msg_path, os.X_OK,), True),
  752. ]
  753. git_cl.main(['config'])
  754. def test_update_reviewers(self):
  755. data = [
  756. ('foo', [], 'foo'),
  757. ('foo\nR=xx', [], 'foo\nR=xx'),
  758. ('foo\nTBR=xx', [], 'foo\nTBR=xx'),
  759. ('foo', ['a@c'], 'foo\n\nR=a@c'),
  760. ('foo\nR=xx', ['a@c'], 'foo\n\nR=a@c, xx'),
  761. ('foo\nTBR=xx', ['a@c'], 'foo\n\nR=a@c\nTBR=xx'),
  762. ('foo\nTBR=xx\nR=yy', ['a@c'], 'foo\n\nR=a@c, yy\nTBR=xx'),
  763. ('foo\nBUG=', ['a@c'], 'foo\nBUG=\nR=a@c'),
  764. ('foo\nR=xx\nTBR=yy\nR=bar', ['a@c'], 'foo\n\nR=a@c, xx, bar\nTBR=yy'),
  765. ('foo', ['a@c', 'b@c'], 'foo\n\nR=a@c, b@c'),
  766. ('foo\nBar\n\nR=\nBUG=', ['c@c'], 'foo\nBar\n\nR=c@c\nBUG='),
  767. ('foo\nBar\n\nR=\nBUG=\nR=', ['c@c'], 'foo\nBar\n\nR=c@c\nBUG='),
  768. # Same as the line before, but full of whitespaces.
  769. (
  770. 'foo\nBar\n\n R = \n BUG = \n R = ', ['c@c'],
  771. 'foo\nBar\n\nR=c@c\n BUG =',
  772. ),
  773. # Whitespaces aren't interpreted as new lines.
  774. ('foo BUG=allo R=joe ', ['c@c'], 'foo BUG=allo R=joe\n\nR=c@c'),
  775. ]
  776. expected = [i[2] for i in data]
  777. actual = []
  778. for orig, reviewers, _expected in data:
  779. obj = git_cl.ChangeDescription(orig)
  780. obj.update_reviewers(reviewers)
  781. actual.append(obj.description)
  782. self.assertEqual(expected, actual)
  783. def test_get_target_ref(self):
  784. # Check remote or remote branch not present.
  785. self.assertEqual(None, git_cl.GetTargetRef('origin', None, 'master', None))
  786. self.assertEqual(None, git_cl.GetTargetRef(None,
  787. 'refs/remotes/origin/master',
  788. 'master', None))
  789. # Check default target refs for branches.
  790. self.assertEqual('refs/heads/master',
  791. git_cl.GetTargetRef('origin', 'refs/remotes/origin/master',
  792. None, None))
  793. self.assertEqual('refs/heads/master',
  794. git_cl.GetTargetRef('origin', 'refs/remotes/origin/lkgr',
  795. None, None))
  796. self.assertEqual('refs/heads/master',
  797. git_cl.GetTargetRef('origin', 'refs/remotes/origin/lkcr',
  798. None, None))
  799. self.assertEqual('refs/branch-heads/123',
  800. git_cl.GetTargetRef('origin',
  801. 'refs/remotes/branch-heads/123',
  802. None, None))
  803. self.assertEqual('refs/diff/test',
  804. git_cl.GetTargetRef('origin',
  805. 'refs/remotes/origin/refs/diff/test',
  806. None, None))
  807. self.assertEqual('refs/heads/chrome/m42',
  808. git_cl.GetTargetRef('origin',
  809. 'refs/remotes/origin/chrome/m42',
  810. None, None))
  811. # Check target refs for user-specified target branch.
  812. for branch in ('branch-heads/123', 'remotes/branch-heads/123',
  813. 'refs/remotes/branch-heads/123'):
  814. self.assertEqual('refs/branch-heads/123',
  815. git_cl.GetTargetRef('origin',
  816. 'refs/remotes/origin/master',
  817. branch, None))
  818. for branch in ('origin/master', 'remotes/origin/master',
  819. 'refs/remotes/origin/master'):
  820. self.assertEqual('refs/heads/master',
  821. git_cl.GetTargetRef('origin',
  822. 'refs/remotes/branch-heads/123',
  823. branch, None))
  824. for branch in ('master', 'heads/master', 'refs/heads/master'):
  825. self.assertEqual('refs/heads/master',
  826. git_cl.GetTargetRef('origin',
  827. 'refs/remotes/branch-heads/123',
  828. branch, None))
  829. # Check target refs for pending prefix.
  830. self.assertEqual('prefix/heads/master',
  831. git_cl.GetTargetRef('origin', 'refs/remotes/origin/master',
  832. None, 'prefix/'))
  833. def test_patch_when_dirty(self):
  834. # Patch when local tree is dirty
  835. self.mock(git_common, 'is_dirty_git_tree', lambda x: True)
  836. self.assertNotEqual(git_cl.main(['patch', '123456']), 0)
  837. def test_diff_when_dirty(self):
  838. # Do 'git cl diff' when local tree is dirty
  839. self.mock(git_common, 'is_dirty_git_tree', lambda x: True)
  840. self.assertNotEqual(git_cl.main(['diff']), 0)
  841. def _patch_common(self):
  842. self.mock(git_cl.Changelist, 'GetMostRecentPatchset', lambda x: '60001')
  843. self.mock(git_cl.Changelist, 'GetPatchSetDiff', lambda *args: None)
  844. self.mock(git_cl.Changelist, 'GetDescription', lambda *args: 'Description')
  845. self.mock(git_cl.Changelist, 'SetIssue', lambda *args: None)
  846. self.mock(git_cl.Changelist, 'SetPatchset', lambda *args: None)
  847. self.mock(git_cl, 'IsGitVersionAtLeast', lambda *args: True)
  848. self.calls = [
  849. ((['git', 'config', 'rietveld.autoupdate'],), ''),
  850. ((['git', 'config', 'rietveld.server'],), 'codereview.example.com'),
  851. ((['git', 'rev-parse', '--show-cdup'],), ''),
  852. ((['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'],), ''),
  853. ]
  854. def test_patch_successful(self):
  855. self._patch_common()
  856. self.calls += [
  857. ((['git', 'apply', '--index', '-p0', '--3way'],), ''),
  858. ((['git', 'commit', '-m',
  859. 'Description\n\n' +
  860. 'patch from issue 123456 at patchset 60001 ' +
  861. '(http://crrev.com/123456#ps60001)'],), ''),
  862. ]
  863. self.assertEqual(git_cl.main(['patch', '123456']), 0)
  864. def test_patch_conflict(self):
  865. self._patch_common()
  866. self.calls += [
  867. ((['git', 'apply', '--index', '-p0', '--3way'],), '',
  868. subprocess2.CalledProcessError(1, '', '', '', '')),
  869. ]
  870. self.assertNotEqual(git_cl.main(['patch', '123456']), 0)
  871. if __name__ == '__main__':
  872. git_cl.logging.basicConfig(
  873. level=git_cl.logging.DEBUG if '-v' in sys.argv else git_cl.logging.ERROR)
  874. unittest.main()