@@ -164,48 +164,97 @@ def externals(self) -> list[External]:
164164 "-R" ,
165165 ],
166166 )
167-
168167 repo_root = SvnRepo .get_info_from_target ()["Repository Root" ]
168+ return SvnRepo ._parse_externals (
169+ result .stdout .decode (), repo_root , toplevel = self ._path
170+ )
169171
170- externals : list [External ] = []
171- # Pattern matches: "path - ..." where path is the local directory
172- path_pattern = r"([^\s^-]+)\s+-"
173- for entry in result .stdout .decode ().split (os .linesep * 2 ):
174- match : re .Match [str ] | None = None
175- local_path : str = ""
176- for match in re .finditer (path_pattern , entry ):
177- pass
178- if match :
179- local_path = match .group (1 )
180- entry = re .sub (path_pattern , "" , entry )
181-
182- # Pattern matches either:
183- # - url@revision name (pinned)
184- # - url name (unpinned)
185- for match in re .finditer (
186- r"([^-\s\d][^\s]+)(?:@)(\d+)\s+([^\s]+)|([^-\s\d][^\s]+)\s+([^\s]+)" ,
187- entry ,
188- ):
189- url = match .group (1 ) or match .group (4 )
190- name = match .group (3 ) or match .group (5 )
191- rev = "" if not match .group (2 ) else match .group (2 ).strip ()
192-
193- url , branch , tag , src = SvnRepo ._split_url (url , repo_root )
194-
195- externals += [
196- External (
197- name = name ,
198- toplevel = self ._path ,
199- path = "/" .join (os .path .join (local_path , name ).split (os .sep )),
200- revision = rev ,
201- url = url ,
202- branch = branch ,
203- tag = tag ,
204- src = src ,
205- )
206- ]
207-
208- return externals
172+ @staticmethod
173+ def externals_from_url (url : str , revision : str = "" ) -> list [External ]:
174+ """Get list of externals from a remote SVN URL."""
175+ cmd = ["svn" , "--non-interactive" , "propget" , "svn:externals" , "-R" ]
176+ if revision :
177+ cmd += ["--revision" , revision ]
178+ cmd += [url ]
179+ result = run_on_cmdline (logger , cmd )
180+ repo_root = SvnRepo .get_info_from_target (url )["Repository Root" ]
181+ normalized = SvnRepo ._normalize_url_prefix (result .stdout .decode (), url )
182+ return SvnRepo ._parse_externals (normalized , repo_root )
183+
184+ @staticmethod
185+ def _normalize_url_prefix (output : str , base_url : str ) -> str :
186+ """Convert URL-mode ``svn propget -R`` output to relative-path format.
187+
188+ When querying a remote URL, each entry is prefixed with the full SVN URL
189+ of the directory that owns the property instead of a relative path.
190+ Strip the base_url so the standard parser receives familiar relative paths.
191+ """
192+ base = base_url .rstrip ("/" )
193+ entries = []
194+ for entry in output .split (os .linesep * 2 ):
195+ if entry .startswith (base + "/" ):
196+ after = entry [len (base ) + 1 :]
197+ sep = after .find (" -" )
198+ if sep >= 0 :
199+ rel = after [:sep ] or "."
200+ entry = rel + after [sep :]
201+ elif entry .startswith (base + " -" ):
202+ entry = "." + entry [len (base ) :]
203+ entries .append (entry )
204+ return (os .linesep * 2 ).join (entries )
205+
206+ @staticmethod
207+ def _parse_externals (
208+ output : str , repo_root : str , toplevel : str = ""
209+ ) -> list [External ]:
210+ """Parse svn propget svn:externals output into External objects.
211+
212+ Args:
213+ output: Raw stdout from ``svn propget svn:externals -R``.
214+ repo_root: Repository root URL (used to resolve ``^/`` relative URLs).
215+ toplevel: Local working-copy root to record in each External.
216+ """
217+ externals : list [External ] = []
218+ path_pattern = r"(.+?)\s+-"
219+ for entry in output .split (os .linesep * 2 ):
220+ match : re .Match [str ] | None = None
221+ local_path : str = ""
222+ for match in re .finditer (path_pattern , entry ):
223+ pass
224+ if match :
225+ local_path = match .group (1 )
226+ entry = re .sub (path_pattern , "" , entry )
227+
228+ for match in re .finditer (
229+ r"-r\s+(\d+)\s+(\S+?)(?:@\d+)?\s+(\S+)|([^@\s-][^@\s]*)(?:@(\d+))?\s+([^\s]+)" ,
230+ entry ,
231+ ):
232+ if match .group (1 ):
233+ rev = match .group (1 )
234+ url = match .group (2 )
235+ name = match .group (3 )
236+ else :
237+ url = match .group (4 )
238+ rev = match .group (5 ) or ""
239+ name = match .group (6 )
240+
241+ url , branch , tag , src = SvnRepo ._split_url (url , repo_root )
242+
243+ raw_path = "/" .join (os .path .join (local_path , name ).split (os .sep ))
244+ externals += [
245+ External (
246+ name = name ,
247+ toplevel = toplevel ,
248+ path = raw_path [2 :] if raw_path .startswith ("./" ) else raw_path ,
249+ revision = rev ,
250+ url = url ,
251+ branch = branch ,
252+ tag = tag ,
253+ src = src ,
254+ )
255+ ]
256+
257+ return externals
209258
210259 @staticmethod
211260 def _split_url (url : str , repo_root : str ) -> tuple [str , str , str , str ]:
0 commit comments