commit - 662b2117d16044d0c348a48e316229a2da046adf
commit + 0078a59e44b767f958279733ef22bc7003d623e1
blob - /dev/null
blob + 7ba030843e34b0f252f25fd96db5b09f75882c1a (mode 644)
--- /dev/null
+++ .github/FUNDING.yml
+github: jelmer
blob - 41df77cb18f9fe41ee34b2ea91f62eb998ccbe15
blob + c51ea2312778272fecff47edc8015963042d288b
--- NEWS
+++ NEWS
+0.20.30 2022-01-08
+
+0.20.29 2022-01-08
+
+ * Support staging submodules.
+ (Jelmer Vernooij)
+
+ * Drop deprecated Index.iterblobs and iter_fresh_blobs.
+ (Jelmer Vernooij)
+
+ * Unify clone behaviour of ``Repo.clone`` and
+ ``porcelain.clone``, and add branch parameter for
+ clone. (Peter Rowlands, #851)
+
+0.20.28 2022-01-05
+
+ * Fix hook test on Mac OSX / Linux when dulwich is
+ not installed system-wide. (Jelmer Vernooij, #919)
+
+ * Cope with gecos being unset.
+ (Jelmer Vernooij, #917)
+
0.20.27 2022-01-04
* Allow adding files to repository in pre-commit hook.
blob - 3f3f472b7348f4aa816b6ebe9616187914f8c6b9
blob + fc2a42b75edd1cae3f05952f180f61368145a86c
--- PKG-INFO
+++ PKG-INFO
Metadata-Version: 2.1
Name: dulwich
-Version: 0.20.27
+Version: 0.20.30
Summary: Python Git Library
Home-page: https://www.dulwich.io/
Author: Jelmer Vernooij
blob - cb4d90567e8c0bd5de88570241991fe0bf84be73
blob + b6d263798a69926bced09fe3528edf516a10fed5
--- dulwich/__init__.py
+++ dulwich/__init__.py
"""Python implementation of the Git file formats and protocols."""
-__version__ = (0, 20, 27)
+__version__ = (0, 20, 30)
blob - 700b2d4eb1dcc569354df45d0c7f0a374dd9b077
blob + 2c5a1735a5a870714373032bc92e38bb63c090fd
--- dulwich/index.py
+++ dulwich/index.py
for path in self:
entry = self[path]
yield path, entry.sha, cleanup_mode(entry.mode)
-
- def iterblobs(self):
- import warnings
-
- warnings.warn("Use iterobjects() instead.", PendingDeprecationWarning)
- return self.iterobjects()
def clear(self):
"""Remove all contents from this index."""
else:
tree_path = fs_path_bytes
return tree_path
+
+
+def index_entry_from_directory(st, path):
+ if os.path.exists(os.path.join(path, b".git")):
+ head = read_submodule_head(path)
+ if head is None:
+ return None
+ return index_entry_from_stat(st, head, 0, mode=S_IFGITLINK)
+ return None
def index_entry_from_path(path, object_store=None):
assert isinstance(path, bytes)
st = os.lstat(path)
if stat.S_ISDIR(st.st_mode):
- if os.path.exists(os.path.join(path, b".git")):
- head = read_submodule_head(path)
- if head is None:
- return None
- return index_entry_from_stat(st, head, 0, mode=S_IFGITLINK)
- return None
+ return index_entry_from_directory(st, path)
if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode):
blob = blob_from_path_and_stat(path, st)
yield path, entry
-def iter_fresh_blobs(index, root_path):
- """Iterate over versions of blobs on disk referenced by index.
-
- Don't use this function; it removes missing entries from index.
-
- Args:
- index: Index file
- root_path: Root path to access from
- include_deleted: Include deleted entries with sha and
- mode set to None
- Returns: Iterator over path, sha, mode
- """
- import warnings
-
- warnings.warn(PendingDeprecationWarning, "Use iter_fresh_objects instead.")
- for entry in iter_fresh_objects(index, root_path, include_deleted=True):
- if entry[1] is None:
- del index[entry[0]]
- else:
- yield entry
-
-
def iter_fresh_objects(paths, root_path, include_deleted=False, object_store=None):
"""Iterate over versions of objecs on disk referenced by index.
blob - 0749f4449fc9b4353974b45f1e15004fd731402c
blob + 8d4ab0eb4f9731223253ae61659977f5a5308aac
--- dulwich/porcelain.py
+++ dulwich/porcelain.py
import os
from pathlib import Path
import posixpath
-import shutil
import stat
import sys
import time
from dulwich.refs import (
ANNOTATED_TAG_SUFFIX,
LOCAL_BRANCH_PREFIX,
+ LOCAL_TAG_PREFIX,
strip_peeled_refs,
RefsContainer,
)
outstream=None,
origin=b"origin",
depth=None,
+ branch=None,
**kwargs
):
"""Clone a local or remote git repository.
outstream: Optional stream to write progress to (deprecated)
origin: Name of remote from the repository used to clone
depth: Depth to fetch at
+ branch: Optional branch or tag to be used as HEAD in the new repository
+ instead of the cloned repository's HEAD.
Returns: The new repository
"""
- # TODO(jelmer): This code overlaps quite a bit with Repo.clone
if outstream is not None:
import warnings
DeprecationWarning,
stacklevel=3,
)
- errstream = outstream
+ # TODO(jelmer): Capture logging output and stream to errstream
if checkout is None:
checkout = not bare
if target is None:
target = source.split("/")[-1]
- if not os.path.exists(target):
- os.mkdir(target)
+ mkdir = not os.path.exists(target)
- if bare:
- r = Repo.init_bare(target)
- else:
- r = Repo.init(target)
-
- reflog_message = b"clone: from " + source.encode("utf-8")
- try:
- target_config = r.get_config()
- if not isinstance(source, bytes):
- source = source.encode(DEFAULT_ENCODING)
- target_config.set((b"remote", origin), b"url", source)
- target_config.set(
- (b"remote", origin),
- b"fetch",
- b"+refs/heads/*:refs/remotes/" + origin + b"/*",
- )
- target_config.write_to_path()
- fetch_result = fetch(
- r,
- origin,
- errstream=errstream,
- message=reflog_message,
- depth=depth,
- **kwargs
+ with open_repo_closing(source) as r:
+ return r.clone(
+ target,
+ mkdir=mkdir,
+ bare=bare,
+ origin=origin,
+ checkout=checkout,
+ branch=branch,
)
- for key, target in fetch_result.symrefs.items():
- r.refs.set_symbolic_ref(key, target)
- try:
- head = r[fetch_result.refs[b"HEAD"]]
- except KeyError:
- head = None
- else:
- r[b"HEAD"] = head.id
- if checkout and not bare and head is not None:
- errstream.write(b"Checking out " + head.id + b"\n")
- r.reset_index(head.tree)
- except BaseException:
- shutil.rmtree(target)
- r.close()
- raise
-
- return r
def add(repo=".", paths=None):
def _make_tag_ref(name):
if getattr(name, "encode", None):
name = name.encode(DEFAULT_ENCODING)
- return b"refs/tags/" + name
+ return LOCAL_TAG_PREFIX + name
def branch_delete(repo, name):
blob - bda6bdc5f13ec88942e195d350fc65e9136844a4
blob + 9d022f44cc481abfb0c44e6923387c0d398ebe0e
--- dulwich/refs.py
+++ dulwich/refs.py
git_line,
valid_hexsha,
ZERO_SHA,
+ Tag,
)
from dulwich.file import (
GitFile,
for (ref, sha) in refs.items()
if not ref.endswith(ANNOTATED_TAG_SUFFIX)
}
+
+
+def _set_origin_head(refs, origin, origin_head):
+ # set refs/remotes/origin/HEAD
+ origin_base = b"refs/remotes/" + origin + b"/"
+ if origin_head and origin_head.startswith(LOCAL_BRANCH_PREFIX):
+ origin_ref = origin_base + b"HEAD"
+ target_ref = origin_base + origin_head[len(LOCAL_BRANCH_PREFIX) :]
+ if target_ref in refs:
+ refs.set_symbolic_ref(origin_ref, target_ref)
+
+
+def _set_default_branch(refs, origin, origin_head, branch, ref_message):
+ origin_base = b"refs/remotes/" + origin + b"/"
+ if branch:
+ origin_ref = origin_base + branch
+ if origin_ref in refs:
+ local_ref = LOCAL_BRANCH_PREFIX + branch
+ refs.add_if_new(
+ local_ref, refs[origin_ref], ref_message
+ )
+ head_ref = local_ref
+ elif LOCAL_TAG_PREFIX + branch in refs:
+ head_ref = LOCAL_TAG_PREFIX + branch
+ else:
+ raise ValueError(
+ "%s is not a valid branch or tag" % os.fsencode(branch)
+ )
+ elif origin_head:
+ head_ref = origin_head
+ if origin_head.startswith(LOCAL_BRANCH_PREFIX):
+ origin_ref = origin_base + origin_head[len(LOCAL_BRANCH_PREFIX) :]
+ else:
+ origin_ref = origin_head
+ try:
+ refs.add_if_new(
+ head_ref, refs[origin_ref], ref_message
+ )
+ except KeyError:
+ pass
+ return head_ref
+
+
+def _set_head(refs, head_ref, ref_message):
+ if head_ref.startswith(LOCAL_TAG_PREFIX):
+ # detach HEAD at specified tag
+ head = refs[head_ref]
+ if isinstance(head, Tag):
+ _cls, obj = head.object
+ head = obj.get_object(obj).id
+ del refs[b"HEAD"]
+ refs.set_if_equals(
+ b"HEAD", None, head, message=ref_message
+ )
+ else:
+ # set HEAD to specific branch
+ try:
+ head = refs[head_ref]
+ refs.set_symbolic_ref(b"HEAD", head_ref)
+ refs.set_if_equals(
+ b"HEAD", None, head, message=ref_message
+ )
+ except KeyError:
+ head = None
+ return head
blob - 29b0e6694b1e27e3cf4ba3de92cdd5f95f3ed00b
blob + e0362ef44e7fc68e42b391ea3ee532c0619d0c7b
--- dulwich/repo.py
+++ dulwich/repo.py
from dulwich.refs import ( # noqa: F401
ANNOTATED_TAG_SUFFIX,
+ LOCAL_BRANCH_PREFIX,
+ LOCAL_TAG_PREFIX,
check_ref_format,
RefsContainer,
DictRefsContainer,
read_packed_refs_with_peeled,
write_packed_refs,
SYMREF,
+ _set_default_branch,
+ _set_head,
+ _set_origin_head,
)
except KeyError:
fullname = None
else:
- fullname = gecos.split(",")[0]
+ if gecos:
+ fullname = gecos.split(",")[0]
+ else:
+ fullname = None
if not fullname:
fullname = username
email = os.environ.get("EMAIL")
from dulwich.index import (
blob_from_path_and_stat,
index_entry_from_stat,
+ index_entry_from_directory,
_fs_to_tree_path,
)
except KeyError:
pass # already removed
else:
- if not stat.S_ISREG(st.st_mode) and not stat.S_ISLNK(st.st_mode):
+ if stat.S_ISDIR(st.st_mode):
+ entry = index_entry_from_directory(st, full_path)
+ if entry:
+ index[tree_path] = entry
+ else:
+ try:
+ del index[tree_path]
+ except KeyError:
+ pass
+ elif not stat.S_ISREG(st.st_mode) and not stat.S_ISLNK(st.st_mode):
try:
del index[tree_path]
except KeyError:
bare=False,
origin=b"origin",
checkout=None,
+ branch=None,
):
"""Clone this repository.
target_path: Target path
mkdir: Create the target directory
bare: Whether to create a bare repository
+ checkout: Whether or not to check-out HEAD after cloning
origin: Base name for refs in target repository
cloned from this repository
+ branch: Optional branch or tag to be used as HEAD in the new repository
+ instead of this repository's HEAD.
Returns: Created repository as `Repo`
"""
- if not bare:
- target = self.init(target_path, mkdir=mkdir)
- else:
- if checkout:
- raise ValueError("checkout and bare are incompatible")
- target = self.init_bare(target_path, mkdir=mkdir)
- self.fetch(target)
+
encoded_path = self.path
if not isinstance(encoded_path, bytes):
encoded_path = os.fsencode(encoded_path)
- ref_message = b"clone: from " + encoded_path
- target.refs.import_refs(
- b"refs/remotes/" + origin,
- self.refs.as_dict(b"refs/heads"),
- message=ref_message,
- )
- target.refs.import_refs(
- b"refs/tags", self.refs.as_dict(b"refs/tags"), message=ref_message
- )
- try:
- target.refs.add_if_new(
- DEFAULT_REF, self.refs[DEFAULT_REF], message=ref_message
- )
- except KeyError:
- pass
- target_config = target.get_config()
- target_config.set(("remote", "origin"), "url", encoded_path)
- target_config.set(
- ("remote", "origin"),
- "fetch",
- "+refs/heads/*:refs/remotes/origin/*",
- )
- target_config.write_to_path()
- # Update target head
- head_chain, head_sha = self.refs.follow(b"HEAD")
- if head_chain and head_sha is not None:
- target.refs.set_symbolic_ref(b"HEAD", head_chain[-1], message=ref_message)
- target[b"HEAD"] = head_sha
-
- if checkout is None:
- checkout = not bare
- if checkout:
- # Checkout HEAD to target dir
- target.reset_index()
+ if mkdir:
+ os.mkdir(target_path)
+
+ try:
+ target = None
+ if not bare:
+ target = Repo.init(target_path)
+ if checkout is None:
+ checkout = True
+ else:
+ if checkout:
+ raise ValueError("checkout and bare are incompatible")
+ target = Repo.init_bare(target_path)
+
+ target_config = target.get_config()
+ target_config.set((b"remote", origin), b"url", encoded_path)
+ target_config.set(
+ (b"remote", origin),
+ b"fetch",
+ b"+refs/heads/*:refs/remotes/" + origin + b"/*",
+ )
+ target_config.write_to_path()
+
+ ref_message = b"clone: from " + encoded_path
+ self.fetch(target)
+ target.refs.import_refs(
+ b"refs/remotes/" + origin,
+ self.refs.as_dict(b"refs/heads"),
+ message=ref_message,
+ )
+ target.refs.import_refs(
+ b"refs/tags", self.refs.as_dict(b"refs/tags"), message=ref_message
+ )
+
+ head_chain, origin_sha = self.refs.follow(b"HEAD")
+ origin_head = head_chain[-1] if head_chain else None
+ if origin_sha and not origin_head:
+ # set detached HEAD
+ target.refs[b"HEAD"] = origin_sha
+
+ _set_origin_head(target.refs, origin, origin_head)
+ head_ref = _set_default_branch(
+ target.refs, origin, origin_head, branch, ref_message
+ )
+
+ # Update target head
+ if head_ref:
+ head = _set_head(target.refs, head_ref, ref_message)
+ else:
+ head = None
+ if checkout and head is not None:
+ target.reset_index()
+ except BaseException:
+ if target is not None:
+ target.close()
+ if mkdir:
+ import shutil
+ shutil.rmtree(target_path)
+ raise
return target
def reset_index(self, tree=None):
)
if tree is None:
- tree = self[b"HEAD"].tree
+ head = self[b"HEAD"]
+ if isinstance(head, Tag):
+ _cls, obj = head.object
+ head = self.get_object(obj)
+ tree = head.tree
config = self.get_config()
honor_filemode = config.get_boolean(b"core", b"filemode", os.name != "nt")
if config.get_boolean(b"core", b"core.protectNTFS", os.name == "nt"):
blob - fc9cf75eea7397ab392858a0ea83c7c0e2e48398
blob + aa6731815124912c8a1ef32acd6911c633d9f21b
--- dulwich/tests/test_index.py
+++ dulwich/tests/test_index.py
import struct
import sys
import tempfile
-import warnings
from dulwich.index import (
Index,
TestCase,
skipIf,
)
-from dulwich.tests.utils import (
- setup_warning_catcher,
-)
def can_symlink():
self.assertEqual(
[(b"bla", b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", 33188)],
list(self.get_simple_index("index").iterobjects()),
- )
-
- def test_iterblobs(self):
- warnings.simplefilter("always", UserWarning)
- self.addCleanup(warnings.resetwarnings)
- warnings_list, restore_warnings = setup_warning_catcher()
- self.addCleanup(restore_warnings)
-
- self.assertEqual(
- [(b"bla", b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", 33188)],
- list(self.get_simple_index("index").iterblobs()),
)
- expected_warning = PendingDeprecationWarning("Use iterobjects() instead.")
- for w in warnings_list:
- if type(w) == type(expected_warning) and w.args == expected_warning.args:
- break
- else:
- raise AssertionError(
- "Expected warning %r not in %r" % (expected_warning, warnings_list)
- )
-
def test_getitem(self):
self.assertEqual(
(
blob - 0334f192e879227d61a3fe1641117cf9a6e824e1
blob + 5618066e8105401fe014c7b5e104a5d1e64f9d30
--- dulwich/tests/test_porcelain.py
+++ dulwich/tests/test_porcelain.py
r.close()
def test_source_broken(self):
- target_path = tempfile.mkdtemp()
- self.assertRaises(Exception, porcelain.clone, "/nonexistant/repo", target_path)
- self.assertFalse(os.path.exists(target_path))
+ with tempfile.TemporaryDirectory() as parent:
+ target_path = os.path.join(parent, "target")
+ self.assertRaises(
+ Exception, porcelain.clone, "/nonexistant/repo", target_path
+ )
+ self.assertFalse(os.path.exists(target_path))
def test_fetch_symref(self):
f1_1 = make_object(Blob, data=b"f1")
self.assertEqual(0, len(target_repo.open_index()))
self.assertEqual(c1.id, target_repo.refs[b"refs/heads/else"])
self.assertEqual(c1.id, target_repo.refs[b"HEAD"])
- self.assertEqual({b"HEAD": b"refs/heads/else"}, target_repo.refs.get_symrefs())
+ self.assertEqual(
+ {b"HEAD": b"refs/heads/else", b"refs/remotes/origin/HEAD": b"refs/remotes/origin/else"},
+ target_repo.refs.get_symrefs(),
+ )
class InitTests(TestCase):
for k, v in remote_refs.items()
if k.startswith(local_ref_prefix)
}
+ if b"HEAD" in locally_known_remote_refs and b"HEAD" in remote_refs:
+ normalized_remote_refs[b"HEAD"] = remote_refs[b"HEAD"]
self.assertEqual(locally_known_remote_refs, normalized_remote_refs)
blob - f8975cad544709687eb17bcaf16907b184744b56
blob + ee328013b16ac8a38bcb848a4eb1455d8453b1dd
--- dulwich/tests/test_repository.py
+++ dulwich/tests/test_repository.py
{
b"HEAD": b"a90fa2d900a17e99b433217e988c4eb4a2e9a097",
b"refs/remotes/origin/master": b"a90fa2d900a17e99b433217e988c4eb4a2e9a097",
+ b"refs/remotes/origin/HEAD": b"a90fa2d900a17e99b433217e988c4eb4a2e9a097",
b"refs/heads/master": b"a90fa2d900a17e99b433217e988c4eb4a2e9a097",
b"refs/tags/mytag": b"28237f4dc30d0d462658d6b937b08a0f0b6ef55a",
b"refs/tags/mytag-packed": b"b0931cadc54336e78a1d980420e3268903b57a50",
self.assertRaises(
ValueError, r.clone, tmp_dir, mkdir=False, checkout=True, bare=True
)
+
+ def test_clone_branch(self):
+ r = self.open_repo("a.git")
+ r.refs[b"refs/heads/mybranch"] = b"28237f4dc30d0d462658d6b937b08a0f0b6ef55a"
+ tmp_dir = self.mkdtemp()
+ self.addCleanup(shutil.rmtree, tmp_dir)
+ with r.clone(tmp_dir, mkdir=False, branch=b"mybranch") as t:
+ # HEAD should point to specified branch and not origin HEAD
+ chain, sha = t.refs.follow(b"HEAD")
+ self.assertEqual(chain[-1], b"refs/heads/mybranch")
+ self.assertEqual(sha, b"28237f4dc30d0d462658d6b937b08a0f0b6ef55a")
+ self.assertEqual(
+ t.refs[b"refs/remotes/origin/HEAD"],
+ b"a90fa2d900a17e99b433217e988c4eb4a2e9a097",
+ )
+ def test_clone_tag(self):
+ r = self.open_repo("a.git")
+ tmp_dir = self.mkdtemp()
+ self.addCleanup(shutil.rmtree, tmp_dir)
+ with r.clone(tmp_dir, mkdir=False, branch=b"mytag") as t:
+ # HEAD should be detached (and not a symbolic ref) at tag
+ self.assertEqual(
+ t.refs.read_ref(b"HEAD"),
+ b"28237f4dc30d0d462658d6b937b08a0f0b6ef55a",
+ )
+ self.assertEqual(
+ t.refs[b"refs/remotes/origin/HEAD"],
+ b"a90fa2d900a17e99b433217e988c4eb4a2e9a097",
+ )
+
+ def test_clone_invalid_branch(self):
+ r = self.open_repo("a.git")
+ tmp_dir = self.mkdtemp()
+ self.addCleanup(shutil.rmtree, tmp_dir)
+ self.assertRaises(
+ ValueError,
+ r.clone,
+ tmp_dir,
+ mkdir=False,
+ branch=b"mybranch",
+ )
+
def test_merge_history(self):
r = self.open_repo("simple_merge.git")
shas = [e.commit.id for e in r.get_walker()]
pre_commit_contents = """#!%(executable)s
import sys
-sys.path.extend(':'.join(%(path)s))
+sys.path.extend(%(path)r)
from dulwich.repo import Repo
with open('foo', 'w') as f:
r = Repo('.')
r.stage(['foo'])
-""" % {'executable': sys.executable, 'path': repr(sys.path)}
+""" % {
+ 'executable': sys.executable,
+ 'path': [os.path.join(os.path.dirname(__file__), '..', '..')] + sys.path}
repo_dir = os.path.join(self.mkdtemp())
self.addCleanup(shutil.rmtree, repo_dir)
os.remove(os.path.join(r.path, "a"))
r.stage(["a"])
r.stage(["a"]) # double-stage a deleted path
+ self.assertEqual([], list(r.open_index()))
def test_stage_directory(self):
r = self._repo
r.stage(["c"])
self.assertEqual([b"a"], list(r.open_index()))
+ def test_stage_submodule(self):
+ r = self._repo
+ s = Repo.init(os.path.join(r.path, "sub"), mkdir=True)
+ s.do_commit(b'message')
+ r.stage(["sub"])
+ self.assertEqual([b"a", b"sub"], list(r.open_index()))
+
def test_unstage_midify_file_with_dir(self):
os.mkdir(os.path.join(self._repo.path, 'new_dir'))
full_path = os.path.join(self._repo.path, 'new_dir', 'foo')
blob - 3f3f472b7348f4aa816b6ebe9616187914f8c6b9
blob + fc2a42b75edd1cae3f05952f180f61368145a86c
--- dulwich.egg-info/PKG-INFO
+++ dulwich.egg-info/PKG-INFO
Metadata-Version: 2.1
Name: dulwich
-Version: 0.20.27
+Version: 0.20.30
Summary: Python Git Library
Home-page: https://www.dulwich.io/
Author: Jelmer Vernooij
blob - a9f8ea6e10a235c8784b00f27a4bd695767ba248
blob + cb1509bcd238b2846f70be01ca0e2cd58bfb5f67
--- dulwich.egg-info/SOURCES.txt
+++ dulwich.egg-info/SOURCES.txt
setup.py
status.yaml
tox.ini
+.github/FUNDING.yml
.github/workflows/pythonpackage.yml
.github/workflows/pythonpublish.yml
bin/dul-receive-pack
blob - bcad72df255a46d39eb9f694dbc8467fb8880de4
blob + 4d8177fc36e4b011df20e44ae387cdc9b50ec33e
--- releaser.conf
+++ releaser.conf
news_file: "NEWS"
timeout_days: 5
tag_name: "dulwich-$VERSION"
-verify_command: "make check"
+verify_command: "flake8 && make check"
update_version {
path: "setup.py"
match: "^dulwich_version_string = '(.*)'$"
blob - 8ca2a2fb253b8e6c74e80d5499feec21834c7deb
blob + e0ee7431ac4e6b4f1f0ec9fe5766e68c227588b9
--- setup.py
+++ setup.py
'For 2.7 support, please install a version prior to 0.20')
-dulwich_version_string = '0.20.27'
+dulwich_version_string = '0.20.30'
class DulwichDistribution(Distribution):