1# This module is part of GitPython and is released under the
2# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
4__all__ = [
5 "sm_section",
6 "sm_name",
7 "mkhead",
8 "find_first_remote_branch",
9 "is_relative_url",
10 "resolve_relative_url",
11 "SubmoduleConfigParser",
12]
13
14from io import BytesIO
15import sys
16import weakref
17
18import git
19from git.config import GitConfigParser
20from git.exc import InvalidGitRepositoryError
21
22# typing -----------------------------------------------------------------------
23
24from typing import Any, Sequence, TYPE_CHECKING, Union
25
26from git.types import PathLike
27
28if TYPE_CHECKING:
29 from weakref import ReferenceType
30
31 from git.refs import Head, RemoteReference
32 from git.remote import Remote
33 from git.repo import Repo
34
35 from .base import Submodule
36
37# { Utilities
38
39
40def sm_section(name: str) -> str:
41 """:return: Section title used in ``.gitmodules`` configuration file"""
42 return f'submodule "{name}"'
43
44
45def sm_name(section: str) -> str:
46 """:return: Name of the submodule as parsed from the section name"""
47 section = section.strip()
48 return section[11:-1]
49
50
51def mkhead(repo: "Repo", path: PathLike) -> "Head":
52 """:return: New branch/head instance"""
53 return git.Head(repo, git.Head.to_full_path(path))
54
55
56def find_first_remote_branch(remotes: Sequence["Remote"], branch_name: str) -> "RemoteReference":
57 """Find the remote branch matching the name of the given branch or raise
58 :exc:`~git.exc.InvalidGitRepositoryError`."""
59 for remote in remotes:
60 try:
61 return remote.refs[branch_name]
62 except IndexError:
63 continue
64 # END exception handling
65 # END for remote
66 raise InvalidGitRepositoryError("Didn't find remote branch '%r' in any of the given remotes" % branch_name)
67
68
69def _is_dir_sep(char: str) -> bool:
70 return char == "/" or (sys.platform == "win32" and char == "\\")
71
72
73def _starts_with_dot_slash(url: str) -> bool:
74 return len(url) >= 2 and url[0] == "." and _is_dir_sep(url[1])
75
76
77def _starts_with_dot_dot_slash(url: str) -> bool:
78 return len(url) >= 3 and url[0:2] == ".." and _is_dir_sep(url[2])
79
80
81def _has_dos_drive_prefix(url: str) -> bool:
82 return sys.platform == "win32" and len(url) >= 2 and url[0].isalpha() and url[1] == ":"
83
84
85def _is_absolute_path(url: str) -> bool:
86 return (len(url) >= 1 and _is_dir_sep(url[0])) or _has_dos_drive_prefix(url)
87
88
89def _url_is_local_not_ssh(url: str) -> bool:
90 colon = url.find(":")
91 slash = url.find("/")
92 return colon == -1 or (slash != -1 and slash < colon) or _has_dos_drive_prefix(url)
93
94
95def _find_last_dir_sep(url: str) -> int:
96 for index in range(len(url) - 1, -1, -1):
97 if _is_dir_sep(url[index]):
98 return index
99 return -1
100
101
102def is_relative_url(url: str) -> bool:
103 """:return: ``True`` if `url` starts with ``./`` or ``../``, which git resolves
104 against the superproject's remote rather than treating as a path"""
105 return _starts_with_dot_slash(url) or _starts_with_dot_dot_slash(url)
106
107
108def resolve_relative_url(remote_url: str, url: str) -> str:
109 """Resolve a submodule URL that starts with ``./`` or ``../`` against the
110 superproject's remote URL, the way git's ``relative_url`` does.
111
112 Git treats a relative submodule URL like a relative directory: each leading
113 ``../`` removes the last component of `remote_url`, whether that is a path
114 component, the host of a URL, or the host of an scp-like ``host:path`` address.
115 The remainder is appended with ``/``, or with ``:`` once an scp-like host was
116 reached. A URL that is not relative is returned unchanged.
117
118 :raise ValueError:
119 If more components are stripped than `remote_url` has, which git rejects with
120 "cannot strip one component off url".
121 """
122 if not is_relative_url(url):
123 return url
124
125 remote = remote_url
126 if remote and _is_dir_sep(remote[-1]):
127 remote = remote[:-1]
128
129 is_relative = _url_is_local_not_ssh(remote) and not _is_absolute_path(remote)
130 if is_relative and not _starts_with_dot_slash(remote) and not _starts_with_dot_dot_slash(remote):
131 remote = "./" + remote
132
133 colonsep = False
134 while url:
135 if _starts_with_dot_dot_slash(url):
136 url = url[3:]
137 separator = _find_last_dir_sep(remote)
138 if separator != -1:
139 remote = remote[:separator]
140 elif ":" in remote:
141 remote = remote[: remote.rfind(":")]
142 colonsep = True
143 elif is_relative or remote == ".":
144 raise ValueError("cannot strip one component off url %r" % remote)
145 else:
146 remote = "."
147 elif _starts_with_dot_slash(url):
148 url = url[2:]
149 else:
150 break
151
152 resolved = remote + (":" if colonsep else "/") + url
153 if url.endswith("/"):
154 resolved = resolved[:-1]
155 if _starts_with_dot_slash(resolved):
156 resolved = resolved[2:]
157 return resolved
158
159
160# } END utilities
161
162# { Classes
163
164
165class SubmoduleConfigParser(GitConfigParser):
166 """Catches calls to :meth:`~git.config.GitConfigParser.write`, and updates the
167 ``.gitmodules`` blob in the index with the new data, if we have written into a
168 stream.
169
170 Otherwise it would add the local file to the index to make it correspond with the
171 working tree. Additionally, the cache must be cleared.
172
173 Please note that no mutating method will work in bare mode.
174 """
175
176 def __init__(self, *args: Any, **kwargs: Any) -> None:
177 self._smref: Union["ReferenceType[Submodule]", None] = None
178 self._index = None
179 self._auto_write = True
180 super().__init__(*args, **kwargs)
181
182 # { Interface
183 def set_submodule(self, submodule: "Submodule") -> None:
184 """Set this instance's submodule. It must be called before the first write
185 operation begins."""
186 self._smref = weakref.ref(submodule)
187
188 def flush_to_index(self) -> None:
189 """Flush changes in our configuration file to the index."""
190 assert self._smref is not None
191 # Should always have a file here.
192 assert not isinstance(self._file_or_files, BytesIO)
193
194 sm = self._smref()
195 if sm is not None:
196 index = self._index
197 if index is None:
198 index = sm.repo.index
199 # END handle index
200 index.add([sm.k_modules_file], write=self._auto_write)
201 sm._clear_cache()
202 # END handle weakref
203
204 # } END interface
205
206 # { Overridden Methods
207 def write(self) -> None: # type: ignore[override]
208 rval: None = super().write()
209 self.flush_to_index()
210 return rval
211
212 # END overridden methods
213
214
215# } END classes