git_common_test.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053
  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. from __future__ import print_function
  8. from __future__ import unicode_literals
  9. import binascii
  10. import collections
  11. import datetime
  12. import os
  13. import shutil
  14. import signal
  15. import sys
  16. import tempfile
  17. import time
  18. import unittest
  19. DEPOT_TOOLS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  20. sys.path.insert(0, DEPOT_TOOLS_ROOT)
  21. from testing_support import coverage_utils
  22. from testing_support import git_test_utils
  23. GitRepo = git_test_utils.GitRepo
  24. class GitCommonTestBase(unittest.TestCase):
  25. @classmethod
  26. def setUpClass(cls):
  27. super(GitCommonTestBase, cls).setUpClass()
  28. import git_common
  29. cls.gc = git_common
  30. cls.gc.TEST_MODE = True
  31. os.environ["GIT_EDITOR"] = ":" # Supress git editor during rebase.
  32. class Support(GitCommonTestBase):
  33. def _testMemoizeOneBody(self, threadsafe):
  34. calls = collections.defaultdict(int)
  35. def double_if_even(val):
  36. calls[val] += 1
  37. return val * 2 if val % 2 == 0 else None
  38. # Use this explicitly as a wrapper fn instead of a decorator. Otherwise
  39. # pylint crashes (!!)
  40. double_if_even = self.gc.memoize_one(threadsafe=threadsafe)(double_if_even)
  41. self.assertEqual(4, double_if_even(2))
  42. self.assertEqual(4, double_if_even(2))
  43. self.assertEqual(None, double_if_even(1))
  44. self.assertEqual(None, double_if_even(1))
  45. self.assertDictEqual({1: 2, 2: 1}, calls)
  46. double_if_even.set(10, 20)
  47. self.assertEqual(20, double_if_even(10))
  48. self.assertDictEqual({1: 2, 2: 1}, calls)
  49. double_if_even.clear()
  50. self.assertEqual(4, double_if_even(2))
  51. self.assertEqual(4, double_if_even(2))
  52. self.assertEqual(None, double_if_even(1))
  53. self.assertEqual(None, double_if_even(1))
  54. self.assertEqual(20, double_if_even(10))
  55. self.assertDictEqual({1: 4, 2: 2, 10: 1}, calls)
  56. def testMemoizeOne(self):
  57. self._testMemoizeOneBody(threadsafe=False)
  58. def testMemoizeOneThreadsafe(self):
  59. self._testMemoizeOneBody(threadsafe=True)
  60. def testOnce(self):
  61. testlist = []
  62. # This works around a bug in pylint
  63. once = self.gc.once
  64. @once
  65. def add_to_list():
  66. testlist.append('dog')
  67. add_to_list()
  68. add_to_list()
  69. add_to_list()
  70. add_to_list()
  71. self.assertEqual(testlist, ['dog'])
  72. def slow_square(i):
  73. """Helper for ScopedPoolTest.
  74. Must be global because non top-level functions aren't pickleable.
  75. """
  76. return i ** 2
  77. class ScopedPoolTest(GitCommonTestBase):
  78. CTRL_C = signal.CTRL_C_EVENT if sys.platform == 'win32' else signal.SIGINT
  79. def testThreads(self):
  80. result = []
  81. with self.gc.ScopedPool(kind='threads') as pool:
  82. result = list(pool.imap(slow_square, range(10)))
  83. self.assertEqual([0, 1, 4, 9, 16, 25, 36, 49, 64, 81], result)
  84. def testThreadsCtrlC(self):
  85. result = []
  86. with self.assertRaises(KeyboardInterrupt):
  87. with self.gc.ScopedPool(kind='threads') as pool:
  88. # Make sure this pool is interrupted in mid-swing
  89. for i in pool.imap(slow_square, range(20)):
  90. if i > 32:
  91. os.kill(os.getpid(), self.CTRL_C)
  92. result.append(i)
  93. self.assertEqual([0, 1, 4, 9, 16, 25], result)
  94. def testProcs(self):
  95. result = []
  96. with self.gc.ScopedPool() as pool:
  97. result = list(pool.imap(slow_square, range(10)))
  98. self.assertEqual([0, 1, 4, 9, 16, 25, 36, 49, 64, 81], result)
  99. def testProcsCtrlC(self):
  100. result = []
  101. with self.assertRaises(KeyboardInterrupt):
  102. with self.gc.ScopedPool() as pool:
  103. # Make sure this pool is interrupted in mid-swing
  104. for i in pool.imap(slow_square, range(20)):
  105. if i > 32:
  106. os.kill(os.getpid(), self.CTRL_C)
  107. result.append(i)
  108. self.assertEqual([0, 1, 4, 9, 16, 25], result)
  109. class ProgressPrinterTest(GitCommonTestBase):
  110. class FakeStream(object):
  111. def __init__(self):
  112. self.data = set()
  113. self.count = 0
  114. def write(self, line):
  115. self.data.add(line)
  116. def flush(self):
  117. self.count += 1
  118. def testBasic(self):
  119. """This test is probably racy, but I don't have a better alternative."""
  120. fmt = '%(count)d/10'
  121. stream = self.FakeStream()
  122. pp = self.gc.ProgressPrinter(fmt, enabled=True, fout=stream, period=0.01)
  123. with pp as inc:
  124. for _ in range(10):
  125. time.sleep(0.02)
  126. inc()
  127. filtered = {x.strip() for x in stream.data}
  128. rslt = {fmt % {'count': i} for i in range(11)}
  129. self.assertSetEqual(filtered, rslt)
  130. self.assertGreaterEqual(stream.count, 10)
  131. class GitReadOnlyFunctionsTest(git_test_utils.GitRepoReadOnlyTestBase,
  132. GitCommonTestBase):
  133. REPO_SCHEMA = """
  134. A B C D
  135. B E D
  136. """
  137. COMMIT_A = {
  138. 'some/files/file1': {'data': b'file1'},
  139. 'some/files/file2': {'data': b'file2'},
  140. 'some/files/file3': {'data': b'file3'},
  141. 'some/other/file': {'data': b'otherfile'},
  142. }
  143. COMMIT_C = {
  144. 'some/files/file2': {
  145. 'mode': 0o755,
  146. 'data': b'file2 - vanilla\n'},
  147. }
  148. COMMIT_E = {
  149. 'some/files/file2': {'data': b'file2 - merged\n'},
  150. }
  151. COMMIT_D = {
  152. 'some/files/file2': {'data': b'file2 - vanilla\nfile2 - merged\n'},
  153. }
  154. def testHashes(self):
  155. ret = self.repo.run(
  156. self.gc.hash_multi, *[
  157. 'main',
  158. 'main~3',
  159. self.repo['E']+'~',
  160. self.repo['D']+'^2',
  161. 'tag_C^{}',
  162. ]
  163. )
  164. self.assertEqual([
  165. self.repo['D'],
  166. self.repo['A'],
  167. self.repo['B'],
  168. self.repo['E'],
  169. self.repo['C'],
  170. ], ret)
  171. self.assertEqual(
  172. self.repo.run(self.gc.hash_one, 'branch_D'),
  173. self.repo['D']
  174. )
  175. self.assertTrue(self.repo['D'].startswith(
  176. self.repo.run(self.gc.hash_one, 'branch_D', short=True)))
  177. def testStream(self):
  178. items = set(self.repo.commit_map.values())
  179. def testfn():
  180. for line in self.gc.run_stream('log', '--format=%H').readlines():
  181. line = line.strip().decode('utf-8')
  182. self.assertIn(line, items)
  183. items.remove(line)
  184. self.repo.run(testfn)
  185. def testStreamWithRetcode(self):
  186. items = set(self.repo.commit_map.values())
  187. def testfn():
  188. with self.gc.run_stream_with_retcode('log', '--format=%H') as stdout:
  189. for line in stdout.readlines():
  190. line = line.strip().decode('utf-8')
  191. self.assertIn(line, items)
  192. items.remove(line)
  193. self.repo.run(testfn)
  194. def testStreamWithRetcodeException(self):
  195. import subprocess2
  196. with self.assertRaises(subprocess2.CalledProcessError):
  197. with self.gc.run_stream_with_retcode('checkout', 'unknown-branch'):
  198. pass
  199. def testCurrentBranch(self):
  200. def cur_branch_out_of_git():
  201. os.chdir('..')
  202. return self.gc.current_branch()
  203. self.assertIsNone(self.repo.run(cur_branch_out_of_git))
  204. self.repo.git('checkout', 'branch_D')
  205. self.assertEqual(self.repo.run(self.gc.current_branch), 'branch_D')
  206. def testBranches(self):
  207. # This check fails with git 2.4 (see crbug.com/487172)
  208. self.assertEqual(self.repo.run(set, self.gc.branches()),
  209. {'main', 'branch_D', 'root_A'})
  210. def testDiff(self):
  211. # Get the names of the blobs being compared (to avoid hard-coding).
  212. c_blob_short = self.repo.git('rev-parse', '--short',
  213. 'tag_C:some/files/file2').stdout.strip()
  214. d_blob_short = self.repo.git('rev-parse', '--short',
  215. 'tag_D:some/files/file2').stdout.strip()
  216. expected_output = [
  217. 'diff --git a/some/files/file2 b/some/files/file2',
  218. 'index %s..%s 100755' % (c_blob_short, d_blob_short),
  219. '--- a/some/files/file2',
  220. '+++ b/some/files/file2',
  221. '@@ -1 +1,2 @@',
  222. ' file2 - vanilla',
  223. '+file2 - merged']
  224. self.assertEqual(expected_output,
  225. self.repo.run(self.gc.diff, 'tag_C', 'tag_D').split('\n'))
  226. def testDormant(self):
  227. self.assertFalse(self.repo.run(self.gc.is_dormant, 'main'))
  228. self.repo.git('config', 'branch.main.dormant', 'true')
  229. self.assertTrue(self.repo.run(self.gc.is_dormant, 'main'))
  230. def testBlame(self):
  231. def get_porcelain_for_commit(commit_name, lines):
  232. format_string = ('%H {}\nauthor %an\nauthor-mail <%ae>\nauthor-time %at\n'
  233. 'author-tz +0000\ncommitter %cn\ncommitter-mail <%ce>\n'
  234. 'committer-time %ct\ncommitter-tz +0000\nsummary {}')
  235. format_string = format_string.format(lines, commit_name)
  236. info = self.repo.show_commit(commit_name, format_string=format_string)
  237. return info.split('\n')
  238. # Expect to blame line 1 on C, line 2 on E.
  239. ABBREV_LEN = 7
  240. c_short = self.repo['C'][:1 + ABBREV_LEN]
  241. c_author = self.repo.show_commit('C', format_string='%an %ai')
  242. e_short = self.repo['E'][:1 + ABBREV_LEN]
  243. e_author = self.repo.show_commit('E', format_string='%an %ai')
  244. expected_output = ['%s (%s 1) file2 - vanilla' % (c_short, c_author),
  245. '%s (%s 2) file2 - merged' % (e_short, e_author)]
  246. self.assertEqual(expected_output,
  247. self.repo.run(self.gc.blame, 'some/files/file2',
  248. 'tag_D', abbrev=ABBREV_LEN).split('\n'))
  249. # Test porcelain.
  250. expected_output = []
  251. expected_output.extend(get_porcelain_for_commit('C', '1 1 1'))
  252. expected_output.append('previous %s some/files/file2' % self.repo['B'])
  253. expected_output.append('filename some/files/file2')
  254. expected_output.append('\tfile2 - vanilla')
  255. expected_output.extend(get_porcelain_for_commit('E', '1 2 1'))
  256. expected_output.append('previous %s some/files/file2' % self.repo['B'])
  257. expected_output.append('filename some/files/file2')
  258. expected_output.append('\tfile2 - merged')
  259. self.assertEqual(expected_output,
  260. self.repo.run(self.gc.blame, 'some/files/file2',
  261. 'tag_D', porcelain=True).split('\n'))
  262. def testParseCommitrefs(self):
  263. ret = self.repo.run(
  264. self.gc.parse_commitrefs, *[
  265. 'main',
  266. 'main~3',
  267. self.repo['E']+'~',
  268. self.repo['D']+'^2',
  269. 'tag_C^{}',
  270. ]
  271. )
  272. hashes = [
  273. self.repo['D'],
  274. self.repo['A'],
  275. self.repo['B'],
  276. self.repo['E'],
  277. self.repo['C'],
  278. ]
  279. self.assertEqual(ret, [binascii.unhexlify(h) for h in hashes])
  280. expected_re = r"one of \(u?'main', u?'bananas'\)"
  281. with self.assertRaisesRegexp(Exception, expected_re):
  282. self.repo.run(self.gc.parse_commitrefs, 'main', 'bananas')
  283. def testRepoRoot(self):
  284. def cd_and_repo_root(path):
  285. os.chdir(path)
  286. return self.gc.repo_root()
  287. self.assertEqual(self.repo.repo_path, self.repo.run(self.gc.repo_root))
  288. # cd to a subdirectory; repo_root should still return the root dir.
  289. self.assertEqual(self.repo.repo_path,
  290. self.repo.run(cd_and_repo_root, 'some/files'))
  291. def testTags(self):
  292. self.assertEqual(set(self.repo.run(self.gc.tags)),
  293. {'tag_'+l for l in 'ABCDE'})
  294. def testTree(self):
  295. tree = self.repo.run(self.gc.tree, 'main:some/files')
  296. file1 = self.COMMIT_A['some/files/file1']['data']
  297. file2 = self.COMMIT_D['some/files/file2']['data']
  298. file3 = self.COMMIT_A['some/files/file3']['data']
  299. self.assertEqual(
  300. tree['file1'],
  301. ('100644', 'blob', git_test_utils.git_hash_data(file1)))
  302. self.assertEqual(
  303. tree['file2'],
  304. ('100755', 'blob', git_test_utils.git_hash_data(file2)))
  305. self.assertEqual(
  306. tree['file3'],
  307. ('100644', 'blob', git_test_utils.git_hash_data(file3)))
  308. tree = self.repo.run(self.gc.tree, 'main:some')
  309. self.assertEqual(len(tree), 2)
  310. # Don't check the tree hash because we're lazy :)
  311. self.assertEqual(tree['files'][:2], ('040000', 'tree'))
  312. tree = self.repo.run(self.gc.tree, 'main:wat')
  313. self.assertEqual(tree, None)
  314. def testTreeRecursive(self):
  315. tree = self.repo.run(self.gc.tree, 'main:some', recurse=True)
  316. file1 = self.COMMIT_A['some/files/file1']['data']
  317. file2 = self.COMMIT_D['some/files/file2']['data']
  318. file3 = self.COMMIT_A['some/files/file3']['data']
  319. other = self.COMMIT_A['some/other/file']['data']
  320. self.assertEqual(
  321. tree['files/file1'],
  322. ('100644', 'blob', git_test_utils.git_hash_data(file1)))
  323. self.assertEqual(
  324. tree['files/file2'],
  325. ('100755', 'blob', git_test_utils.git_hash_data(file2)))
  326. self.assertEqual(
  327. tree['files/file3'],
  328. ('100644', 'blob', git_test_utils.git_hash_data(file3)))
  329. self.assertEqual(
  330. tree['other/file'],
  331. ('100644', 'blob', git_test_utils.git_hash_data(other)))
  332. class GitMutableFunctionsTest(git_test_utils.GitRepoReadWriteTestBase,
  333. GitCommonTestBase):
  334. REPO_SCHEMA = ''
  335. def _intern_data(self, data):
  336. with tempfile.TemporaryFile('wb') as f:
  337. f.write(data.encode('utf-8'))
  338. f.seek(0)
  339. return self.repo.run(self.gc.intern_f, f)
  340. def testInternF(self):
  341. data = 'CoolBobcatsBro'
  342. data_hash = self._intern_data(data)
  343. self.assertEqual(git_test_utils.git_hash_data(data.encode()), data_hash)
  344. self.assertEqual(data, self.repo.git('cat-file', 'blob', data_hash).stdout)
  345. def testMkTree(self):
  346. tree = {}
  347. for i in 1, 2, 3:
  348. name = '✔ file%d' % i
  349. tree[name] = ('100644', 'blob', self._intern_data(name))
  350. tree_hash = self.repo.run(self.gc.mktree, tree)
  351. self.assertEqual('b524c02ba0e1cf482f8eb08c3d63e97b8895c89c', tree_hash)
  352. def testConfig(self):
  353. self.repo.git('config', '--add', 'happy.derpies', 'food')
  354. self.assertEqual(self.repo.run(self.gc.get_config_list, 'happy.derpies'),
  355. ['food'])
  356. self.assertEqual(self.repo.run(self.gc.get_config_list, 'sad.derpies'), [])
  357. self.repo.git('config', '--add', 'happy.derpies', 'cat')
  358. self.assertEqual(self.repo.run(self.gc.get_config_list, 'happy.derpies'),
  359. ['food', 'cat'])
  360. self.assertEqual(
  361. 'cat', self.repo.run(self.gc.get_config, 'dude.bob', 'cat'))
  362. self.repo.run(self.gc.set_config, 'dude.bob', 'dog')
  363. self.assertEqual(
  364. 'dog', self.repo.run(self.gc.get_config, 'dude.bob', 'cat'))
  365. self.repo.run(self.gc.del_config, 'dude.bob')
  366. # This should work without raising an exception
  367. self.repo.run(self.gc.del_config, 'dude.bob')
  368. self.assertEqual(
  369. 'cat', self.repo.run(self.gc.get_config, 'dude.bob', 'cat'))
  370. self.assertEqual('origin/main', self.repo.run(self.gc.root))
  371. self.repo.git('config', 'depot-tools.upstream', 'catfood')
  372. self.assertEqual('catfood', self.repo.run(self.gc.root))
  373. def testRoot(self):
  374. origin_schema = git_test_utils.GitRepoSchema("""
  375. A B C
  376. B D
  377. """, self.getRepoContent)
  378. origin = origin_schema.reify()
  379. # Set the default branch to branch_D instead of main.
  380. origin.git('checkout', 'branch_D')
  381. self.repo.git('remote', 'add', 'origin', origin.repo_path)
  382. self.repo.git('fetch', 'origin')
  383. self.repo.git('remote', 'set-head', 'origin', '-a')
  384. self.assertEqual('origin/branch_D', self.repo.run(self.gc.root))
  385. def testUpstream(self):
  386. self.repo.git('commit', '--allow-empty', '-am', 'foooooo')
  387. self.assertEqual(self.repo.run(self.gc.upstream, 'bobly'), None)
  388. self.assertEqual(self.repo.run(self.gc.upstream, 'main'), None)
  389. self.repo.git('checkout', '-t', '-b', 'happybranch', 'main')
  390. self.assertEqual(self.repo.run(self.gc.upstream, 'happybranch'),
  391. 'main')
  392. def testNormalizedVersion(self):
  393. self.assertTrue(all(
  394. isinstance(x, int) for x in self.repo.run(self.gc.get_git_version)))
  395. def testGetBranchesInfo(self):
  396. self.repo.git('commit', '--allow-empty', '-am', 'foooooo')
  397. self.repo.git('checkout', '-t', '-b', 'happybranch', 'main')
  398. self.repo.git('commit', '--allow-empty', '-am', 'foooooo')
  399. self.repo.git('checkout', '-t', '-b', 'child', 'happybranch')
  400. self.repo.git('checkout', '-t', '-b', 'to_delete', 'main')
  401. self.repo.git('checkout', '-t', '-b', 'parent_gone', 'to_delete')
  402. self.repo.git('branch', '-D', 'to_delete')
  403. supports_track = (
  404. self.repo.run(self.gc.get_git_version)
  405. >= self.gc.MIN_UPSTREAM_TRACK_GIT_VERSION)
  406. actual = self.repo.run(self.gc.get_branches_info, supports_track)
  407. expected = {
  408. 'happybranch': (
  409. self.repo.run(self.gc.hash_one, 'happybranch', short=True),
  410. 'main',
  411. 1 if supports_track else None,
  412. None
  413. ),
  414. 'child': (
  415. self.repo.run(self.gc.hash_one, 'child', short=True),
  416. 'happybranch',
  417. None,
  418. None
  419. ),
  420. 'main': (
  421. self.repo.run(self.gc.hash_one, 'main', short=True),
  422. '',
  423. None,
  424. None
  425. ),
  426. '': None,
  427. 'parent_gone': (
  428. self.repo.run(self.gc.hash_one, 'parent_gone', short=True),
  429. 'to_delete',
  430. None,
  431. None
  432. ),
  433. 'to_delete': None
  434. }
  435. self.assertEqual(expected, actual)
  436. def testGetBranchesInfoWithReset(self):
  437. self.repo.git('commit', '--allow-empty', '-am', 'foooooo')
  438. self.repo.git('checkout','-t', '-b', 'foobarA', 'main')
  439. self.repo.git('config', 'branch.foobarA.base',
  440. self.repo.run(self.gc.hash_one, 'main'))
  441. self.repo.git('config', 'branch.foobarA.base-upstream', 'main')
  442. with self.repo.open('foobar1', 'w') as f:
  443. f.write('hello')
  444. self.repo.git('add', 'foobar1')
  445. self.repo.git_commit('commit1')
  446. with self.repo.open('foobar2', 'w') as f:
  447. f.write('goodbye')
  448. self.repo.git('add', 'foobar2')
  449. self.repo.git_commit('commit2')
  450. self.repo.git('checkout','-t', '-b', 'foobarB', 'foobarA')
  451. self.repo.git('config', 'branch.foobarB.base',
  452. self.repo.run(self.gc.hash_one, 'foobarA'))
  453. self.repo.git('config', 'branch.foobarB.base-upstream', 'foobarA')
  454. self.repo.git('checkout', 'foobarA')
  455. self.repo.git('reset', '--hard', 'HEAD~')
  456. with self.repo.open('foobar', 'w') as f:
  457. f.write('world')
  458. self.repo.git('add', 'foobar')
  459. self.repo.git_commit('commit1.2')
  460. actual = self.repo.run(self.gc.get_branches_info, True)
  461. expected = {
  462. 'foobarA': (
  463. self.repo.run(self.gc.hash_one, 'foobarA', short=True),
  464. 'main',
  465. 2,
  466. None
  467. ),
  468. 'foobarB': (
  469. self.repo.run(self.gc.hash_one, 'foobarB', short=True),
  470. 'foobarA',
  471. None,
  472. 1
  473. ),
  474. 'main': (
  475. self.repo.run(self.gc.hash_one, 'main', short=True),
  476. '',
  477. None,
  478. None
  479. ),
  480. '': None
  481. }
  482. self.assertEqual(expected, actual)
  483. class GitMutableStructuredTest(git_test_utils.GitRepoReadWriteTestBase,
  484. GitCommonTestBase):
  485. REPO_SCHEMA = """
  486. A B C D E F G
  487. B H I J K
  488. J L
  489. X Y Z
  490. CAT DOG
  491. """
  492. COMMIT_B = {'file': {'data': b'B'}}
  493. COMMIT_H = {'file': {'data': b'H'}}
  494. COMMIT_I = {'file': {'data': b'I'}}
  495. COMMIT_J = {'file': {'data': b'J'}}
  496. COMMIT_K = {'file': {'data': b'K'}}
  497. COMMIT_L = {'file': {'data': b'L'}}
  498. def setUp(self):
  499. super(GitMutableStructuredTest, self).setUp()
  500. self.repo.git('branch', '--set-upstream-to', 'root_X', 'branch_Z')
  501. self.repo.git('branch', '--set-upstream-to', 'branch_G', 'branch_K')
  502. self.repo.git('branch', '--set-upstream-to', 'branch_K', 'branch_L')
  503. self.repo.git('branch', '--set-upstream-to', 'root_A', 'branch_G')
  504. self.repo.git('branch', '--set-upstream-to', 'root_X', 'root_A')
  505. def testTooManyBranches(self):
  506. for i in range(30):
  507. self.repo.git('branch', 'a'*i)
  508. _, rslt = self.repo.capture_stdio(list, self.gc.branches())
  509. self.assertIn('too many branches (39/20)', rslt)
  510. self.repo.git('config', 'depot-tools.branch-limit', 'cat')
  511. _, rslt = self.repo.capture_stdio(list, self.gc.branches())
  512. self.assertIn('too many branches (39/20)', rslt)
  513. self.repo.git('config', 'depot-tools.branch-limit', '100')
  514. # should not raise
  515. # This check fails with git 2.4 (see crbug.com/487172)
  516. self.assertEqual(38, len(self.repo.run(list, self.gc.branches())))
  517. def testMergeBase(self):
  518. self.repo.git('checkout', 'branch_K')
  519. self.assertEqual(
  520. self.repo['B'],
  521. self.repo.run(self.gc.get_or_create_merge_base, 'branch_K', 'branch_G')
  522. )
  523. self.assertEqual(
  524. self.repo['J'],
  525. self.repo.run(self.gc.get_or_create_merge_base, 'branch_L', 'branch_K')
  526. )
  527. self.assertEqual(
  528. self.repo['B'], self.repo.run(self.gc.get_config, 'branch.branch_K.base')
  529. )
  530. self.assertEqual(
  531. 'branch_G', self.repo.run(self.gc.get_config,
  532. 'branch.branch_K.base-upstream')
  533. )
  534. # deadbeef is a bad hash, so this will result in repo['B']
  535. self.repo.run(self.gc.manual_merge_base, 'branch_K', 'deadbeef', 'branch_G')
  536. self.assertEqual(
  537. self.repo['B'],
  538. self.repo.run(self.gc.get_or_create_merge_base, 'branch_K', 'branch_G')
  539. )
  540. # but if we pick a real ancestor, then it'll work
  541. self.repo.run(self.gc.manual_merge_base, 'branch_K', self.repo['I'],
  542. 'branch_G')
  543. self.assertEqual(
  544. self.repo['I'],
  545. self.repo.run(self.gc.get_or_create_merge_base, 'branch_K', 'branch_G')
  546. )
  547. self.assertEqual({'branch_K': self.repo['I'], 'branch_L': self.repo['J']},
  548. self.repo.run(self.gc.branch_config_map, 'base'))
  549. self.repo.run(self.gc.remove_merge_base, 'branch_K')
  550. self.repo.run(self.gc.remove_merge_base, 'branch_L')
  551. self.assertEqual(
  552. None, self.repo.run(self.gc.get_config, 'branch.branch_K.base'))
  553. self.assertEqual({}, self.repo.run(self.gc.branch_config_map, 'base'))
  554. # if it's too old, then it caps at merge-base
  555. self.repo.run(self.gc.manual_merge_base, 'branch_K', self.repo['A'],
  556. 'branch_G')
  557. self.assertEqual(
  558. self.repo['B'],
  559. self.repo.run(self.gc.get_or_create_merge_base, 'branch_K', 'branch_G')
  560. )
  561. # If the user does --set-upstream-to something else, then we discard the
  562. # base and recompute it.
  563. self.repo.run(self.gc.run, 'branch', '-u', 'root_A')
  564. self.assertEqual(
  565. self.repo['A'],
  566. self.repo.run(self.gc.get_or_create_merge_base, 'branch_K')
  567. )
  568. self.assertIsNone(
  569. self.repo.run(self.gc.get_or_create_merge_base, 'branch_DOG'))
  570. def testGetBranchTree(self):
  571. skipped, tree = self.repo.run(self.gc.get_branch_tree)
  572. # This check fails with git 2.4 (see crbug.com/487172)
  573. self.assertEqual(skipped, {'main', 'root_X', 'branch_DOG', 'root_CAT'})
  574. self.assertEqual(tree, {
  575. 'branch_G': 'root_A',
  576. 'root_A': 'root_X',
  577. 'branch_K': 'branch_G',
  578. 'branch_L': 'branch_K',
  579. 'branch_Z': 'root_X'
  580. })
  581. topdown = list(self.gc.topo_iter(tree))
  582. bottomup = list(self.gc.topo_iter(tree, top_down=False))
  583. self.assertEqual(topdown, [
  584. ('branch_Z', 'root_X'),
  585. ('root_A', 'root_X'),
  586. ('branch_G', 'root_A'),
  587. ('branch_K', 'branch_G'),
  588. ('branch_L', 'branch_K'),
  589. ])
  590. self.assertEqual(bottomup, [
  591. ('branch_L', 'branch_K'),
  592. ('branch_Z', 'root_X'),
  593. ('branch_K', 'branch_G'),
  594. ('branch_G', 'root_A'),
  595. ('root_A', 'root_X'),
  596. ])
  597. def testIsGitTreeDirty(self):
  598. retval = []
  599. self.repo.capture_stdio(
  600. lambda: retval.append(self.repo.run(self.gc.is_dirty_git_tree, 'foo')))
  601. self.assertEqual(False, retval[0])
  602. self.repo.open('test.file', 'w').write('test data')
  603. self.repo.git('add', 'test.file')
  604. retval = []
  605. self.repo.capture_stdio(
  606. lambda: retval.append(self.repo.run(self.gc.is_dirty_git_tree, 'foo')))
  607. self.assertEqual(True, retval[0])
  608. def testSquashBranch(self):
  609. self.repo.git('checkout', 'branch_K')
  610. self.assertEqual(
  611. True, self.repo.run(self.gc.squash_current_branch, '✔ cool message'))
  612. lines = ['✔ cool message', '']
  613. for l in 'HIJK':
  614. lines.extend((self.repo[l], l, ''))
  615. lines.pop()
  616. msg = '\n'.join(lines)
  617. self.assertEqual(self.repo.run(self.gc.run, 'log', '-n1', '--format=%B'),
  618. msg)
  619. self.assertEqual(
  620. self.repo.git('cat-file', 'blob', 'branch_K:file').stdout,
  621. 'K'
  622. )
  623. def testSquashBranchDefaultMessage(self):
  624. self.repo.git('checkout', 'branch_K')
  625. self.assertEqual(True, self.repo.run(self.gc.squash_current_branch))
  626. self.assertEqual(self.repo.run(self.gc.run, 'log', '-n1', '--format=%s'),
  627. 'git squash commit for branch_K.')
  628. def testSquashBranchEmpty(self):
  629. self.repo.git('checkout', 'branch_K')
  630. self.repo.git('checkout', 'branch_G', '.')
  631. self.repo.git('commit', '-m', 'revert all changes no branch')
  632. # Should return False since the quash would result in an empty commit
  633. stdout = self.repo.capture_stdio(self.gc.squash_current_branch)[0]
  634. self.assertEqual(stdout, 'Nothing to commit; squashed branch is empty\n')
  635. def testRebase(self):
  636. self.assertSchema("""
  637. A B C D E F G
  638. B H I J K
  639. J L
  640. X Y Z
  641. CAT DOG
  642. """)
  643. rslt = self.repo.run(
  644. self.gc.rebase, 'branch_G', 'branch_K~4', 'branch_K')
  645. self.assertTrue(rslt.success)
  646. self.assertSchema("""
  647. A B C D E F G H I J K
  648. B H I J L
  649. X Y Z
  650. CAT DOG
  651. """)
  652. rslt = self.repo.run(
  653. self.gc.rebase, 'branch_K', 'branch_L~1', 'branch_L', abort=True)
  654. self.assertFalse(rslt.success)
  655. self.assertFalse(self.repo.run(self.gc.in_rebase))
  656. rslt = self.repo.run(
  657. self.gc.rebase, 'branch_K', 'branch_L~1', 'branch_L', abort=False)
  658. self.assertFalse(rslt.success)
  659. self.assertTrue(self.repo.run(self.gc.in_rebase))
  660. self.assertEqual(self.repo.git('status', '--porcelain').stdout, 'UU file\n')
  661. self.repo.git('checkout', '--theirs', 'file')
  662. self.repo.git('add', 'file')
  663. self.repo.git('rebase', '--continue')
  664. self.assertSchema("""
  665. A B C D E F G H I J K L
  666. X Y Z
  667. CAT DOG
  668. """)
  669. def testStatus(self):
  670. def inner():
  671. dictified_status = lambda: {
  672. k: dict(v._asdict()) # pylint: disable=protected-access
  673. for k, v in self.repo.run(self.gc.status)
  674. }
  675. self.repo.git('mv', 'file', 'cat')
  676. with open('COOL', 'w') as f:
  677. f.write('Super cool file!')
  678. self.assertDictEqual(
  679. dictified_status(),
  680. {'cat': {'lstat': 'R', 'rstat': ' ', 'src': 'file'},
  681. 'COOL': {'lstat': '?', 'rstat': '?', 'src': 'COOL'}}
  682. )
  683. self.repo.run(inner)
  684. class GitFreezeThaw(git_test_utils.GitRepoReadWriteTestBase):
  685. @classmethod
  686. def setUpClass(cls):
  687. super(GitFreezeThaw, cls).setUpClass()
  688. import git_common
  689. cls.gc = git_common
  690. cls.gc.TEST_MODE = True
  691. REPO_SCHEMA = """
  692. A B C D
  693. B E D
  694. """
  695. COMMIT_A = {
  696. 'some/files/file1': {'data': b'file1'},
  697. 'some/files/file2': {'data': b'file2'},
  698. 'some/files/file3': {'data': b'file3'},
  699. 'some/other/file': {'data': b'otherfile'},
  700. }
  701. COMMIT_C = {
  702. 'some/files/file2': {
  703. 'mode': 0o755,
  704. 'data': b'file2 - vanilla'},
  705. }
  706. COMMIT_E = {
  707. 'some/files/file2': {'data': b'file2 - merged'},
  708. }
  709. COMMIT_D = {
  710. 'some/files/file2': {'data': b'file2 - vanilla\nfile2 - merged'},
  711. }
  712. def testNothing(self):
  713. self.assertIsNotNone(self.repo.run(self.gc.thaw)) # 'Nothing to thaw'
  714. self.assertIsNotNone(self.repo.run(self.gc.freeze)) # 'Nothing to freeze'
  715. def testAll(self):
  716. def inner():
  717. with open('some/files/file2', 'a') as f2:
  718. print('cool appended line', file=f2)
  719. os.mkdir('some/other_files')
  720. with open('some/other_files/subdir_file', 'w') as f3:
  721. print('new file!', file=f3)
  722. with open('some/files/file5', 'w') as f5:
  723. print('New file!1!one!', file=f5)
  724. STATUS_1 = '\n'.join((
  725. ' M some/files/file2',
  726. 'A some/files/file5',
  727. '?? some/other_files/'
  728. )) + '\n'
  729. self.repo.git('add', 'some/files/file5')
  730. # Freeze group 1
  731. self.assertEqual(self.repo.git('status', '--porcelain').stdout, STATUS_1)
  732. self.assertIsNone(self.gc.freeze())
  733. self.assertEqual(self.repo.git('status', '--porcelain').stdout, '')
  734. # Freeze group 2
  735. with open('some/files/file2', 'a') as f2:
  736. print('new! appended line!', file=f2)
  737. self.assertEqual(self.repo.git('status', '--porcelain').stdout,
  738. ' M some/files/file2\n')
  739. self.assertIsNone(self.gc.freeze())
  740. self.assertEqual(self.repo.git('status', '--porcelain').stdout, '')
  741. # Thaw it out!
  742. self.assertIsNone(self.gc.thaw())
  743. self.assertIsNotNone(self.gc.thaw()) # One thaw should thaw everything
  744. self.assertEqual(self.repo.git('status', '--porcelain').stdout, STATUS_1)
  745. self.repo.run(inner)
  746. def testTooBig(self):
  747. def inner():
  748. self.repo.git('config', 'depot-tools.freeze-size-limit', '1')
  749. with open('bigfile', 'w') as f:
  750. chunk = 'NERDFACE' * 1024
  751. for _ in range(128 * 2 + 1): # Just over 2 mb
  752. f.write(chunk)
  753. _, err = self.repo.capture_stdio(self.gc.freeze)
  754. self.assertIn('too much untracked+unignored', err)
  755. self.repo.run(inner)
  756. def testTooBigMultipleFiles(self):
  757. def inner():
  758. self.repo.git('config', 'depot-tools.freeze-size-limit', '1')
  759. for i in range(3):
  760. with open('file%d' % i, 'w') as f:
  761. chunk = 'NERDFACE' * 1024
  762. for _ in range(50): # About 400k
  763. f.write(chunk)
  764. _, err = self.repo.capture_stdio(self.gc.freeze)
  765. self.assertIn('too much untracked+unignored', err)
  766. self.repo.run(inner)
  767. def testMerge(self):
  768. def inner():
  769. self.repo.git('checkout', '-b', 'bad_merge_branch')
  770. with open('bad_merge', 'w') as f:
  771. f.write('bad_merge_left')
  772. self.repo.git('add', 'bad_merge')
  773. self.repo.git('commit', '-m', 'bad_merge')
  774. self.repo.git('checkout', 'branch_D')
  775. with open('bad_merge', 'w') as f:
  776. f.write('bad_merge_right')
  777. self.repo.git('add', 'bad_merge')
  778. self.repo.git('commit', '-m', 'bad_merge_d')
  779. self.repo.git('merge', 'bad_merge_branch')
  780. _, err = self.repo.capture_stdio(self.gc.freeze)
  781. self.assertIn('Cannot freeze unmerged changes', err)
  782. self.repo.run(inner)
  783. def testAddError(self):
  784. def inner():
  785. self.repo.git('checkout', '-b', 'unreadable_file_branch')
  786. with open('bad_file', 'w') as f:
  787. f.write('some text')
  788. os.chmod('bad_file', 0o0111)
  789. ret = self.repo.run(self.gc.freeze)
  790. self.assertIn('Failed to index some unindexed files.', ret)
  791. self.repo.run(inner)
  792. class GitMakeWorkdir(git_test_utils.GitRepoReadOnlyTestBase, GitCommonTestBase):
  793. def setUp(self):
  794. self._tempdir = tempfile.mkdtemp()
  795. def tearDown(self):
  796. shutil.rmtree(self._tempdir)
  797. REPO_SCHEMA = """
  798. A
  799. """
  800. @unittest.skipIf(not hasattr(os, 'symlink'), "OS doesn't support symlink")
  801. def testMakeWorkdir(self):
  802. workdir = os.path.join(self._tempdir, 'workdir')
  803. self.gc.make_workdir(os.path.join(self.repo.repo_path, '.git'),
  804. os.path.join(workdir, '.git'))
  805. EXPECTED_LINKS = [
  806. 'config', 'info', 'hooks', 'logs/refs', 'objects', 'refs',
  807. ]
  808. for path in EXPECTED_LINKS:
  809. self.assertTrue(os.path.islink(os.path.join(workdir, '.git', path)))
  810. self.assertEqual(os.path.realpath(os.path.join(workdir, '.git', path)),
  811. os.path.join(self.repo.repo_path, '.git', path))
  812. self.assertFalse(os.path.islink(os.path.join(workdir, '.git', 'HEAD')))
  813. class GitTestUtilsTest(git_test_utils.GitRepoReadOnlyTestBase):
  814. REPO_SCHEMA = """
  815. A B C
  816. """
  817. COMMIT_A = {
  818. 'file1': {'data': b'file1'},
  819. }
  820. COMMIT_B = {
  821. 'file1': {'data': b'file1 changed'},
  822. }
  823. # Test special keys (custom commit data).
  824. COMMIT_C = {
  825. GitRepo.AUTHOR_NAME: 'Custom Author',
  826. GitRepo.AUTHOR_EMAIL: 'author@example.com',
  827. GitRepo.AUTHOR_DATE: datetime.datetime(1980, 9, 8, 7, 6, 5,
  828. tzinfo=git_test_utils.UTC),
  829. GitRepo.COMMITTER_NAME: 'Custom Committer',
  830. GitRepo.COMMITTER_EMAIL: 'committer@example.com',
  831. GitRepo.COMMITTER_DATE: datetime.datetime(1990, 4, 5, 6, 7, 8,
  832. tzinfo=git_test_utils.UTC),
  833. 'file1': {'data': b'file1 changed again'},
  834. }
  835. def testAutomaticCommitDates(self):
  836. # The dates should start from 1970-01-01 and automatically increment. They
  837. # must be in UTC (otherwise the tests are system-dependent, and if your
  838. # local timezone is positive, timestamps will be <0 which causes bizarre
  839. # behaviour in Git; http://crbug.com/581895).
  840. self.assertEqual('Author McAuthorly 1970-01-01 00:00:00 +0000',
  841. self.repo.show_commit('A', format_string='%an %ai'))
  842. self.assertEqual('Charles Committish 1970-01-02 00:00:00 +0000',
  843. self.repo.show_commit('A', format_string='%cn %ci'))
  844. self.assertEqual('Author McAuthorly 1970-01-03 00:00:00 +0000',
  845. self.repo.show_commit('B', format_string='%an %ai'))
  846. self.assertEqual('Charles Committish 1970-01-04 00:00:00 +0000',
  847. self.repo.show_commit('B', format_string='%cn %ci'))
  848. def testCustomCommitData(self):
  849. self.assertEqual('Custom Author author@example.com '
  850. '1980-09-08 07:06:05 +0000',
  851. self.repo.show_commit('C', format_string='%an %ae %ai'))
  852. self.assertEqual('Custom Committer committer@example.com '
  853. '1990-04-05 06:07:08 +0000',
  854. self.repo.show_commit('C', format_string='%cn %ce %ci'))
  855. if __name__ == '__main__':
  856. sys.exit(coverage_utils.covered_main(
  857. os.path.join(DEPOT_TOOLS_ROOT, 'git_common.py')))