gclient_scm_test.py 61 KB

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