Skip to content

Commit ef7a651

Browse files
authored
Merge pull request #2244 from jeffro256/submodule_update_no_fetch
submodule: option to update without fetching
2 parents dafafff + a83262b commit ef7a651

3 files changed

Lines changed: 529 additions & 90 deletions

File tree

git/objects/submodule/base.py

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,32 @@
44
__all__ = ["Submodule", "UpdateProgress"]
55

66
import gc
7-
from io import BytesIO
87
import logging
98
import ntpath
109
import os
1110
import os.path as osp
12-
from pathlib import Path
1311
import shlex
1412
import stat
1513
import sys
16-
import uuid
1714
import urllib.parse
15+
import uuid
16+
from io import BytesIO
17+
from pathlib import Path
18+
19+
# typing ----------------------------------------------------------------------
20+
from typing import (
21+
TYPE_CHECKING,
22+
Any,
23+
Callable,
24+
Dict,
25+
Iterator,
26+
List,
27+
Literal,
28+
Mapping,
29+
Sequence,
30+
Union,
31+
cast,
32+
)
1833

1934
import git
2035
from git.cmd import Git
@@ -46,23 +61,7 @@
4661
sm_section,
4762
)
4863

49-
# typing ----------------------------------------------------------------------
50-
51-
from typing import (
52-
Any,
53-
Callable,
54-
Dict,
55-
Iterator,
56-
List,
57-
Literal,
58-
Mapping,
59-
Sequence,
60-
TYPE_CHECKING,
61-
Union,
62-
cast,
63-
)
64-
65-
from git.types import Commit_ish, PathLike, TBD
64+
from git.types import TBD, Commit_ish, PathLike
6665

6766
if TYPE_CHECKING:
6867
from git.index import IndexFile
@@ -729,6 +728,7 @@ def update(
729728
clone_multi_options: Union[Sequence[TBD], None] = None,
730729
allow_unsafe_options: bool = False,
731730
allow_unsafe_protocols: bool = False,
731+
no_fetch: bool = False,
732732
) -> "Submodule":
733733
"""Update the repository of this submodule to point to the checkout we point at
734734
with the binsha of this instance.
@@ -791,6 +791,11 @@ def update(
791791
:param allow_unsafe_options:
792792
Allow unsafe options to be used, like ``--upload-pack``.
793793
794+
:param no_fetch:
795+
If ``True``, update using locally available objects and remote-tracking
796+
refs without fetching or cloning. Repositories retained after
797+
:meth:`deinit` can be restored without fetching.
798+
794799
:note:
795800
Does nothing in bare repositories.
796801
@@ -853,7 +858,8 @@ def fetch_remotes(module_repo: "Repo") -> None:
853858
#######################################
854859
try:
855860
mrepo = self.module()
856-
fetch_remotes(mrepo)
861+
if not no_fetch:
862+
fetch_remotes(mrepo)
857863
except InvalidGitRepositoryError:
858864
mrepo = None
859865
if not init:
@@ -884,7 +890,8 @@ def fetch_remotes(module_repo: "Repo") -> None:
884890
self._write_git_file_and_module_config(checkout_module_abspath, module_abspath)
885891
mrepo = git.Repo(checkout_module_abspath)
886892
mrepo.head.reset(mrepo.head.commit, index=True, working_tree=True)
887-
fetch_remotes(mrepo)
893+
if not no_fetch:
894+
fetch_remotes(mrepo)
888895
with self.repo.config_writer() as writer:
889896
writer.set_value(sm_section(self.name), "url", self.url)
890897

@@ -909,6 +916,8 @@ def fetch_remotes(module_repo: "Repo") -> None:
909916
+ "Cloning url '%s' to '%s' in submodule %r" % (self.url, checkout_module_abspath, self.name),
910917
)
911918
if not dry_run:
919+
if no_fetch:
920+
raise ValueError("Missing module at %r but fetching is disabled" % self.path) from None
912921
if self.url.startswith("."):
913922
url = urllib.parse.urljoin(self.repo.remotes.origin.url + "/", self.url)
914923
else:
@@ -1057,6 +1066,7 @@ def fetch_remotes(module_repo: "Repo") -> None:
10571066
dry_run=dry_run,
10581067
force=force,
10591068
keep_going=keep_going,
1069+
no_fetch=no_fetch,
10601070
)
10611071
# END handle recursive update
10621072
# END handle dry run

git/objects/submodule/root.py

Lines changed: 80 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,17 @@
55

66
import logging
77

8+
# typing -------------------------------------------------------------------
9+
from typing import TYPE_CHECKING, Union
10+
811
import git
912
from git.exc import InvalidGitRepositoryError
13+
from git.types import Commit_ish
1014
from git.util import IterableList
1115

1216
from .base import Submodule, UpdateProgress
1317
from .util import find_first_remote_branch
1418

15-
# typing -------------------------------------------------------------------
16-
17-
from typing import TYPE_CHECKING, Union
18-
19-
from git.types import Commit_ish
20-
2119
if TYPE_CHECKING:
2220
from git.repo import Repo
2321

@@ -87,6 +85,7 @@ def update( # type: ignore[override]
8785
dry_run: bool = False,
8886
force_reset: bool = False,
8987
keep_going: bool = False,
88+
no_fetch: bool = False,
9089
) -> "RootModule":
9190
"""Update the submodules of this repository to the current HEAD commit.
9291
@@ -146,6 +145,11 @@ def update( # type: ignore[override]
146145
In conjunction with `dry_run`, this can be useful to anticipate all errors
147146
when updating submodules.
148147
148+
:param no_fetch:
149+
If ``True``, update using locally available objects and remote-tracking
150+
refs without fetching or cloning. Cached refs are preserved and used even
151+
when a submodule's URL changes.
152+
149153
:return:
150154
self
151155
"""
@@ -254,7 +258,7 @@ def update( # type: ignore[override]
254258
# HANDLE URL CHANGE
255259
###################
256260
if sm.url != psm.url:
257-
# Add the new remote, remove the old one.
261+
# When fetching, add the new remote and remove the old one.
258262
# This way, if the url just changes, the commits will not have
259263
# to be re-retrieved.
260264
nn = "__new_origin__"
@@ -272,33 +276,19 @@ def update( # type: ignore[override]
272276
)
273277

274278
if not dry_run:
275-
assert nn not in [r.name for r in rmts]
276-
smr = smm.create_remote(nn, sm.url)
277-
smr.fetch(progress=progress)
278-
279-
# If we have a tracking branch, it should be available
280-
# in the new remote as well.
281-
if len([r for r in smr.refs if r.remote_head == sm.branch_name]) == 0:
282-
raise ValueError(
283-
"Submodule branch named %r was not available in new submodule remote at %r"
284-
% (sm.branch_name, sm.url)
285-
)
286-
# END head is not detached
287-
288-
# Now delete the changed one.
289-
rmt_for_deletion = None
279+
previous_remote = None
290280
for remote in rmts:
291281
if remote.url == psm.url:
292-
rmt_for_deletion = remote
282+
previous_remote = remote
293283
break
294284
# END if urls match
295285
# END for each remote
296286

297287
# If we didn't find a matching remote, but have exactly
298288
# one, we can safely use this one.
299-
if rmt_for_deletion is None:
289+
if previous_remote is None:
300290
if len(rmts) == 1:
301-
rmt_for_deletion = rmts[0]
291+
previous_remote = rmts[0]
302292
else:
303293
# If we have not found any remote with the
304294
# original URL we may not have a name. This is a
@@ -311,45 +301,64 @@ def update( # type: ignore[override]
311301
# END handle one single remote
312302
# END handle check we found a remote
313303

314-
orig_name = rmt_for_deletion.name
315-
smm.delete_remote(rmt_for_deletion)
316-
# NOTE: Currently we leave tags from the deleted remotes
317-
# as well as separate tracking branches in the possibly
318-
# totally changed repository (someone could have changed
319-
# the url to another project). At some point, one might
320-
# want to clean it up, but the danger is high to remove
321-
# stuff the user has added explicitly.
322-
323-
# Rename the new remote back to what it was.
324-
smr.rename(orig_name)
325-
326-
# Early on, we verified that the our current tracking
327-
# branch exists in the remote. Now we have to ensure
328-
# that the sha we point to is still contained in the new
329-
# remote tracking branch.
330-
smsha = sm.binsha
331-
found = False
332-
rref = smr.refs[self.branch_name]
333-
for c in rref.commit.traverse():
334-
if c.binsha == smsha:
335-
found = True
336-
break
337-
# END traverse all commits in search for sha
338-
# END for each commit
339-
340-
if not found:
341-
# Adjust our internal binsha to use the one of the
342-
# remote this way, it will be checked out in the
343-
# next step. This will change the submodule relative
344-
# to us, so the user will be able to commit the
345-
# change easily.
346-
_logger.warning(
347-
"Current sha %s was not contained in the tracking\
304+
if no_fetch:
305+
# A new remote would have no cached refs. Preserve
306+
# the existing refs and tracking configuration for
307+
# offline updates instead of replacing the remote.
308+
previous_remote.set_url(git.Git.polish_url(sm.url, expand_vars=False))
309+
else:
310+
assert nn not in [r.name for r in rmts]
311+
smr = smm.create_remote(nn, sm.url)
312+
smr.fetch(progress=progress)
313+
314+
# If we have a tracking branch, it should be available
315+
# in the new remote as well.
316+
if len([r for r in smr.refs if r.remote_head == sm.branch_name]) == 0:
317+
raise ValueError(
318+
"Submodule branch named %r was not available in new submodule remote at %r"
319+
% (sm.branch_name, sm.url)
320+
)
321+
# END head is not detached
322+
323+
orig_name = previous_remote.name
324+
smm.delete_remote(previous_remote)
325+
# NOTE: Currently we leave tags from the deleted remotes
326+
# as well as separate tracking branches in the possibly
327+
# totally changed repository (someone could have changed
328+
# the url to another project). At some point, one might
329+
# want to clean it up, but the danger is high to remove
330+
# stuff the user has added explicitly.
331+
332+
# Rename the new remote back to what it was.
333+
smr.rename(orig_name)
334+
335+
# Early on, we verified that the our current tracking
336+
# branch exists in the remote. Now we have to ensure
337+
# that the sha we point to is still contained in the new
338+
# remote tracking branch.
339+
smsha = sm.binsha
340+
found = False
341+
rref = smr.refs[self.branch_name]
342+
for c in rref.commit.traverse():
343+
if c.binsha == smsha:
344+
found = True
345+
break
346+
# END traverse all commits in search for sha
347+
# END for each commit
348+
349+
if not found:
350+
# Adjust our internal binsha to use the one of the
351+
# remote this way, it will be checked out in the
352+
# next step. This will change the submodule relative
353+
# to us, so the user will be able to commit the
354+
# change easily.
355+
_logger.warning(
356+
"Current sha %s was not contained in the tracking\
348357
branch at the new remote, setting it the the remote's tracking branch",
349-
sm.hexsha,
350-
)
351-
sm.binsha = rref.commit.binsha
352-
# END reset binsha
358+
sm.hexsha,
359+
)
360+
sm.binsha = rref.commit.binsha
361+
# END reset binsha
353362

354363
# NOTE: All checkout is performed by the base
355364
# implementation of update.
@@ -379,11 +388,12 @@ def update( # type: ignore[override]
379388
if not dry_run:
380389
smm = sm.module()
381390
smmr = smm.remotes
382-
# As the branch might not exist yet, we will have to fetch
383-
# all remotes to be sure...
384-
for remote in smmr:
385-
remote.fetch(progress=progress)
386-
# END for each remote
391+
# As the branch might not exist yet, fetch all remotes
392+
# unless restricted to locally cached refs.
393+
if not no_fetch:
394+
for remote in smmr:
395+
remote.fetch(progress=progress)
396+
# END for each remote
387397

388398
try:
389399
tbr = git.Head.create(
@@ -433,6 +443,7 @@ def update( # type: ignore[override]
433443
dry_run=dry_run,
434444
force=force_reset,
435445
keep_going=keep_going,
446+
no_fetch=no_fetch,
436447
)
437448

438449
# Update recursively depth first - question is which inconsistent state will
@@ -451,6 +462,7 @@ def update( # type: ignore[override]
451462
dry_run=dry_run,
452463
force_reset=force_reset,
453464
keep_going=keep_going,
465+
no_fetch=no_fetch,
454466
)
455467
# END handle dry_run
456468
# END handle recursive

0 commit comments

Comments
 (0)