git_common_test.py 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389
  1. #!/usr/bin/env vpython3
  2. # coding=utf-8
  3. # Copyright 2013 The Chromium Authors. All rights reserved.
  4. # Use of this source code is governed by a BSD-style license that can be
  5. # found in the LICENSE file.
  6. """Unit tests for git_common.py"""
  7. import binascii
  8. import collections
  9. import datetime
  10. import os
  11. import shutil
  12. import signal
  13. import sys
  14. import tempfile
  15. import time
  16. import unittest
  17. from io import StringIO
  18. from unittest import mock
  19. DEPOT_TOOLS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  20. sys.path.insert(0, DEPOT_TOOLS_ROOT)
  21. import subprocess2
  22. from testing_support import coverage_utils
  23. from testing_support import git_test_utils
  24. GitRepo = git_test_utils.GitRepo
  25. class GitCommonTestBase(unittest.TestCase):
  26. @classmethod
  27. def setUpClass(cls):
  28. super(GitCommonTestBase, cls).setUpClass()
  29. import git_common
  30. cls.gc = git_common
  31. cls.gc.TEST_MODE = True
  32. os.environ["GIT_EDITOR"] = ":" # Supress git editor during rebase.
  33. class Support(GitCommonTestBase):
  34. def _testMemoizeOneBody(self, threadsafe):
  35. calls = collections.defaultdict(int)
  36. def double_if_even(val):
  37. calls[val] += 1
  38. return val * 2 if val % 2 == 0 else None
  39. # Use this explicitly as a wrapper fn instead of a decorator. Otherwise
  40. # pylint crashes (!!)
  41. double_if_even = self.gc.memoize_one(
  42. threadsafe=threadsafe)(double_if_even)
  43. self.assertEqual(4, double_if_even(2))
  44. self.assertEqual(4, double_if_even(2))
  45. self.assertEqual(None, double_if_even(1))
  46. self.assertEqual(None, double_if_even(1))
  47. self.assertDictEqual({1: 2, 2: 1}, calls)
  48. double_if_even.set(10, 20)
  49. self.assertEqual(20, double_if_even(10))
  50. self.assertDictEqual({1: 2, 2: 1}, calls)
  51. double_if_even.clear()
  52. self.assertEqual(4, double_if_even(2))
  53. self.assertEqual(4, double_if_even(2))
  54. self.assertEqual(None, double_if_even(1))
  55. self.assertEqual(None, double_if_even(1))
  56. self.assertEqual(20, double_if_even(10))
  57. self.assertDictEqual({1: 4, 2: 2, 10: 1}, calls)
  58. def testMemoizeOne(self):
  59. self._testMemoizeOneBody(threadsafe=False)
  60. def testMemoizeOneThreadsafe(self):
  61. self._testMemoizeOneBody(threadsafe=True)
  62. def testOnce(self):
  63. testlist = []
  64. # This works around a bug in pylint
  65. once = self.gc.once
  66. @once
  67. def add_to_list():
  68. testlist.append('dog')
  69. add_to_list()
  70. add_to_list()
  71. add_to_list()
  72. add_to_list()
  73. self.assertEqual(testlist, ['dog'])
  74. def slow_square(i):
  75. """Helper for ScopedPoolTest.
  76. Must be global because non top-level functions aren't pickleable.
  77. """
  78. return i**2
  79. class ScopedPoolTest(GitCommonTestBase):
  80. CTRL_C = signal.CTRL_C_EVENT if sys.platform == 'win32' else signal.SIGINT
  81. def testThreads(self):
  82. result = []
  83. with self.gc.ScopedPool(kind='threads') as pool:
  84. result = list(pool.imap(slow_square, range(10)))
  85. self.assertEqual([0, 1, 4, 9, 16, 25, 36, 49, 64, 81], result)
  86. def testThreadsCtrlC(self):
  87. result = []
  88. with self.assertRaises(KeyboardInterrupt):
  89. with self.gc.ScopedPool(kind='threads') as pool:
  90. # Make sure this pool is interrupted in mid-swing
  91. for i in pool.imap(slow_square, range(20)):
  92. if i > 32:
  93. os.kill(os.getpid(), self.CTRL_C)
  94. result.append(i)
  95. self.assertEqual([0, 1, 4, 9, 16, 25], result)
  96. def testProcs(self):
  97. result = []
  98. with self.gc.ScopedPool() as pool:
  99. result = list(pool.imap(slow_square, range(10)))
  100. self.assertEqual([0, 1, 4, 9, 16, 25, 36, 49, 64, 81], result)
  101. def testProcsCtrlC(self):
  102. result = []
  103. with self.assertRaises(KeyboardInterrupt):
  104. with self.gc.ScopedPool() as pool:
  105. # Make sure this pool is interrupted in mid-swing
  106. for i in pool.imap(slow_square, range(20)):
  107. if i > 32:
  108. os.kill(os.getpid(), self.CTRL_C)
  109. result.append(i)
  110. self.assertEqual([0, 1, 4, 9, 16, 25], result)
  111. class ProgressPrinterTest(GitCommonTestBase):
  112. class FakeStream(object):
  113. def __init__(self):
  114. self.data = set()
  115. self.count = 0
  116. def write(self, line):
  117. self.data.add(line)
  118. def flush(self):
  119. self.count += 1
  120. def testBasic(self):
  121. """This test is probably racy, but I don't have a better alternative."""
  122. fmt = '%(count)d/10'
  123. stream = self.FakeStream()
  124. pp = self.gc.ProgressPrinter(fmt,
  125. enabled=True,
  126. fout=stream,
  127. period=0.01)
  128. with pp as inc:
  129. for _ in range(10):
  130. time.sleep(0.02)
  131. inc()
  132. filtered = {x.strip() for x in stream.data}
  133. rslt = {fmt % {'count': i} for i in range(11)}
  134. self.assertSetEqual(filtered, rslt)
  135. self.assertGreaterEqual(stream.count, 10)
  136. class GitReadOnlyFunctionsTest(git_test_utils.GitRepoReadOnlyTestBase,
  137. GitCommonTestBase):
  138. REPO_SCHEMA = """
  139. A B C D
  140. B E D
  141. """
  142. COMMIT_A = {
  143. 'some/files/file1': {
  144. 'data': b'file1'
  145. },
  146. 'some/files/file2': {
  147. 'data': b'file2'
  148. },
  149. 'some/files/file3': {
  150. 'data': b'file3'
  151. },
  152. 'some/other/file': {
  153. 'data': b'otherfile'
  154. },
  155. }
  156. COMMIT_C = {
  157. 'some/files/file2': {
  158. 'mode': 0o755,
  159. 'data': b'file2 - vanilla\n'
  160. },
  161. }
  162. COMMIT_E = {
  163. 'some/files/file2': {
  164. 'data': b'file2 - merged\n'
  165. },
  166. }
  167. COMMIT_D = {
  168. 'some/files/file2': {
  169. 'data': b'file2 - vanilla\nfile2 - merged\n'
  170. },
  171. }
  172. def testHashes(self):
  173. ret = self.repo.run(
  174. self.gc.hash_multi, *[
  175. 'main',
  176. 'main~3',
  177. self.repo['E'] + '~',
  178. self.repo['D'] + '^2',
  179. 'tag_C^{}',
  180. ])
  181. self.assertEqual([
  182. self.repo['D'],
  183. self.repo['A'],
  184. self.repo['B'],
  185. self.repo['E'],
  186. self.repo['C'],
  187. ], ret)
  188. self.assertEqual(self.repo.run(self.gc.hash_one, 'branch_D'),
  189. self.repo['D'])
  190. self.assertTrue(self.repo['D'].startswith(
  191. self.repo.run(self.gc.hash_one, 'branch_D', short=True)))
  192. def testStream(self):
  193. items = set(self.repo.commit_map.values())
  194. def testfn():
  195. for line in self.gc.run_stream('log', '--format=%H').readlines():
  196. line = line.strip().decode('utf-8')
  197. self.assertIn(line, items)
  198. items.remove(line)
  199. self.repo.run(testfn)
  200. def testStreamWithRetcode(self):
  201. items = set(self.repo.commit_map.values())
  202. def testfn():
  203. with self.gc.run_stream_with_retcode('log',
  204. '--format=%H') as stdout:
  205. for line in stdout.readlines():
  206. line = line.strip().decode('utf-8')
  207. self.assertIn(line, items)
  208. items.remove(line)
  209. self.repo.run(testfn)
  210. def testStreamWithRetcodeException(self):
  211. with self.assertRaises(subprocess2.CalledProcessError):
  212. with self.gc.run_stream_with_retcode('checkout', 'unknown-branch'):
  213. pass
  214. def testCurrentBranch(self):
  215. def cur_branch_out_of_git():
  216. os.chdir('..')
  217. return self.gc.current_branch()
  218. self.assertIsNone(self.repo.run(cur_branch_out_of_git))
  219. self.repo.git('checkout', 'branch_D')
  220. self.assertEqual(self.repo.run(self.gc.current_branch), 'branch_D')
  221. def testBranches(self):
  222. # This check fails with git 2.4 (see crbug.com/487172)
  223. self.assertEqual(self.repo.run(set, self.gc.branches()),
  224. {'main', 'branch_D', 'root_A'})
  225. def testDiff(self):
  226. # Get the names of the blobs being compared (to avoid hard-coding).
  227. c_blob_short = self.repo.git('rev-parse', '--short',
  228. 'tag_C:some/files/file2').stdout.strip()
  229. d_blob_short = self.repo.git('rev-parse', '--short',
  230. 'tag_D:some/files/file2').stdout.strip()
  231. expected_output = [
  232. 'diff --git a/some/files/file2 b/some/files/file2',
  233. 'index %s..%s 100755' % (c_blob_short, d_blob_short),
  234. '--- a/some/files/file2', '+++ b/some/files/file2', '@@ -1 +1,2 @@',
  235. ' file2 - vanilla', '+file2 - merged'
  236. ]
  237. self.assertEqual(
  238. expected_output,
  239. self.repo.run(self.gc.diff, 'tag_C', 'tag_D').split('\n'))
  240. def testDormant(self):
  241. self.assertFalse(self.repo.run(self.gc.is_dormant, 'main'))
  242. self.gc.scm.GIT.SetConfig(self.repo.repo_path, 'branch.main.dormant',
  243. 'true')
  244. self.assertTrue(self.repo.run(self.gc.is_dormant, 'main'))
  245. def testBlame(self):
  246. def get_porcelain_for_commit(commit_name, lines):
  247. format_string = (
  248. '%H {}\nauthor %an\nauthor-mail <%ae>\nauthor-time %at\n'
  249. 'author-tz +0000\ncommitter %cn\ncommitter-mail <%ce>\n'
  250. 'committer-time %ct\ncommitter-tz +0000\nsummary {}')
  251. format_string = format_string.format(lines, commit_name)
  252. info = self.repo.show_commit(commit_name,
  253. format_string=format_string)
  254. return info.split('\n')
  255. # Expect to blame line 1 on C, line 2 on E.
  256. ABBREV_LEN = 7
  257. c_short = self.repo['C'][:1 + ABBREV_LEN]
  258. c_author = self.repo.show_commit('C', format_string='%an %ai')
  259. e_short = self.repo['E'][:1 + ABBREV_LEN]
  260. e_author = self.repo.show_commit('E', format_string='%an %ai')
  261. expected_output = [
  262. '%s (%s 1) file2 - vanilla' % (c_short, c_author),
  263. '%s (%s 2) file2 - merged' % (e_short, e_author)
  264. ]
  265. self.assertEqual(
  266. expected_output,
  267. self.repo.run(self.gc.blame,
  268. 'some/files/file2',
  269. 'tag_D',
  270. abbrev=ABBREV_LEN).split('\n'))
  271. # Test porcelain.
  272. expected_output = []
  273. expected_output.extend(get_porcelain_for_commit('C', '1 1 1'))
  274. expected_output.append('previous %s some/files/file2' % self.repo['B'])
  275. expected_output.append('filename some/files/file2')
  276. expected_output.append('\tfile2 - vanilla')
  277. expected_output.extend(get_porcelain_for_commit('E', '1 2 1'))
  278. expected_output.append('previous %s some/files/file2' % self.repo['B'])
  279. expected_output.append('filename some/files/file2')
  280. expected_output.append('\tfile2 - merged')
  281. self.assertEqual(
  282. expected_output,
  283. self.repo.run(self.gc.blame,
  284. 'some/files/file2',
  285. 'tag_D',
  286. porcelain=True).split('\n'))
  287. def testParseCommitrefs(self):
  288. ret = self.repo.run(
  289. self.gc.parse_commitrefs, *[
  290. 'main',
  291. 'main~3',
  292. self.repo['E'] + '~',
  293. self.repo['D'] + '^2',
  294. 'tag_C^{}',
  295. ])
  296. hashes = [
  297. self.repo['D'],
  298. self.repo['A'],
  299. self.repo['B'],
  300. self.repo['E'],
  301. self.repo['C'],
  302. ]
  303. self.assertEqual(ret, [binascii.unhexlify(h) for h in hashes])
  304. expected_re = r"one of \(u?'main', u?'bananas'\)"
  305. with self.assertRaisesRegexp(Exception, expected_re):
  306. self.repo.run(self.gc.parse_commitrefs, 'main', 'bananas')
  307. def testRepoRoot(self):
  308. def cd_and_repo_root(path):
  309. os.chdir(path)
  310. return self.gc.repo_root()
  311. self.assertEqual(self.repo.repo_path, self.repo.run(self.gc.repo_root))
  312. # cd to a subdirectory; repo_root should still return the root dir.
  313. self.assertEqual(self.repo.repo_path,
  314. self.repo.run(cd_and_repo_root, 'some/files'))
  315. def testTags(self):
  316. self.assertEqual(set(self.repo.run(self.gc.tags)),
  317. {'tag_' + l
  318. for l in 'ABCDE'})
  319. def testTree(self):
  320. tree = self.repo.run(self.gc.tree, 'main:some/files')
  321. file1 = self.COMMIT_A['some/files/file1']['data']
  322. file2 = self.COMMIT_D['some/files/file2']['data']
  323. file3 = self.COMMIT_A['some/files/file3']['data']
  324. self.assertEqual(
  325. tree['file1'],
  326. ('100644', 'blob', git_test_utils.git_hash_data(file1)))
  327. self.assertEqual(
  328. tree['file2'],
  329. ('100755', 'blob', git_test_utils.git_hash_data(file2)))
  330. self.assertEqual(
  331. tree['file3'],
  332. ('100644', 'blob', git_test_utils.git_hash_data(file3)))
  333. tree = self.repo.run(self.gc.tree, 'main:some')
  334. self.assertEqual(len(tree), 2)
  335. # Don't check the tree hash because we're lazy :)
  336. self.assertEqual(tree['files'][:2], ('040000', 'tree'))
  337. tree = self.repo.run(self.gc.tree, 'main:wat')
  338. self.assertEqual(tree, None)
  339. def testTreeRecursive(self):
  340. tree = self.repo.run(self.gc.tree, 'main:some', recurse=True)
  341. file1 = self.COMMIT_A['some/files/file1']['data']
  342. file2 = self.COMMIT_D['some/files/file2']['data']
  343. file3 = self.COMMIT_A['some/files/file3']['data']
  344. other = self.COMMIT_A['some/other/file']['data']
  345. self.assertEqual(
  346. tree['files/file1'],
  347. ('100644', 'blob', git_test_utils.git_hash_data(file1)))
  348. self.assertEqual(
  349. tree['files/file2'],
  350. ('100755', 'blob', git_test_utils.git_hash_data(file2)))
  351. self.assertEqual(
  352. tree['files/file3'],
  353. ('100644', 'blob', git_test_utils.git_hash_data(file3)))
  354. self.assertEqual(
  355. tree['other/file'],
  356. ('100644', 'blob', git_test_utils.git_hash_data(other)))
  357. class GitMutableFunctionsTest(git_test_utils.GitRepoReadWriteTestBase,
  358. GitCommonTestBase):
  359. REPO_SCHEMA = ''
  360. def _intern_data(self, data):
  361. with tempfile.TemporaryFile('wb') as f:
  362. f.write(data.encode('utf-8'))
  363. f.seek(0)
  364. return self.repo.run(self.gc.intern_f, f)
  365. def testInternF(self):
  366. data = 'CoolBobcatsBro'
  367. data_hash = self._intern_data(data)
  368. self.assertEqual(git_test_utils.git_hash_data(data.encode()), data_hash)
  369. self.assertEqual(data,
  370. self.repo.git('cat-file', 'blob', data_hash).stdout)
  371. def testMkTree(self):
  372. tree = {}
  373. for i in 1, 2, 3:
  374. name = '✔ file%d' % i
  375. tree[name] = ('100644', 'blob', self._intern_data(name))
  376. tree_hash = self.repo.run(self.gc.mktree, tree)
  377. self.assertEqual('b524c02ba0e1cf482f8eb08c3d63e97b8895c89c', tree_hash)
  378. def testConfig(self):
  379. self.repo.git('config', '--add', 'happy.derpies', 'food')
  380. self.assertEqual(
  381. self.repo.run(self.gc.get_config_list, 'happy.derpies'), ['food'])
  382. self.assertEqual(self.repo.run(self.gc.get_config_list, 'sad.derpies'),
  383. [])
  384. self.repo.git('config', '--add', 'happy.derpies', 'cat')
  385. self.gc.scm.GIT.drop_config_cache()
  386. self.assertEqual(
  387. self.repo.run(self.gc.get_config_list, 'happy.derpies'),
  388. ['food', 'cat'])
  389. self.assertEqual('cat',
  390. self.repo.run(self.gc.get_config, 'dude.bob', 'cat'))
  391. self.gc.scm.GIT.SetConfig(self.repo.repo_path, 'dude.bob', 'dog')
  392. self.assertEqual('dog',
  393. self.repo.run(self.gc.get_config, 'dude.bob', 'cat'))
  394. self.repo.run(self.gc.del_config, 'dude.bob')
  395. # This should work without raising an exception
  396. self.repo.run(self.gc.del_config, 'dude.bob')
  397. self.assertEqual('cat',
  398. self.repo.run(self.gc.get_config, 'dude.bob', 'cat'))
  399. self.assertEqual('origin/main', self.repo.run(self.gc.root))
  400. self.repo.git('config', 'depot-tools.upstream', 'catfood')
  401. self.gc.scm.GIT.drop_config_cache()
  402. self.assertEqual('catfood', self.repo.run(self.gc.root))
  403. self.repo.git('config', '--add', 'core.fsmonitor', 'true')
  404. self.gc.scm.GIT.drop_config_cache()
  405. self.assertEqual(True, self.repo.run(self.gc.is_fsmonitor_enabled))
  406. self.repo.git('config', '--add', 'core.fsmonitor', 't')
  407. self.gc.scm.GIT.drop_config_cache()
  408. self.assertEqual(False, self.repo.run(self.gc.is_fsmonitor_enabled))
  409. self.repo.git('config', '--add', 'core.fsmonitor', 'false')
  410. self.gc.scm.GIT.drop_config_cache()
  411. self.assertEqual(False, self.repo.run(self.gc.is_fsmonitor_enabled))
  412. def testRoot(self):
  413. origin_schema = git_test_utils.GitRepoSchema(
  414. """
  415. A B C
  416. B D
  417. """, self.getRepoContent)
  418. origin = origin_schema.reify()
  419. # Set the default branch to branch_D instead of main.
  420. origin.git('checkout', 'branch_D')
  421. self.repo.git('remote', 'add', 'origin', origin.repo_path)
  422. self.repo.git('fetch', 'origin')
  423. self.repo.git('remote', 'set-head', 'origin', '-a')
  424. self.assertEqual('origin/branch_D', self.repo.run(self.gc.root))
  425. def testUpstream(self):
  426. self.repo.git('commit', '--allow-empty', '-am', 'foooooo')
  427. self.assertEqual(self.repo.run(self.gc.upstream, 'bobly'), None)
  428. self.assertEqual(self.repo.run(self.gc.upstream, 'main'), None)
  429. self.repo.git('checkout', '-t', '-b', 'happybranch', 'main')
  430. self.assertEqual(self.repo.run(self.gc.upstream, 'happybranch'), 'main')
  431. def testNormalizedVersion(self):
  432. self.assertTrue(
  433. all(
  434. isinstance(x, int)
  435. for x in self.repo.run(self.gc.get_git_version)))
  436. def testGetBranchesInfo(self):
  437. self.repo.git('commit', '--allow-empty', '-am', 'foooooo')
  438. self.repo.git('checkout', '-t', '-b', 'happybranch', 'main')
  439. self.repo.git('commit', '--allow-empty', '-am', 'foooooo')
  440. self.repo.git('checkout', '-t', '-b', 'child', 'happybranch')
  441. self.repo.git('checkout', '-t', '-b', 'to_delete', 'main')
  442. self.repo.git('checkout', '-t', '-b', 'parent_gone', 'to_delete')
  443. self.repo.git('branch', '-D', 'to_delete')
  444. supports_track = (self.repo.run(self.gc.get_git_version) >=
  445. self.gc.MIN_UPSTREAM_TRACK_GIT_VERSION)
  446. actual = self.repo.run(self.gc.get_branches_info, supports_track)
  447. expected = {
  448. 'happybranch': (self.repo.run(self.gc.hash_one,
  449. 'happybranch',
  450. short=True), 'main',
  451. 1 if supports_track else None, None),
  452. 'child': (self.repo.run(self.gc.hash_one, 'child',
  453. short=True), 'happybranch', None, None),
  454. 'main': (self.repo.run(self.gc.hash_one, 'main',
  455. short=True), '', None, None),
  456. '':
  457. None,
  458. 'parent_gone': (self.repo.run(self.gc.hash_one,
  459. 'parent_gone',
  460. short=True), 'to_delete', None, None),
  461. 'to_delete':
  462. None
  463. }
  464. self.assertEqual(expected, actual)
  465. def testGetBranchesInfoWithReset(self):
  466. self.repo.git('commit', '--allow-empty', '-am', 'foooooo')
  467. self.repo.git('checkout', '-t', '-b', 'foobarA', 'main')
  468. self.repo.git('config', 'branch.foobarA.base',
  469. self.repo.run(self.gc.hash_one, 'main'))
  470. self.repo.git('config', 'branch.foobarA.base-upstream', 'main')
  471. with self.repo.open('foobar1', 'w') as f:
  472. f.write('hello')
  473. self.repo.git('add', 'foobar1')
  474. self.repo.git_commit('commit1')
  475. with self.repo.open('foobar2', 'w') as f:
  476. f.write('goodbye')
  477. self.repo.git('add', 'foobar2')
  478. self.repo.git_commit('commit2')
  479. self.repo.git('checkout', '-t', '-b', 'foobarB', 'foobarA')
  480. self.repo.git('config', 'branch.foobarB.base',
  481. self.repo.run(self.gc.hash_one, 'foobarA'))
  482. self.repo.git('config', 'branch.foobarB.base-upstream', 'foobarA')
  483. self.repo.git('checkout', 'foobarA')
  484. self.repo.git('reset', '--hard', 'HEAD~')
  485. with self.repo.open('foobar', 'w') as f:
  486. f.write('world')
  487. self.repo.git('add', 'foobar')
  488. self.repo.git_commit('commit1.2')
  489. actual = self.repo.run(self.gc.get_branches_info, True)
  490. expected = {
  491. 'foobarA': (self.repo.run(self.gc.hash_one, 'foobarA',
  492. short=True), 'main', 2, None),
  493. 'foobarB': (self.repo.run(self.gc.hash_one, 'foobarB',
  494. short=True), 'foobarA', None, 1),
  495. 'main': (self.repo.run(self.gc.hash_one, 'main',
  496. short=True), '', None, None),
  497. '':
  498. None
  499. }
  500. self.assertEqual(expected, actual)
  501. class GitMutableStructuredTest(git_test_utils.GitRepoReadWriteTestBase,
  502. GitCommonTestBase):
  503. REPO_SCHEMA = """
  504. A B C D E F G
  505. B H I J K
  506. J L
  507. X Y Z
  508. CAT DOG
  509. """
  510. COMMIT_B = {'file': {'data': b'B'}}
  511. COMMIT_H = {'file': {'data': b'H'}}
  512. COMMIT_I = {'file': {'data': b'I'}}
  513. COMMIT_J = {'file': {'data': b'J'}}
  514. COMMIT_K = {'file': {'data': b'K'}}
  515. COMMIT_L = {'file': {'data': b'L'}}
  516. def setUp(self):
  517. super(GitMutableStructuredTest, self).setUp()
  518. self.repo.git('branch', '--set-upstream-to', 'root_X', 'branch_Z')
  519. self.repo.git('branch', '--set-upstream-to', 'branch_G', 'branch_K')
  520. self.repo.git('branch', '--set-upstream-to', 'branch_K', 'branch_L')
  521. self.repo.git('branch', '--set-upstream-to', 'root_A', 'branch_G')
  522. self.repo.git('branch', '--set-upstream-to', 'root_X', 'root_A')
  523. def testTooManyBranches(self):
  524. for i in range(30):
  525. self.repo.git('branch', 'a' * i)
  526. _, rslt = self.repo.capture_stdio(list, self.gc.branches())
  527. self.assertIn('too many branches (39/20)', rslt)
  528. self.repo.git('config', 'depot-tools.branch-limit', 'cat')
  529. _, rslt = self.repo.capture_stdio(list, self.gc.branches())
  530. self.assertIn('too many branches (39/20)', rslt)
  531. self.gc.scm.GIT.SetConfig(self.repo.repo_path,
  532. 'depot-tools.branch-limit', '100')
  533. # should not raise
  534. # This check fails with git 2.4 (see crbug.com/487172)
  535. self.assertEqual(38, len(self.repo.run(list, self.gc.branches())))
  536. def testMergeBase(self):
  537. self.repo.git('checkout', 'branch_K')
  538. self.assertEqual(
  539. self.repo['B'],
  540. self.repo.run(self.gc.get_or_create_merge_base, 'branch_K',
  541. 'branch_G'))
  542. self.assertEqual(
  543. self.repo['J'],
  544. self.repo.run(self.gc.get_or_create_merge_base, 'branch_L',
  545. 'branch_K'))
  546. self.gc.scm.GIT.drop_config_cache()
  547. self.assertEqual(
  548. self.repo['B'],
  549. self.repo.run(self.gc.get_config, 'branch.branch_K.base'))
  550. self.assertEqual(
  551. 'branch_G',
  552. self.repo.run(self.gc.get_config, 'branch.branch_K.base-upstream'))
  553. # deadbeef is a bad hash, so this will result in repo['B']
  554. self.repo.run(self.gc.manual_merge_base, 'branch_K', 'deadbeef',
  555. 'branch_G')
  556. self.assertEqual(
  557. self.repo['B'],
  558. self.repo.run(self.gc.get_or_create_merge_base, 'branch_K',
  559. 'branch_G'))
  560. # but if we pick a real ancestor, then it'll work
  561. self.repo.run(self.gc.manual_merge_base, 'branch_K', self.repo['I'],
  562. 'branch_G')
  563. self.gc.scm.GIT.drop_config_cache()
  564. self.assertEqual(
  565. self.repo['I'],
  566. self.repo.run(self.gc.get_or_create_merge_base, 'branch_K',
  567. 'branch_G'))
  568. self.assertEqual(
  569. {
  570. 'branch_K': self.repo['I'],
  571. 'branch_L': self.repo['J']
  572. }, self.repo.run(self.gc.branch_config_map, 'base'))
  573. self.repo.run(self.gc.remove_merge_base, 'branch_K')
  574. self.repo.run(self.gc.remove_merge_base, 'branch_L')
  575. self.assertEqual(
  576. None, self.repo.run(self.gc.get_config, 'branch.branch_K.base'))
  577. self.assertEqual({}, self.repo.run(self.gc.branch_config_map, 'base'))
  578. # if it's too old, then it caps at merge-base
  579. self.repo.run(self.gc.manual_merge_base, 'branch_K', self.repo['A'],
  580. 'branch_G')
  581. self.assertEqual(
  582. self.repo['B'],
  583. self.repo.run(self.gc.get_or_create_merge_base, 'branch_K',
  584. 'branch_G'))
  585. # If the user does --set-upstream-to something else, then we discard the
  586. # base and recompute it.
  587. self.repo.run(self.gc.run, 'branch', '-u', 'root_A')
  588. self.assertEqual(
  589. self.repo['A'],
  590. self.repo.run(self.gc.get_or_create_merge_base, 'branch_K'))
  591. self.assertIsNone(
  592. self.repo.run(self.gc.get_or_create_merge_base, 'branch_DOG'))
  593. def testGetBranchTree(self):
  594. skipped, tree = self.repo.run(self.gc.get_branch_tree)
  595. # This check fails with git 2.4 (see crbug.com/487172)
  596. self.assertEqual(skipped, {'main', 'root_X', 'branch_DOG', 'root_CAT'})
  597. self.assertEqual(
  598. tree, {
  599. 'branch_G': 'root_A',
  600. 'root_A': 'root_X',
  601. 'branch_K': 'branch_G',
  602. 'branch_L': 'branch_K',
  603. 'branch_Z': 'root_X'
  604. })
  605. topdown = list(self.gc.topo_iter(tree))
  606. bottomup = list(self.gc.topo_iter(tree, top_down=False))
  607. self.assertEqual(topdown, [
  608. ('branch_Z', 'root_X'),
  609. ('root_A', 'root_X'),
  610. ('branch_G', 'root_A'),
  611. ('branch_K', 'branch_G'),
  612. ('branch_L', 'branch_K'),
  613. ])
  614. self.assertEqual(bottomup, [
  615. ('branch_L', 'branch_K'),
  616. ('branch_Z', 'root_X'),
  617. ('branch_K', 'branch_G'),
  618. ('branch_G', 'root_A'),
  619. ('root_A', 'root_X'),
  620. ])
  621. def testGetHashes(self):
  622. hashes = self.repo.run(self.gc.get_hashes)
  623. for branch, branch_hash in hashes.items():
  624. self.assertEqual(self.repo.run(self.gc.hash_one, branch),
  625. branch_hash)
  626. def testGetDownstreamBranches(self):
  627. downstream_branches = self.repo.run(self.gc.get_downstream_branches)
  628. self.assertEqual(
  629. downstream_branches, {
  630. 'root_A': ['branch_G'],
  631. 'branch_G': ['branch_K'],
  632. 'branch_K': ['branch_L'],
  633. 'root_X': ['branch_Z', 'root_A'],
  634. })
  635. def testGetDivergedBranches(self):
  636. # root_X and root_A don't actually have a common base commit due to the
  637. # test repo's structure, which causes get_diverged_branches to throw
  638. # an error.
  639. self.repo.git('branch', '--unset-upstream', 'root_A')
  640. # K is setup with G as it's root, but it's branched at B.
  641. # L is setup with K as it's root, but it's branched at J.
  642. diverged_branches = self.repo.run(self.gc.get_diverged_branches)
  643. self.assertEqual(diverged_branches, ['branch_K', 'branch_L'])
  644. def testIsGitTreeDirty(self):
  645. retval = []
  646. self.repo.capture_stdio(lambda: retval.append(
  647. self.repo.run(self.gc.is_dirty_git_tree, 'foo')))
  648. self.assertEqual(False, retval[0])
  649. self.repo.open('test.file', 'w').write('test data')
  650. self.repo.git('add', 'test.file')
  651. retval = []
  652. self.repo.capture_stdio(lambda: retval.append(
  653. self.repo.run(self.gc.is_dirty_git_tree, 'foo')))
  654. self.assertEqual(True, retval[0])
  655. def testSquashBranch(self):
  656. self.repo.git('checkout', 'branch_K')
  657. self.assertEqual(
  658. True, self.repo.run(self.gc.squash_current_branch,
  659. '✔ cool message'))
  660. lines = ['✔ cool message', '']
  661. for l in 'HIJK':
  662. lines.extend((self.repo[l], l, ''))
  663. lines.pop()
  664. msg = '\n'.join(lines)
  665. self.assertEqual(
  666. self.repo.run(self.gc.run, 'log', '-n1', '--format=%B'), msg)
  667. self.assertEqual(
  668. self.repo.git('cat-file', 'blob', 'branch_K:file').stdout, 'K')
  669. def testSquashBranchDefaultMessage(self):
  670. self.repo.git('checkout', 'branch_K')
  671. self.assertEqual(True, self.repo.run(self.gc.squash_current_branch))
  672. self.assertEqual(
  673. self.repo.run(self.gc.run, 'log', '-n1', '--format=%s'),
  674. 'git squash commit for branch_K.')
  675. def testSquashBranchEmpty(self):
  676. self.repo.git('checkout', 'branch_K')
  677. self.repo.git('checkout', 'branch_G', '.')
  678. self.repo.git('commit', '-m', 'revert all changes no branch')
  679. # Should return False since the quash would result in an empty commit
  680. stdout = self.repo.capture_stdio(self.gc.squash_current_branch)[0]
  681. self.assertEqual(stdout,
  682. 'Nothing to commit; squashed branch is empty\n')
  683. def testRebase(self):
  684. self.assertSchema("""
  685. A B C D E F G
  686. B H I J K
  687. J L
  688. X Y Z
  689. CAT DOG
  690. """)
  691. rslt = self.repo.run(self.gc.rebase, 'branch_G', 'branch_K~4',
  692. 'branch_K')
  693. self.assertTrue(rslt.success)
  694. self.assertSchema("""
  695. A B C D E F G H I J K
  696. B H I J L
  697. X Y Z
  698. CAT DOG
  699. """)
  700. rslt = self.repo.run(self.gc.rebase,
  701. 'branch_K',
  702. 'branch_L~1',
  703. 'branch_L',
  704. abort=True)
  705. self.assertFalse(rslt.success)
  706. self.assertFalse(self.repo.run(self.gc.in_rebase))
  707. rslt = self.repo.run(self.gc.rebase,
  708. 'branch_K',
  709. 'branch_L~1',
  710. 'branch_L',
  711. abort=False)
  712. self.assertFalse(rslt.success)
  713. self.assertTrue(self.repo.run(self.gc.in_rebase))
  714. self.assertEqual(
  715. self.repo.git('status', '--porcelain').stdout, 'UU file\n')
  716. self.repo.git('checkout', '--theirs', 'file')
  717. self.repo.git('add', 'file')
  718. self.repo.git('rebase', '--continue')
  719. self.assertSchema("""
  720. A B C D E F G H I J K L
  721. X Y Z
  722. CAT DOG
  723. """)
  724. def testStatus(self):
  725. def inner():
  726. dictified_status = lambda: {
  727. k: dict(v._asdict()) # pylint: disable=protected-access
  728. for k, v in self.repo.run(self.gc.status)
  729. }
  730. self.repo.git('mv', 'file', 'cat')
  731. with open('COOL', 'w') as f:
  732. f.write('Super cool file!')
  733. self.assertDictEqual(
  734. dictified_status(), {
  735. 'cat': {
  736. 'lstat': 'R',
  737. 'rstat': ' ',
  738. 'src': 'file'
  739. },
  740. 'COOL': {
  741. 'lstat': '?',
  742. 'rstat': '?',
  743. 'src': 'COOL'
  744. }
  745. })
  746. self.repo.run(inner)
  747. class GitFreezeThaw(git_test_utils.GitRepoReadWriteTestBase):
  748. @classmethod
  749. def setUpClass(cls):
  750. super(GitFreezeThaw, cls).setUpClass()
  751. import git_common
  752. cls.gc = git_common
  753. cls.gc.TEST_MODE = True
  754. REPO_SCHEMA = """
  755. A B C D
  756. B E D
  757. """
  758. COMMIT_A = {
  759. 'some/files/file1': {
  760. 'data': b'file1'
  761. },
  762. 'some/files/file2': {
  763. 'data': b'file2'
  764. },
  765. 'some/files/file3': {
  766. 'data': b'file3'
  767. },
  768. 'some/other/file': {
  769. 'data': b'otherfile'
  770. },
  771. }
  772. COMMIT_C = {
  773. 'some/files/file2': {
  774. 'mode': 0o755,
  775. 'data': b'file2 - vanilla'
  776. },
  777. }
  778. COMMIT_E = {
  779. 'some/files/file2': {
  780. 'data': b'file2 - merged'
  781. },
  782. }
  783. COMMIT_D = {
  784. 'some/files/file2': {
  785. 'data': b'file2 - vanilla\nfile2 - merged'
  786. },
  787. }
  788. def testNothing(self):
  789. self.assertIsNotNone(self.repo.run(self.gc.thaw)) # 'Nothing to thaw'
  790. self.assertIsNotNone(self.repo.run(
  791. self.gc.freeze)) # 'Nothing to freeze'
  792. def testAll(self):
  793. def inner():
  794. with open('some/files/file2', 'a') as f2:
  795. print('cool appended line', file=f2)
  796. with open('some/files/file3', 'w') as f3:
  797. print('hello', file=f3)
  798. self.repo.git('add', 'some/files/file3')
  799. with open('some/files/file3', 'a') as f3:
  800. print('world', file=f3)
  801. os.mkdir('some/other_files')
  802. with open('some/other_files/subdir_file', 'w') as f3:
  803. print('new file!', file=f3)
  804. with open('some/files/file5', 'w') as f5:
  805. print('New file!1!one!', file=f5)
  806. with open('some/files/file6', 'w') as f6:
  807. print('hello', file=f6)
  808. self.repo.git('add', 'some/files/file6')
  809. with open('some/files/file6', 'w') as f6:
  810. print('world', file=f6)
  811. with open('some/files/file7', 'w') as f7:
  812. print('hello', file=f7)
  813. self.repo.git('add', 'some/files/file7')
  814. os.remove('some/files/file7')
  815. STATUS_1 = '\n'.join(
  816. (' M some/files/file2', 'MM some/files/file3',
  817. 'A some/files/file5', 'AM some/files/file6',
  818. 'AD some/files/file7', '?? some/other_files/')) + '\n'
  819. self.repo.git('add', 'some/files/file5')
  820. # Freeze group 1
  821. self.assertEqual(
  822. self.repo.git('status', '--porcelain').stdout, STATUS_1)
  823. self.assertIsNone(self.gc.freeze())
  824. self.assertEqual(self.repo.git('status', '--porcelain').stdout, '')
  825. # Freeze group 2
  826. with open('some/files/file2', 'a') as f2:
  827. print('new! appended line!', file=f2)
  828. self.assertEqual(
  829. self.repo.git('status', '--porcelain').stdout,
  830. ' M some/files/file2\n')
  831. self.assertIsNone(self.gc.freeze())
  832. self.assertEqual(self.repo.git('status', '--porcelain').stdout, '')
  833. # Thaw it out!
  834. self.assertIsNone(self.gc.thaw())
  835. self.assertIsNotNone(
  836. self.gc.thaw()) # One thaw should thaw everything
  837. self.assertEqual(
  838. self.repo.git('status', '--porcelain').stdout, STATUS_1)
  839. self.repo.run(inner)
  840. def testTooBig(self):
  841. def inner():
  842. self.repo.git('config', 'depot-tools.freeze-size-limit', '1')
  843. with open('bigfile', 'w') as f:
  844. chunk = 'NERDFACE' * 1024
  845. for _ in range(128 * 2 + 1): # Just over 2 mb
  846. f.write(chunk)
  847. _, err = self.repo.capture_stdio(self.gc.freeze)
  848. self.assertIn('too much untracked+unignored', err)
  849. self.repo.run(inner)
  850. def testTooBigMultipleFiles(self):
  851. def inner():
  852. self.repo.git('config', 'depot-tools.freeze-size-limit', '1')
  853. for i in range(3):
  854. with open('file%d' % i, 'w') as f:
  855. chunk = 'NERDFACE' * 1024
  856. for _ in range(50): # About 400k
  857. f.write(chunk)
  858. _, err = self.repo.capture_stdio(self.gc.freeze)
  859. self.assertIn('too much untracked+unignored', err)
  860. self.repo.run(inner)
  861. def testMerge(self):
  862. def inner():
  863. self.repo.git('checkout', '-b', 'bad_merge_branch')
  864. with open('bad_merge', 'w') as f:
  865. f.write('bad_merge_left')
  866. self.repo.git('add', 'bad_merge')
  867. self.repo.git('commit', '-m', 'bad_merge')
  868. self.repo.git('checkout', 'branch_D')
  869. with open('bad_merge', 'w') as f:
  870. f.write('bad_merge_right')
  871. self.repo.git('add', 'bad_merge')
  872. self.repo.git('commit', '-m', 'bad_merge_d')
  873. self.repo.git('merge', 'bad_merge_branch')
  874. _, err = self.repo.capture_stdio(self.gc.freeze)
  875. self.assertIn('Cannot freeze unmerged changes', err)
  876. self.repo.run(inner)
  877. def testAddError(self):
  878. def inner():
  879. self.repo.git('checkout', '-b', 'unreadable_file_branch')
  880. with open('bad_file', 'w') as f:
  881. f.write('some text')
  882. os.chmod('bad_file', 0o0111)
  883. ret = self.repo.run(self.gc.freeze)
  884. self.assertIn('Failed to index some unindexed files.', ret)
  885. self.repo.run(inner)
  886. class GitMakeWorkdir(git_test_utils.GitRepoReadOnlyTestBase, GitCommonTestBase):
  887. def setUp(self):
  888. self._tempdir = tempfile.mkdtemp()
  889. def tearDown(self):
  890. shutil.rmtree(self._tempdir)
  891. REPO_SCHEMA = """
  892. A
  893. """
  894. @unittest.skipIf(not hasattr(os, 'symlink'), "OS doesn't support symlink")
  895. def testMakeWorkdir(self):
  896. workdir = os.path.join(self._tempdir, 'workdir')
  897. self.gc.make_workdir(os.path.join(self.repo.repo_path, '.git'),
  898. os.path.join(workdir, '.git'))
  899. EXPECTED_LINKS = [
  900. 'config',
  901. 'info',
  902. 'hooks',
  903. 'logs/refs',
  904. 'objects',
  905. 'refs',
  906. ]
  907. for path in EXPECTED_LINKS:
  908. self.assertTrue(os.path.islink(os.path.join(workdir, '.git', path)))
  909. self.assertEqual(
  910. os.path.realpath(os.path.join(workdir, '.git', path)),
  911. os.path.join(self.repo.repo_path, '.git', path))
  912. self.assertFalse(os.path.islink(os.path.join(workdir, '.git', 'HEAD')))
  913. class GitTestUtilsTest(git_test_utils.GitRepoReadOnlyTestBase):
  914. REPO_SCHEMA = """
  915. A B C
  916. """
  917. COMMIT_A = {
  918. 'file1': {
  919. 'data': b'file1'
  920. },
  921. }
  922. COMMIT_B = {
  923. 'file1': {
  924. 'data': b'file1 changed'
  925. },
  926. }
  927. # Test special keys (custom commit data).
  928. COMMIT_C = {
  929. GitRepo.AUTHOR_NAME:
  930. 'Custom Author',
  931. GitRepo.AUTHOR_EMAIL:
  932. 'author@example.com',
  933. GitRepo.AUTHOR_DATE:
  934. datetime.datetime(1980, 9, 8, 7, 6, 5, tzinfo=git_test_utils.UTC),
  935. GitRepo.COMMITTER_NAME:
  936. 'Custom Committer',
  937. GitRepo.COMMITTER_EMAIL:
  938. 'committer@example.com',
  939. GitRepo.COMMITTER_DATE:
  940. datetime.datetime(1990, 4, 5, 6, 7, 8, tzinfo=git_test_utils.UTC),
  941. 'file1': {
  942. 'data': b'file1 changed again'
  943. },
  944. }
  945. def testAutomaticCommitDates(self):
  946. # The dates should start from 1970-01-01 and automatically increment.
  947. # They must be in UTC (otherwise the tests are system-dependent, and if
  948. # your local timezone is positive, timestamps will be <0 which causes
  949. # bizarre behaviour in Git; http://crbug.com/581895).
  950. self.assertEqual('Author McAuthorly 1970-01-01 00:00:00 +0000',
  951. self.repo.show_commit('A', format_string='%an %ai'))
  952. self.assertEqual('Charles Committish 1970-01-02 00:00:00 +0000',
  953. self.repo.show_commit('A', format_string='%cn %ci'))
  954. self.assertEqual('Author McAuthorly 1970-01-03 00:00:00 +0000',
  955. self.repo.show_commit('B', format_string='%an %ai'))
  956. self.assertEqual('Charles Committish 1970-01-04 00:00:00 +0000',
  957. self.repo.show_commit('B', format_string='%cn %ci'))
  958. def testCustomCommitData(self):
  959. self.assertEqual(
  960. 'Custom Author author@example.com '
  961. '1980-09-08 07:06:05 +0000',
  962. self.repo.show_commit('C', format_string='%an %ae %ai'))
  963. self.assertEqual(
  964. 'Custom Committer committer@example.com '
  965. '1990-04-05 06:07:08 +0000',
  966. self.repo.show_commit('C', format_string='%cn %ce %ci'))
  967. class CheckGitVersionTest(GitCommonTestBase):
  968. def setUp(self):
  969. self.addCleanup(self.gc.check_git_version.cache_clear)
  970. self.addCleanup(self.gc.get_git_version.cache_clear)
  971. @mock.patch('gclient_utils.IsEnvCog')
  972. def testNonGitEnv(self, mockCog):
  973. mockCog.return_value = True
  974. self.assertIsNone(self.gc.check_git_version())
  975. @mock.patch('gclient_utils.IsEnvCog')
  976. @mock.patch('shutil.which')
  977. def testGitNotInstalled(self, mockWhich, mockCog):
  978. mockCog.return_value = False
  979. mockWhich.return_value = None
  980. recommendation = self.gc.check_git_version()
  981. self.assertIsNotNone(recommendation)
  982. self.assertTrue('Please install' in recommendation)
  983. mockWhich.assert_called_once()
  984. @mock.patch('gclient_utils.IsEnvCog')
  985. @mock.patch('shutil.which')
  986. @mock.patch('git_common.run')
  987. def testGitOldVersion(self, mockRun, mockWhich, mockCog):
  988. mockCog.return_value = False
  989. mockWhich.return_value = '/example/bin/git'
  990. mockRun.return_value = 'git version 2.2.40-abc'
  991. recommendation = self.gc.check_git_version()
  992. self.assertIsNotNone(recommendation)
  993. self.assertTrue('update is recommended' in recommendation)
  994. mockWhich.assert_called_once()
  995. mockRun.assert_called_once()
  996. @mock.patch('gclient_utils.IsEnvCog')
  997. @mock.patch('shutil.which')
  998. @mock.patch('git_common.run')
  999. def testGitSufficientVersion(self, mockRun, mockWhich, mockCog):
  1000. mockCog.return_value = False
  1001. mockWhich.return_value = '/example/bin/git'
  1002. mockRun.return_value = 'git version 2.30.1.456'
  1003. self.assertIsNone(self.gc.check_git_version())
  1004. mockWhich.assert_called_once()
  1005. mockRun.assert_called_once()
  1006. @mock.patch('gclient_utils.IsEnvCog')
  1007. @mock.patch('shutil.which')
  1008. @mock.patch('git_common.run')
  1009. def testHandlesErrorGettingVersion(self, mockRun, mockWhich, mockCog):
  1010. mockCog.return_value = False
  1011. mockWhich.return_value = '/example/bin/git'
  1012. mockRun.return_value = 'Error running git version'
  1013. recommendation = self.gc.check_git_version()
  1014. self.assertIsNotNone(recommendation)
  1015. self.assertTrue('update is recommended' in recommendation)
  1016. mockWhich.assert_called_once()
  1017. mockRun.assert_called_once()
  1018. class WarnSubmoduleTest(unittest.TestCase):
  1019. def setUp(self):
  1020. import git_common
  1021. self.warn_submodule = git_common.warn_submodule
  1022. mock.patch('sys.stdout', StringIO()).start()
  1023. self.addCleanup(mock.patch.stopall)
  1024. def testWarnFSMonitorOldVersion(self):
  1025. mock.patch('git_common.is_fsmonitor_enabled', lambda: True).start()
  1026. mock.patch('sys.platform', 'darwin').start()
  1027. mock.patch('git_common.run', lambda _: 'git version 2.40.0').start()
  1028. self.warn_submodule()
  1029. self.assertTrue('WARNING: You have fsmonitor enabled.' in \
  1030. sys.stdout.getvalue())
  1031. def testWarnFSMonitorNewVersion(self):
  1032. mock.patch('git_common.is_fsmonitor_enabled', lambda: True).start()
  1033. mock.patch('sys.platform', 'darwin').start()
  1034. mock.patch('git_common.run', lambda _: 'git version 2.43.1').start()
  1035. self.warn_submodule()
  1036. self.assertFalse('WARNING: You have fsmonitor enabled.' in \
  1037. sys.stdout.getvalue())
  1038. def testWarnFSMonitorGoogVersion(self):
  1039. mock.patch('git_common.is_fsmonitor_enabled', lambda: True).start()
  1040. mock.patch('sys.platform', 'darwin').start()
  1041. mock.patch('git_common.run',
  1042. lambda _: 'git version 2.42.0.515.A-goog').start()
  1043. self.warn_submodule()
  1044. self.assertFalse('WARNING: You have fsmonitor enabled.' in \
  1045. sys.stdout.getvalue())
  1046. @mock.patch('time.sleep')
  1047. @mock.patch('git_common._run_with_stderr')
  1048. class RunWithStderr(GitCommonTestBase):
  1049. def setUp(self):
  1050. super(RunWithStderr, self).setUp()
  1051. msg = 'error: could not lock config file .git/config: File exists'
  1052. self.lock_failure = self.calledProcessError(msg)
  1053. msg = 'error: wrong number of arguments, should be 2'
  1054. self.wrong_param = self.calledProcessError(msg)
  1055. def calledProcessError(self, stderr):
  1056. return subprocess2.CalledProcessError(
  1057. 2,
  1058. cmd=['a', 'b', 'c'], # doesn't matter
  1059. cwd='/',
  1060. stdout='',
  1061. stderr=stderr.encode('utf-8'),
  1062. )
  1063. def runGitCheckout(self, ex, retry_lock):
  1064. with self.assertRaises(type(ex)):
  1065. self.gc.run_with_stderr('checkout', 'a', retry_lock=retry_lock)
  1066. def runGitConfig(self, ex, retry_lock):
  1067. with self.assertRaises(type(ex)):
  1068. self.gc.run_with_stderr('config', 'set', retry_lock=retry_lock)
  1069. def test_config_with_non_lock_failure(self, run_mock, _):
  1070. """Tests git-config with a non-lock-failure."""
  1071. ex = self.wrong_param
  1072. run_mock.side_effect = ex
  1073. # retry_lock == True
  1074. self.runGitConfig(ex, retry_lock=True)
  1075. self.assertEqual(run_mock.call_count, 1) # 1 + 0 (retry)
  1076. # retry_lock == False
  1077. run_mock.reset_mock()
  1078. self.runGitConfig(ex, retry_lock=False)
  1079. self.assertEqual(run_mock.call_count, 1) # 1 + 0 (retry)
  1080. def test_config_with_lock_failure(self, run_mock, _):
  1081. """Tests git-config with a lock-failure."""
  1082. ex = self.lock_failure
  1083. run_mock.side_effect = ex
  1084. # retry_lock == True
  1085. self.runGitConfig(ex, retry_lock=True)
  1086. self.assertEqual(run_mock.call_count, 6) # 1 + 5 (retry)
  1087. # retry_lock == False
  1088. run_mock.reset_mock()
  1089. self.runGitConfig(ex, retry_lock=False)
  1090. self.assertEqual(run_mock.call_count, 1) # 1 + 0 (retry)
  1091. def test_checkout_with_non_lock_failure(self, run_mock, _):
  1092. """Tests git-checkout with a non-lock-failure."""
  1093. ex = self.wrong_param
  1094. run_mock.side_effect = ex
  1095. # retry_lock == True
  1096. self.runGitCheckout(ex, retry_lock=True)
  1097. self.assertEqual(run_mock.call_count, 1) # 1 + 0 (retry)
  1098. # retry_lock == False
  1099. run_mock.reset_mock()
  1100. self.runGitCheckout(ex, retry_lock=False)
  1101. self.assertEqual(run_mock.call_count, 1) # 1 + 0 (retry)
  1102. def test_checkout_with_lock_failure(self, run_mock, _):
  1103. """Tests git-checkout with a lock-failure."""
  1104. ex = self.lock_failure
  1105. run_mock.side_effect = ex
  1106. # retry_lock == True
  1107. self.runGitCheckout(ex, retry_lock=True)
  1108. self.assertEqual(run_mock.call_count, 1) # 1 + 0 (retry)
  1109. # retry_lock == False
  1110. run_mock.reset_mock()
  1111. self.runGitCheckout(ex, retry_lock=False)
  1112. self.assertEqual(run_mock.call_count, 1) # 1 + 0 (retry)
  1113. class ExtractGitPathFromGitBatTest(GitCommonTestBase):
  1114. def test_unexpected_format(self):
  1115. git_bat = os.path.join(DEPOT_TOOLS_ROOT, 'tests',
  1116. 'git_common_test.inputs',
  1117. 'testGitBatUnexpectedFormat', 'git.bat')
  1118. actual = self.gc._extract_git_path_from_git_bat(git_bat)
  1119. self.assertEqual(actual, git_bat)
  1120. def test_non_exe(self):
  1121. git_bat = os.path.join(DEPOT_TOOLS_ROOT, 'tests',
  1122. 'git_common_test.inputs', 'testGitBatNonExe',
  1123. 'git.bat')
  1124. actual = self.gc._extract_git_path_from_git_bat(git_bat)
  1125. self.assertEqual(actual, git_bat)
  1126. def test_absolute_path(self):
  1127. git_bat = os.path.join(DEPOT_TOOLS_ROOT, 'tests',
  1128. 'git_common_test.inputs',
  1129. 'testGitBatAbsolutePath', 'git.bat')
  1130. actual = self.gc._extract_git_path_from_git_bat(git_bat)
  1131. expected = 'C:\\Absolute\\Path\\To\\Git\\cmd\\git.exe'
  1132. self.assertEqual(actual, expected)
  1133. def test_relative_path(self):
  1134. git_bat = os.path.join(DEPOT_TOOLS_ROOT, 'tests',
  1135. 'git_common_test.inputs',
  1136. 'testGitBatRelativePath', 'git.bat')
  1137. actual = self.gc._extract_git_path_from_git_bat(git_bat)
  1138. expected = os.path.join(DEPOT_TOOLS_ROOT, 'tests',
  1139. 'git_common_test.inputs',
  1140. 'testGitBatRelativePath',
  1141. 'Relative\\Path\\To\\Git\\cmd\\git.exe')
  1142. self.assertEqual(actual, expected)
  1143. if __name__ == '__main__':
  1144. sys.exit(
  1145. coverage_utils.covered_main(
  1146. os.path.join(DEPOT_TOOLS_ROOT, 'git_common.py')))