From 3873ba72b463b9b947a0578874325a39a1b46045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Mon, 11 Jul 2022 13:55:31 +0100 Subject: [PATCH 1/2] Add really basic submodule subcommands. See #506 --- NEWS | 3 ++ dulwich/cli.py | 10 +++++++ dulwich/porcelain.py | 51 +++++++++++++++++++++++++++++++-- dulwich/submodule.py | 40 ++++++++++++++++++++++++++ dulwich/tests/test_porcelain.py | 22 ++++++++++++++ 5 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 dulwich/submodule.py diff --git a/NEWS b/NEWS index e4fe1c2f8..b348b9079 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,8 @@ 0.20.45 UNRELEASED + * Add basic ``dulwich.porcelain.submodule_list`` and ``dulwich.porcelain.submodule_add`` + (Jelmer Vernooij) + 0.20.44 2022-06-30 * Fix reading of chunks in server. (Jelmer Vernooij, #977) diff --git a/dulwich/cli.py b/dulwich/cli.py index 235f80b99..4cc5a09ae 100755 --- a/dulwich/cli.py +++ b/dulwich/cli.py @@ -321,6 +321,15 @@ def run(self, args): porcelain.rev_list(".", args) +class cmd_submodule(Command): + def run(self, args): + parser = optparse.OptionParser() + options, args = parser.parse_args(args) + for path, sha in porcelain.submodule_list("."): + sys.stdout.write(' %s %s\n' % (sha, path)) + + + class cmd_tag(Command): def run(self, args): parser = optparse.OptionParser() @@ -721,6 +730,7 @@ def run(self, args): "stash": cmd_stash, "status": cmd_status, "symbolic-ref": cmd_symbolic_ref, + "submodule": cmd_submodule, "tag": cmd_tag, "update-server-info": cmd_update_server_info, "upload-pack": cmd_upload_pack, diff --git a/dulwich/porcelain.py b/dulwich/porcelain.py index d34dac7e8..9d697e8cb 100644 --- a/dulwich/porcelain.py +++ b/dulwich/porcelain.py @@ -43,6 +43,7 @@ * remote{_add} * receive-pack * reset + * submodule_list * rev-list * tag{_create,_delete,_list} * upload-pack @@ -86,6 +87,7 @@ get_transport_and_path, ) from dulwich.config import ( + ConfigFile, StackedConfig, ) from dulwich.diff_tree import ( @@ -858,6 +860,51 @@ def rev_list(repo, commits, outstream=sys.stdout): outstream.write(entry.commit.id + b"\n") +def _canonical_part(url: str) -> str: + name = url.rsplit('/', 1)[-1] + if name.endswith('.git'): + name = name[:-4] + return name + + +def submodule_add(repo, url, path=None, name=None): + """Add a new submodule. + + Args: + repo: Path to repository + url: URL of repository to add as submodule + path: Path where submodule should live + """ + with open_repo_closing(repo) as r: + if path is None: + path = os.path.relpath(canonical_part(url), repo.path) + if name is None: + name = path + + # TODO(jelmer): Move this logic to dulwich.submodule + gitmodules_path = os.path.join(repo.path, ".gitmodules") + try: + config = ConfigFile.from_path(gitmodules_path) + except FileNotFoundError: + config = ConfigFile() + config.path = gitmodules_path + config.set(("submodule", name), "url", url) + config.set(("submodule", name), "path", path) + config.write_to_path() + + +def submodule_list(repo): + """List submodules. + + Args: + repo: Path to repository + """ + from .submodule import iter_cached_submodules + with open_repo_closing(repo) as r: + for path, sha in iter_cached_submodules(r.object_store, r[r.head()].tree): + yield path.decode(DEFAULT_ENCODING), sha.decode(DEFAULT_ENCODING) + + def tag(*args, **kwargs): import warnings @@ -1456,7 +1503,7 @@ def branch_create(repo, name, objectish=None, force=False): objectish = "HEAD" object = parse_object(r, objectish) refname = _make_branch_ref(name) - ref_message = b"branch: Created from " + objectish.encode("utf-8") + ref_message = b"branch: Created from " + objectish.encode(DEFAULT_ENCODING) if force: r.refs.set_if_equals(refname, None, object.id, message=ref_message) else: @@ -1541,7 +1588,7 @@ def fetch( with open_repo_closing(repo) as r: (remote_name, remote_location) = get_remote_repo(r, remote_location) if message is None: - message = b"fetch: from " + remote_location.encode("utf-8") + message = b"fetch: from " + remote_location.encode(DEFAULT_ENCODING) client, path = get_transport_and_path( remote_location, config=r.get_config_stack(), **kwargs ) diff --git a/dulwich/submodule.py b/dulwich/submodule.py new file mode 100644 index 000000000..dc98d8792 --- /dev/null +++ b/dulwich/submodule.py @@ -0,0 +1,40 @@ +# config.py - Reading and writing Git config files +# Copyright (C) 2011-2013 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. +# + +"""Working with Git submodules. +""" + +from typing import Iterator, Tuple +from .objects import S_ISGITLINK + + +def iter_cached_submodules(store, root_tree_id: bytes) -> Iterator[Tuple[str, bytes]]: + """iterate over cached submodules. + + Args: + store: Object store to iterate + root_tree_id: SHA of root tree + + Returns: + Iterator over over (path, sha) tuples + """ + for entry in store.iter_tree_contents(root_tree_id): + if S_ISGITLINK(entry.mode): + yield entry.path, entry.sha diff --git a/dulwich/tests/test_porcelain.py b/dulwich/tests/test_porcelain.py index 6ed9cac2c..666a9198b 100644 --- a/dulwich/tests/test_porcelain.py +++ b/dulwich/tests/test_porcelain.py @@ -1408,6 +1408,28 @@ def test_resetfile_with_dir(self): self.assertEqual('hello', f.read()) +class SubmoduleTests(PorcelainTestCase): + + def test_empty(self): + porcelain.commit( + repo=self.repo.path, + message=b"init", + author=b"author ", + committer=b"committer ", + ) + + self.assertEqual([], list(porcelain.submodule_list(self.repo))) + + def test_add(self): + porcelain.submodule_add(self.repo, "../bar.git", "bar") + with open('%s/.gitmodules' % self.repo.path, 'r') as f: + self.assertEqual("""\ +[submodule "bar"] + url = ../bar.git + path = bar +""", f.read()) + + class PushTests(PorcelainTestCase): def test_simple(self): """ From 2832f4e7a97560c39b8667c1607dae2db145ef93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Mon, 11 Jul 2022 14:07:30 +0100 Subject: [PATCH 2/2] Typing. --- dulwich/cli.py | 1 - dulwich/porcelain.py | 4 ++-- dulwich/tests/test_porcelain.py | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/dulwich/cli.py b/dulwich/cli.py index 4cc5a09ae..f95b9f261 100755 --- a/dulwich/cli.py +++ b/dulwich/cli.py @@ -329,7 +329,6 @@ def run(self, args): sys.stdout.write(' %s %s\n' % (sha, path)) - class cmd_tag(Command): def run(self, args): parser = optparse.OptionParser() diff --git a/dulwich/porcelain.py b/dulwich/porcelain.py index 9d697e8cb..e79e90df8 100644 --- a/dulwich/porcelain.py +++ b/dulwich/porcelain.py @@ -877,12 +877,12 @@ def submodule_add(repo, url, path=None, name=None): """ with open_repo_closing(repo) as r: if path is None: - path = os.path.relpath(canonical_part(url), repo.path) + path = os.path.relpath(_canonical_part(url), r.path) if name is None: name = path # TODO(jelmer): Move this logic to dulwich.submodule - gitmodules_path = os.path.join(repo.path, ".gitmodules") + gitmodules_path = os.path.join(r.path, ".gitmodules") try: config = ConfigFile.from_path(gitmodules_path) except FileNotFoundError: diff --git a/dulwich/tests/test_porcelain.py b/dulwich/tests/test_porcelain.py index 666a9198b..acd8a6b46 100644 --- a/dulwich/tests/test_porcelain.py +++ b/dulwich/tests/test_porcelain.py @@ -1425,8 +1425,8 @@ def test_add(self): with open('%s/.gitmodules' % self.repo.path, 'r') as f: self.assertEqual("""\ [submodule "bar"] - url = ../bar.git - path = bar +\turl = ../bar.git +\tpath = bar """, f.read())