def test_index_file_base(self): # read from file index = IndexFile(self.rorepo, fixture_path("index")) assert index.entries assert index.version > 0 # test entry entry = next(iter(index.entries.values())) for attr in ("path", "ctime", "mtime", "dev", "inode", "mode", "uid", "gid", "size", "binsha", "hexsha", "stage"): getattr(entry, attr) # END for each method # test update entries = index.entries assert isinstance(index.update(), IndexFile) assert entries is not index.entries # test stage index_merge = IndexFile(self.rorepo, fixture_path("index_merge")) self.assertEqual(len(index_merge.entries), 106) assert len(list(e for e in index_merge.entries.values() if e.stage != 0)) # write the data - it must match the original tmpfile = tempfile.mktemp() index_merge.write(tmpfile) with open(tmpfile, 'rb') as fp: self.assertEqual(fp.read(), fixture("index_merge")) os.remove(tmpfile)
def test_index_merge_tree(self, rw_repo): # A bit out of place, but we need a different repo for this: self.assertNotEqual(self.rorepo, rw_repo) self.assertEqual(len(set((self.rorepo, self.rorepo, rw_repo, rw_repo))), 2) # SINGLE TREE MERGE # current index is at the (virtual) cur_commit next_commit = "4c39f9da792792d4e73fc3a5effde66576ae128c" parent_commit = rw_repo.head.commit.parents[0] manifest_key = IndexFile.entry_key('MANIFEST.in', 0) manifest_entry = rw_repo.index.entries[manifest_key] rw_repo.index.merge_tree(next_commit) # only one change should be recorded assert manifest_entry.binsha != rw_repo.index.entries[manifest_key].binsha rw_repo.index.reset(rw_repo.head) self.assertEqual(rw_repo.index.entries[manifest_key].binsha, manifest_entry.binsha) # FAKE MERGE ############# # Add a change with a NULL sha that should conflict with next_commit. We # pretend there was a change, but we do not even bother adding a proper # sha for it ( which makes things faster of course ) manifest_fake_entry = BaseIndexEntry((manifest_entry[0], b"\0" * 20, 0, manifest_entry[3])) # try write flag self._assert_entries(rw_repo.index.add([manifest_fake_entry], write=False)) # add actually resolves the null-hex-sha for us as a feature, but we can # edit the index manually assert rw_repo.index.entries[manifest_key].binsha != Object.NULL_BIN_SHA # must operate on the same index for this ! Its a bit problematic as # it might confuse people index = rw_repo.index index.entries[manifest_key] = IndexEntry.from_base(manifest_fake_entry) index.write() self.assertEqual(rw_repo.index.entries[manifest_key].hexsha, Diff.NULL_HEX_SHA) # write an unchanged index ( just for the fun of it ) rw_repo.index.write() # a three way merge would result in a conflict and fails as the command will # not overwrite any entries in our index and hence leave them unmerged. This is # mainly a protection feature as the current index is not yet in a tree self.failUnlessRaises(GitCommandError, index.merge_tree, next_commit, base=parent_commit) # the only way to get the merged entries is to safe the current index away into a tree, # which is like a temporary commit for us. This fails as well as the NULL sha deos not # have a corresponding object # NOTE: missing_ok is not a kwarg anymore, missing_ok is always true # self.failUnlessRaises(GitCommandError, index.write_tree) # if missing objects are okay, this would work though ( they are always okay now ) # As we can't read back the tree with NULL_SHA, we rather set it to something else index.entries[manifest_key] = IndexEntry(manifest_entry[:1] + (hex_to_bin('f' * 40),) + manifest_entry[2:]) tree = index.write_tree() # now make a proper three way merge with unmerged entries unmerged_tree = IndexFile.from_tree(rw_repo, parent_commit, tree, next_commit) unmerged_blobs = unmerged_tree.unmerged_blobs() self.assertEqual(len(unmerged_blobs), 1) self.assertEqual(list(unmerged_blobs.keys())[0], manifest_key[0])
def _git_rel_path(path): try: repo = Repo(path, search_parent_directories=True) index = IndexFile(repo) path = Path(index._to_relative_path(path)) return path.as_posix() except Exception: return ''
def test_index_file_from_tree(self, rw_repo): if sys.version_info < (2, 7): ## Skipped, not `assertRaisesRegexp` in py2.6 return common_ancestor_sha = "5117c9c8a4d3af19a9958677e45cda9269de1541" cur_sha = "4b43ca7ff72d5f535134241e7c797ddc9c7a3573" other_sha = "39f85c4358b7346fee22169da9cad93901ea9eb9" # simple index from tree base_index = IndexFile.from_tree(rw_repo, common_ancestor_sha) assert base_index.entries self._cmp_tree_index(common_ancestor_sha, base_index) # merge two trees - its like a fast-forward two_way_index = IndexFile.from_tree(rw_repo, common_ancestor_sha, cur_sha) assert two_way_index.entries self._cmp_tree_index(cur_sha, two_way_index) # merge three trees - here we have a merge conflict three_way_index = IndexFile.from_tree(rw_repo, common_ancestor_sha, cur_sha, other_sha) assert len( list(e for e in three_way_index.entries.values() if e.stage != 0)) # ITERATE BLOBS merge_required = lambda t: t[0] != 0 merge_blobs = list(three_way_index.iter_blobs(merge_required)) assert merge_blobs assert merge_blobs[0][0] in (1, 2, 3) assert isinstance(merge_blobs[0][1], Blob) # test BlobFilter prefix = 'lib/git' for stage, blob in base_index.iter_blobs(BlobFilter( [prefix])): # @UnusedVariable assert blob.path.startswith(prefix) # writing a tree should fail with an unmerged index self.failUnlessRaises(UnmergedEntriesError, three_way_index.write_tree) # removed unmerged entries unmerged_blob_map = three_way_index.unmerged_blobs() assert unmerged_blob_map # pick the first blob at the first stage we find and use it as resolved version three_way_index.resolve_blobs(l[0][1] for l in unmerged_blob_map.values()) tree = three_way_index.write_tree() assert isinstance(tree, Tree) num_blobs = 0 for blob in tree.traverse( predicate=lambda item, d: item.type == "blob"): assert (blob.path, 0) in three_way_index.entries num_blobs += 1 # END for each blob self.assertEqual(num_blobs, len(three_way_index.entries))
def test_index_file_from_tree(self, rw_repo): if sys.version_info < (2, 7): ## Skipped, not `assertRaisesRegexp` in py2.6 return common_ancestor_sha = "5117c9c8a4d3af19a9958677e45cda9269de1541" cur_sha = "4b43ca7ff72d5f535134241e7c797ddc9c7a3573" other_sha = "39f85c4358b7346fee22169da9cad93901ea9eb9" # simple index from tree base_index = IndexFile.from_tree(rw_repo, common_ancestor_sha) assert base_index.entries self._cmp_tree_index(common_ancestor_sha, base_index) # merge two trees - its like a fast-forward two_way_index = IndexFile.from_tree(rw_repo, common_ancestor_sha, cur_sha) assert two_way_index.entries self._cmp_tree_index(cur_sha, two_way_index) # merge three trees - here we have a merge conflict three_way_index = IndexFile.from_tree(rw_repo, common_ancestor_sha, cur_sha, other_sha) assert len(list(e for e in three_way_index.entries.values() if e.stage != 0)) # ITERATE BLOBS merge_required = lambda t: t[0] != 0 merge_blobs = list(three_way_index.iter_blobs(merge_required)) assert merge_blobs assert merge_blobs[0][0] in (1, 2, 3) assert isinstance(merge_blobs[0][1], Blob) # test BlobFilter prefix = 'lib/git' for stage, blob in base_index.iter_blobs(BlobFilter([prefix])): # @UnusedVariable assert blob.path.startswith(prefix) # writing a tree should fail with an unmerged index self.failUnlessRaises(UnmergedEntriesError, three_way_index.write_tree) # removed unmerged entries unmerged_blob_map = three_way_index.unmerged_blobs() assert unmerged_blob_map # pick the first blob at the first stage we find and use it as resolved version three_way_index.resolve_blobs(l[0][1] for l in unmerged_blob_map.values()) tree = three_way_index.write_tree() assert isinstance(tree, Tree) num_blobs = 0 for blob in tree.traverse(predicate=lambda item, d: item.type == "blob"): assert (blob.path, 0) in three_way_index.entries num_blobs += 1 # END for each blob self.assertEqual(num_blobs, len(three_way_index.entries))
def test__to_relative_path_at_root(self): root = osp.abspath(os.sep) class Mocked(object): bare = False git_dir = root working_tree_dir = root repo = Mocked() path = os.path.join(root, 'file') index = IndexFile(repo) rel = index._to_relative_path(path) self.assertEqual(rel, os.path.relpath(path, root))
def dependencies(self, revision='HEAD', paths=None): """Return dependencies from a revision or paths.""" result = [] if paths: paths = (self.normalize_path(path) for path in paths) else: if revision == 'HEAD': index = self.client.repo.index else: from git import IndexFile index = IndexFile.from_tree(self.client.repo, revision) paths = (path for path, _ in index.entries.keys()) for path in paths: try: result.append( Usage.from_revision( self.client, path=path, revision=revision, )) except KeyError: continue return result
def test_index_new(self): B = self.rorepo.tree("6d9b1f4f9fa8c9f030e3207e7deacc5d5f8bba4e") H = self.rorepo.tree("25dca42bac17d511b7e2ebdd9d1d679e7626db5f") M = self.rorepo.tree("e746f96bcc29238b79118123028ca170adc4ff0f") for args in ((B, ), (B, H), (B, H, M)): index = IndexFile.new(self.rorepo, *args) assert isinstance(index, IndexFile)
def test_index_new(self): B = self.rorepo.tree("6d9b1f4f9fa8c9f030e3207e7deacc5d5f8bba4e") H = self.rorepo.tree("25dca42bac17d511b7e2ebdd9d1d679e7626db5f") M = self.rorepo.tree("e746f96bcc29238b79118123028ca170adc4ff0f") for args in ((B,), (B, H), (B, H, M)): index = IndexFile.new(self.rorepo, *args) assert isinstance(index, IndexFile)
def test_index_file_base(self): # read from file index = IndexFile(self.rorepo, fixture_path("index")) assert index.entries assert index.version > 0 # test entry entry = next(iter(index.entries.values())) for attr in ("path", "ctime", "mtime", "dev", "inode", "mode", "uid", "gid", "size", "binsha", "hexsha", "stage"): getattr(entry, attr) # END for each method # test update entries = index.entries assert isinstance(index.update(), IndexFile) assert entries is not index.entries # test stage index_merge = IndexFile(self.rorepo, fixture_path("index_merge")) self.assertEqual(len(index_merge.entries), 106) assert len([e for e in index_merge.entries.values() if e.stage != 0]) # write the data - it must match the original tmpfile = tempfile.mktemp() index_merge.write(tmpfile) with open(tmpfile, 'rb') as fp: self.assertEqual(fp.read(), fixture("index_merge")) os.remove(tmpfile)
def add_data(self, index: git.IndexFile, path: str, data: bytes): """ Adds physical files to the database. WARNING: this function touches physical files in the Git Repo which can result in a race condition to modify a file while it is also being pushed. ONLY CALL THIS FUNCTION INSIDE A COMMIT_LOCK. @param index: @param path: @param data: @return: """ fullpath = os.path.join(os.path.dirname(index.repo.git_dir), path) pathlib.Path(fullpath).parent.mkdir(parents=True, exist_ok=True) with open(fullpath, 'wb') as fp: fp.write(data) index.add([fullpath])
def time_warp(self, c_o): """ History rewriting occurs here. We read everything from the original commit, reformat the python, and checkin, mirroring the original commit history. :param c_o: Commit object representing "before" :return: None """ log.info('warping: {} | {} | {:f} MB | {}s'.format( time_convert(c_o.authored_date, c_o.author_tz_offset), c_o.summary, resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1000, time.clock())) items = self.handle_commit(c_o) parent_commits = tuple(self.converted[v] for v in c_o.parents) # for the singular case of init / root / the genesis if len(parent_commits) == 0: parent_commits = {c_o} self.repo.head.reference = c_o self.repo.head.reset(index=True, working_tree=True) idx = IndexFile(self.repo) idx.add(items) idx.write_tree() com_msg = [c_o.message] com_msg.extend('\n'.join(self.convert_errors)) self.convert_errors = [] # todo: rearchitect - too easy to forget com_msg.append('\n[gitreformat yapf-ify (github/ghtdak) on {}]'.format( time.strftime('%c'))) com_msg.append('\n[from commit: {}]'.format(c_o.hexsha)) c_n = idx.commit(''.join(com_msg), parent_commits=parent_commits, author=c_o.author, author_date=time_convert(c_o.authored_date, c_o.author_tz_offset), committer=c_o.committer, commit_date=time_convert(c_o.committed_date, c_o.committer_tz_offset)) self.repo.head.reference = c_n self.repo.head.reset(index=True, working_tree=True) self.verify_paths(c_o.tree, c_n.tree) self.converted[c_o] = c_n return c_n
def git_commit(index: IndexFile, message: str) -> Commit: """ Commit all staged files in the current repo. :param IndexFile index: the current index of the current branch of the current repo :param message: the commit message to include with the files to be committed :return: a record of the commit (:py:class:`~git.objects.commit.Commit`), itself """ return index.commit(message, author=get_author())
def git_add(index: IndexFile, files) -> List[BaseIndexEntry]: """ `git add` all of the given files within the current repo :param IndexFile index: the current index of the current branch of the current repo :param files: the files to be `git-add`'ed :return: the entries just added """ return index.add(files)
def time_warp(self, c_o): """ History rewriting occurs here. We read everything from the original commit, reformat the python, and checkin, mirroring the original commit history. :param c_o: Commit object representing "before" :return: None """ log.info('warping: {} | {} | {:f} MB | {}s'.format( time_convert(c_o.authored_date, c_o.author_tz_offset), c_o.summary, resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1000, time.clock())) items = self.handle_commit(c_o) parent_commits = tuple(self.converted[v] for v in c_o.parents) # for the singular case of init / root / the genesis if len(parent_commits) == 0: parent_commits = {c_o} self.repo.head.reference = c_o self.repo.head.reset(index=True, working_tree=True) idx = IndexFile(self.repo) idx.add(items) idx.write_tree() com_msg = [c_o.message] com_msg.extend('\n'.join(self.convert_errors)) self.convert_errors = [] # todo: rearchitect - too easy to forget com_msg.append( '\n[gitreformat yapf-ify (github/ghtdak) on {}]'.format( time.strftime('%c'))) com_msg.append('\n[from commit: {}]'.format(c_o.hexsha)) c_n = idx.commit( ''.join(com_msg), parent_commits=parent_commits, author=c_o.author, author_date=time_convert(c_o.authored_date, c_o.author_tz_offset), committer=c_o.committer, commit_date=time_convert(c_o.committed_date, c_o.committer_tz_offset)) self.repo.head.reference = c_n self.repo.head.reset(index=True, working_tree=True) self.verify_paths(c_o.tree, c_n.tree) self.converted[c_o] = c_n return c_n
def build_status(self, revision='HEAD'): """Return files from the revision grouped by their status.""" index = self.client.git.index if revision == 'HEAD' \ else IndexFile.from_tree(self.client.git, revision) current_files = set() for filepath, _ in index.entries.keys(): if not filepath.startswith('.renku') and \ filepath not in {'.gitignore', '.gitattributes'}: self.add_file(filepath, revision=revision) current_files.add(filepath) # Prepare status info for each file. self._need_update() graph_files = sorted( ((commit, filepath) for (commit, filepath) in self.G if filepath in current_files), key=itemgetter(1)) status = {'up-to-date': {}, 'outdated': {}, 'multiple-versions': {}} for filepath, keys in groupby(graph_files, itemgetter(1)): keys = list(keys) if len(keys) > 1: status['multiple-versions'][filepath] = keys nodes = [self.G.nodes[key] for key in keys] # Any latest version of a file needs an update. is_outdated = any( len(node['_need_update']) > 1 for node in nodes if node['latest'] is None) if is_outdated: updates = list(self.G.nodes[key]['_need_update'] for key in keys) status['outdated'][filepath] = updates else: status['up-to-date'][filepath] = keys[0][0] return status
def dependencies(self, revision='HEAD', paths=None): """Return dependencies from a revision or paths.""" if paths: return { Usage.from_revision(self.client, path=path, revision=revision) for path in paths } if revision == 'HEAD': index = self.client.git.index else: from git import IndexFile index = IndexFile.from_tree(self.client.git, revision) return { Usage.from_revision( client=self.client, path=path, revision=revision, ) for path, _ in index.entries.keys() }
def add_data(index: git.IndexFile, path: str, data: bytes): fullpath = os.path.join(os.path.dirname(index.repo.git_dir), path) pathlib.Path(fullpath).parent.mkdir(parents=True, exist_ok=True) with open(fullpath, 'wb') as fp: fp.write(data) index.add([fullpath])
def squash_blame(preserve_committers): """Squash history while preserving blame""" repo = Repo() head_commit = repo.head.commit mutated_blobs = [] author_blob_lists = defaultdict(list) with click.progressbar(head_commit.tree.traverse()) as git_objects: for blob in git_objects: if isinstance(blob, Blob): blob_bytes = blob.data_stream.read() mutated_blob_bytes = blob_bytes.replace(b"\n", b"\r\n") mutated_blob = make_blob(repo, mutated_blob_bytes, blob.mode, blob.path) mutated_blobs.append(mutated_blob) author_lines = defaultdict(set) for blame in repo.blame_incremental(head_commit, blob.path): author_lines[blame.commit.author].update(blame.linenos) for author, authors_line_numbers in author_lines.items(): authors_fixed_blob_bytes_lines = [] for line_number, line in enumerate( mutated_blob_bytes.splitlines(keepends=True), start=1): if line_number in authors_line_numbers: line = line.replace(b"\r\n", b"\n") authors_fixed_blob_bytes_lines.append(line) authors_fixed_blob_bytes = b"".join( authors_fixed_blob_bytes_lines) authors_fixed_blob = make_blob(repo, authors_fixed_blob_bytes, blob.mode, blob.path) author_blob_lists[author].append(authors_fixed_blob) base_index = IndexFile.from_tree(repo, head_commit) base_index.add(mutated_blobs, write=False) new_root = base_index.commit(message="Initial commit", parent_commits=[], head=False) author_commits = [] for author, author_blobs in author_blob_lists.items(): index = IndexFile.from_tree(repo, new_root) index.add(author_blobs, write=False) author_commits.append( index.commit( message=f"Squash while preserving git-blame for {author.name}", parent_commits=[new_root], head=False, author=author, committer=author if preserve_committers else None, )) final_index = IndexFile.from_tree(repo, head_commit) final_commit = final_index.commit( message="Merge commits that preserve git-blame", parent_commits=author_commits, head=False, ) print(final_commit.hexsha)
def remove_data(self, index: git.IndexFile, path: str): fullpath = os.path.join(os.path.dirname(index.repo.git_dir), path) pathlib.Path(fullpath).parent.mkdir(parents=True, exist_ok=True) index.remove([fullpath], working_tree=True)
def test_index_file_diffing(self, rw_repo): # default Index instance points to our index index = IndexFile(rw_repo) assert index.path is not None assert len(index.entries) # write the file back index.write() # could sha it, or check stats # test diff # resetting the head will leave the index in a different state, and the # diff will yield a few changes cur_head_commit = rw_repo.head.reference.commit rw_repo.head.reset('HEAD~6', index=True, working_tree=False) # diff against same index is 0 diff = index.diff() self.assertEqual(len(diff), 0) # against HEAD as string, must be the same as it matches index diff = index.diff('HEAD') self.assertEqual(len(diff), 0) # against previous head, there must be a difference diff = index.diff(cur_head_commit) assert len(diff) # we reverse the result adiff = index.diff(str(cur_head_commit), R=True) odiff = index.diff(cur_head_commit, R=False) # now its not reversed anymore assert adiff != odiff self.assertEqual(odiff, diff) # both unreversed diffs against HEAD # against working copy - its still at cur_commit wdiff = index.diff(None) assert wdiff != adiff assert wdiff != odiff # against something unusual self.failUnlessRaises(ValueError, index.diff, int) # adjust the index to match an old revision cur_branch = rw_repo.active_branch cur_commit = cur_branch.commit rev_head_parent = 'HEAD~1' assert index.reset(rev_head_parent) is index self.assertEqual(cur_branch, rw_repo.active_branch) self.assertEqual(cur_commit, rw_repo.head.commit) # there must be differences towards the working tree which is in the 'future' assert index.diff(None) # reset the working copy as well to current head,to pull 'back' as well new_data = b"will be reverted" file_path = osp.join(rw_repo.working_tree_dir, "CHANGES") with open(file_path, "wb") as fp: fp.write(new_data) index.reset(rev_head_parent, working_tree=True) assert not index.diff(None) self.assertEqual(cur_branch, rw_repo.active_branch) self.assertEqual(cur_commit, rw_repo.head.commit) with open(file_path, 'rb') as fp: assert fp.read() != new_data # test full checkout test_file = osp.join(rw_repo.working_tree_dir, "CHANGES") with open(test_file, 'ab') as fd: fd.write(b"some data") rval = index.checkout(None, force=True, fprogress=self._fprogress) assert 'CHANGES' in list(rval) self._assert_fprogress([None]) assert osp.isfile(test_file) os.remove(test_file) rval = index.checkout(None, force=False, fprogress=self._fprogress) assert 'CHANGES' in list(rval) self._assert_fprogress([None]) assert osp.isfile(test_file) # individual file os.remove(test_file) rval = index.checkout(test_file, fprogress=self._fprogress) self.assertEqual(list(rval)[0], 'CHANGES') self._assert_fprogress([test_file]) assert osp.exists(test_file) # checking out non-existing file throws self.failUnlessRaises(CheckoutError, index.checkout, "doesnt_exist_ever.txt.that") self.failUnlessRaises(CheckoutError, index.checkout, paths=["doesnt/exist"]) # checkout file with modifications append_data = b"hello" with open(test_file, "ab") as fp: fp.write(append_data) try: index.checkout(test_file) except CheckoutError as e: self.assertEqual(len(e.failed_files), 1) self.assertEqual(e.failed_files[0], osp.basename(test_file)) self.assertEqual(len(e.failed_files), len(e.failed_reasons)) self.assertIsInstance(e.failed_reasons[0], str) self.assertEqual(len(e.valid_files), 0) with open(test_file, 'rb') as fd: s = fd.read() self.assertTrue(s.endswith(append_data), s) else: raise AssertionError("Exception CheckoutError not thrown") # if we force it it should work index.checkout(test_file, force=True) assert not open(test_file, 'rb').read().endswith(append_data) # checkout directory rmtree(osp.join(rw_repo.working_tree_dir, "lib")) rval = index.checkout('lib') assert len(list(rval)) > 1
def test_references_and_objects(self, rw_dir): # [1-test_references_and_objects] import git repo = git.Repo.clone_from(self._small_repo_url(), os.path.join(rw_dir, 'repo'), branch='master') heads = repo.heads master = heads.master # lists can be accessed by name for convenience master.commit # the commit pointed to by head called master master.rename('new_name') # rename heads master.rename('master') # ![1-test_references_and_objects] # [2-test_references_and_objects] tags = repo.tags tagref = tags[0] tagref.tag # tags may have tag objects carrying additional information tagref.commit # but they always point to commits repo.delete_tag(tagref) # delete or repo.create_tag("my_tag") # create tags using the repo for convenience # ![2-test_references_and_objects] # [3-test_references_and_objects] head = repo.head # the head points to the active branch/ref master = head.reference # retrieve the reference the head points to master.commit # from here you use it as any other reference # ![3-test_references_and_objects] # [4-test_references_and_objects] log = master.log() log[0] # first (i.e. oldest) reflog entry log[-1] # last (i.e. most recent) reflog entry # ![4-test_references_and_objects] # [5-test_references_and_objects] new_branch = repo.create_head('new') # create a new one new_branch.commit = 'HEAD~10' # set branch to another commit without changing index or working trees repo.delete_head(new_branch) # delete an existing head - only works if it is not checked out # ![5-test_references_and_objects] # [6-test_references_and_objects] new_tag = repo.create_tag('my_new_tag', message='my message') # You cannot change the commit a tag points to. Tags need to be re-created self.failUnlessRaises(AttributeError, setattr, new_tag, 'commit', repo.commit('HEAD~1')) repo.delete_tag(new_tag) # ![6-test_references_and_objects] # [7-test_references_and_objects] new_branch = repo.create_head('another-branch') repo.head.reference = new_branch # ![7-test_references_and_objects] # [8-test_references_and_objects] hc = repo.head.commit hct = hc.tree hc != hct hc != repo.tags[0] hc == repo.head.reference.commit # ![8-test_references_and_objects] # [9-test_references_and_objects] assert hct.type == 'tree' # preset string type, being a class attribute assert hct.size > 0 # size in bytes assert len(hct.hexsha) == 40 assert len(hct.binsha) == 20 # ![9-test_references_and_objects] # [10-test_references_and_objects] assert hct.path == '' # root tree has no path assert hct.trees[0].path != '' # the first contained item has one though assert hct.mode == 0o40000 # trees have the mode of a linux directory assert hct.blobs[0].mode == 0o100644 # blobs have a specific mode though comparable to a standard linux fs # ![10-test_references_and_objects] # [11-test_references_and_objects] hct.blobs[0].data_stream.read() # stream object to read data from hct.blobs[0].stream_data(open(os.path.join(rw_dir, 'blob_data'), 'wb')) # write data to given stream # ![11-test_references_and_objects] # [12-test_references_and_objects] repo.commit('master') repo.commit('v0.8.1') repo.commit('HEAD~10') # ![12-test_references_and_objects] # [13-test_references_and_objects] fifty_first_commits = list(repo.iter_commits('master', max_count=50)) assert len(fifty_first_commits) == 50 # this will return commits 21-30 from the commit list as traversed backwards master ten_commits_past_twenty = list(repo.iter_commits('master', max_count=10, skip=20)) assert len(ten_commits_past_twenty) == 10 assert fifty_first_commits[20:30] == ten_commits_past_twenty # ![13-test_references_and_objects] # [14-test_references_and_objects] headcommit = repo.head.commit assert len(headcommit.hexsha) == 40 assert len(headcommit.parents) > 0 assert headcommit.tree.type == 'tree' assert headcommit.author.name == 'Sebastian Thiel' assert isinstance(headcommit.authored_date, int) assert headcommit.committer.name == 'Sebastian Thiel' assert isinstance(headcommit.committed_date, int) assert headcommit.message != '' # ![14-test_references_and_objects] # [15-test_references_and_objects] import time time.asctime(time.gmtime(headcommit.committed_date)) time.strftime("%a, %d %b %Y %H:%M", time.gmtime(headcommit.committed_date)) # ![15-test_references_and_objects] # [16-test_references_and_objects] assert headcommit.parents[0].parents[0].parents[0] == repo.commit('master^^^') # ![16-test_references_and_objects] # [17-test_references_and_objects] tree = repo.heads.master.commit.tree assert len(tree.hexsha) == 40 # ![17-test_references_and_objects] # [18-test_references_and_objects] assert len(tree.trees) > 0 # trees are subdirectories assert len(tree.blobs) > 0 # blobs are files assert len(tree.blobs) + len(tree.trees) == len(tree) # ![18-test_references_and_objects] # [19-test_references_and_objects] assert tree['smmap'] == tree / 'smmap' # access by index and by sub-path for entry in tree: # intuitive iteration of tree members print(entry) blob = tree.trees[0].blobs[0] # let's get a blob in a sub-tree assert blob.name assert len(blob.path) < len(blob.abspath) assert tree.trees[0].name + '/' + blob.name == blob.path # this is how the relative blob path is generated assert tree[blob.path] == blob # you can use paths like 'dir/file' in tree[...] # ![19-test_references_and_objects] # [20-test_references_and_objects] assert tree / 'smmap' == tree['smmap'] assert tree / blob.path == tree[blob.path] # ![20-test_references_and_objects] # [21-test_references_and_objects] # This example shows the various types of allowed ref-specs assert repo.tree() == repo.head.commit.tree past = repo.commit('HEAD~5') assert repo.tree(past) == repo.tree(past.hexsha) assert repo.tree('v0.8.1').type == 'tree' # yes, you can provide any refspec - works everywhere # ![21-test_references_and_objects] # [22-test_references_and_objects] assert len(tree) < len(list(tree.traverse())) # ![22-test_references_and_objects] # [23-test_references_and_objects] index = repo.index # The index contains all blobs in a flat list assert len(list(index.iter_blobs())) == len([o for o in repo.head.commit.tree.traverse() if o.type == 'blob']) # Access blob objects for (path, stage), entry in index.entries.items(): pass new_file_path = os.path.join(repo.working_tree_dir, 'new-file-name') open(new_file_path, 'w').close() index.add([new_file_path]) # add a new file to the index index.remove(['LICENSE']) # remove an existing one assert os.path.isfile(os.path.join(repo.working_tree_dir, 'LICENSE')) # working tree is untouched assert index.commit("my commit message").type == 'commit' # commit changed index repo.active_branch.commit = repo.commit('HEAD~1') # forget last commit from git import Actor author = Actor("An author", "*****@*****.**") committer = Actor("A committer", "*****@*****.**") # commit by commit message and author and committer index.commit("my commit message", author=author, committer=committer) # ![23-test_references_and_objects] # [24-test_references_and_objects] from git import IndexFile # loads a tree into a temporary index, which exists just in memory IndexFile.from_tree(repo, 'HEAD~1') # merge two trees three-way into memory merge_index = IndexFile.from_tree(repo, 'HEAD~10', 'HEAD', repo.merge_base('HEAD~10', 'HEAD')) # and persist it merge_index.write(os.path.join(rw_dir, 'merged_index')) # ![24-test_references_and_objects] # [25-test_references_and_objects] empty_repo = git.Repo.init(os.path.join(rw_dir, 'empty')) origin = empty_repo.create_remote('origin', repo.remotes.origin.url) assert origin.exists() assert origin == empty_repo.remotes.origin == empty_repo.remotes['origin'] origin.fetch() # assure we actually have data. fetch() returns useful information # Setup a local tracking branch of a remote branch empty_repo.create_head('master', origin.refs.master).set_tracking_branch(origin.refs.master) origin.rename('new_origin') # rename remotes # push and pull behaves similarly to `git push|pull` origin.pull() origin.push() # assert not empty_repo.delete_remote(origin).exists() # create and delete remotes # ![25-test_references_and_objects] # [26-test_references_and_objects] assert origin.url == repo.remotes.origin.url cw = origin.config_writer cw.set("pushurl", "other_url") cw.release() # Please note that in python 2, writing origin.config_writer.set(...) is totally safe. # In py3 __del__ calls can be delayed, thus not writing changes in time. # ![26-test_references_and_objects] # [27-test_references_and_objects] hcommit = repo.head.commit hcommit.diff() # diff tree against index hcommit.diff('HEAD~1') # diff tree against previous tree hcommit.diff(None) # diff tree against working tree index = repo.index index.diff() # diff index against itself yielding empty diff index.diff(None) # diff index against working copy index.diff('HEAD') # diff index against current HEAD tree # ![27-test_references_and_objects] # [28-test_references_and_objects] # Traverse added Diff objects only for diff_added in hcommit.diff('HEAD~1').iter_change_type('A'): print(diff_added) # ![28-test_references_and_objects] # [29-test_references_and_objects] # Reset our working tree 10 commits into the past past_branch = repo.create_head('past_branch', 'HEAD~10') repo.head.reference = past_branch assert not repo.head.is_detached # reset the index and working tree to match the pointed-to commit repo.head.reset(index=True, working_tree=True) # To detach your head, you have to point to a commit directy repo.head.reference = repo.commit('HEAD~5') assert repo.head.is_detached # now our head points 15 commits into the past, whereas the working tree # and index are 10 commits in the past # ![29-test_references_and_objects] # [30-test_references_and_objects] # checkout the branch using git-checkout. It will fail as the working tree appears dirty self.failUnlessRaises(git.GitCommandError, repo.heads.master.checkout) repo.heads.past_branch.checkout() # ![30-test_references_and_objects] # [31-test_references_and_objects] git = repo.git git.checkout('HEAD', b="my_new_branch") # create a new branch git.branch('another-new-one') git.branch('-D', 'another-new-one') # pass strings for full control over argument order git.for_each_ref() # '-' becomes '_' when calling it # ![31-test_references_and_objects] # [32-test_references_and_objects] ssh_executable = os.path.join(rw_dir, 'my_ssh_executable.sh') with repo.git.custom_environment(GIT_SSH=ssh_executable): # Note that we don't actually make the call here, as our test-setup doesn't permit it to # succeed. # It will in your case :) repo.remotes.origin.fetch
def test_index_file_diffing(self, rw_repo): # default Index instance points to our index index = IndexFile(rw_repo) assert index.path is not None assert len(index.entries) # write the file back index.write() # could sha it, or check stats # test diff # resetting the head will leave the index in a different state, and the # diff will yield a few changes cur_head_commit = rw_repo.head.reference.commit rw_repo.head.reset('HEAD~6', index=True, working_tree=False) # diff against same index is 0 diff = index.diff() self.assertEqual(len(diff), 0) # against HEAD as string, must be the same as it matches index diff = index.diff('HEAD') self.assertEqual(len(diff), 0) # against previous head, there must be a difference diff = index.diff(cur_head_commit) assert len(diff) # we reverse the result adiff = index.diff(str(cur_head_commit), R=True) odiff = index.diff(cur_head_commit, R=False) # now its not reversed anymore assert adiff != odiff self.assertEqual(odiff, diff) # both unreversed diffs against HEAD # against working copy - its still at cur_commit wdiff = index.diff(None) assert wdiff != adiff assert wdiff != odiff # against something unusual self.failUnlessRaises(ValueError, index.diff, int) # adjust the index to match an old revision cur_branch = rw_repo.active_branch cur_commit = cur_branch.commit rev_head_parent = 'HEAD~1' assert index.reset(rev_head_parent) is index self.assertEqual(cur_branch, rw_repo.active_branch) self.assertEqual(cur_commit, rw_repo.head.commit) # there must be differences towards the working tree which is in the 'future' assert index.diff(None) # reset the working copy as well to current head,to pull 'back' as well new_data = b"will be reverted" file_path = osp.join(rw_repo.working_tree_dir, "CHANGES") with open(file_path, "wb") as fp: fp.write(new_data) index.reset(rev_head_parent, working_tree=True) assert not index.diff(None) self.assertEqual(cur_branch, rw_repo.active_branch) self.assertEqual(cur_commit, rw_repo.head.commit) with open(file_path, 'rb') as fp: assert fp.read() != new_data # test full checkout test_file = osp.join(rw_repo.working_tree_dir, "CHANGES") with open(test_file, 'ab') as fd: fd.write(b"some data") rval = index.checkout(None, force=True, fprogress=self._fprogress) assert 'CHANGES' in list(rval) self._assert_fprogress([None]) assert osp.isfile(test_file) os.remove(test_file) rval = index.checkout(None, force=False, fprogress=self._fprogress) assert 'CHANGES' in list(rval) self._assert_fprogress([None]) assert osp.isfile(test_file) # individual file os.remove(test_file) rval = index.checkout(test_file, fprogress=self._fprogress) self.assertEqual(list(rval)[0], 'CHANGES') self._assert_fprogress([test_file]) assert osp.exists(test_file) # checking out non-existing file throws self.failUnlessRaises(CheckoutError, index.checkout, "doesnt_exist_ever.txt.that") self.failUnlessRaises(CheckoutError, index.checkout, paths=["doesnt/exist"]) # checkout file with modifications append_data = b"hello" with open(test_file, "ab") as fp: fp.write(append_data) try: index.checkout(test_file) except CheckoutError as e: self.assertEqual(len(e.failed_files), 1) self.assertEqual(e.failed_files[0], osp.basename(test_file)) self.assertEqual(len(e.failed_files), len(e.failed_reasons)) self.assertIsInstance(e.failed_reasons[0], string_types) self.assertEqual(len(e.valid_files), 0) with open(test_file, 'rb') as fd: s = fd.read() self.assertTrue(s.endswith(append_data), s) else: raise AssertionError("Exception CheckoutError not thrown") # if we force it it should work index.checkout(test_file, force=True) assert not open(test_file, 'rb').read().endswith(append_data) # checkout directory rmtree(osp.join(rw_repo.working_tree_dir, "lib")) rval = index.checkout('lib') assert len(list(rval)) > 1