git_common_test.py 40 KB

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