You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

939 lines
41KB

  1. # -*- coding: utf-8 -*-
  2. # This module is part of GitPython and is released under
  3. # the BSD License: http://www.opensource.org/licenses/bsd-license.php
  4. import os
  5. import shutil
  6. import sys
  7. from unittest import skipIf
  8. import git
  9. from git.cmd import Git
  10. from git.compat import string_types, is_win
  11. from git.exc import (
  12. InvalidGitRepositoryError,
  13. RepositoryDirtyError
  14. )
  15. from git.objects.submodule.base import Submodule
  16. from git.objects.submodule.root import RootModule, RootUpdateProgress
  17. from git.repo.fun import (
  18. find_submodule_git_dir,
  19. touch
  20. )
  21. from git.test.lib import (
  22. TestBase,
  23. with_rw_repo
  24. )
  25. from git.test.lib import with_rw_directory
  26. from git.util import HIDE_WINDOWS_KNOWN_ERRORS
  27. from git.util import to_native_path_linux, join_path_native
  28. import os.path as osp
  29. class TestRootProgress(RootUpdateProgress):
  30. """Just prints messages, for now without checking the correctness of the states"""
  31. def update(self, op, cur_count, max_count, message=''):
  32. print(op, cur_count, max_count, message)
  33. prog = TestRootProgress()
  34. class TestSubmodule(TestBase):
  35. def tearDown(self):
  36. import gc
  37. gc.collect()
  38. k_subm_current = "c15a6e1923a14bc760851913858a3942a4193cdb"
  39. k_subm_changed = "394ed7006ee5dc8bddfd132b64001d5dfc0ffdd3"
  40. k_no_subm_tag = "0.1.6"
  41. def _do_base_tests(self, rwrepo):
  42. """Perform all tests in the given repository, it may be bare or nonbare"""
  43. # manual instantiation
  44. smm = Submodule(rwrepo, "\0" * 20)
  45. # name needs to be set in advance
  46. self.failUnlessRaises(AttributeError, getattr, smm, 'name')
  47. # iterate - 1 submodule
  48. sms = Submodule.list_items(rwrepo, self.k_subm_current)
  49. assert len(sms) == 1
  50. sm = sms[0]
  51. # at a different time, there is None
  52. assert len(Submodule.list_items(rwrepo, self.k_no_subm_tag)) == 0
  53. assert sm.path == 'git/ext/gitdb'
  54. assert sm.path != sm.name # in our case, we have ids there, which don't equal the path
  55. assert sm.url.endswith('github.com/gitpython-developers/gitdb.git')
  56. assert sm.branch_path == 'refs/heads/master' # the default ...
  57. assert sm.branch_name == 'master'
  58. assert sm.parent_commit == rwrepo.head.commit
  59. # size is always 0
  60. assert sm.size == 0
  61. # the module is not checked-out yet
  62. self.failUnlessRaises(InvalidGitRepositoryError, sm.module)
  63. # which is why we can't get the branch either - it points into the module() repository
  64. self.failUnlessRaises(InvalidGitRepositoryError, getattr, sm, 'branch')
  65. # branch_path works, as its just a string
  66. assert isinstance(sm.branch_path, string_types)
  67. # some commits earlier we still have a submodule, but its at a different commit
  68. smold = next(Submodule.iter_items(rwrepo, self.k_subm_changed))
  69. assert smold.binsha != sm.binsha
  70. assert smold != sm # the name changed
  71. # force it to reread its information
  72. del(smold._url)
  73. smold.url == sm.url # @NoEffect
  74. # test config_reader/writer methods
  75. sm.config_reader()
  76. new_smclone_path = None # keep custom paths for later
  77. new_csmclone_path = None #
  78. if rwrepo.bare:
  79. with self.assertRaises(InvalidGitRepositoryError):
  80. with sm.config_writer() as cw:
  81. pass
  82. else:
  83. with sm.config_writer() as writer:
  84. # for faster checkout, set the url to the local path
  85. new_smclone_path = Git.polish_url(osp.join(self.rorepo.working_tree_dir, sm.path))
  86. writer.set_value('url', new_smclone_path)
  87. writer.release()
  88. assert sm.config_reader().get_value('url') == new_smclone_path
  89. assert sm.url == new_smclone_path
  90. # END handle bare repo
  91. smold.config_reader()
  92. # cannot get a writer on historical submodules
  93. if not rwrepo.bare:
  94. with self.assertRaises(ValueError):
  95. with smold.config_writer():
  96. pass
  97. # END handle bare repo
  98. # make the old into a new - this doesn't work as the name changed
  99. self.failUnlessRaises(ValueError, smold.set_parent_commit, self.k_subm_current)
  100. # the sha is properly updated
  101. smold.set_parent_commit(self.k_subm_changed + "~1")
  102. assert smold.binsha != sm.binsha
  103. # raises if the sm didn't exist in new parent - it keeps its
  104. # parent_commit unchanged
  105. self.failUnlessRaises(ValueError, smold.set_parent_commit, self.k_no_subm_tag)
  106. # TEST TODO: if a path in the gitmodules file, but not in the index, it raises
  107. # TEST UPDATE
  108. ##############
  109. # module retrieval is not always possible
  110. if rwrepo.bare:
  111. self.failUnlessRaises(InvalidGitRepositoryError, sm.module)
  112. self.failUnlessRaises(InvalidGitRepositoryError, sm.remove)
  113. self.failUnlessRaises(InvalidGitRepositoryError, sm.add, rwrepo, 'here', 'there')
  114. else:
  115. # its not checked out in our case
  116. self.failUnlessRaises(InvalidGitRepositoryError, sm.module)
  117. assert not sm.module_exists()
  118. # currently there is only one submodule
  119. assert len(list(rwrepo.iter_submodules())) == 1
  120. assert sm.binsha != "\0" * 20
  121. # TEST ADD
  122. ###########
  123. # preliminary tests
  124. # adding existing returns exactly the existing
  125. sma = Submodule.add(rwrepo, sm.name, sm.path)
  126. assert sma.path == sm.path
  127. # no url and no module at path fails
  128. self.failUnlessRaises(ValueError, Submodule.add, rwrepo, "newsubm", "pathtorepo", url=None)
  129. # CONTINUE UPDATE
  130. #################
  131. # lets update it - its a recursive one too
  132. newdir = osp.join(sm.abspath, 'dir')
  133. os.makedirs(newdir)
  134. # update fails if the path already exists non-empty
  135. self.failUnlessRaises(OSError, sm.update)
  136. os.rmdir(newdir)
  137. # dry-run does nothing
  138. sm.update(dry_run=True, progress=prog)
  139. assert not sm.module_exists()
  140. assert sm.update() is sm
  141. sm_repopath = sm.path # cache for later
  142. assert sm.module_exists()
  143. assert isinstance(sm.module(), git.Repo)
  144. assert sm.module().working_tree_dir == sm.abspath
  145. # INTERLEAVE ADD TEST
  146. #####################
  147. # url must match the one in the existing repository ( if submodule name suggests a new one )
  148. # or we raise
  149. self.failUnlessRaises(ValueError, Submodule.add, rwrepo, "newsubm", sm.path, "git://someurl/repo.git")
  150. # CONTINUE UPDATE
  151. #################
  152. # we should have setup a tracking branch, which is also active
  153. assert sm.module().head.ref.tracking_branch() is not None
  154. # delete the whole directory and re-initialize
  155. assert len(sm.children()) != 0
  156. # shutil.rmtree(sm.abspath)
  157. sm.remove(force=True, configuration=False)
  158. assert len(sm.children()) == 0
  159. # dry-run does nothing
  160. sm.update(dry_run=True, recursive=False, progress=prog)
  161. assert len(sm.children()) == 0
  162. sm.update(recursive=False)
  163. assert len(list(rwrepo.iter_submodules())) == 2
  164. assert len(sm.children()) == 1 # its not checked out yet
  165. csm = sm.children()[0]
  166. assert not csm.module_exists()
  167. csm_repopath = csm.path
  168. # adjust the path of the submodules module to point to the local destination
  169. new_csmclone_path = Git.polish_url(osp.join(self.rorepo.working_tree_dir, sm.path, csm.path))
  170. with csm.config_writer() as writer:
  171. writer.set_value('url', new_csmclone_path)
  172. assert csm.url == new_csmclone_path
  173. # dry-run does nothing
  174. assert not csm.module_exists()
  175. sm.update(recursive=True, dry_run=True, progress=prog)
  176. assert not csm.module_exists()
  177. # update recursively again
  178. sm.update(recursive=True)
  179. assert csm.module_exists()
  180. # tracking branch once again
  181. csm.module().head.ref.tracking_branch() is not None # @NoEffect
  182. # this flushed in a sub-submodule
  183. assert len(list(rwrepo.iter_submodules())) == 2
  184. # reset both heads to the previous version, verify that to_latest_revision works
  185. smods = (sm.module(), csm.module())
  186. for repo in smods:
  187. repo.head.reset('HEAD~2', working_tree=1)
  188. # END for each repo to reset
  189. # dry run does nothing
  190. self.failUnlessRaises(RepositoryDirtyError, sm.update, recursive=True, dry_run=True, progress=prog)
  191. sm.update(recursive=True, dry_run=True, progress=prog, force=True)
  192. for repo in smods:
  193. assert repo.head.commit != repo.head.ref.tracking_branch().commit
  194. # END for each repo to check
  195. self.failUnlessRaises(RepositoryDirtyError, sm.update, recursive=True, to_latest_revision=True)
  196. sm.update(recursive=True, to_latest_revision=True, force=True)
  197. for repo in smods:
  198. assert repo.head.commit == repo.head.ref.tracking_branch().commit
  199. # END for each repo to check
  200. del(smods)
  201. # if the head is detached, it still works ( but warns )
  202. smref = sm.module().head.ref
  203. sm.module().head.ref = 'HEAD~1'
  204. # if there is no tracking branch, we get a warning as well
  205. csm_tracking_branch = csm.module().head.ref.tracking_branch()
  206. csm.module().head.ref.set_tracking_branch(None)
  207. sm.update(recursive=True, to_latest_revision=True)
  208. # to_latest_revision changes the child submodule's commit, it needs an
  209. # update now
  210. csm.set_parent_commit(csm.repo.head.commit)
  211. # undo the changes
  212. sm.module().head.ref = smref
  213. csm.module().head.ref.set_tracking_branch(csm_tracking_branch)
  214. # REMOVAL OF REPOSITOTRY
  215. ########################
  216. # must delete something
  217. self.failUnlessRaises(ValueError, csm.remove, module=False, configuration=False)
  218. # module() is supposed to point to gitdb, which has a child-submodule whose URL is still pointing
  219. # to GitHub. To save time, we will change it to
  220. csm.set_parent_commit(csm.repo.head.commit)
  221. with csm.config_writer() as cw:
  222. cw.set_value('url', self._small_repo_url())
  223. csm.repo.index.commit("adjusted URL to point to local source, instead of the internet")
  224. # We have modified the configuration, hence the index is dirty, and the
  225. # deletion will fail
  226. # NOTE: As we did a few updates in the meanwhile, the indices were reset
  227. # Hence we create some changes
  228. csm.set_parent_commit(csm.repo.head.commit)
  229. with sm.config_writer() as writer:
  230. writer.set_value("somekey", "somevalue")
  231. with csm.config_writer() as writer:
  232. writer.set_value("okey", "ovalue")
  233. self.failUnlessRaises(InvalidGitRepositoryError, sm.remove)
  234. # if we remove the dirty index, it would work
  235. sm.module().index.reset()
  236. # still, we have the file modified
  237. self.failUnlessRaises(InvalidGitRepositoryError, sm.remove, dry_run=True)
  238. sm.module().index.reset(working_tree=True)
  239. # enforce the submodule to be checked out at the right spot as well.
  240. csm.update()
  241. assert csm.module_exists()
  242. assert csm.exists()
  243. assert osp.isdir(csm.module().working_tree_dir)
  244. # this would work
  245. assert sm.remove(force=True, dry_run=True) is sm
  246. assert sm.module_exists()
  247. sm.remove(force=True, dry_run=True)
  248. assert sm.module_exists()
  249. # but ... we have untracked files in the child submodule
  250. fn = join_path_native(csm.module().working_tree_dir, "newfile")
  251. with open(fn, 'w') as fd:
  252. fd.write("hi")
  253. self.failUnlessRaises(InvalidGitRepositoryError, sm.remove)
  254. # forcibly delete the child repository
  255. prev_count = len(sm.children())
  256. self.failUnlessRaises(ValueError, csm.remove, force=True)
  257. # We removed sm, which removed all submodules. However, the instance we
  258. # have still points to the commit prior to that, where it still existed
  259. csm.set_parent_commit(csm.repo.commit(), check=False)
  260. assert not csm.exists()
  261. assert not csm.module_exists()
  262. assert len(sm.children()) == prev_count
  263. # now we have a changed index, as configuration was altered.
  264. # fix this
  265. sm.module().index.reset(working_tree=True)
  266. # now delete only the module of the main submodule
  267. assert sm.module_exists()
  268. sm.remove(configuration=False, force=True)
  269. assert sm.exists()
  270. assert not sm.module_exists()
  271. assert sm.config_reader().get_value('url')
  272. # delete the rest
  273. sm_path = sm.path
  274. sm.remove()
  275. assert not sm.exists()
  276. assert not sm.module_exists()
  277. self.failUnlessRaises(ValueError, getattr, sm, 'path')
  278. assert len(rwrepo.submodules) == 0
  279. # ADD NEW SUBMODULE
  280. ###################
  281. # add a simple remote repo - trailing slashes are no problem
  282. smid = "newsub"
  283. osmid = "othersub"
  284. nsm = Submodule.add(rwrepo, smid, sm_repopath, new_smclone_path + "/", None, no_checkout=True)
  285. assert nsm.name == smid
  286. assert nsm.module_exists()
  287. assert nsm.exists()
  288. # its not checked out
  289. assert not osp.isfile(join_path_native(nsm.module().working_tree_dir, Submodule.k_modules_file))
  290. assert len(rwrepo.submodules) == 1
  291. # add another submodule, but into the root, not as submodule
  292. osm = Submodule.add(rwrepo, osmid, csm_repopath, new_csmclone_path, Submodule.k_head_default)
  293. assert osm != nsm
  294. assert osm.module_exists()
  295. assert osm.exists()
  296. assert osp.isfile(join_path_native(osm.module().working_tree_dir, 'setup.py'))
  297. assert len(rwrepo.submodules) == 2
  298. # commit the changes, just to finalize the operation
  299. rwrepo.index.commit("my submod commit")
  300. assert len(rwrepo.submodules) == 2
  301. # needs update as the head changed, it thinks its in the history
  302. # of the repo otherwise
  303. nsm.set_parent_commit(rwrepo.head.commit)
  304. osm.set_parent_commit(rwrepo.head.commit)
  305. # MOVE MODULE
  306. #############
  307. # invalid input
  308. self.failUnlessRaises(ValueError, nsm.move, 'doesntmatter', module=False, configuration=False)
  309. # renaming to the same path does nothing
  310. assert nsm.move(sm_path) is nsm
  311. # rename a module
  312. nmp = join_path_native("new", "module", "dir") + "/" # new module path
  313. pmp = nsm.path
  314. assert nsm.move(nmp) is nsm
  315. nmp = nmp[:-1] # cut last /
  316. nmpl = to_native_path_linux(nmp)
  317. assert nsm.path == nmpl
  318. assert rwrepo.submodules[0].path == nmpl
  319. mpath = 'newsubmodule'
  320. absmpath = join_path_native(rwrepo.working_tree_dir, mpath)
  321. open(absmpath, 'w').write('')
  322. self.failUnlessRaises(ValueError, nsm.move, mpath)
  323. os.remove(absmpath)
  324. # now it works, as we just move it back
  325. nsm.move(pmp)
  326. assert nsm.path == pmp
  327. assert rwrepo.submodules[0].path == pmp
  328. # REMOVE 'EM ALL
  329. ################
  330. # if a submodule's repo has no remotes, it can't be added without an explicit url
  331. osmod = osm.module()
  332. osm.remove(module=False)
  333. for remote in osmod.remotes:
  334. remote.remove(osmod, remote.name)
  335. assert not osm.exists()
  336. self.failUnlessRaises(ValueError, Submodule.add, rwrepo, osmid, csm_repopath, url=None)
  337. # END handle bare mode
  338. # Error if there is no submodule file here
  339. self.failUnlessRaises(IOError, Submodule._config_parser, rwrepo, rwrepo.commit(self.k_no_subm_tag), True)
  340. # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, ## ACTUALLY skipped by `git.submodule.base#L869`.
  341. # "FIXME: fails with: PermissionError: [WinError 32] The process cannot access the file because"
  342. # "it is being used by another process: "
  343. # "'C:\\Users\\ankostis\\AppData\\Local\\Temp\\tmp95c3z83bnon_bare_test_base_rw\\git\\ext\\gitdb\\gitdb\\ext\\smmap'") # noqa E501
  344. @with_rw_repo(k_subm_current)
  345. def test_base_rw(self, rwrepo):
  346. self._do_base_tests(rwrepo)
  347. @with_rw_repo(k_subm_current, bare=True)
  348. def test_base_bare(self, rwrepo):
  349. self._do_base_tests(rwrepo)
  350. @skipIf(HIDE_WINDOWS_KNOWN_ERRORS and sys.version_info[:2] == (3, 5), """
  351. File "C:\\projects\\gitpython\\git\\cmd.py", line 559, in execute
  352. raise GitCommandNotFound(command, err)
  353. git.exc.GitCommandNotFound: Cmd('git') not found due to: OSError('[WinError 6] The handle is invalid')
  354. cmdline: git clone -n --shared -v C:\\projects\\gitpython\\.git Users\\appveyor\\AppData\\Local\\Temp\\1\\tmplyp6kr_rnon_bare_test_root_module""") # noqa E501
  355. @with_rw_repo(k_subm_current, bare=False)
  356. def test_root_module(self, rwrepo):
  357. # Can query everything without problems
  358. rm = RootModule(self.rorepo)
  359. assert rm.module() is self.rorepo
  360. # try attributes
  361. rm.binsha
  362. rm.mode
  363. rm.path
  364. assert rm.name == rm.k_root_name
  365. assert rm.parent_commit == self.rorepo.head.commit
  366. rm.url
  367. rm.branch
  368. assert len(rm.list_items(rm.module())) == 1
  369. rm.config_reader()
  370. with rm.config_writer():
  371. pass
  372. # deep traversal gitdb / async
  373. rsmsp = [sm.path for sm in rm.traverse()]
  374. assert len(rsmsp) >= 2 # gitdb and async [and smmap], async being a child of gitdb
  375. # cannot set the parent commit as root module's path didn't exist
  376. self.failUnlessRaises(ValueError, rm.set_parent_commit, 'HEAD')
  377. # TEST UPDATE
  378. #############
  379. # setup commit which remove existing, add new and modify existing submodules
  380. rm = RootModule(rwrepo)
  381. assert len(rm.children()) == 1
  382. # modify path without modifying the index entry
  383. # ( which is what the move method would do properly )
  384. #==================================================
  385. sm = rm.children()[0]
  386. pp = "path/prefix"
  387. fp = join_path_native(pp, sm.path)
  388. prep = sm.path
  389. assert not sm.module_exists() # was never updated after rwrepo's clone
  390. # assure we clone from a local source
  391. with sm.config_writer() as writer:
  392. writer.set_value('url', Git.polish_url(osp.join(self.rorepo.working_tree_dir, sm.path)))
  393. # dry-run does nothing
  394. sm.update(recursive=False, dry_run=True, progress=prog)
  395. assert not sm.module_exists()
  396. sm.update(recursive=False)
  397. assert sm.module_exists()
  398. with sm.config_writer() as writer:
  399. writer.set_value('path', fp) # change path to something with prefix AFTER url change
  400. # update doesn't fail, because list_items ignores the wrong path in such situations.
  401. rm.update(recursive=False)
  402. # move it properly - doesn't work as it its path currently points to an indexentry
  403. # which doesn't exist ( move it to some path, it doesn't matter here )
  404. self.failUnlessRaises(InvalidGitRepositoryError, sm.move, pp)
  405. # reset the path(cache) to where it was, now it works
  406. sm.path = prep
  407. sm.move(fp, module=False) # leave it at the old location
  408. assert not sm.module_exists()
  409. cpathchange = rwrepo.index.commit("changed sm path") # finally we can commit
  410. # update puts the module into place
  411. rm.update(recursive=False, progress=prog)
  412. sm.set_parent_commit(cpathchange)
  413. assert sm.module_exists()
  414. # add submodule
  415. #================
  416. nsmn = "newsubmodule"
  417. nsmp = "submrepo"
  418. subrepo_url = Git.polish_url(osp.join(self.rorepo.working_tree_dir, rsmsp[0], rsmsp[1]))
  419. nsm = Submodule.add(rwrepo, nsmn, nsmp, url=subrepo_url)
  420. csmadded = rwrepo.index.commit("Added submodule").hexsha # make sure we don't keep the repo reference
  421. nsm.set_parent_commit(csmadded)
  422. assert nsm.module_exists()
  423. # in our case, the module should not exist, which happens if we update a parent
  424. # repo and a new submodule comes into life
  425. nsm.remove(configuration=False, module=True)
  426. assert not nsm.module_exists() and nsm.exists()
  427. # dry-run does nothing
  428. rm.update(recursive=False, dry_run=True, progress=prog)
  429. # otherwise it will work
  430. rm.update(recursive=False, progress=prog)
  431. assert nsm.module_exists()
  432. # remove submodule - the previous one
  433. #====================================
  434. sm.set_parent_commit(csmadded)
  435. smp = sm.abspath
  436. assert not sm.remove(module=False).exists()
  437. assert osp.isdir(smp) # module still exists
  438. csmremoved = rwrepo.index.commit("Removed submodule")
  439. # an update will remove the module
  440. # not in dry_run
  441. rm.update(recursive=False, dry_run=True, force_remove=True)
  442. assert osp.isdir(smp)
  443. # when removing submodules, we may get new commits as nested submodules are auto-committing changes
  444. # to allow deletions without force, as the index would be dirty otherwise.
  445. # QUESTION: Why does this seem to work in test_git_submodule_compatibility() ?
  446. self.failUnlessRaises(InvalidGitRepositoryError, rm.update, recursive=False, force_remove=False)
  447. rm.update(recursive=False, force_remove=True)
  448. assert not osp.isdir(smp)
  449. # 'apply work' to the nested submodule and assure this is not removed/altered during updates
  450. # Need to commit first, otherwise submodule.update wouldn't have a reason to change the head
  451. touch(osp.join(nsm.module().working_tree_dir, 'new-file'))
  452. # We cannot expect is_dirty to even run as we wouldn't reset a head to the same location
  453. assert nsm.module().head.commit.hexsha == nsm.hexsha
  454. nsm.module().index.add([nsm])
  455. nsm.module().index.commit("added new file")
  456. rm.update(recursive=False, dry_run=True, progress=prog) # would not change head, and thus doens't fail
  457. # Everything we can do from now on will trigger the 'future' check, so no is_dirty() check will even run
  458. # This would only run if our local branch is in the past and we have uncommitted changes
  459. prev_commit = nsm.module().head.commit
  460. rm.update(recursive=False, dry_run=False, progress=prog)
  461. assert prev_commit == nsm.module().head.commit, "head shouldn't change, as it is in future of remote branch"
  462. # this kills the new file
  463. rm.update(recursive=True, progress=prog, force_reset=True)
  464. assert prev_commit != nsm.module().head.commit, "head changed, as the remote url and its commit changed"
  465. # change url ...
  466. #===============
  467. # ... to the first repository, this way we have a fast checkout, and a completely different
  468. # repository at the different url
  469. nsm.set_parent_commit(csmremoved)
  470. nsmurl = Git.polish_url(osp.join(self.rorepo.working_tree_dir, rsmsp[0]))
  471. with nsm.config_writer() as writer:
  472. writer.set_value('url', nsmurl)
  473. csmpathchange = rwrepo.index.commit("changed url")
  474. nsm.set_parent_commit(csmpathchange)
  475. # Now nsm head is in the future of the tracked remote branch
  476. prev_commit = nsm.module().head.commit
  477. # dry-run does nothing
  478. rm.update(recursive=False, dry_run=True, progress=prog)
  479. assert nsm.module().remotes.origin.url != nsmurl
  480. rm.update(recursive=False, progress=prog, force_reset=True)
  481. assert nsm.module().remotes.origin.url == nsmurl
  482. assert prev_commit != nsm.module().head.commit, "Should now point to gitdb"
  483. assert len(rwrepo.submodules) == 1
  484. assert not rwrepo.submodules[0].children()[0].module_exists(), "nested submodule should not be checked out"
  485. # add the submodule's changed commit to the index, which is what the
  486. # user would do
  487. # beforehand, update our instance's binsha with the new one
  488. nsm.binsha = nsm.module().head.commit.binsha
  489. rwrepo.index.add([nsm])
  490. # change branch
  491. #=================
  492. # we only have one branch, so we switch to a virtual one, and back
  493. # to the current one to trigger the difference
  494. cur_branch = nsm.branch
  495. nsmm = nsm.module()
  496. prev_commit = nsmm.head.commit
  497. for branch in ("some_virtual_branch", cur_branch.name):
  498. with nsm.config_writer() as writer:
  499. writer.set_value(Submodule.k_head_option, git.Head.to_full_path(branch))
  500. csmbranchchange = rwrepo.index.commit("changed branch to %s" % branch)
  501. nsm.set_parent_commit(csmbranchchange)
  502. # END for each branch to change
  503. # Lets remove our tracking branch to simulate some changes
  504. nsmmh = nsmm.head
  505. assert nsmmh.ref.tracking_branch() is None # never set it up until now
  506. assert not nsmmh.is_detached
  507. # dry run does nothing
  508. rm.update(recursive=False, dry_run=True, progress=prog)
  509. assert nsmmh.ref.tracking_branch() is None
  510. # the real thing does
  511. rm.update(recursive=False, progress=prog)
  512. assert nsmmh.ref.tracking_branch() is not None
  513. assert not nsmmh.is_detached
  514. # recursive update
  515. # =================
  516. # finally we recursively update a module, just to run the code at least once
  517. # remove the module so that it has more work
  518. assert len(nsm.children()) >= 1 # could include smmap
  519. assert nsm.exists() and nsm.module_exists() and len(nsm.children()) >= 1
  520. # assure we pull locally only
  521. nsmc = nsm.children()[0]
  522. with nsmc.config_writer() as writer:
  523. writer.set_value('url', subrepo_url)
  524. rm.update(recursive=True, progress=prog, dry_run=True) # just to run the code
  525. rm.update(recursive=True, progress=prog)
  526. # gitdb: has either 1 or 2 submodules depending on the version
  527. assert len(nsm.children()) >= 1 and nsmc.module_exists()
  528. @with_rw_repo(k_no_subm_tag, bare=False)
  529. def test_first_submodule(self, rwrepo):
  530. assert len(list(rwrepo.iter_submodules())) == 0
  531. for sm_name, sm_path in (('first', 'submodules/first'),
  532. ('second', osp.join(rwrepo.working_tree_dir, 'submodules/second'))):
  533. sm = rwrepo.create_submodule(sm_name, sm_path, rwrepo.git_dir, no_checkout=True)
  534. assert sm.exists() and sm.module_exists()
  535. rwrepo.index.commit("Added submodule " + sm_name)
  536. # end for each submodule path to add
  537. self.failUnlessRaises(ValueError, rwrepo.create_submodule, 'fail', osp.expanduser('~'))
  538. self.failUnlessRaises(ValueError, rwrepo.create_submodule, 'fail-too',
  539. rwrepo.working_tree_dir + osp.sep)
  540. @with_rw_directory
  541. def test_add_empty_repo(self, rwdir):
  542. empty_repo_dir = osp.join(rwdir, 'empty-repo')
  543. parent = git.Repo.init(osp.join(rwdir, 'parent'))
  544. git.Repo.init(empty_repo_dir)
  545. for checkout_mode in range(2):
  546. name = 'empty' + str(checkout_mode)
  547. self.failUnlessRaises(ValueError, parent.create_submodule, name, name,
  548. url=empty_repo_dir, no_checkout=checkout_mode and True or False)
  549. # end for each checkout mode
  550. @with_rw_directory
  551. def test_list_only_valid_submodules(self, rwdir):
  552. repo_path = osp.join(rwdir, 'parent')
  553. repo = git.Repo.init(repo_path)
  554. repo.git.submodule('add', self._small_repo_url(), 'module')
  555. repo.index.commit("add submodule")
  556. assert len(repo.submodules) == 1
  557. # Delete the directory from submodule
  558. submodule_path = osp.join(repo_path, 'module')
  559. shutil.rmtree(submodule_path)
  560. repo.git.add([submodule_path])
  561. repo.index.commit("remove submodule")
  562. repo = git.Repo(repo_path)
  563. assert len(repo.submodules) == 0
  564. @skipIf(HIDE_WINDOWS_KNOWN_ERRORS,
  565. """FIXME on cygwin: File "C:\\projects\\gitpython\\git\\cmd.py", line 671, in execute
  566. raise GitCommandError(command, status, stderr_value, stdout_value)
  567. GitCommandError: Cmd('git') failed due to: exit code(128)
  568. cmdline: git add 1__Xava verbXXten 1_test _myfile 1_test_other_file 1_XXava-----verbXXten
  569. stderr: 'fatal: pathspec '"1__çava verböten"' did not match any files'
  570. FIXME on appveyor: see https://ci.appveyor.com/project/Byron/gitpython/build/1.0.185
  571. """)
  572. @with_rw_directory
  573. def test_git_submodules_and_add_sm_with_new_commit(self, rwdir):
  574. parent = git.Repo.init(osp.join(rwdir, 'parent'))
  575. parent.git.submodule('add', self._small_repo_url(), 'module')
  576. parent.index.commit("added submodule")
  577. assert len(parent.submodules) == 1
  578. sm = parent.submodules[0]
  579. assert sm.exists() and sm.module_exists()
  580. clone = git.Repo.clone_from(self._small_repo_url(),
  581. osp.join(parent.working_tree_dir, 'existing-subrepository'))
  582. sm2 = parent.create_submodule('nongit-file-submodule', clone.working_tree_dir)
  583. assert len(parent.submodules) == 2
  584. for _ in range(2):
  585. for init in (False, True):
  586. sm.update(init=init)
  587. sm2.update(init=init)
  588. # end for each init state
  589. # end for each iteration
  590. sm.move(sm.path + '_moved')
  591. sm2.move(sm2.path + '_moved')
  592. parent.index.commit("moved submodules")
  593. with sm.config_writer() as writer:
  594. writer.set_value('user.email', 'example@example.com')
  595. writer.set_value('user.name', 'me')
  596. smm = sm.module()
  597. fp = osp.join(smm.working_tree_dir, 'empty-file')
  598. with open(fp, 'w'):
  599. pass
  600. smm.git.add(Git.polish_url(fp))
  601. smm.git.commit(m="new file added")
  602. # submodules are retrieved from the current commit's tree, therefore we can't really get a new submodule
  603. # object pointing to the new submodule commit
  604. sm_too = parent.submodules['module_moved']
  605. assert parent.head.commit.tree[sm.path].binsha == sm.binsha
  606. assert sm_too.binsha == sm.binsha, "cached submodule should point to the same commit as updated one"
  607. added_bies = parent.index.add([sm]) # addded base-index-entries
  608. assert len(added_bies) == 1
  609. parent.index.commit("add same submodule entry")
  610. commit_sm = parent.head.commit.tree[sm.path]
  611. assert commit_sm.binsha == added_bies[0].binsha
  612. assert commit_sm.binsha == sm.binsha
  613. sm_too.binsha = sm_too.module().head.commit.binsha
  614. added_bies = parent.index.add([sm_too])
  615. assert len(added_bies) == 1
  616. parent.index.commit("add new submodule entry")
  617. commit_sm = parent.head.commit.tree[sm.path]
  618. assert commit_sm.binsha == added_bies[0].binsha
  619. assert commit_sm.binsha == sm_too.binsha
  620. assert sm_too.binsha != sm.binsha
  621. # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, ## ACTUALLY skipped by `git.submodule.base#L869`.
  622. # "FIXME: helper.wrapper fails with: PermissionError: [WinError 5] Access is denied: "
  623. # "'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\test_work_tree_unsupportedryfa60di\\master_repo\\.git\\objects\\pack\\pack-bc9e0787aef9f69e1591ef38ea0a6f566ec66fe3.idx") # noqa E501
  624. @with_rw_directory
  625. def test_git_submodule_compatibility(self, rwdir):
  626. parent = git.Repo.init(osp.join(rwdir, 'parent'))
  627. sm_path = join_path_native('submodules', 'intermediate', 'one')
  628. sm = parent.create_submodule('mymodules/myname', sm_path, url=self._small_repo_url())
  629. parent.index.commit("added submodule")
  630. def assert_exists(sm, value=True):
  631. assert sm.exists() == value
  632. assert sm.module_exists() == value
  633. # end
  634. # As git is backwards compatible itself, it would still recognize what we do here ... unless we really
  635. # muss it up. That's the only reason why the test is still here ... .
  636. assert len(parent.git.submodule().splitlines()) == 1
  637. module_repo_path = osp.join(sm.module().working_tree_dir, '.git')
  638. assert module_repo_path.startswith(osp.join(parent.working_tree_dir, sm_path))
  639. if not sm._need_gitfile_submodules(parent.git):
  640. assert osp.isdir(module_repo_path)
  641. assert not sm.module().has_separate_working_tree()
  642. else:
  643. assert osp.isfile(module_repo_path)
  644. assert sm.module().has_separate_working_tree()
  645. assert find_submodule_git_dir(module_repo_path) is not None, "module pointed to by .git file must be valid"
  646. # end verify submodule 'style'
  647. # test move
  648. new_sm_path = join_path_native('submodules', 'one')
  649. sm.move(new_sm_path)
  650. assert_exists(sm)
  651. # Add additional submodule level
  652. csm = sm.module().create_submodule('nested-submodule', join_path_native('nested-submodule', 'working-tree'),
  653. url=self._small_repo_url())
  654. sm.module().index.commit("added nested submodule")
  655. sm_head_commit = sm.module().commit()
  656. assert_exists(csm)
  657. # Fails because there are new commits, compared to the remote we cloned from
  658. self.failUnlessRaises(InvalidGitRepositoryError, sm.remove, dry_run=True)
  659. assert_exists(sm)
  660. assert sm.module().commit() == sm_head_commit
  661. assert_exists(csm)
  662. # rename nested submodule
  663. # This name would move itself one level deeper - needs special handling internally
  664. new_name = csm.name + '/mine'
  665. assert csm.rename(new_name).name == new_name
  666. assert_exists(csm)
  667. assert csm.repo.is_dirty(index=True, working_tree=False), "index must contain changed .gitmodules file"
  668. csm.repo.index.commit("renamed module")
  669. # keep_going evaluation
  670. rsm = parent.submodule_update()
  671. assert_exists(sm)
  672. assert_exists(csm)
  673. with csm.config_writer().set_value('url', 'bar'):
  674. pass
  675. csm.repo.index.commit("Have to commit submodule change for algorithm to pick it up")
  676. assert csm.url == 'bar'
  677. self.failUnlessRaises(Exception, rsm.update, recursive=True, to_latest_revision=True, progress=prog)
  678. assert_exists(csm)
  679. rsm.update(recursive=True, to_latest_revision=True, progress=prog, keep_going=True)
  680. # remove
  681. sm_module_path = sm.module().git_dir
  682. for dry_run in (True, False):
  683. sm.remove(dry_run=dry_run, force=True)
  684. assert_exists(sm, value=dry_run)
  685. assert osp.isdir(sm_module_path) == dry_run
  686. # end for each dry-run mode
  687. @with_rw_directory
  688. def test_remove_norefs(self, rwdir):
  689. parent = git.Repo.init(osp.join(rwdir, 'parent'))
  690. sm_name = 'mymodules/myname'
  691. sm = parent.create_submodule(sm_name, sm_name, url=self._small_repo_url())
  692. assert sm.exists()
  693. parent.index.commit("Added submodule")
  694. assert sm.repo is parent # yoh was surprised since expected sm repo!!
  695. # so created a new instance for submodule
  696. smrepo = git.Repo(osp.join(rwdir, 'parent', sm.path))
  697. # Adding a remote without fetching so would have no references
  698. smrepo.create_remote('special', 'git@server-shouldnotmatter:repo.git')
  699. # And we should be able to remove it just fine
  700. sm.remove()
  701. assert not sm.exists()
  702. @with_rw_directory
  703. def test_rename(self, rwdir):
  704. parent = git.Repo.init(osp.join(rwdir, 'parent'))
  705. sm_name = 'mymodules/myname'
  706. sm = parent.create_submodule(sm_name, sm_name, url=self._small_repo_url())
  707. parent.index.commit("Added submodule")
  708. assert sm.rename(sm_name) is sm and sm.name == sm_name
  709. assert not sm.repo.is_dirty(index=True, working_tree=False, untracked_files=False)
  710. new_path = 'renamed/myname'
  711. assert sm.move(new_path).name == new_path
  712. new_sm_name = "shortname"
  713. assert sm.rename(new_sm_name) is sm
  714. assert sm.repo.is_dirty(index=True, working_tree=False, untracked_files=False)
  715. assert sm.exists()
  716. sm_mod = sm.module()
  717. if osp.isfile(osp.join(sm_mod.working_tree_dir, '.git')) == sm._need_gitfile_submodules(parent.git):
  718. assert sm_mod.git_dir.endswith(join_path_native('.git', 'modules', new_sm_name))
  719. # end
  720. @with_rw_directory
  721. def test_branch_renames(self, rw_dir):
  722. # Setup initial sandbox:
  723. # parent repo has one submodule, which has all the latest changes
  724. source_url = self._small_repo_url()
  725. sm_source_repo = git.Repo.clone_from(source_url, osp.join(rw_dir, 'sm-source'), b='master')
  726. parent_repo = git.Repo.init(osp.join(rw_dir, 'parent'))
  727. sm = parent_repo.create_submodule('mysubmodule', 'subdir/submodule',
  728. sm_source_repo.working_tree_dir, branch='master')
  729. parent_repo.index.commit('added submodule')
  730. assert sm.exists()
  731. # Create feature branch with one new commit in submodule source
  732. sm_fb = sm_source_repo.create_head('feature')
  733. sm_fb.checkout()
  734. new_file = touch(osp.join(sm_source_repo.working_tree_dir, 'new-file'))
  735. sm_source_repo.index.add([new_file])
  736. sm.repo.index.commit("added new file")
  737. # change designated submodule checkout branch to the new upstream feature branch
  738. with sm.config_writer() as smcw:
  739. smcw.set_value('branch', sm_fb.name)
  740. assert sm.repo.is_dirty(index=True, working_tree=False)
  741. sm.repo.index.commit("changed submodule branch to '%s'" % sm_fb)
  742. # verify submodule update with feature branch that leaves currently checked out branch in it's past
  743. sm_mod = sm.module()
  744. prev_commit = sm_mod.commit()
  745. assert sm_mod.head.ref.name == 'master'
  746. assert parent_repo.submodule_update()
  747. assert sm_mod.head.ref.name == sm_fb.name
  748. assert sm_mod.commit() == prev_commit, "Without to_latest_revision, we don't change the commit"
  749. assert parent_repo.submodule_update(to_latest_revision=True)
  750. assert sm_mod.head.ref.name == sm_fb.name
  751. assert sm_mod.commit() == sm_fb.commit
  752. # Create new branch which is in our past, and thus seemingly unrelated to the currently checked out one
  753. # To make it even 'harder', we shall fork and create a new commit
  754. sm_pfb = sm_source_repo.create_head('past-feature', commit='HEAD~20')
  755. sm_pfb.checkout()
  756. sm_source_repo.index.add([touch(osp.join(sm_source_repo.working_tree_dir, 'new-file'))])
  757. sm_source_repo.index.commit("new file added, to past of '%r'" % sm_fb)
  758. # Change designated submodule checkout branch to a new commit in its own past
  759. with sm.config_writer() as smcw:
  760. smcw.set_value('branch', sm_pfb.path)
  761. sm.repo.index.commit("changed submodule branch to '%s'" % sm_pfb)
  762. # Test submodule updates - must fail if submodule is dirty
  763. touch(osp.join(sm_mod.working_tree_dir, 'unstaged file'))
  764. # This doesn't fail as our own submodule binsha didn't change, and the reset is only triggered if
  765. # to latest revision is True.
  766. parent_repo.submodule_update(to_latest_revision=False)
  767. sm_mod.head.ref.name == sm_pfb.name, "should have been switched to past head"
  768. sm_mod.commit() == sm_fb.commit, "Head wasn't reset"
  769. self.failUnlessRaises(RepositoryDirtyError, parent_repo.submodule_update, to_latest_revision=True)
  770. parent_repo.submodule_update(to_latest_revision=True, force_reset=True)
  771. assert sm_mod.commit() == sm_pfb.commit, "Now head should have been reset"
  772. assert sm_mod.head.ref.name == sm_pfb.name
  773. @skipIf(not is_win, "Specifically for Windows.")
  774. def test_to_relative_path_with_super_at_root_drive(self):
  775. class Repo(object):
  776. working_tree_dir = 'D:\\'
  777. super_repo = Repo()
  778. submodule_path = 'D:\\submodule_path'
  779. relative_path = Submodule._to_relative_path(super_repo, submodule_path)
  780. msg = '_to_relative_path should be "submodule_path" but was "%s"' % relative_path
  781. assert relative_path == 'submodule_path', msg