From 231eca441cac937dda81c8ba71978ee8808f470f Mon Sep 17 00:00:00 2001 From: Peter Rowlands Date: Tue, 16 Feb 2021 18:23:22 +0900 Subject: [PATCH 1/3] reflog: add drop_reflog_entry method --- dulwich/reflog.py | 59 ++++++++++++++++++++++++++++++++++ dulwich/tests/test_reflog.py | 62 ++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/dulwich/reflog.py b/dulwich/reflog.py index 73d11886a..0574ddc3d 100644 --- a/dulwich/reflog.py +++ b/dulwich/reflog.py @@ -93,3 +93,62 @@ def read_reflog(f): """ for line in f: yield parse_reflog_line(line) + + +def drop_reflog_entry(f, index, rewrite=False): + """Drop the specified reflog entry. + + Args: + f: File-like object + index: Reflog entry index (in Git reflog reverse 0-indexed order) + rewrite: If a reflog entry's predecessor is removed, set its + old SHA to the new SHA of the entry that now precedes it + """ + if index < 0: + raise ValueError("Invalid reflog index %d" % index) + + log = [] + offset = f.tell() + for line in f: + log.append((offset, parse_reflog_line(line))) + offset = f.tell() + + inverse_index = len(log) - index - 1 + write_offset = log[inverse_index][0] + f.seek(write_offset) + + if index == 0: + f.truncate() + return + + del log[inverse_index] + if rewrite and index > 0 and log: + if inverse_index == 0: + previous_new = ZERO_SHA + else: + previous_new = log[inverse_index - 1][1].new_sha + offset, entry = log[inverse_index] + log[inverse_index] = ( + offset, + Entry( + previous_new, + entry.new_sha, + entry.committer, + entry.timestamp, + entry.timezone, + entry.message, + ), + ) + + for _, entry in log[inverse_index:]: + f.write( + format_reflog_line( + entry.old_sha, + entry.new_sha, + entry.committer, + entry.timestamp, + entry.timezone, + entry.message, + ) + ) + f.truncate() diff --git a/dulwich/tests/test_reflog.py b/dulwich/tests/test_reflog.py index 2f6df051c..c8bfcaf52 100644 --- a/dulwich/tests/test_reflog.py +++ b/dulwich/tests/test_reflog.py @@ -21,10 +21,14 @@ """Tests for dulwich.reflog.""" +from io import BytesIO +from dulwich.objects import ZERO_SHA from dulwich.reflog import ( + drop_reflog_entry, format_reflog_line, parse_reflog_line, + read_reflog, ) from dulwich.tests import ( @@ -82,3 +86,61 @@ def test_parse(self): ), parse_reflog_line(reflog_line), ) + + +_TEST_REFLOG = ( + b"0000000000000000000000000000000000000000 " + b"49030649db3dfec5a9bc03e5dde4255a14499f16 Jelmer Vernooij " + b" 1446552482 +0000 " + b"clone: from git://jelmer.uk/samba\n" + b"49030649db3dfec5a9bc03e5dde4255a14499f16 " + b"42d06bd4b77fed026b154d16493e5deab78f02ec Jelmer Vernooij " + b" 1446552483 +0000 " + b"clone: from git://jelmer.uk/samba\n" + b"42d06bd4b77fed026b154d16493e5deab78f02ec " + b"df6800012397fb85c56e7418dd4eb9405dee075c Jelmer Vernooij " + b" 1446552484 +0000 " + b"clone: from git://jelmer.uk/samba\n" +) + + +class ReflogDropTests(TestCase): + def setUp(self): + TestCase.setUp(self) + self.f = BytesIO(_TEST_REFLOG) + self.original_log = list(read_reflog(self.f)) + self.f.seek(0) + + def _read_log(self): + self.f.seek(0) + return list(read_reflog(self.f)) + + def test_invalid(self): + self.assertRaises(ValueError, drop_reflog_entry, self.f, -1) + + def test_drop_entry(self): + drop_reflog_entry(self.f, 0) + log = self._read_log() + self.assertEqual(len(log), 2) + self.assertEqual(self.original_log[0:2], log) + + self.f.seek(0) + drop_reflog_entry(self.f, 1) + log = self._read_log() + self.assertEqual(len(log), 1) + self.assertEqual(self.original_log[1], log[0]) + + def test_drop_entry_with_rewrite(self): + drop_reflog_entry(self.f, 1, True) + log = self._read_log() + self.assertEqual(len(log), 2) + self.assertEqual(self.original_log[0], log[0]) + self.assertEqual(self.original_log[0].new_sha, log[1].old_sha) + self.assertEqual(self.original_log[2].new_sha, log[1].new_sha) + + self.f.seek(0) + drop_reflog_entry(self.f, 1, True) + log = self._read_log() + self.assertEqual(len(log), 1) + self.assertEqual(ZERO_SHA, log[0].old_sha) + self.assertEqual(self.original_log[2].new_sha, log[0].new_sha) From 7c5d593860643f02d69740e1aaf52fc0b33dadca Mon Sep 17 00:00:00 2001 From: Peter Rowlands Date: Wed, 17 Feb 2021 14:59:17 +0900 Subject: [PATCH 2/3] stash: implement drop --- dulwich/stash.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/dulwich/stash.py b/dulwich/stash.py index 11a28f861..6a77ed4ce 100644 --- a/dulwich/stash.py +++ b/dulwich/stash.py @@ -29,7 +29,7 @@ commit_tree, iter_fresh_objects, ) -from dulwich.reflog import read_reflog +from dulwich.reflog import drop_reflog_entry, read_reflog DEFAULT_STASH_REF = b"refs/stash" @@ -44,13 +44,13 @@ class Stash(object): def __init__(self, repo, ref=DEFAULT_STASH_REF): self._ref = ref self._repo = repo - - def stashes(self): - reflog_path = os.path.join( + self._reflog_path = os.path.join( self._repo.commondir(), "logs", os.fsdecode(self._ref) ) + + def stashes(self): try: - with GitFile(reflog_path, "rb") as f: + with GitFile(self._reflog_path, "rb") as f: return reversed(list(read_reflog(f))) except FileNotFoundError: return [] @@ -62,7 +62,14 @@ def from_repo(cls, repo): def drop(self, index): """Drop entry with specified index.""" - raise NotImplementedError(self.drop) + with open(self._reflog_path, "rb+") as f: + drop_reflog_entry(f, index, rewrite=True) + if len(self) == 0: + os.remove(self._reflog_path) + del self._repo.refs[self._ref] + return + if index == 0: + self._repo.refs[self._ref] = self[0].new_sha def pop(self, index): raise NotImplementedError(self.drop) From 1039e7c0de5ef0bf4642e7030a5f2bbfd196a2ce Mon Sep 17 00:00:00 2001 From: Peter Rowlands Date: Wed, 17 Feb 2021 15:11:44 +0900 Subject: [PATCH 3/3] make stash._reflog_path a property --- dulwich/stash.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dulwich/stash.py b/dulwich/stash.py index 6a77ed4ce..2df0106f2 100644 --- a/dulwich/stash.py +++ b/dulwich/stash.py @@ -44,7 +44,10 @@ class Stash(object): def __init__(self, repo, ref=DEFAULT_STASH_REF): self._ref = ref self._repo = repo - self._reflog_path = os.path.join( + + @property + def _reflog_path(self): + return os.path.join( self._repo.commondir(), "logs", os.fsdecode(self._ref) )