commit 3873ba72b463b9b947a0578874325a39a1b46045 from: Jelmer Vernooij date: Mon Jul 11 12:55:31 2022 UTC Add really basic submodule subcommands. See #506 commit - 3971b9ccd9e783bdb978185ca7ceb03db3da3bf6 commit + 3873ba72b463b9b947a0578874325a39a1b46045 blob - e4fe1c2f831459919e5aff2f7d79553ba64152f5 blob + b348b90793c0825266c1719ceb82d678912668b6 --- NEWS +++ 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) blob - 235f80b996fbd0254f44349709de633d1769f7a9 blob + 4cc5a09aeea0f75e629040405fe595d27447818e --- dulwich/cli.py +++ dulwich/cli.py @@ -319,6 +319,15 @@ class cmd_rev_list(Command): print("Usage: dulwich rev-list COMMITID...") sys.exit(1) 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): @@ -721,6 +730,7 @@ commands = { "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, blob - d34dac7e8a6f3a6113e2b30e97f9f4f27dbe1a1a blob + 9d697e8cb6383a0d482f14306db6d2718791a376 --- dulwich/porcelain.py +++ dulwich/porcelain.py @@ -43,6 +43,7 @@ Currently implemented: * remote{_add} * receive-pack * reset + * submodule_list * rev-list * tag{_create,_delete,_list} * upload-pack @@ -86,6 +87,7 @@ from dulwich.client import ( get_transport_and_path, ) from dulwich.config import ( + ConfigFile, StackedConfig, ) from dulwich.diff_tree import ( @@ -856,8 +858,53 @@ def rev_list(repo, commits, outstream=sys.stdout): with open_repo_closing(repo) as r: for entry in r.get_walker(include=[r[c].id for c in commits]): 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=Fa 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 ) blob - 6ed9cac2c3b9e595b3eba070fd0ccef979c56148 blob + 666a9198bb408cc8aee3763217a7fcf464cf7ea0 --- dulwich/tests/test_porcelain.py +++ dulwich/tests/test_porcelain.py @@ -1406,6 +1406,28 @@ class ResetFileTests(PorcelainTestCase): porcelain.reset_file(self.repo, os.path.join('new_dir', 'foo'), target=sha) with open(full_path, 'r') as f: 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): blob - /dev/null blob + dc98d8792450299ff187445bbf197eda758a6580 (mode 644) --- /dev/null +++ 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