gclient_scm_test.py 47 KB

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