git_cl_test.py 35 KB

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