Skip to content

Commit 3873ba7

Browse files
committed
Add really basic submodule subcommands. See #506
1 parent 3971b9c commit 3873ba7

File tree

5 files changed

+124
-2
lines changed

5 files changed

+124
-2
lines changed

NEWS

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
0.20.45 UNRELEASED
22

3+
* Add basic ``dulwich.porcelain.submodule_list`` and ``dulwich.porcelain.submodule_add``
4+
(Jelmer Vernooij)
5+
36
0.20.44 2022-06-30
47

58
* Fix reading of chunks in server. (Jelmer Vernooij, #977)

dulwich/cli.py

+10
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,15 @@ def run(self, args):
321321
porcelain.rev_list(".", args)
322322

323323

324+
class cmd_submodule(Command):
325+
def run(self, args):
326+
parser = optparse.OptionParser()
327+
options, args = parser.parse_args(args)
328+
for path, sha in porcelain.submodule_list("."):
329+
sys.stdout.write(' %s %s\n' % (sha, path))
330+
331+
332+
324333
class cmd_tag(Command):
325334
def run(self, args):
326335
parser = optparse.OptionParser()
@@ -721,6 +730,7 @@ def run(self, args):
721730
"stash": cmd_stash,
722731
"status": cmd_status,
723732
"symbolic-ref": cmd_symbolic_ref,
733+
"submodule": cmd_submodule,
724734
"tag": cmd_tag,
725735
"update-server-info": cmd_update_server_info,
726736
"upload-pack": cmd_upload_pack,

dulwich/porcelain.py

+49-2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
* remote{_add}
4444
* receive-pack
4545
* reset
46+
* submodule_list
4647
* rev-list
4748
* tag{_create,_delete,_list}
4849
* upload-pack
@@ -86,6 +87,7 @@
8687
get_transport_and_path,
8788
)
8889
from dulwich.config import (
90+
ConfigFile,
8991
StackedConfig,
9092
)
9193
from dulwich.diff_tree import (
@@ -858,6 +860,51 @@ def rev_list(repo, commits, outstream=sys.stdout):
858860
outstream.write(entry.commit.id + b"\n")
859861

860862

863+
def _canonical_part(url: str) -> str:
864+
name = url.rsplit('/', 1)[-1]
865+
if name.endswith('.git'):
866+
name = name[:-4]
867+
return name
868+
869+
870+
def submodule_add(repo, url, path=None, name=None):
871+
"""Add a new submodule.
872+
873+
Args:
874+
repo: Path to repository
875+
url: URL of repository to add as submodule
876+
path: Path where submodule should live
877+
"""
878+
with open_repo_closing(repo) as r:
879+
if path is None:
880+
path = os.path.relpath(canonical_part(url), repo.path)
881+
if name is None:
882+
name = path
883+
884+
# TODO(jelmer): Move this logic to dulwich.submodule
885+
gitmodules_path = os.path.join(repo.path, ".gitmodules")
886+
try:
887+
config = ConfigFile.from_path(gitmodules_path)
888+
except FileNotFoundError:
889+
config = ConfigFile()
890+
config.path = gitmodules_path
891+
config.set(("submodule", name), "url", url)
892+
config.set(("submodule", name), "path", path)
893+
config.write_to_path()
894+
895+
896+
def submodule_list(repo):
897+
"""List submodules.
898+
899+
Args:
900+
repo: Path to repository
901+
"""
902+
from .submodule import iter_cached_submodules
903+
with open_repo_closing(repo) as r:
904+
for path, sha in iter_cached_submodules(r.object_store, r[r.head()].tree):
905+
yield path.decode(DEFAULT_ENCODING), sha.decode(DEFAULT_ENCODING)
906+
907+
861908
def tag(*args, **kwargs):
862909
import warnings
863910

@@ -1456,7 +1503,7 @@ def branch_create(repo, name, objectish=None, force=False):
14561503
objectish = "HEAD"
14571504
object = parse_object(r, objectish)
14581505
refname = _make_branch_ref(name)
1459-
ref_message = b"branch: Created from " + objectish.encode("utf-8")
1506+
ref_message = b"branch: Created from " + objectish.encode(DEFAULT_ENCODING)
14601507
if force:
14611508
r.refs.set_if_equals(refname, None, object.id, message=ref_message)
14621509
else:
@@ -1541,7 +1588,7 @@ def fetch(
15411588
with open_repo_closing(repo) as r:
15421589
(remote_name, remote_location) = get_remote_repo(r, remote_location)
15431590
if message is None:
1544-
message = b"fetch: from " + remote_location.encode("utf-8")
1591+
message = b"fetch: from " + remote_location.encode(DEFAULT_ENCODING)
15451592
client, path = get_transport_and_path(
15461593
remote_location, config=r.get_config_stack(), **kwargs
15471594
)

dulwich/submodule.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# config.py - Reading and writing Git config files
2+
# Copyright (C) 2011-2013 Jelmer Vernooij <[email protected]>
3+
#
4+
# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
5+
# General Public License as public by the Free Software Foundation; version 2.0
6+
# or (at your option) any later version. You can redistribute it and/or
7+
# modify it under the terms of either of these two licenses.
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# You should have received a copy of the licenses; if not, see
16+
# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
17+
# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
18+
# License, Version 2.0.
19+
#
20+
21+
"""Working with Git submodules.
22+
"""
23+
24+
from typing import Iterator, Tuple
25+
from .objects import S_ISGITLINK
26+
27+
28+
def iter_cached_submodules(store, root_tree_id: bytes) -> Iterator[Tuple[str, bytes]]:
29+
"""iterate over cached submodules.
30+
31+
Args:
32+
store: Object store to iterate
33+
root_tree_id: SHA of root tree
34+
35+
Returns:
36+
Iterator over over (path, sha) tuples
37+
"""
38+
for entry in store.iter_tree_contents(root_tree_id):
39+
if S_ISGITLINK(entry.mode):
40+
yield entry.path, entry.sha

dulwich/tests/test_porcelain.py

+22
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,28 @@ def test_resetfile_with_dir(self):
14081408
self.assertEqual('hello', f.read())
14091409

14101410

1411+
class SubmoduleTests(PorcelainTestCase):
1412+
1413+
def test_empty(self):
1414+
porcelain.commit(
1415+
repo=self.repo.path,
1416+
message=b"init",
1417+
author=b"author <email>",
1418+
committer=b"committer <email>",
1419+
)
1420+
1421+
self.assertEqual([], list(porcelain.submodule_list(self.repo)))
1422+
1423+
def test_add(self):
1424+
porcelain.submodule_add(self.repo, "../bar.git", "bar")
1425+
with open('%s/.gitmodules' % self.repo.path, 'r') as f:
1426+
self.assertEqual("""\
1427+
[submodule "bar"]
1428+
url = ../bar.git
1429+
path = bar
1430+
""", f.read())
1431+
1432+
14111433
class PushTests(PorcelainTestCase):
14121434
def test_simple(self):
14131435
"""

0 commit comments

Comments
 (0)