git_common_test.py 34 KB

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