gclient_scm_test.py 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545
  1. #!/usr/bin/env vpython3
  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 gclient_scm.py."""
  6. # pylint: disable=E1103
  7. from __future__ import unicode_literals
  8. from subprocess import Popen, PIPE, STDOUT
  9. import json
  10. import logging
  11. import os
  12. import re
  13. import sys
  14. import tempfile
  15. import unittest
  16. if sys.version_info.major == 2:
  17. from cStringIO import StringIO
  18. import mock
  19. else:
  20. from io import StringIO
  21. from unittest import mock
  22. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  23. from testing_support import fake_repos
  24. from testing_support import test_case_utils
  25. import gclient_scm
  26. import gclient_utils
  27. import git_cache
  28. import subprocess2
  29. GIT = 'git' if sys.platform != 'win32' else 'git.bat'
  30. # Disable global git cache
  31. git_cache.Mirror.SetCachePath(None)
  32. # Shortcut since this function is used often
  33. join = gclient_scm.os.path.join
  34. TIMESTAMP_RE = re.compile(r'\[[0-9]{1,2}:[0-9]{2}:[0-9]{2}\] (.*)', re.DOTALL)
  35. def strip_timestamps(value):
  36. lines = value.splitlines(True)
  37. for i in range(len(lines)):
  38. m = TIMESTAMP_RE.match(lines[i])
  39. if m:
  40. lines[i] = m.group(1)
  41. return ''.join(lines)
  42. class BasicTests(unittest.TestCase):
  43. @mock.patch('gclient_scm.scm.GIT.Capture')
  44. def testGetFirstRemoteUrl(self, mockCapture):
  45. REMOTE_STRINGS = [('remote.origin.url E:\\foo\\bar', 'E:\\foo\\bar'),
  46. ('remote.origin.url /b/foo/bar', '/b/foo/bar'),
  47. ('remote.origin.url https://foo/bar', 'https://foo/bar'),
  48. ('remote.origin.url E:\\Fo Bar\\bax', 'E:\\Fo Bar\\bax'),
  49. ('remote.origin.url git://what/"do', 'git://what/"do')]
  50. FAKE_PATH = '/fake/path'
  51. mockCapture.side_effect = [question for question, _ in REMOTE_STRINGS]
  52. for _, answer in REMOTE_STRINGS:
  53. self.assertEqual(
  54. gclient_scm.SCMWrapper._get_first_remote_url(FAKE_PATH), answer)
  55. expected_calls = [
  56. mock.call(['config', '--local', '--get-regexp', r'remote.*.url'],
  57. cwd=FAKE_PATH)
  58. for _ in REMOTE_STRINGS
  59. ]
  60. self.assertEqual(mockCapture.mock_calls, expected_calls)
  61. class BaseGitWrapperTestCase(unittest.TestCase, test_case_utils.TestCaseUtils):
  62. """This class doesn't use pymox."""
  63. class OptionsObject(object):
  64. def __init__(self, verbose=False, revision=None):
  65. self.auto_rebase = False
  66. self.verbose = verbose
  67. self.revision = revision
  68. self.deps_os = None
  69. self.force = False
  70. self.reset = False
  71. self.nohooks = False
  72. self.no_history = False
  73. self.upstream = False
  74. self.cache_dir = None
  75. self.merge = False
  76. self.jobs = 1
  77. self.break_repo_locks = False
  78. self.delete_unversioned_trees = False
  79. self.patch_ref = None
  80. self.patch_repo = None
  81. self.rebase_patch_ref = True
  82. self.reset_patch_ref = True
  83. sample_git_import = """blob
  84. mark :1
  85. data 6
  86. Hello
  87. blob
  88. mark :2
  89. data 4
  90. Bye
  91. reset refs/heads/main
  92. commit refs/heads/main
  93. mark :3
  94. author Bob <bob@example.com> 1253744361 -0700
  95. committer Bob <bob@example.com> 1253744361 -0700
  96. data 8
  97. A and B
  98. M 100644 :1 a
  99. M 100644 :2 b
  100. blob
  101. mark :4
  102. data 10
  103. Hello
  104. You
  105. blob
  106. mark :5
  107. data 8
  108. Bye
  109. You
  110. commit refs/heads/origin
  111. mark :6
  112. author Alice <alice@example.com> 1253744424 -0700
  113. committer Alice <alice@example.com> 1253744424 -0700
  114. data 13
  115. Personalized
  116. from :3
  117. M 100644 :4 a
  118. M 100644 :5 b
  119. blob
  120. mark :7
  121. data 5
  122. Mooh
  123. commit refs/heads/feature
  124. mark :8
  125. author Bob <bob@example.com> 1390311986 -0000
  126. committer Bob <bob@example.com> 1390311986 -0000
  127. data 6
  128. Add C
  129. from :3
  130. M 100644 :7 c
  131. reset refs/heads/main
  132. from :3
  133. """
  134. def Options(self, *args, **kwargs):
  135. return self.OptionsObject(*args, **kwargs)
  136. def checkstdout(self, expected):
  137. # pylint: disable=no-member
  138. value = sys.stdout.getvalue()
  139. sys.stdout.close()
  140. # Check that the expected output appears.
  141. self.assertIn(expected, strip_timestamps(value))
  142. @staticmethod
  143. def CreateGitRepo(git_import, path):
  144. """Do it for real."""
  145. try:
  146. Popen([GIT, 'init', '-q'], stdout=PIPE, stderr=STDOUT,
  147. cwd=path).communicate()
  148. except OSError:
  149. # git is not available, skip this test.
  150. return False
  151. Popen([GIT, 'fast-import', '--quiet'], stdin=PIPE, stdout=PIPE,
  152. stderr=STDOUT, cwd=path).communicate(input=git_import.encode())
  153. Popen([GIT, 'checkout', '-q'], stdout=PIPE, stderr=STDOUT,
  154. cwd=path).communicate()
  155. Popen([GIT, 'remote', 'add', '-f', 'origin', '.'], stdout=PIPE,
  156. stderr=STDOUT, cwd=path).communicate()
  157. Popen([GIT, 'checkout', '-b', 'new', 'origin/main', '-q'], stdout=PIPE,
  158. stderr=STDOUT, cwd=path).communicate()
  159. Popen([GIT, 'push', 'origin', 'origin/origin:origin/main', '-q'],
  160. stdout=PIPE, stderr=STDOUT, cwd=path).communicate()
  161. Popen([GIT, 'config', '--unset', 'remote.origin.fetch'], stdout=PIPE,
  162. stderr=STDOUT, cwd=path).communicate()
  163. Popen([GIT, 'config', 'user.email', 'someuser@chromium.org'], stdout=PIPE,
  164. stderr=STDOUT, cwd=path).communicate()
  165. Popen([GIT, 'config', 'user.name', 'Some User'], stdout=PIPE,
  166. stderr=STDOUT, cwd=path).communicate()
  167. # Set HEAD back to main
  168. Popen([GIT, 'checkout', 'main', '-q'],
  169. stdout=PIPE,
  170. stderr=STDOUT,
  171. cwd=path).communicate()
  172. return True
  173. def _GetAskForDataCallback(self, expected_prompt, return_value):
  174. def AskForData(prompt, options):
  175. self.assertEqual(prompt, expected_prompt)
  176. return return_value
  177. return AskForData
  178. def setUp(self):
  179. unittest.TestCase.setUp(self)
  180. test_case_utils.TestCaseUtils.setUp(self)
  181. self.url = 'git://foo'
  182. # The .git suffix allows gclient_scm to recognize the dir as a git repo
  183. # when cloning it locally
  184. self.root_dir = tempfile.mkdtemp('.git')
  185. self.relpath = '.'
  186. self.base_path = join(self.root_dir, self.relpath)
  187. self.enabled = self.CreateGitRepo(self.sample_git_import, self.base_path)
  188. mock.patch('sys.stdout', StringIO()).start()
  189. self.addCleanup(mock.patch.stopall)
  190. self.addCleanup(gclient_utils.rmtree, self.root_dir)
  191. class ManagedGitWrapperTestCase(BaseGitWrapperTestCase):
  192. @mock.patch('gclient_scm.GitWrapper._IsCog')
  193. @mock.patch('gclient_scm.GitWrapper._Run', return_value=True)
  194. @mock.patch('gclient_scm.GitWrapper._SetFetchConfig')
  195. @mock.patch('gclient_scm.GitWrapper._GetCurrentBranch')
  196. def testCloneInCog(self, mockGetCurrentBranch, mockSetFetchConfig, mockRun,
  197. _mockIsCog):
  198. """Test that we call the correct commands when in a cog workspace."""
  199. if not self.enabled:
  200. return
  201. options = self.Options()
  202. scm = gclient_scm.GitWrapper(self.url, self.root_dir, self.relpath)
  203. scm._Clone('123123ab', self.url, options)
  204. mockRun.assert_called_once_with(
  205. ['citc', 'clone-repo', self.url, scm.checkout_path, '123123ab'],
  206. options,
  207. cwd=scm._root_dir,
  208. retry=True,
  209. print_stdout=False,
  210. filter_fn=scm.filter)
  211. mockSetFetchConfig.assert_called_once()
  212. mockGetCurrentBranch.assert_called_once()
  213. def testRevertMissing(self):
  214. if not self.enabled:
  215. return
  216. options = self.Options()
  217. file_path = join(self.base_path, 'a')
  218. scm = gclient_scm.GitWrapper(self.url, self.root_dir,
  219. self.relpath)
  220. file_list = []
  221. scm.update(options, None, file_list)
  222. gclient_scm.os.remove(file_path)
  223. file_list = []
  224. scm.revert(options, self.args, file_list)
  225. self.assertEqual(file_list, [file_path])
  226. file_list = []
  227. scm.diff(options, self.args, file_list)
  228. self.assertEqual(file_list, [])
  229. sys.stdout.close()
  230. def testRevertNone(self):
  231. if not self.enabled:
  232. return
  233. options = self.Options()
  234. scm = gclient_scm.GitWrapper(self.url, self.root_dir,
  235. self.relpath)
  236. file_list = []
  237. scm.update(options, None, file_list)
  238. file_list = []
  239. scm.revert(options, self.args, file_list)
  240. self.assertEqual(file_list, [])
  241. self.assertEqual(scm.revinfo(options, self.args, None),
  242. 'a7142dc9f0009350b96a11f372b6ea658592aa95')
  243. sys.stdout.close()
  244. def testRevertModified(self):
  245. if not self.enabled:
  246. return
  247. options = self.Options()
  248. scm = gclient_scm.GitWrapper(self.url, self.root_dir,
  249. self.relpath)
  250. file_list = []
  251. scm.update(options, None, file_list)
  252. file_path = join(self.base_path, 'a')
  253. with open(file_path, 'a') as f:
  254. f.writelines('touched\n')
  255. file_list = []
  256. scm.revert(options, self.args, file_list)
  257. self.assertEqual(file_list, [file_path])
  258. file_list = []
  259. scm.diff(options, self.args, file_list)
  260. self.assertEqual(file_list, [])
  261. self.assertEqual(scm.revinfo(options, self.args, None),
  262. 'a7142dc9f0009350b96a11f372b6ea658592aa95')
  263. sys.stdout.close()
  264. def testRevertNew(self):
  265. if not self.enabled:
  266. return
  267. options = self.Options()
  268. scm = gclient_scm.GitWrapper(self.url, self.root_dir,
  269. self.relpath)
  270. file_list = []
  271. scm.update(options, None, file_list)
  272. file_path = join(self.base_path, 'c')
  273. with open(file_path, 'w') as f:
  274. f.writelines('new\n')
  275. Popen([GIT, 'add', 'c'], stdout=PIPE,
  276. stderr=STDOUT, cwd=self.base_path).communicate()
  277. file_list = []
  278. scm.revert(options, self.args, file_list)
  279. self.assertEqual(file_list, [file_path])
  280. file_list = []
  281. scm.diff(options, self.args, file_list)
  282. self.assertEqual(file_list, [])
  283. self.assertEqual(scm.revinfo(options, self.args, None),
  284. 'a7142dc9f0009350b96a11f372b6ea658592aa95')
  285. sys.stdout.close()
  286. def testStatusNew(self):
  287. if not self.enabled:
  288. return
  289. options = self.Options()
  290. file_path = join(self.base_path, 'a')
  291. with open(file_path, 'a') as f:
  292. f.writelines('touched\n')
  293. scm = gclient_scm.GitWrapper(
  294. self.url + '@069c602044c5388d2d15c3f875b057c852003458', self.root_dir,
  295. self.relpath)
  296. file_list = []
  297. scm.status(options, self.args, file_list)
  298. self.assertEqual(file_list, [file_path])
  299. self.checkstdout(
  300. ('\n________ running \'git -c core.quotePath=false diff --name-status '
  301. '069c602044c5388d2d15c3f875b057c852003458\' in \'%s\'\n\nM\ta\n') %
  302. join(self.root_dir, '.'))
  303. def testStatusNewNoBaseRev(self):
  304. if not self.enabled:
  305. return
  306. options = self.Options()
  307. file_path = join(self.base_path, 'a')
  308. with open(file_path, 'a') as f:
  309. f.writelines('touched\n')
  310. scm = gclient_scm.GitWrapper(self.url, self.root_dir, self.relpath)
  311. file_list = []
  312. scm.status(options, self.args, file_list)
  313. self.assertEqual(file_list, [file_path])
  314. self.checkstdout(
  315. ('\n________ running \'git -c core.quotePath=false diff --name-status'
  316. '\' in \'%s\'\n\nM\ta\n') % join(self.root_dir, '.'))
  317. def testStatus2New(self):
  318. if not self.enabled:
  319. return
  320. options = self.Options()
  321. expected_file_list = []
  322. for f in ['a', 'b']:
  323. file_path = join(self.base_path, f)
  324. with open(file_path, 'a') as f:
  325. f.writelines('touched\n')
  326. expected_file_list.extend([file_path])
  327. scm = gclient_scm.GitWrapper(
  328. self.url + '@069c602044c5388d2d15c3f875b057c852003458', self.root_dir,
  329. self.relpath)
  330. file_list = []
  331. scm.status(options, self.args, file_list)
  332. expected_file_list = [join(self.base_path, x) for x in ['a', 'b']]
  333. self.assertEqual(sorted(file_list), expected_file_list)
  334. self.checkstdout(
  335. ('\n________ running \'git -c core.quotePath=false diff --name-status '
  336. '069c602044c5388d2d15c3f875b057c852003458\' in \'%s\'\n\nM\ta\nM\tb\n')
  337. % join(self.root_dir, '.'))
  338. def testUpdateUpdate(self):
  339. if not self.enabled:
  340. return
  341. options = self.Options()
  342. expected_file_list = [join(self.base_path, x) for x in ['a', 'b']]
  343. scm = gclient_scm.GitWrapper(self.url, self.root_dir,
  344. self.relpath)
  345. file_list = []
  346. scm.update(options, (), file_list)
  347. self.assertEqual(file_list, expected_file_list)
  348. self.assertEqual(scm.revinfo(options, (), None),
  349. 'a7142dc9f0009350b96a11f372b6ea658592aa95')
  350. sys.stdout.close()
  351. def testUpdateMerge(self):
  352. if not self.enabled:
  353. return
  354. options = self.Options()
  355. options.merge = True
  356. scm = gclient_scm.GitWrapper(self.url, self.root_dir,
  357. self.relpath)
  358. scm._Run(['checkout', '-q', 'feature'], options)
  359. rev = scm.revinfo(options, (), None)
  360. file_list = []
  361. scm.update(options, (), file_list)
  362. self.assertEqual(file_list, [join(self.base_path, x)
  363. for x in ['a', 'b', 'c']])
  364. # The actual commit that is created is unstable, so we verify its tree and
  365. # parents instead.
  366. self.assertEqual(scm._Capture(['rev-parse', 'HEAD:']),
  367. 'd2e35c10ac24d6c621e14a1fcadceb533155627d')
  368. parent = 'HEAD^' if sys.platform != 'win32' else 'HEAD^^'
  369. self.assertEqual(scm._Capture(['rev-parse', parent + '1']), rev)
  370. self.assertEqual(scm._Capture(['rev-parse', parent + '2']),
  371. scm._Capture(['rev-parse', 'origin/main']))
  372. sys.stdout.close()
  373. def testUpdateRebase(self):
  374. if not self.enabled:
  375. return
  376. options = self.Options()
  377. scm = gclient_scm.GitWrapper(self.url, self.root_dir,
  378. self.relpath)
  379. scm._Run(['checkout', '-q', 'feature'], options)
  380. file_list = []
  381. # Fake a 'y' key press.
  382. scm._AskForData = self._GetAskForDataCallback(
  383. 'Cannot fast-forward merge, attempt to rebase? '
  384. '(y)es / (q)uit / (s)kip : ', 'y')
  385. scm.update(options, (), file_list)
  386. self.assertEqual(file_list, [join(self.base_path, x)
  387. for x in ['a', 'b', 'c']])
  388. # The actual commit that is created is unstable, so we verify its tree and
  389. # parent instead.
  390. self.assertEqual(scm._Capture(['rev-parse', 'HEAD:']),
  391. 'd2e35c10ac24d6c621e14a1fcadceb533155627d')
  392. parent = 'HEAD^' if sys.platform != 'win32' else 'HEAD^^'
  393. self.assertEqual(scm._Capture(['rev-parse', parent + '1']),
  394. scm._Capture(['rev-parse', 'origin/main']))
  395. sys.stdout.close()
  396. def testUpdateReset(self):
  397. if not self.enabled:
  398. return
  399. options = self.Options()
  400. options.reset = True
  401. dir_path = join(self.base_path, 'c')
  402. os.mkdir(dir_path)
  403. with open(join(dir_path, 'nested'), 'w') as f:
  404. f.writelines('new\n')
  405. file_path = join(self.base_path, 'file')
  406. with open(file_path, 'w') as f:
  407. f.writelines('new\n')
  408. scm = gclient_scm.GitWrapper(self.url, self.root_dir,
  409. self.relpath)
  410. file_list = []
  411. scm.update(options, (), file_list)
  412. self.assert_(gclient_scm.os.path.isdir(dir_path))
  413. self.assert_(gclient_scm.os.path.isfile(file_path))
  414. sys.stdout.close()
  415. def testUpdateResetUnsetsFetchConfig(self):
  416. if not self.enabled:
  417. return
  418. options = self.Options()
  419. options.reset = True
  420. scm = gclient_scm.GitWrapper(self.url, self.root_dir,
  421. self.relpath)
  422. scm._Run(['config', 'remote.origin.fetch',
  423. '+refs/heads/bad/ref:refs/remotes/origin/bad/ref'], options)
  424. file_list = []
  425. scm.update(options, (), file_list)
  426. self.assertEqual(scm.revinfo(options, (), None),
  427. '069c602044c5388d2d15c3f875b057c852003458')
  428. sys.stdout.close()
  429. def testUpdateResetDeleteUnversionedTrees(self):
  430. if not self.enabled:
  431. return
  432. options = self.Options()
  433. options.reset = True
  434. options.delete_unversioned_trees = True
  435. dir_path = join(self.base_path, 'dir')
  436. os.mkdir(dir_path)
  437. with open(join(dir_path, 'nested'), 'w') as f:
  438. f.writelines('new\n')
  439. file_path = join(self.base_path, 'file')
  440. with open(file_path, 'w') as f:
  441. f.writelines('new\n')
  442. scm = gclient_scm.GitWrapper(self.url, self.root_dir,
  443. self.relpath)
  444. file_list = []
  445. scm.update(options, (), file_list)
  446. self.assert_(not gclient_scm.os.path.isdir(dir_path))
  447. self.assert_(gclient_scm.os.path.isfile(file_path))
  448. sys.stdout.close()
  449. def testUpdateUnstagedConflict(self):
  450. if not self.enabled:
  451. return
  452. options = self.Options()
  453. scm = gclient_scm.GitWrapper(self.url, self.root_dir,
  454. self.relpath)
  455. file_path = join(self.base_path, 'b')
  456. with open(file_path, 'w') as f:
  457. f.writelines('conflict\n')
  458. try:
  459. scm.update(options, (), [])
  460. self.fail()
  461. except (gclient_scm.gclient_utils.Error, subprocess2.CalledProcessError):
  462. # The exact exception text varies across git versions so it's not worth
  463. # verifying it. It's fine as long as it throws.
  464. pass
  465. # Manually flush stdout since we can't verify it's content accurately across
  466. # git versions.
  467. sys.stdout.getvalue()
  468. sys.stdout.close()
  469. @unittest.skip('Skipping until crbug.com/670884 is resolved.')
  470. def testUpdateLocked(self):
  471. if not self.enabled:
  472. return
  473. options = self.Options()
  474. scm = gclient_scm.GitWrapper(self.url, self.root_dir,
  475. self.relpath)
  476. file_path = join(self.base_path, '.git', 'index.lock')
  477. with open(file_path, 'w'):
  478. pass
  479. with self.assertRaises(subprocess2.CalledProcessError):
  480. scm.update(options, (), [])
  481. sys.stdout.close()
  482. def testUpdateLockedBreak(self):
  483. if not self.enabled:
  484. return
  485. options = self.Options()
  486. options.break_repo_locks = True
  487. scm = gclient_scm.GitWrapper(self.url, self.root_dir,
  488. self.relpath)
  489. file_path = join(self.base_path, '.git', 'index.lock')
  490. with open(file_path, 'w'):
  491. pass
  492. scm.update(options, (), [])
  493. self.assertRegexpMatches(sys.stdout.getvalue(),
  494. r'breaking lock.*\.git[/|\\]index\.lock')
  495. self.assertFalse(os.path.exists(file_path))
  496. sys.stdout.close()
  497. def testUpdateConflict(self):
  498. if not self.enabled:
  499. return
  500. options = self.Options()
  501. scm = gclient_scm.GitWrapper(self.url, self.root_dir,
  502. self.relpath)
  503. file_path = join(self.base_path, 'b')
  504. with open(file_path, 'w') as f:
  505. f.writelines('conflict\n')
  506. scm._Run(['commit', '-am', 'test'], options)
  507. scm._AskForData = self._GetAskForDataCallback(
  508. 'Cannot fast-forward merge, attempt to rebase? '
  509. '(y)es / (q)uit / (s)kip : ', 'y')
  510. with self.assertRaises(gclient_scm.gclient_utils.Error) as e:
  511. scm.update(options, (), [])
  512. self.assertEqual(
  513. e.exception.args[0],
  514. 'Conflict while rebasing this branch.\n'
  515. 'Fix the conflict and run gclient again.\n'
  516. 'See \'man git-rebase\' for details.\n')
  517. with self.assertRaises(gclient_scm.gclient_utils.Error) as e:
  518. scm.update(options, (), [])
  519. self.assertEqual(
  520. e.exception.args[0],
  521. '\n____ . at refs/remotes/origin/main\n'
  522. '\tYou have unstaged changes.\n'
  523. '\tPlease commit, stash, or reset.\n')
  524. sys.stdout.close()
  525. def testRevinfo(self):
  526. if not self.enabled:
  527. return
  528. options = self.Options()
  529. scm = gclient_scm.GitWrapper(self.url, self.root_dir,
  530. self.relpath)
  531. rev_info = scm.revinfo(options, (), None)
  532. self.assertEqual(rev_info, '069c602044c5388d2d15c3f875b057c852003458')
  533. class ManagedGitWrapperTestCaseMock(unittest.TestCase):
  534. class OptionsObject(object):
  535. def __init__(self, verbose=False, revision=None, force=False):
  536. self.verbose = verbose
  537. self.revision = revision
  538. self.deps_os = None
  539. self.force = force
  540. self.reset = False
  541. self.nohooks = False
  542. self.break_repo_locks = False
  543. # TODO(maruel): Test --jobs > 1.
  544. self.jobs = 1
  545. self.patch_ref = None
  546. self.patch_repo = None
  547. self.rebase_patch_ref = True
  548. def Options(self, *args, **kwargs):
  549. return self.OptionsObject(*args, **kwargs)
  550. def checkstdout(self, expected):
  551. # pylint: disable=no-member
  552. value = sys.stdout.getvalue()
  553. sys.stdout.close()
  554. # Check that the expected output appears.
  555. self.assertIn(expected, strip_timestamps(value))
  556. def setUp(self):
  557. self.fake_hash_1 = 't0ta11yf4k3'
  558. self.fake_hash_2 = '3v3nf4k3r'
  559. self.url = 'git://foo'
  560. self.root_dir = '/tmp' if sys.platform != 'win32' else 't:\\tmp'
  561. self.relpath = 'fake'
  562. self.base_path = os.path.join(self.root_dir, self.relpath)
  563. self.backup_base_path = os.path.join(self.root_dir,
  564. 'old_%s.git' % self.relpath)
  565. mock.patch('gclient_scm.scm.GIT.ApplyEnvVars').start()
  566. mock.patch('gclient_scm.GitWrapper._CheckMinVersion').start()
  567. mock.patch('gclient_scm.GitWrapper._Fetch').start()
  568. mock.patch('gclient_scm.GitWrapper._DeleteOrMove').start()
  569. mock.patch('sys.stdout', StringIO()).start()
  570. self.addCleanup(mock.patch.stopall)
  571. @mock.patch('scm.GIT.IsValidRevision')
  572. @mock.patch('os.path.isdir', lambda _: True)
  573. def testGetUsableRevGit(self, mockIsValidRevision):
  574. # pylint: disable=no-member
  575. options = self.Options(verbose=True)
  576. mockIsValidRevision.side_effect = lambda cwd, rev: rev != '1'
  577. git_scm = gclient_scm.GitWrapper(self.url, self.root_dir,
  578. self.relpath)
  579. # A [fake] git sha1 with a git repo should work (this is in the case that
  580. # the LKGR gets flipped to git sha1's some day).
  581. self.assertEqual(git_scm.GetUsableRev(self.fake_hash_1, options),
  582. self.fake_hash_1)
  583. # An SVN rev with an existing purely git repo should raise an exception.
  584. self.assertRaises(gclient_scm.gclient_utils.Error,
  585. git_scm.GetUsableRev, '1', options)
  586. @mock.patch('gclient_scm.GitWrapper._Clone')
  587. @mock.patch('os.path.isdir')
  588. @mock.patch('os.path.exists')
  589. @mock.patch('subprocess2.check_output')
  590. def testUpdateNoDotGit(
  591. self, mockCheckOutput, mockExists, mockIsdir, mockClone):
  592. mockIsdir.side_effect = lambda path: path == self.base_path
  593. mockExists.side_effect = lambda path: path == self.base_path
  594. mockCheckOutput.side_effect = [b'refs/remotes/origin/main', b'', b'']
  595. options = self.Options()
  596. scm = gclient_scm.GitWrapper(
  597. self.url, self.root_dir, self.relpath)
  598. scm.update(options, None, [])
  599. env = gclient_scm.scm.GIT.ApplyEnvVars({})
  600. self.assertEqual(mockCheckOutput.mock_calls, [
  601. mock.call(['git', 'symbolic-ref', 'refs/remotes/origin/HEAD'],
  602. cwd=self.base_path,
  603. env=env,
  604. stderr=-1),
  605. mock.call(['git', '-c', 'core.quotePath=false', 'ls-files'],
  606. cwd=self.base_path,
  607. env=env,
  608. stderr=-1),
  609. mock.call(['git', 'rev-parse', '--verify', 'HEAD'],
  610. cwd=self.base_path,
  611. env=env,
  612. stderr=-1),
  613. ])
  614. mockClone.assert_called_with('refs/remotes/origin/main', self.url, options)
  615. self.checkstdout('\n')
  616. @mock.patch('gclient_scm.GitWrapper._Clone')
  617. @mock.patch('os.path.isdir')
  618. @mock.patch('os.path.exists')
  619. @mock.patch('subprocess2.check_output')
  620. def testUpdateConflict(
  621. self, mockCheckOutput, mockExists, mockIsdir, mockClone):
  622. mockIsdir.side_effect = lambda path: path == self.base_path
  623. mockExists.side_effect = lambda path: path == self.base_path
  624. mockCheckOutput.side_effect = [b'refs/remotes/origin/main', b'', b'']
  625. mockClone.side_effect = [
  626. gclient_scm.subprocess2.CalledProcessError(
  627. None, None, None, None, None),
  628. None,
  629. ]
  630. options = self.Options()
  631. scm = gclient_scm.GitWrapper(self.url, self.root_dir,
  632. self.relpath)
  633. scm.update(options, None, [])
  634. env = gclient_scm.scm.GIT.ApplyEnvVars({})
  635. self.assertEqual(mockCheckOutput.mock_calls, [
  636. mock.call(['git', 'symbolic-ref', 'refs/remotes/origin/HEAD'],
  637. cwd=self.base_path,
  638. env=env,
  639. stderr=-1),
  640. mock.call(['git', '-c', 'core.quotePath=false', 'ls-files'],
  641. cwd=self.base_path,
  642. env=env,
  643. stderr=-1),
  644. mock.call(['git', 'rev-parse', '--verify', 'HEAD'],
  645. cwd=self.base_path,
  646. env=env,
  647. stderr=-1),
  648. ])
  649. mockClone.assert_called_with('refs/remotes/origin/main', self.url, options)
  650. self.checkstdout('\n')
  651. class UnmanagedGitWrapperTestCase(BaseGitWrapperTestCase):
  652. def checkInStdout(self, expected):
  653. # pylint: disable=no-member
  654. value = sys.stdout.getvalue()
  655. sys.stdout.close()
  656. self.assertIn(expected, value)
  657. def checkNotInStdout(self, expected):
  658. # pylint: disable=no-member
  659. value = sys.stdout.getvalue()
  660. sys.stdout.close()
  661. self.assertNotIn(expected, value)
  662. def getCurrentBranch(self):
  663. # Returns name of current branch or HEAD for detached HEAD
  664. branch = gclient_scm.scm.GIT.Capture(['rev-parse', '--abbrev-ref', 'HEAD'],
  665. cwd=self.base_path)
  666. if branch == 'HEAD':
  667. return None
  668. return branch
  669. def testUpdateClone(self):
  670. if not self.enabled:
  671. return
  672. options = self.Options()
  673. origin_root_dir = self.root_dir
  674. self.addCleanup(gclient_utils.rmtree, origin_root_dir)
  675. self.root_dir = tempfile.mkdtemp()
  676. self.relpath = '.'
  677. self.base_path = join(self.root_dir, self.relpath)
  678. scm = gclient_scm.GitWrapper(origin_root_dir,
  679. self.root_dir,
  680. self.relpath)
  681. expected_file_list = [join(self.base_path, "a"),
  682. join(self.base_path, "b")]
  683. file_list = []
  684. options.revision = 'unmanaged'
  685. scm.update(options, (), file_list)
  686. self.assertEqual(file_list, expected_file_list)
  687. self.assertEqual(scm.revinfo(options, (), None),
  688. '069c602044c5388d2d15c3f875b057c852003458')
  689. # indicates detached HEAD
  690. self.assertEqual(self.getCurrentBranch(), None)
  691. self.checkInStdout(
  692. 'Checked out refs/remotes/origin/main to a detached HEAD')
  693. def testUpdateCloneOnCommit(self):
  694. if not self.enabled:
  695. return
  696. options = self.Options()
  697. origin_root_dir = self.root_dir
  698. self.addCleanup(gclient_utils.rmtree, origin_root_dir)
  699. self.root_dir = tempfile.mkdtemp()
  700. self.relpath = '.'
  701. self.base_path = join(self.root_dir, self.relpath)
  702. url_with_commit_ref = origin_root_dir +\
  703. '@a7142dc9f0009350b96a11f372b6ea658592aa95'
  704. scm = gclient_scm.GitWrapper(url_with_commit_ref,
  705. self.root_dir,
  706. self.relpath)
  707. expected_file_list = [join(self.base_path, "a"),
  708. join(self.base_path, "b")]
  709. file_list = []
  710. options.revision = 'unmanaged'
  711. scm.update(options, (), file_list)
  712. self.assertEqual(file_list, expected_file_list)
  713. self.assertEqual(scm.revinfo(options, (), None),
  714. 'a7142dc9f0009350b96a11f372b6ea658592aa95')
  715. # indicates detached HEAD
  716. self.assertEqual(self.getCurrentBranch(), None)
  717. self.checkInStdout(
  718. 'Checked out a7142dc9f0009350b96a11f372b6ea658592aa95 to a detached HEAD')
  719. def testUpdateCloneOnBranch(self):
  720. if not self.enabled:
  721. return
  722. options = self.Options()
  723. origin_root_dir = self.root_dir
  724. self.addCleanup(gclient_utils.rmtree, origin_root_dir)
  725. self.root_dir = tempfile.mkdtemp()
  726. self.relpath = '.'
  727. self.base_path = join(self.root_dir, self.relpath)
  728. url_with_branch_ref = origin_root_dir + '@feature'
  729. scm = gclient_scm.GitWrapper(url_with_branch_ref,
  730. self.root_dir,
  731. self.relpath)
  732. expected_file_list = [join(self.base_path, "a"),
  733. join(self.base_path, "b"),
  734. join(self.base_path, "c")]
  735. file_list = []
  736. options.revision = 'unmanaged'
  737. scm.update(options, (), file_list)
  738. self.assertEqual(file_list, expected_file_list)
  739. self.assertEqual(scm.revinfo(options, (), None),
  740. '9a51244740b25fa2ded5252ca00a3178d3f665a9')
  741. # indicates detached HEAD
  742. self.assertEqual(self.getCurrentBranch(), None)
  743. self.checkInStdout(
  744. 'Checked out 9a51244740b25fa2ded5252ca00a3178d3f665a9 '
  745. 'to a detached HEAD')
  746. def testUpdateCloneOnFetchedRemoteBranch(self):
  747. if not self.enabled:
  748. return
  749. options = self.Options()
  750. origin_root_dir = self.root_dir
  751. self.addCleanup(gclient_utils.rmtree, origin_root_dir)
  752. self.root_dir = tempfile.mkdtemp()
  753. self.relpath = '.'
  754. self.base_path = join(self.root_dir, self.relpath)
  755. url_with_branch_ref = origin_root_dir + '@refs/remotes/origin/feature'
  756. scm = gclient_scm.GitWrapper(url_with_branch_ref,
  757. self.root_dir,
  758. self.relpath)
  759. expected_file_list = [join(self.base_path, "a"),
  760. join(self.base_path, "b"),
  761. join(self.base_path, "c")]
  762. file_list = []
  763. options.revision = 'unmanaged'
  764. scm.update(options, (), file_list)
  765. self.assertEqual(file_list, expected_file_list)
  766. self.assertEqual(scm.revinfo(options, (), None),
  767. '9a51244740b25fa2ded5252ca00a3178d3f665a9')
  768. # indicates detached HEAD
  769. self.assertEqual(self.getCurrentBranch(), None)
  770. self.checkInStdout(
  771. 'Checked out refs/remotes/origin/feature to a detached HEAD')
  772. def testUpdateCloneOnTrueRemoteBranch(self):
  773. if not self.enabled:
  774. return
  775. options = self.Options()
  776. origin_root_dir = self.root_dir
  777. self.addCleanup(gclient_utils.rmtree, origin_root_dir)
  778. self.root_dir = tempfile.mkdtemp()
  779. self.relpath = '.'
  780. self.base_path = join(self.root_dir, self.relpath)
  781. url_with_branch_ref = origin_root_dir + '@refs/heads/feature'
  782. scm = gclient_scm.GitWrapper(url_with_branch_ref,
  783. self.root_dir,
  784. self.relpath)
  785. expected_file_list = [join(self.base_path, "a"),
  786. join(self.base_path, "b"),
  787. join(self.base_path, "c")]
  788. file_list = []
  789. options.revision = 'unmanaged'
  790. scm.update(options, (), file_list)
  791. self.assertEqual(file_list, expected_file_list)
  792. self.assertEqual(scm.revinfo(options, (), None),
  793. '9a51244740b25fa2ded5252ca00a3178d3f665a9')
  794. # @refs/heads/feature is AKA @refs/remotes/origin/feature in the clone, so
  795. # should be treated as such by gclient.
  796. # TODO(mmoss): Though really, we should only allow DEPS to specify branches
  797. # as they are known in the upstream repo, since the mapping into the local
  798. # repo can be modified by users (or we might even want to change the gclient
  799. # defaults at some point). But that will take more work to stop using
  800. # refs/remotes/ everywhere that we do (and to stop assuming a DEPS ref will
  801. # always resolve locally, like when passing them to show-ref or rev-list).
  802. self.assertEqual(self.getCurrentBranch(), None)
  803. self.checkInStdout(
  804. 'Checked out refs/remotes/origin/feature to a detached HEAD')
  805. def testUpdateUpdate(self):
  806. if not self.enabled:
  807. return
  808. options = self.Options()
  809. expected_file_list = []
  810. scm = gclient_scm.GitWrapper(self.url, self.root_dir,
  811. self.relpath)
  812. file_list = []
  813. options.revision = 'unmanaged'
  814. scm.update(options, (), file_list)
  815. self.assertEqual(file_list, expected_file_list)
  816. self.assertEqual(scm.revinfo(options, (), None),
  817. '069c602044c5388d2d15c3f875b057c852003458')
  818. self.checkstdout('________ unmanaged solution; skipping .\n')
  819. class CipdWrapperTestCase(unittest.TestCase):
  820. def setUp(self):
  821. # Create this before setting up mocks.
  822. self._cipd_root_dir = tempfile.mkdtemp()
  823. self._workdir = tempfile.mkdtemp()
  824. self._cipd_instance_url = 'https://chrome-infra-packages.appspot.com'
  825. self._cipd_root = gclient_scm.CipdRoot(
  826. self._cipd_root_dir,
  827. self._cipd_instance_url)
  828. self._cipd_packages = [
  829. self._cipd_root.add_package('f', 'foo_package', 'foo_version'),
  830. self._cipd_root.add_package('b', 'bar_package', 'bar_version'),
  831. self._cipd_root.add_package('b', 'baz_package', 'baz_version'),
  832. ]
  833. mock.patch('tempfile.mkdtemp', lambda: self._workdir).start()
  834. mock.patch('gclient_scm.CipdRoot.add_package').start()
  835. mock.patch('gclient_scm.CipdRoot.clobber').start()
  836. mock.patch('gclient_scm.CipdRoot.ensure').start()
  837. self.addCleanup(mock.patch.stopall)
  838. self.addCleanup(gclient_utils.rmtree, self._cipd_root_dir)
  839. self.addCleanup(gclient_utils.rmtree, self._workdir)
  840. def createScmWithPackageThatSatisfies(self, condition):
  841. return gclient_scm.CipdWrapper(
  842. url=self._cipd_instance_url,
  843. root_dir=self._cipd_root_dir,
  844. relpath='fake_relpath',
  845. root=self._cipd_root,
  846. package=self.getPackageThatSatisfies(condition))
  847. def getPackageThatSatisfies(self, condition):
  848. for p in self._cipd_packages:
  849. if condition(p):
  850. return p
  851. self.fail('Unable to find a satisfactory package.')
  852. def testRevert(self):
  853. """Checks that revert does nothing."""
  854. scm = self.createScmWithPackageThatSatisfies(lambda _: True)
  855. scm.revert(None, (), [])
  856. @mock.patch('gclient_scm.gclient_utils.CheckCallAndFilter')
  857. @mock.patch('gclient_scm.gclient_utils.rmtree')
  858. def testRevinfo(self, mockRmtree, mockCheckCallAndFilter):
  859. """Checks that revinfo uses the JSON from cipd describe."""
  860. scm = self.createScmWithPackageThatSatisfies(lambda _: True)
  861. expected_revinfo = '0123456789abcdef0123456789abcdef01234567'
  862. json_contents = {
  863. 'result': {
  864. 'pin': {
  865. 'instance_id': expected_revinfo,
  866. }
  867. }
  868. }
  869. describe_json_path = join(self._workdir, 'describe.json')
  870. with open(describe_json_path, 'w') as describe_json:
  871. json.dump(json_contents, describe_json)
  872. revinfo = scm.revinfo(None, (), [])
  873. self.assertEqual(revinfo, expected_revinfo)
  874. mockRmtree.assert_called_with(self._workdir)
  875. mockCheckCallAndFilter.assert_called_with([
  876. 'cipd', 'describe', 'foo_package',
  877. '-log-level', 'error',
  878. '-version', 'foo_version',
  879. '-json-output', describe_json_path,
  880. ])
  881. def testUpdate(self):
  882. """Checks that update does nothing."""
  883. scm = self.createScmWithPackageThatSatisfies(lambda _: True)
  884. scm.update(None, (), [])
  885. class BranchHeadsFakeRepo(fake_repos.FakeReposBase):
  886. def populateGit(self):
  887. # Creates a tree that looks like this:
  888. #
  889. # 5 refs/branch-heads/5
  890. # |
  891. # 4
  892. # |
  893. # 1--2--3 refs/heads/main
  894. self._commit_git('repo_1', {'commit 1': 'touched'})
  895. self._commit_git('repo_1', {'commit 2': 'touched'})
  896. self._commit_git('repo_1', {'commit 3': 'touched'})
  897. self._create_ref('repo_1', 'refs/heads/main', 3)
  898. self._commit_git('repo_1', {'commit 4': 'touched'}, base=2)
  899. self._commit_git('repo_1', {'commit 5': 'touched'}, base=2)
  900. self._create_ref('repo_1', 'refs/branch-heads/5', 5)
  901. class BranchHeadsTest(fake_repos.FakeReposTestBase):
  902. FAKE_REPOS_CLASS = BranchHeadsFakeRepo
  903. def setUp(self):
  904. super(BranchHeadsTest, self).setUp()
  905. self.enabled = self.FAKE_REPOS.set_up_git()
  906. self.options = BaseGitWrapperTestCase.OptionsObject()
  907. self.url = self.git_base + 'repo_1'
  908. self.mirror = None
  909. mock.patch('sys.stdout', StringIO()).start()
  910. self.addCleanup(mock.patch.stopall)
  911. def setUpMirror(self):
  912. self.mirror = tempfile.mkdtemp('mirror')
  913. git_cache.Mirror.SetCachePath(self.mirror)
  914. self.addCleanup(gclient_utils.rmtree, self.mirror)
  915. self.addCleanup(git_cache.Mirror.SetCachePath, None)
  916. def testCheckoutBranchHeads(self):
  917. scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
  918. file_list = []
  919. self.options.revision = 'refs/branch-heads/5'
  920. scm.update(self.options, None, file_list)
  921. self.assertEqual(self.githash('repo_1', 5), self.gitrevparse(self.root_dir))
  922. def testCheckoutUpdatedBranchHeads(self):
  923. # Travel back in time, and set refs/branch-heads/5 to its parent.
  924. subprocess2.check_call(
  925. ['git', 'update-ref', 'refs/branch-heads/5', self.githash('repo_1', 4)],
  926. cwd=self.url)
  927. # Sync to refs/branch-heads/5
  928. scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
  929. self.options.revision = 'refs/branch-heads/5'
  930. scm.update(self.options, None, [])
  931. # Set refs/branch-heads/5 back to its original value.
  932. subprocess2.check_call(
  933. ['git', 'update-ref', 'refs/branch-heads/5', self.githash('repo_1', 5)],
  934. cwd=self.url)
  935. # Attempt to sync to refs/branch-heads/5 again.
  936. self.testCheckoutBranchHeads()
  937. def testCheckoutBranchHeadsMirror(self):
  938. self.setUpMirror()
  939. self.testCheckoutBranchHeads()
  940. def testCheckoutUpdatedBranchHeadsMirror(self):
  941. self.setUpMirror()
  942. self.testCheckoutUpdatedBranchHeads()
  943. class GerritChangesFakeRepo(fake_repos.FakeReposBase):
  944. def populateGit(self):
  945. # Creates a tree that looks like this:
  946. #
  947. # 6 refs/changes/35/1235/1
  948. # |
  949. # 5 refs/changes/34/1234/1
  950. # |
  951. # 1--2--3--4 refs/heads/main
  952. # | |
  953. # | 11(5)--12 refs/heads/main-with-5
  954. # |
  955. # 7--8--9 refs/heads/feature
  956. # |
  957. # 10 refs/changes/36/1236/1
  958. #
  959. self._commit_git('repo_1', {'commit 1': 'touched'})
  960. self._commit_git('repo_1', {'commit 2': 'touched'})
  961. self._commit_git('repo_1', {'commit 3': 'touched'})
  962. self._commit_git('repo_1', {'commit 4': 'touched'})
  963. self._create_ref('repo_1', 'refs/heads/main', 4)
  964. # Create a change on top of commit 3 that consists of two commits.
  965. self._commit_git('repo_1',
  966. {'commit 5': 'touched',
  967. 'change': '1234'},
  968. base=3)
  969. self._create_ref('repo_1', 'refs/changes/34/1234/1', 5)
  970. self._commit_git('repo_1',
  971. {'commit 6': 'touched',
  972. 'change': '1235'})
  973. self._create_ref('repo_1', 'refs/changes/35/1235/1', 6)
  974. # Create a refs/heads/feature branch on top of commit 2, consisting of three
  975. # commits.
  976. self._commit_git('repo_1', {'commit 7': 'touched'}, base=2)
  977. self._commit_git('repo_1', {'commit 8': 'touched'})
  978. self._commit_git('repo_1', {'commit 9': 'touched'})
  979. self._create_ref('repo_1', 'refs/heads/feature', 9)
  980. # Create a change of top of commit 8.
  981. self._commit_git('repo_1',
  982. {'commit 10': 'touched',
  983. 'change': '1236'},
  984. base=8)
  985. self._create_ref('repo_1', 'refs/changes/36/1236/1', 10)
  986. # Create a refs/heads/main-with-5 on top of commit 3 which is a branch
  987. # where refs/changes/34/1234/1 (commit 5) has already landed as commit 11.
  988. self._commit_git('repo_1',
  989. # This is really commit 11, but has the changes of commit 5
  990. {'commit 5': 'touched',
  991. 'change': '1234'},
  992. base=3)
  993. self._commit_git('repo_1', {'commit 12': 'touched'})
  994. self._create_ref('repo_1', 'refs/heads/main-with-5', 12)
  995. class GerritChangesTest(fake_repos.FakeReposTestBase):
  996. FAKE_REPOS_CLASS = GerritChangesFakeRepo
  997. def setUp(self):
  998. super(GerritChangesTest, self).setUp()
  999. self.enabled = self.FAKE_REPOS.set_up_git()
  1000. self.options = BaseGitWrapperTestCase.OptionsObject()
  1001. self.url = self.git_base + 'repo_1'
  1002. self.mirror = None
  1003. mock.patch('sys.stdout', StringIO()).start()
  1004. self.addCleanup(mock.patch.stopall)
  1005. def setUpMirror(self):
  1006. self.mirror = tempfile.mkdtemp()
  1007. git_cache.Mirror.SetCachePath(self.mirror)
  1008. self.addCleanup(gclient_utils.rmtree, self.mirror)
  1009. self.addCleanup(git_cache.Mirror.SetCachePath, None)
  1010. def assertCommits(self, commits):
  1011. """Check that all, and only |commits| are present in the current checkout.
  1012. """
  1013. for i in commits:
  1014. name = os.path.join(self.root_dir, 'commit ' + str(i))
  1015. self.assertTrue(os.path.exists(name), 'Commit not found: %s' % name)
  1016. all_commits = set(range(1, len(self.FAKE_REPOS.git_hashes['repo_1'])))
  1017. for i in all_commits - set(commits):
  1018. name = os.path.join(self.root_dir, 'commit ' + str(i))
  1019. self.assertFalse(os.path.exists(name), 'Unexpected commit: %s' % name)
  1020. def testCanCloneGerritChange(self):
  1021. scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
  1022. file_list = []
  1023. self.options.revision = 'refs/changes/35/1235/1'
  1024. scm.update(self.options, None, file_list)
  1025. self.assertEqual(self.githash('repo_1', 6), self.gitrevparse(self.root_dir))
  1026. def testCanSyncToGerritChange(self):
  1027. scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
  1028. file_list = []
  1029. self.options.revision = self.githash('repo_1', 1)
  1030. scm.update(self.options, None, file_list)
  1031. self.assertEqual(self.githash('repo_1', 1), self.gitrevparse(self.root_dir))
  1032. self.options.revision = 'refs/changes/35/1235/1'
  1033. scm.update(self.options, None, file_list)
  1034. self.assertEqual(self.githash('repo_1', 6), self.gitrevparse(self.root_dir))
  1035. def testCanCloneGerritChangeMirror(self):
  1036. self.setUpMirror()
  1037. self.testCanCloneGerritChange()
  1038. def testCanSyncToGerritChangeMirror(self):
  1039. self.setUpMirror()
  1040. self.testCanSyncToGerritChange()
  1041. def testMirrorPushUrl(self):
  1042. self.setUpMirror()
  1043. scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
  1044. file_list = []
  1045. self.assertIsNotNone(scm._GetMirror(self.url, self.options))
  1046. scm.update(self.options, None, file_list)
  1047. fetch_url = scm._Capture(['remote', 'get-url', 'origin'])
  1048. self.assertTrue(
  1049. fetch_url.startswith(self.mirror),
  1050. msg='\n'.join([
  1051. 'Repository fetch url should be in the git cache mirror directory.',
  1052. ' fetch_url: %s' % fetch_url,
  1053. ' mirror: %s' % self.mirror]))
  1054. push_url = scm._Capture(['remote', 'get-url', '--push', 'origin'])
  1055. self.assertEqual(push_url, self.url)
  1056. def testAppliesPatchOnTopOfMasterByDefault(self):
  1057. """Test the default case, where we apply a patch on top of main."""
  1058. scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
  1059. file_list = []
  1060. # Make sure we don't specify a revision.
  1061. self.options.revision = None
  1062. scm.update(self.options, None, file_list)
  1063. self.assertEqual(self.githash('repo_1', 4), self.gitrevparse(self.root_dir))
  1064. scm.apply_patch_ref(
  1065. self.url, 'refs/changes/35/1235/1', 'refs/heads/main', self.options,
  1066. file_list)
  1067. self.assertCommits([1, 2, 3, 4, 5, 6])
  1068. self.assertEqual(self.githash('repo_1', 4), self.gitrevparse(self.root_dir))
  1069. def testCheckoutOlderThanPatchBase(self):
  1070. """Test applying a patch on an old checkout.
  1071. We first checkout commit 1, and try to patch refs/changes/35/1235/1, which
  1072. contains commits 5 and 6, and is based on top of commit 3.
  1073. The final result should contain commits 1, 5 and 6, but not commits 2 or 3.
  1074. """
  1075. scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
  1076. file_list = []
  1077. # Sync to commit 1
  1078. self.options.revision = self.githash('repo_1', 1)
  1079. scm.update(self.options, None, file_list)
  1080. self.assertEqual(self.githash('repo_1', 1), self.gitrevparse(self.root_dir))
  1081. # Apply the change on top of that.
  1082. scm.apply_patch_ref(
  1083. self.url, 'refs/changes/35/1235/1', 'refs/heads/main', self.options,
  1084. file_list)
  1085. self.assertCommits([1, 5, 6])
  1086. self.assertEqual(self.githash('repo_1', 1), self.gitrevparse(self.root_dir))
  1087. def testCheckoutOriginFeature(self):
  1088. """Tests that we can apply a patch on a branch other than main."""
  1089. scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
  1090. file_list = []
  1091. # Sync to remote's refs/heads/feature
  1092. self.options.revision = 'refs/heads/feature'
  1093. scm.update(self.options, None, file_list)
  1094. self.assertEqual(self.githash('repo_1', 9), self.gitrevparse(self.root_dir))
  1095. # Apply the change on top of that.
  1096. scm.apply_patch_ref(
  1097. self.url, 'refs/changes/36/1236/1', 'refs/heads/feature', self.options,
  1098. file_list)
  1099. self.assertCommits([1, 2, 7, 8, 9, 10])
  1100. self.assertEqual(self.githash('repo_1', 9), self.gitrevparse(self.root_dir))
  1101. def testCheckoutOriginFeatureOnOldRevision(self):
  1102. """Tests that we can apply a patch on an old checkout, on a branch other
  1103. than main."""
  1104. scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
  1105. file_list = []
  1106. # Sync to remote's refs/heads/feature on an old revision
  1107. self.options.revision = self.githash('repo_1', 7)
  1108. scm.update(self.options, None, file_list)
  1109. self.assertEqual(self.githash('repo_1', 7), self.gitrevparse(self.root_dir))
  1110. # Apply the change on top of that.
  1111. scm.apply_patch_ref(
  1112. self.url, 'refs/changes/36/1236/1', 'refs/heads/feature', self.options,
  1113. file_list)
  1114. # We shouldn't have rebased on top of 2 (which is the merge base between
  1115. # remote's main branch and the change) but on top of 7 (which is the
  1116. # merge base between remote's feature branch and the change).
  1117. self.assertCommits([1, 2, 7, 10])
  1118. self.assertEqual(self.githash('repo_1', 7), self.gitrevparse(self.root_dir))
  1119. def testCheckoutOriginFeaturePatchBranch(self):
  1120. scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
  1121. file_list = []
  1122. # Sync to the hash instead of remote's refs/heads/feature.
  1123. self.options.revision = self.githash('repo_1', 9)
  1124. scm.update(self.options, None, file_list)
  1125. self.assertEqual(self.githash('repo_1', 9), self.gitrevparse(self.root_dir))
  1126. # Apply refs/changes/34/1234/1, created for remote's main branch on top of
  1127. # remote's feature branch.
  1128. scm.apply_patch_ref(
  1129. self.url, 'refs/changes/35/1235/1', 'refs/heads/main', self.options,
  1130. file_list)
  1131. # Commits 5 and 6 are part of the patch, and commits 1, 2, 7, 8 and 9 are
  1132. # part of remote's feature branch.
  1133. self.assertCommits([1, 2, 5, 6, 7, 8, 9])
  1134. self.assertEqual(self.githash('repo_1', 9), self.gitrevparse(self.root_dir))
  1135. def testDoesntRebasePatchMaster(self):
  1136. """Tests that we can apply a patch without rebasing it.
  1137. """
  1138. scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
  1139. file_list = []
  1140. self.options.rebase_patch_ref = False
  1141. scm.update(self.options, None, file_list)
  1142. self.assertEqual(self.githash('repo_1', 4), self.gitrevparse(self.root_dir))
  1143. # Apply the change on top of that.
  1144. scm.apply_patch_ref(
  1145. self.url, 'refs/changes/35/1235/1', 'refs/heads/main', self.options,
  1146. file_list)
  1147. self.assertCommits([1, 2, 3, 5, 6])
  1148. self.assertEqual(self.githash('repo_1', 5), self.gitrevparse(self.root_dir))
  1149. def testDoesntRebasePatchOldCheckout(self):
  1150. """Tests that we can apply a patch without rebasing it on an old checkout.
  1151. """
  1152. scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
  1153. file_list = []
  1154. # Sync to commit 1
  1155. self.options.revision = self.githash('repo_1', 1)
  1156. self.options.rebase_patch_ref = False
  1157. scm.update(self.options, None, file_list)
  1158. self.assertEqual(self.githash('repo_1', 1), self.gitrevparse(self.root_dir))
  1159. # Apply the change on top of that.
  1160. scm.apply_patch_ref(
  1161. self.url, 'refs/changes/35/1235/1', 'refs/heads/main', self.options,
  1162. file_list)
  1163. self.assertCommits([1, 2, 3, 5, 6])
  1164. self.assertEqual(self.githash('repo_1', 5), self.gitrevparse(self.root_dir))
  1165. def testDoesntSoftResetIfNotAskedTo(self):
  1166. """Test that we can apply a patch without doing a soft reset."""
  1167. scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
  1168. file_list = []
  1169. self.options.reset_patch_ref = False
  1170. scm.update(self.options, None, file_list)
  1171. self.assertEqual(self.githash('repo_1', 4), self.gitrevparse(self.root_dir))
  1172. scm.apply_patch_ref(
  1173. self.url, 'refs/changes/35/1235/1', 'refs/heads/main', self.options,
  1174. file_list)
  1175. self.assertCommits([1, 2, 3, 4, 5, 6])
  1176. # The commit hash after cherry-picking is not known, but it must be
  1177. # different from what the repo was synced at before patching.
  1178. self.assertNotEqual(self.githash('repo_1', 4),
  1179. self.gitrevparse(self.root_dir))
  1180. @mock.patch('gerrit_util.GetChange', return_value={'topic': 'test_topic'})
  1181. @mock.patch('gerrit_util.QueryChanges', return_value=[
  1182. {'_number': 1234},
  1183. {'_number': 1235, 'current_revision': 'abc',
  1184. 'revisions': {'abc': {'ref': 'refs/changes/35/1235/1'}}}])
  1185. def testDownloadTopics(self, query_changes_mock, get_change_mock):
  1186. scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
  1187. file_list = []
  1188. self.options.revision = 'refs/changes/34/1234/1'
  1189. scm.update(self.options, None, file_list)
  1190. self.assertEqual(self.githash('repo_1', 5), self.gitrevparse(self.root_dir))
  1191. # pylint: disable=attribute-defined-outside-init
  1192. self.options.download_topics = True
  1193. scm.url = 'https://test-repo.googlesource.com/repo_1.git'
  1194. scm.apply_patch_ref(
  1195. self.url, 'refs/changes/34/1234/1', 'refs/heads/main', self.options,
  1196. file_list)
  1197. get_change_mock.assert_called_once_with(
  1198. mock.ANY, '1234')
  1199. query_changes_mock.assert_called_once_with(
  1200. mock.ANY,
  1201. [('topic', 'test_topic'), ('status', 'open'), ('repo', 'repo_1')],
  1202. o_params=['ALL_REVISIONS'])
  1203. self.assertCommits([1, 2, 3, 5, 6])
  1204. # The commit hash after the two cherry-picks is not known, but it must be
  1205. # different from what the repo was synced at before patching.
  1206. self.assertNotEqual(self.githash('repo_1', 4),
  1207. self.gitrevparse(self.root_dir))
  1208. def testRecoversAfterPatchFailure(self):
  1209. scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
  1210. file_list = []
  1211. self.options.revision = 'refs/changes/34/1234/1'
  1212. scm.update(self.options, None, file_list)
  1213. self.assertEqual(self.githash('repo_1', 5), self.gitrevparse(self.root_dir))
  1214. # Checkout 'refs/changes/34/1234/1' modifies the 'change' file, so trying to
  1215. # patch 'refs/changes/36/1236/1' creates a patch failure.
  1216. with self.assertRaises(subprocess2.CalledProcessError) as cm:
  1217. scm.apply_patch_ref(
  1218. self.url, 'refs/changes/36/1236/1', 'refs/heads/main', self.options,
  1219. file_list)
  1220. self.assertEqual(cm.exception.cmd[:2], ['git', 'cherry-pick'])
  1221. self.assertIn(b'error: could not apply', cm.exception.stderr)
  1222. # Try to apply 'refs/changes/35/1235/1', which doesn't have a merge
  1223. # conflict.
  1224. scm.apply_patch_ref(
  1225. self.url, 'refs/changes/35/1235/1', 'refs/heads/main', self.options,
  1226. file_list)
  1227. self.assertCommits([1, 2, 3, 5, 6])
  1228. self.assertEqual(self.githash('repo_1', 5), self.gitrevparse(self.root_dir))
  1229. def testIgnoresAlreadyMergedCommits(self):
  1230. scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
  1231. file_list = []
  1232. self.options.revision = 'refs/heads/main-with-5'
  1233. scm.update(self.options, None, file_list)
  1234. self.assertEqual(self.githash('repo_1', 12),
  1235. self.gitrevparse(self.root_dir))
  1236. # When we try 'refs/changes/35/1235/1' on top of 'refs/heads/feature',
  1237. # 'refs/changes/34/1234/1' will be an empty commit, since the changes were
  1238. # already present in the tree as commit 11.
  1239. # Make sure we deal with this gracefully.
  1240. scm.apply_patch_ref(
  1241. self.url, 'refs/changes/35/1235/1', 'refs/heads/feature', self.options,
  1242. file_list)
  1243. self.assertCommits([1, 2, 3, 5, 6, 12])
  1244. self.assertEqual(self.githash('repo_1', 12),
  1245. self.gitrevparse(self.root_dir))
  1246. def testRecoversFromExistingCherryPick(self):
  1247. scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
  1248. file_list = []
  1249. self.options.revision = 'refs/changes/34/1234/1'
  1250. scm.update(self.options, None, file_list)
  1251. self.assertEqual(self.githash('repo_1', 5), self.gitrevparse(self.root_dir))
  1252. # Checkout 'refs/changes/34/1234/1' modifies the 'change' file, so trying to
  1253. # cherry-pick 'refs/changes/36/1236/1' raises an error.
  1254. scm._Run(['fetch', 'origin', 'refs/changes/36/1236/1'], self.options)
  1255. with self.assertRaises(subprocess2.CalledProcessError) as cm:
  1256. scm._Run(['cherry-pick', 'FETCH_HEAD'], self.options)
  1257. self.assertEqual(cm.exception.cmd[:2], ['git', 'cherry-pick'])
  1258. # Try to apply 'refs/changes/35/1235/1', which doesn't have a merge
  1259. # conflict.
  1260. scm.apply_patch_ref(
  1261. self.url, 'refs/changes/35/1235/1', 'refs/heads/main', self.options,
  1262. file_list)
  1263. self.assertCommits([1, 2, 3, 5, 6])
  1264. self.assertEqual(self.githash('repo_1', 5), self.gitrevparse(self.root_dir))
  1265. class DepsChangesFakeRepo(fake_repos.FakeReposBase):
  1266. def populateGit(self):
  1267. self._commit_git('repo_1', {'DEPS': 'versionA', 'doesnotmatter': 'B'})
  1268. self._commit_git('repo_1', {'DEPS': 'versionA', 'doesnotmatter': 'C'})
  1269. self._commit_git('repo_1', {'DEPS': 'versionB'})
  1270. self._commit_git('repo_1', {'DEPS': 'versionA', 'doesnotmatter': 'C'})
  1271. self._create_ref('repo_1', 'refs/heads/main', 4)
  1272. class CheckDiffTest(fake_repos.FakeReposTestBase):
  1273. FAKE_REPOS_CLASS = DepsChangesFakeRepo
  1274. def setUp(self):
  1275. super(CheckDiffTest, self).setUp()
  1276. self.enabled = self.FAKE_REPOS.set_up_git()
  1277. self.options = BaseGitWrapperTestCase.OptionsObject()
  1278. self.url = self.git_base + 'repo_1'
  1279. self.mirror = None
  1280. mock.patch('sys.stdout', StringIO()).start()
  1281. self.addCleanup(mock.patch.stopall)
  1282. def setUpMirror(self):
  1283. self.mirror = tempfile.mkdtemp()
  1284. git_cache.Mirror.SetCachePath(self.mirror)
  1285. self.addCleanup(gclient_utils.rmtree, self.mirror)
  1286. self.addCleanup(git_cache.Mirror.SetCachePath, None)
  1287. def testCheckDiff(self):
  1288. """Correctly check for diffs."""
  1289. scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
  1290. file_list = []
  1291. # Make sure we don't specify a revision.
  1292. self.options.revision = None
  1293. scm.update(self.options, None, file_list)
  1294. self.assertEqual(self.githash('repo_1', 4), self.gitrevparse(self.root_dir))
  1295. self.assertFalse(scm.check_diff(self.githash('repo_1', 1), files=['DEPS']))
  1296. self.assertTrue(scm.check_diff(self.githash('repo_1', 1)))
  1297. self.assertTrue(scm.check_diff(self.githash('repo_1', 3), files=['DEPS']))
  1298. self.assertFalse(
  1299. scm.check_diff(self.githash('repo_1', 2),
  1300. files=['DEPS', 'doesnotmatter']))
  1301. self.assertFalse(scm.check_diff(self.githash('repo_1', 2)))
  1302. if 'unittest.util' in __import__('sys').modules:
  1303. # Show full diff in self.assertEqual.
  1304. __import__('sys').modules['unittest.util']._MAX_LENGTH = 999999999
  1305. if __name__ == '__main__':
  1306. level = logging.DEBUG if '-v' in sys.argv else logging.FATAL
  1307. logging.basicConfig(
  1308. level=level,
  1309. format='%(asctime).19s %(levelname)s %(filename)s:'
  1310. '%(lineno)s %(message)s')
  1311. unittest.main()
  1312. # vim: ts=2:sw=2:tw=80:et: