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.

494 lines
25KB

  1. # -*- coding: utf-8 -*-
  2. # test_git.py
  3. # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
  4. #
  5. # This module is part of GitPython and is released under
  6. # the BSD License: http://www.opensource.org/licenses/bsd-license.php
  7. import os
  8. from git.test.lib import TestBase
  9. from git.test.lib.helper import with_rw_directory
  10. import os.path
  11. class Tutorials(TestBase):
  12. def tearDown(self):
  13. import gc
  14. gc.collect()
  15. # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, ## ACTUALLY skipped by `git.submodule.base#L869`.
  16. # "FIXME: helper.wrapper fails with: PermissionError: [WinError 5] Access is denied: "
  17. # "'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\test_work_tree_unsupportedryfa60di\\master_repo\\.git\\objects\\pack\\pack-bc9e0787aef9f69e1591ef38ea0a6f566ec66fe3.idx") # noqa E501
  18. @with_rw_directory
  19. def test_init_repo_object(self, rw_dir):
  20. # [1-test_init_repo_object]
  21. from git import Repo
  22. # rorepo is a Repo instance pointing to the git-python repository.
  23. # For all you know, the first argument to Repo is a path to the repository
  24. # you want to work with
  25. repo = Repo(self.rorepo.working_tree_dir)
  26. assert not repo.bare
  27. # ![1-test_init_repo_object]
  28. # [2-test_init_repo_object]
  29. bare_repo = Repo.init(os.path.join(rw_dir, 'bare-repo'), bare=True)
  30. assert bare_repo.bare
  31. # ![2-test_init_repo_object]
  32. # [3-test_init_repo_object]
  33. repo.config_reader() # get a config reader for read-only access
  34. with repo.config_writer(): # get a config writer to change configuration
  35. pass # call release() to be sure changes are written and locks are released
  36. # ![3-test_init_repo_object]
  37. # [4-test_init_repo_object]
  38. assert not bare_repo.is_dirty() # check the dirty state
  39. repo.untracked_files # retrieve a list of untracked files
  40. # ['my_untracked_file']
  41. # ![4-test_init_repo_object]
  42. # [5-test_init_repo_object]
  43. cloned_repo = repo.clone(os.path.join(rw_dir, 'to/this/path'))
  44. assert cloned_repo.__class__ is Repo # clone an existing repository
  45. assert Repo.init(os.path.join(rw_dir, 'path/for/new/repo')).__class__ is Repo
  46. # ![5-test_init_repo_object]
  47. # [6-test_init_repo_object]
  48. with open(os.path.join(rw_dir, 'repo.tar'), 'wb') as fp:
  49. repo.archive(fp)
  50. # ![6-test_init_repo_object]
  51. # repository paths
  52. # [7-test_init_repo_object]
  53. assert os.path.isdir(cloned_repo.working_tree_dir) # directory with your work files
  54. assert cloned_repo.git_dir.startswith(cloned_repo.working_tree_dir) # directory containing the git repository
  55. assert bare_repo.working_tree_dir is None # bare repositories have no working tree
  56. # ![7-test_init_repo_object]
  57. # heads, tags and references
  58. # heads are branches in git-speak
  59. # [8-test_init_repo_object]
  60. self.assertEqual(repo.head.ref, repo.heads.master, # head is a sym-ref pointing to master
  61. "It's ok if TC not running from `master`.")
  62. self.assertEqual(repo.tags['0.3.5'], repo.tag('refs/tags/0.3.5')) # you can access tags in various ways too
  63. self.assertEqual(repo.refs.master, repo.heads['master']) # .refs provides all refs, ie heads ...
  64. if 'TRAVIS' not in os.environ:
  65. self.assertEqual(repo.refs['origin/master'], repo.remotes.origin.refs.master) # ... remotes ...
  66. self.assertEqual(repo.refs['0.3.5'], repo.tags['0.3.5']) # ... and tags
  67. # ![8-test_init_repo_object]
  68. # create a new head/branch
  69. # [9-test_init_repo_object]
  70. new_branch = cloned_repo.create_head('feature') # create a new branch ...
  71. assert cloned_repo.active_branch != new_branch # which wasn't checked out yet ...
  72. self.assertEqual(new_branch.commit, cloned_repo.active_branch.commit) # pointing to the checked-out commit
  73. # It's easy to let a branch point to the previous commit, without affecting anything else
  74. # Each reference provides access to the git object it points to, usually commits
  75. assert new_branch.set_commit('HEAD~1').commit == cloned_repo.active_branch.commit.parents[0]
  76. # ![9-test_init_repo_object]
  77. # create a new tag reference
  78. # [10-test_init_repo_object]
  79. past = cloned_repo.create_tag('past', ref=new_branch,
  80. message="This is a tag-object pointing to %s" % new_branch.name)
  81. self.assertEqual(past.commit, new_branch.commit) # the tag points to the specified commit
  82. assert past.tag.message.startswith("This is") # and its object carries the message provided
  83. now = cloned_repo.create_tag('now') # This is a tag-reference. It may not carry meta-data
  84. assert now.tag is None
  85. # ![10-test_init_repo_object]
  86. # Object handling
  87. # [11-test_init_repo_object]
  88. assert now.commit.message != past.commit.message
  89. # You can read objects directly through binary streams, no working tree required
  90. assert (now.commit.tree / 'VERSION').data_stream.read().decode('ascii').startswith('3')
  91. # You can traverse trees as well to handle all contained files of a particular commit
  92. file_count = 0
  93. tree_count = 0
  94. tree = past.commit.tree
  95. for item in tree.traverse():
  96. file_count += item.type == 'blob'
  97. tree_count += item.type == 'tree'
  98. assert file_count and tree_count # we have accumulated all directories and files
  99. self.assertEqual(len(tree.blobs) + len(tree.trees), len(tree)) # a tree is iterable on its children
  100. # ![11-test_init_repo_object]
  101. # remotes allow handling push, pull and fetch operations
  102. # [12-test_init_repo_object]
  103. from git import RemoteProgress
  104. class MyProgressPrinter(RemoteProgress):
  105. def update(self, op_code, cur_count, max_count=None, message=''):
  106. print(op_code, cur_count, max_count, cur_count / (max_count or 100.0), message or "NO MESSAGE")
  107. # end
  108. self.assertEqual(len(cloned_repo.remotes), 1) # we have been cloned, so should be one remote
  109. self.assertEqual(len(bare_repo.remotes), 0) # this one was just initialized
  110. origin = bare_repo.create_remote('origin', url=cloned_repo.working_tree_dir)
  111. assert origin.exists()
  112. for fetch_info in origin.fetch(progress=MyProgressPrinter()):
  113. print("Updated %s to %s" % (fetch_info.ref, fetch_info.commit))
  114. # create a local branch at the latest fetched master. We specify the name statically, but you have all
  115. # information to do it programatically as well.
  116. bare_master = bare_repo.create_head('master', origin.refs.master)
  117. bare_repo.head.set_reference(bare_master)
  118. assert not bare_repo.delete_remote(origin).exists()
  119. # push and pull behave very similarly
  120. # ![12-test_init_repo_object]
  121. # index
  122. # [13-test_init_repo_object]
  123. self.assertEqual(new_branch.checkout(), cloned_repo.active_branch) # checking out branch adjusts the wtree
  124. self.assertEqual(new_branch.commit, past.commit) # Now the past is checked out
  125. new_file_path = os.path.join(cloned_repo.working_tree_dir, 'my-new-file')
  126. open(new_file_path, 'wb').close() # create new file in working tree
  127. cloned_repo.index.add([new_file_path]) # add it to the index
  128. # Commit the changes to deviate masters history
  129. cloned_repo.index.commit("Added a new file in the past - for later merege")
  130. # prepare a merge
  131. master = cloned_repo.heads.master # right-hand side is ahead of us, in the future
  132. merge_base = cloned_repo.merge_base(new_branch, master) # allwos for a three-way merge
  133. cloned_repo.index.merge_tree(master, base=merge_base) # write the merge result into index
  134. cloned_repo.index.commit("Merged past and now into future ;)",
  135. parent_commits=(new_branch.commit, master.commit))
  136. # now new_branch is ahead of master, which probably should be checked out and reset softly.
  137. # note that all these operations didn't touch the working tree, as we managed it ourselves.
  138. # This definitely requires you to know what you are doing :) !
  139. assert os.path.basename(new_file_path) in new_branch.commit.tree # new file is now in tree
  140. master.commit = new_branch.commit # let master point to most recent commit
  141. cloned_repo.head.reference = master # we adjusted just the reference, not the working tree or index
  142. # ![13-test_init_repo_object]
  143. # submodules
  144. # [14-test_init_repo_object]
  145. # create a new submodule and check it out on the spot, setup to track master branch of `bare_repo`
  146. # As our GitPython repository has submodules already that point to GitHub, make sure we don't
  147. # interact with them
  148. for sm in cloned_repo.submodules:
  149. assert not sm.remove().exists() # after removal, the sm doesn't exist anymore
  150. sm = cloned_repo.create_submodule('mysubrepo', 'path/to/subrepo', url=bare_repo.git_dir, branch='master')
  151. # .gitmodules was written and added to the index, which is now being committed
  152. cloned_repo.index.commit("Added submodule")
  153. assert sm.exists() and sm.module_exists() # this submodule is defintely available
  154. sm.remove(module=True, configuration=False) # remove the working tree
  155. assert sm.exists() and not sm.module_exists() # the submodule itself is still available
  156. # update all submodules, non-recursively to save time, this method is very powerful, go have a look
  157. cloned_repo.submodule_update(recursive=False)
  158. assert sm.module_exists() # The submodules working tree was checked out by update
  159. # ![14-test_init_repo_object]
  160. @with_rw_directory
  161. def test_references_and_objects(self, rw_dir):
  162. # [1-test_references_and_objects]
  163. import git
  164. repo = git.Repo.clone_from(self._small_repo_url(), os.path.join(rw_dir, 'repo'), branch='master')
  165. heads = repo.heads
  166. master = heads.master # lists can be accessed by name for convenience
  167. master.commit # the commit pointed to by head called master
  168. master.rename('new_name') # rename heads
  169. master.rename('master')
  170. # ![1-test_references_and_objects]
  171. # [2-test_references_and_objects]
  172. tags = repo.tags
  173. tagref = tags[0]
  174. tagref.tag # tags may have tag objects carrying additional information
  175. tagref.commit # but they always point to commits
  176. repo.delete_tag(tagref) # delete or
  177. repo.create_tag("my_tag") # create tags using the repo for convenience
  178. # ![2-test_references_and_objects]
  179. # [3-test_references_and_objects]
  180. head = repo.head # the head points to the active branch/ref
  181. master = head.reference # retrieve the reference the head points to
  182. master.commit # from here you use it as any other reference
  183. # ![3-test_references_and_objects]
  184. #
  185. # [4-test_references_and_objects]
  186. log = master.log()
  187. log[0] # first (i.e. oldest) reflog entry
  188. log[-1] # last (i.e. most recent) reflog entry
  189. # ![4-test_references_and_objects]
  190. # [5-test_references_and_objects]
  191. new_branch = repo.create_head('new') # create a new one
  192. new_branch.commit = 'HEAD~10' # set branch to another commit without changing index or working trees
  193. repo.delete_head(new_branch) # delete an existing head - only works if it is not checked out
  194. # ![5-test_references_and_objects]
  195. # [6-test_references_and_objects]
  196. new_tag = repo.create_tag('my_new_tag', message='my message')
  197. # You cannot change the commit a tag points to. Tags need to be re-created
  198. self.failUnlessRaises(AttributeError, setattr, new_tag, 'commit', repo.commit('HEAD~1'))
  199. repo.delete_tag(new_tag)
  200. # ![6-test_references_and_objects]
  201. # [7-test_references_and_objects]
  202. new_branch = repo.create_head('another-branch')
  203. repo.head.reference = new_branch
  204. # ![7-test_references_and_objects]
  205. # [8-test_references_and_objects]
  206. hc = repo.head.commit
  207. hct = hc.tree
  208. hc != hct # @NoEffect
  209. hc != repo.tags[0] # @NoEffect
  210. hc == repo.head.reference.commit # @NoEffect
  211. # ![8-test_references_and_objects]
  212. # [9-test_references_and_objects]
  213. self.assertEqual(hct.type, 'tree') # preset string type, being a class attribute
  214. assert hct.size > 0 # size in bytes
  215. assert len(hct.hexsha) == 40
  216. assert len(hct.binsha) == 20
  217. # ![9-test_references_and_objects]
  218. # [10-test_references_and_objects]
  219. self.assertEqual(hct.path, '') # root tree has no path
  220. assert hct.trees[0].path != '' # the first contained item has one though
  221. self.assertEqual(hct.mode, 0o40000) # trees have the mode of a linux directory
  222. self.assertEqual(hct.blobs[0].mode, 0o100644) # blobs have specific mode, comparable to a standard linux fs
  223. # ![10-test_references_and_objects]
  224. # [11-test_references_and_objects]
  225. hct.blobs[0].data_stream.read() # stream object to read data from
  226. hct.blobs[0].stream_data(open(os.path.join(rw_dir, 'blob_data'), 'wb')) # write data to given stream
  227. # ![11-test_references_and_objects]
  228. # [12-test_references_and_objects]
  229. repo.commit('master')
  230. repo.commit('v0.8.1')
  231. repo.commit('HEAD~10')
  232. # ![12-test_references_and_objects]
  233. # [13-test_references_and_objects]
  234. fifty_first_commits = list(repo.iter_commits('master', max_count=50))
  235. assert len(fifty_first_commits) == 50
  236. # this will return commits 21-30 from the commit list as traversed backwards master
  237. ten_commits_past_twenty = list(repo.iter_commits('master', max_count=10, skip=20))
  238. assert len(ten_commits_past_twenty) == 10
  239. assert fifty_first_commits[20:30] == ten_commits_past_twenty
  240. # ![13-test_references_and_objects]
  241. # [14-test_references_and_objects]
  242. headcommit = repo.head.commit
  243. assert len(headcommit.hexsha) == 40
  244. assert len(headcommit.parents) > 0
  245. assert headcommit.tree.type == 'tree'
  246. assert len(headcommit.author.name) != 0
  247. assert isinstance(headcommit.authored_date, int)
  248. assert len(headcommit.committer.name) != 0
  249. assert isinstance(headcommit.committed_date, int)
  250. assert headcommit.message != ''
  251. # ![14-test_references_and_objects]
  252. # [15-test_references_and_objects]
  253. import time
  254. time.asctime(time.gmtime(headcommit.committed_date))
  255. time.strftime("%a, %d %b %Y %H:%M", time.gmtime(headcommit.committed_date))
  256. # ![15-test_references_and_objects]
  257. # [16-test_references_and_objects]
  258. assert headcommit.parents[0].parents[0].parents[0] == repo.commit('master^^^')
  259. # ![16-test_references_and_objects]
  260. # [17-test_references_and_objects]
  261. tree = repo.heads.master.commit.tree
  262. assert len(tree.hexsha) == 40
  263. # ![17-test_references_and_objects]
  264. # [18-test_references_and_objects]
  265. assert len(tree.trees) > 0 # trees are subdirectories
  266. assert len(tree.blobs) > 0 # blobs are files
  267. assert len(tree.blobs) + len(tree.trees) == len(tree)
  268. # ![18-test_references_and_objects]
  269. # [19-test_references_and_objects]
  270. self.assertEqual(tree['smmap'], tree / 'smmap') # access by index and by sub-path
  271. for entry in tree: # intuitive iteration of tree members
  272. print(entry)
  273. blob = tree.trees[0].blobs[0] # let's get a blob in a sub-tree
  274. assert blob.name
  275. assert len(blob.path) < len(blob.abspath)
  276. self.assertEqual(tree.trees[0].name + '/' + blob.name, blob.path) # this is how relative blob path generated
  277. self.assertEqual(tree[blob.path], blob) # you can use paths like 'dir/file' in tree
  278. # ![19-test_references_and_objects]
  279. # [20-test_references_and_objects]
  280. assert tree / 'smmap' == tree['smmap']
  281. assert tree / blob.path == tree[blob.path]
  282. # ![20-test_references_and_objects]
  283. # [21-test_references_and_objects]
  284. # This example shows the various types of allowed ref-specs
  285. assert repo.tree() == repo.head.commit.tree
  286. past = repo.commit('HEAD~5')
  287. assert repo.tree(past) == repo.tree(past.hexsha)
  288. self.assertEqual(repo.tree('v0.8.1').type, 'tree') # yes, you can provide any refspec - works everywhere
  289. # ![21-test_references_and_objects]
  290. # [22-test_references_and_objects]
  291. assert len(tree) < len(list(tree.traverse()))
  292. # ![22-test_references_and_objects]
  293. # [23-test_references_and_objects]
  294. index = repo.index
  295. # The index contains all blobs in a flat list
  296. assert len(list(index.iter_blobs())) == len([o for o in repo.head.commit.tree.traverse() if o.type == 'blob'])
  297. # Access blob objects
  298. for (path, stage), entry in index.entries.items(): # @UnusedVariable
  299. pass
  300. new_file_path = os.path.join(repo.working_tree_dir, 'new-file-name')
  301. open(new_file_path, 'w').close()
  302. index.add([new_file_path]) # add a new file to the index
  303. index.remove(['LICENSE']) # remove an existing one
  304. assert os.path.isfile(os.path.join(repo.working_tree_dir, 'LICENSE')) # working tree is untouched
  305. self.assertEqual(index.commit("my commit message").type, 'commit') # commit changed index
  306. repo.active_branch.commit = repo.commit('HEAD~1') # forget last commit
  307. from git import Actor
  308. author = Actor("An author", "author@example.com")
  309. committer = Actor("A committer", "committer@example.com")
  310. # commit by commit message and author and committer
  311. index.commit("my commit message", author=author, committer=committer)
  312. # ![23-test_references_and_objects]
  313. # [24-test_references_and_objects]
  314. from git import IndexFile
  315. # loads a tree into a temporary index, which exists just in memory
  316. IndexFile.from_tree(repo, 'HEAD~1')
  317. # merge two trees three-way into memory
  318. merge_index = IndexFile.from_tree(repo, 'HEAD~10', 'HEAD', repo.merge_base('HEAD~10', 'HEAD'))
  319. # and persist it
  320. merge_index.write(os.path.join(rw_dir, 'merged_index'))
  321. # ![24-test_references_and_objects]
  322. # [25-test_references_and_objects]
  323. empty_repo = git.Repo.init(os.path.join(rw_dir, 'empty'))
  324. origin = empty_repo.create_remote('origin', repo.remotes.origin.url)
  325. assert origin.exists()
  326. assert origin == empty_repo.remotes.origin == empty_repo.remotes['origin']
  327. origin.fetch() # assure we actually have data. fetch() returns useful information
  328. # Setup a local tracking branch of a remote branch
  329. empty_repo.create_head('master', origin.refs.master) # create local branch "master" from remote "master"
  330. empty_repo.heads.master.set_tracking_branch(origin.refs.master) # set local "master" to track remote "master
  331. empty_repo.heads.master.checkout() # checkout local "master" to working tree
  332. # Three above commands in one:
  333. empty_repo.create_head('master', origin.refs.master).set_tracking_branch(origin.refs.master).checkout()
  334. # rename remotes
  335. origin.rename('new_origin')
  336. # push and pull behaves similarly to `git push|pull`
  337. origin.pull()
  338. origin.push()
  339. # assert not empty_repo.delete_remote(origin).exists() # create and delete remotes
  340. # ![25-test_references_and_objects]
  341. # [26-test_references_and_objects]
  342. assert origin.url == repo.remotes.origin.url
  343. with origin.config_writer as cw:
  344. cw.set("pushurl", "other_url")
  345. # Please note that in python 2, writing origin.config_writer.set(...) is totally safe.
  346. # In py3 __del__ calls can be delayed, thus not writing changes in time.
  347. # ![26-test_references_and_objects]
  348. # [27-test_references_and_objects]
  349. hcommit = repo.head.commit
  350. hcommit.diff() # diff tree against index
  351. hcommit.diff('HEAD~1') # diff tree against previous tree
  352. hcommit.diff(None) # diff tree against working tree
  353. index = repo.index
  354. index.diff() # diff index against itself yielding empty diff
  355. index.diff(None) # diff index against working copy
  356. index.diff('HEAD') # diff index against current HEAD tree
  357. # ![27-test_references_and_objects]
  358. # [28-test_references_and_objects]
  359. # Traverse added Diff objects only
  360. for diff_added in hcommit.diff('HEAD~1').iter_change_type('A'):
  361. print(diff_added)
  362. # ![28-test_references_and_objects]
  363. # [29-test_references_and_objects]
  364. # Reset our working tree 10 commits into the past
  365. past_branch = repo.create_head('past_branch', 'HEAD~10')
  366. repo.head.reference = past_branch
  367. assert not repo.head.is_detached
  368. # reset the index and working tree to match the pointed-to commit
  369. repo.head.reset(index=True, working_tree=True)
  370. # To detach your head, you have to point to a commit directly
  371. repo.head.reference = repo.commit('HEAD~5')
  372. assert repo.head.is_detached
  373. # now our head points 15 commits into the past, whereas the working tree
  374. # and index are 10 commits in the past
  375. # ![29-test_references_and_objects]
  376. # [30-test_references_and_objects]
  377. # checkout the branch using git-checkout. It will fail as the working tree appears dirty
  378. self.failUnlessRaises(git.GitCommandError, repo.heads.master.checkout)
  379. repo.heads.past_branch.checkout()
  380. # ![30-test_references_and_objects]
  381. # [31-test_references_and_objects]
  382. git = repo.git
  383. git.checkout('HEAD', b="my_new_branch") # create a new branch
  384. git.branch('another-new-one')
  385. git.branch('-D', 'another-new-one') # pass strings for full control over argument order
  386. git.for_each_ref() # '-' becomes '_' when calling it
  387. # ![31-test_references_and_objects]
  388. repo.git.clear_cache()
  389. def test_submodules(self):
  390. # [1-test_submodules]
  391. repo = self.rorepo
  392. sms = repo.submodules
  393. assert len(sms) == 1
  394. sm = sms[0]
  395. self.assertEqual(sm.name, 'gitdb') # git-python has gitdb as single submodule ...
  396. self.assertEqual(sm.children()[0].name, 'smmap') # ... which has smmap as single submodule
  397. # The module is the repository referenced by the submodule
  398. assert sm.module_exists() # the module is available, which doesn't have to be the case.
  399. assert sm.module().working_tree_dir.endswith('gitdb')
  400. # the submodule's absolute path is the module's path
  401. assert sm.abspath == sm.module().working_tree_dir
  402. self.assertEqual(len(sm.hexsha), 40) # Its sha defines the commit to checkout
  403. assert sm.exists() # yes, this submodule is valid and exists
  404. # read its configuration conveniently
  405. assert sm.config_reader().get_value('path') == sm.path
  406. self.assertEqual(len(sm.children()), 1) # query the submodule hierarchy
  407. # ![1-test_submodules]
  408. @with_rw_directory
  409. def test_add_file_and_commit(self, rw_dir):
  410. import git
  411. repo_dir = os.path.join(rw_dir, 'my-new-repo')
  412. file_name = os.path.join(repo_dir, 'new-file')
  413. r = git.Repo.init(repo_dir)
  414. # This function just creates an empty file ...
  415. open(file_name, 'wb').close()
  416. r.index.add([file_name])
  417. r.index.commit("initial commit")
  418. # ![test_add_file_and_commit]