diff --git a/dulwich/reflog.py b/dulwich/reflog.py
index 73d11886..0574ddc3 100644
--- a/dulwich/reflog.py
+++ b/dulwich/reflog.py
@@ -1,95 +1,154 @@
# reflog.py -- Parsing and writing reflog files
# Copyright (C) 2015 Jelmer Vernooij and others.
#
# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
# General Public License as public by the Free Software Foundation; version 2.0
# or (at your option) any later version. You can redistribute it and/or
# modify it under the terms of either of these two licenses.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# You should have received a copy of the licenses; if not, see
# for a copy of the GNU General Public License
# and for a copy of the Apache
# License, Version 2.0.
#
"""Utilities for reading and generating reflogs.
"""
import collections
from dulwich.objects import (
format_timezone,
parse_timezone,
ZERO_SHA,
)
Entry = collections.namedtuple(
"Entry",
["old_sha", "new_sha", "committer", "timestamp", "timezone", "message"],
)
def format_reflog_line(old_sha, new_sha, committer, timestamp, timezone, message):
"""Generate a single reflog line.
Args:
old_sha: Old Commit SHA
new_sha: New Commit SHA
committer: Committer name and e-mail
timestamp: Timestamp
timezone: Timezone
message: Message
"""
if old_sha is None:
old_sha = ZERO_SHA
return (
old_sha
+ b" "
+ new_sha
+ b" "
+ committer
+ b" "
+ str(int(timestamp)).encode("ascii")
+ b" "
+ format_timezone(timezone)
+ b"\t"
+ message
)
def parse_reflog_line(line):
"""Parse a reflog line.
Args:
line: Line to parse
Returns: Tuple of (old_sha, new_sha, committer, timestamp, timezone,
message)
"""
(begin, message) = line.split(b"\t", 1)
(old_sha, new_sha, rest) = begin.split(b" ", 2)
(committer, timestamp_str, timezone_str) = rest.rsplit(b" ", 2)
return Entry(
old_sha,
new_sha,
committer,
int(timestamp_str),
parse_timezone(timezone_str)[0],
message,
)
def read_reflog(f):
"""Read reflog.
Args:
f: File-like object
Returns: Iterator over Entry objects
"""
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 2f6df051..c8bfcaf5 100644
--- a/dulwich/tests/test_reflog.py
+++ b/dulwich/tests/test_reflog.py
@@ -1,84 +1,146 @@
# test_reflog.py -- tests for reflog.py
# encoding: utf-8
# Copyright (C) 2015 Jelmer Vernooij
#
# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
# General Public License as public by the Free Software Foundation; version 2.0
# or (at your option) any later version. You can redistribute it and/or
# modify it under the terms of either of these two licenses.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# You should have received a copy of the licenses; if not, see
# for a copy of the GNU General Public License
# and for a copy of the Apache
# License, Version 2.0.
#
"""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 (
TestCase,
)
class ReflogLineTests(TestCase):
def test_format(self):
self.assertEqual(
b"0000000000000000000000000000000000000000 "
b"49030649db3dfec5a9bc03e5dde4255a14499f16 Jelmer Vernooij "
b" 1446552482 +0000 "
b"clone: from git://jelmer.uk/samba",
format_reflog_line(
b"0000000000000000000000000000000000000000",
b"49030649db3dfec5a9bc03e5dde4255a14499f16",
b"Jelmer Vernooij ",
1446552482,
0,
b"clone: from git://jelmer.uk/samba",
),
)
self.assertEqual(
b"0000000000000000000000000000000000000000 "
b"49030649db3dfec5a9bc03e5dde4255a14499f16 Jelmer Vernooij "
b" 1446552482 +0000 "
b"clone: from git://jelmer.uk/samba",
format_reflog_line(
None,
b"49030649db3dfec5a9bc03e5dde4255a14499f16",
b"Jelmer Vernooij ",
1446552482,
0,
b"clone: from git://jelmer.uk/samba",
),
)
def test_parse(self):
reflog_line = (
b"0000000000000000000000000000000000000000 "
b"49030649db3dfec5a9bc03e5dde4255a14499f16 Jelmer Vernooij "
b" 1446552482 +0000 "
b"clone: from git://jelmer.uk/samba"
)
self.assertEqual(
(
b"0000000000000000000000000000000000000000",
b"49030649db3dfec5a9bc03e5dde4255a14499f16",
b"Jelmer Vernooij ",
1446552482,
0,
b"clone: from git://jelmer.uk/samba",
),
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)