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/stash.py b/dulwich/stash.py
index 11a28f86..a2b6e13a 100644
--- a/dulwich/stash.py
+++ b/dulwich/stash.py
@@ -1,125 +1,135 @@
# stash.py
# Copyright (C) 2018 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.
#
"""Stash handling."""
from __future__ import absolute_import
import os
from dulwich.file import GitFile
from dulwich.index import (
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"
class Stash(object):
"""A Git stash.
Note that this doesn't currently update the working tree.
"""
def __init__(self, repo, ref=DEFAULT_STASH_REF):
self._ref = ref
self._repo = repo
- def stashes(self):
- reflog_path = os.path.join(
+ @property
+ def _reflog_path(self):
+ return 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 []
@classmethod
def from_repo(cls, repo):
"""Create a new stash from a Repo object."""
return 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)
+ raise NotImplementedError(self.pop)
def push(self, committer=None, author=None, message=None):
"""Create a new stash.
Args:
committer: Optional committer name to use
author: Optional author name to use
message: Optional commit message
"""
# First, create the index commit.
commit_kwargs = {}
if committer is not None:
commit_kwargs["committer"] = committer
if author is not None:
commit_kwargs["author"] = author
index = self._repo.open_index()
index_tree_id = index.commit(self._repo.object_store)
index_commit_id = self._repo.do_commit(
ref=None,
tree=index_tree_id,
message=b"Index stash",
merge_heads=[self._repo.head()],
**commit_kwargs
)
# Then, the working tree one.
stash_tree_id = commit_tree(
self._repo.object_store,
iter_fresh_objects(
index,
os.fsencode(self._repo.path),
object_store=self._repo.object_store,
),
)
if message is None:
message = b"A stash on " + self._repo.head()
# TODO(jelmer): Just pass parents into do_commit()?
self._repo.refs[self._ref] = self._repo.head()
cid = self._repo.do_commit(
ref=self._ref,
tree=stash_tree_id,
message=message,
merge_heads=[index_commit_id],
**commit_kwargs
)
return cid
def __getitem__(self, index):
return list(self.stashes())[index]
def __len__(self):
return len(list(self.stashes()))
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)