Skip to content

Commit 8c7697c

Browse files
Added multi-level inheritance support for import.
Signed-off-by: Leander Stephen D'Souza <leanderdsouza1234@gmail.com>
1 parent 62bf913 commit 8c7697c

File tree

7 files changed

+176
-24
lines changed

7 files changed

+176
-24
lines changed

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,64 @@ For instance, consider the following two files:
135135
```
136136
The resulting extension import would import vcs2l at version `main`, `immutable/hash` at version `25e4ae2` and `immutable/tag` at version `1.1.5`.
137137

138+
#### Multiple Extensions
139+
140+
The `extends` key also supports a list of files to extend from. The files are imported in the order they are specified and the precedence is given to the last file in case of duplicate repository entries.
141+
142+
For instance, consider the following three files:
143+
144+
- **`base_1.repos`**: contains two repositories `vcs2l` and `immutable/hash`, checked out at `1.1.3`.
145+
146+
```yaml
147+
---
148+
repositories:
149+
immutable/hash:
150+
type: git
151+
url: https://github.com/ros-infrastructure/vcs2l.git
152+
version: e700793cb2b8d25ce83a611561bd167293fd66eb # 1.1.3
153+
vcs2l:
154+
type: git
155+
url: https://github.com/ros-infrastructure/vcs2l.git
156+
version: 1.1.3
157+
```
158+
159+
- **`base_2.repos`**: contains two repositories `vcs2l` and `immutable/hash`, checked out at `1.1.4`.
160+
161+
```yaml
162+
---
163+
repositories:
164+
immutable/hash:
165+
type: git
166+
url: https://github.com/ros-infrastructure/vcs2l.git
167+
version: 2c7ff89d12d8a77c36b60d1f7ba3039cdd3f742b # 1.1.4
168+
vcs2l:
169+
type: git
170+
url: https://github.com/ros-infrastructure/vcs2l.git
171+
version: 1.1.4
172+
```
173+
174+
- **`multiple_extension.repos`**: extends both base files and overrides the version of `vcs2l` repository.
175+
176+
```yaml
177+
---
178+
extends:
179+
- base_1.repos # Lower priority
180+
- base_2.repos # Higher priority
181+
repositories:
182+
vcs2l:
183+
type: git
184+
url: https://github.com/ros-infrastructure/vcs2l.git
185+
version: 1.1.5
186+
```
187+
188+
The resulting extension import would import `immutable/hash` at version `1.1.4` (from `base_2.repos`) and `vcs2l` at version `1.1.5`.
189+
190+
Duplicate file names in the `extends` list are not allowed and would raise the following error:
191+
192+
```bash
193+
Duplicate entries found in extends in file: <relative-path>/multiple_extension.repos
194+
```
195+
138196
#### Circular Loop Protection
139197

140198
In order to avoid infinite loops in case of circular imports the tool detects already imported files and raises an error if such a file is encountered again.

test/import_multiple_extends.txt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
......
2+
=== ./immutable/hash (git) ===
3+
Cloning into '.'...
4+
Note: switching to '2c7ff89d12d8a77c36b60d1f7ba3039cdd3f742b'.
5+
6+
You are in 'detached HEAD' state. You can look around, make experimental
7+
changes and commit them, and you can discard any commits you make in this
8+
state without impacting any branches by switching back to a branch.
9+
10+
If you want to create a new branch to retain commits you create, you may
11+
do so (now or later) by using -c with the switch command. Example:
12+
13+
git switch -c <new-branch-name>
14+
15+
Or undo this operation with:
16+
17+
git switch -
18+
19+
Turn off this advice by setting config variable advice.detachedHead to false
20+
21+
HEAD is now at 2c7ff89 1.1.4
22+
=== ./immutable/hash_tar (tar) ===
23+
Downloaded tarball from 'https://github.com/ros-infrastructure/vcs2l/archive/377d5b3d03c212f015cc832fdb368f4534d0d583.tar.gz' and unpacked it
24+
=== ./immutable/hash_zip (zip) ===
25+
Downloaded zipfile from 'https://github.com/ros-infrastructure/vcs2l/archive/377d5b3d03c212f015cc832fdb368f4534d0d583.zip' and unpacked it
26+
=== ./immutable/tag (git) ===
27+
Cloning into '.'...
28+
Note: switching to 'tags/1.1.5'.
29+
30+
You are in 'detached HEAD' state. You can look around, make experimental
31+
changes and commit them, and you can discard any commits you make in this
32+
state without impacting any branches by switching back to a branch.
33+
34+
If you want to create a new branch to retain commits you create, you may
35+
do so (now or later) by using -c with the switch command. Example:
36+
37+
git switch -c <new-branch-name>
38+
39+
Or undo this operation with:
40+
41+
git switch -
42+
43+
Turn off this advice by setting config variable advice.detachedHead to false
44+
45+
HEAD is now at 25e4ae2 1.1.5
46+
=== ./vcs2l (git) ===
47+
Cloning into '.'...
48+
=== ./without_version (git) ===
49+
Cloning into '.'...

test/list_extension.repos

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Check for hash, tag, and branch imports with extends functionality
1+
# Check for hash, tag, and branch imports with extends functionality with predominant tag 1.1.3
22
---
33
extends: list.repos
44
repositories:

test/list_extension_2.repos

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Check for hash, tag, and branch imports with extends functionality with predominant tag 1.1.4
2+
---
3+
extends: list.repos
4+
repositories:
5+
immutable/hash:
6+
type: git
7+
url: https://github.com/ros-infrastructure/vcs2l.git
8+
version: 2c7ff89d12d8a77c36b60d1f7ba3039cdd3f742b # 1.1.4
9+
immutable/tag:
10+
type: git
11+
url: https://github.com/ros-infrastructure/vcs2l.git
12+
version: tags/1.1.4
13+
vcs2l:
14+
type: git
15+
url: https://github.com/ros-infrastructure/vcs2l.git
16+
version: 1.1.4

test/list_multiple_extension.repos

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Check for multiple levels of extension in repositories files.
2+
---
3+
extends:
4+
- list_extension.repos # predominant tag is 1.1.3 # Lower priority
5+
- list_extension_2.repos # predominant tag is 1.1.4 # Higher priority
6+
repositories:
7+
immutable/tag:
8+
type: git
9+
url: https://github.com/ros-infrastructure/vcs2l.git
10+
version: tags/1.1.5
11+
vcs2l:
12+
type: git
13+
url: https://github.com/ros-infrastructure/vcs2l.git
14+
version: main

test/test_commands.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
REPOS_FILE_URL = file_uri_scheme + REPOS_FILE
1616
REPOS2_FILE = os.path.join(os.path.dirname(__file__), 'list2.repos')
1717
REPOS_EXTENDS_FILE = os.path.join(os.path.dirname(__file__), 'list_extension.repos')
18+
REPOS_MULTIPLE_EXTENDS_FILE = os.path.join(
19+
os.path.dirname(__file__), 'list_multiple_extension.repos'
20+
)
1821
REPOS_EXTENDS_LOOP_FILE = os.path.join(
1922
os.path.dirname(__file__), 'loop_extension.repos'
2023
)
@@ -351,6 +354,10 @@ def test_import_extends_loop(self):
351354
run_command('import', ['--input', REPOS_EXTENDS_LOOP_FILE, '.'])
352355
self.assertIn(b'Circular import detected:', e.exception.output)
353356

357+
def test_import_multiple_extends(self):
358+
"""Test import with multiple extends functionality."""
359+
self.import_common('import_multiple_extends', REPOS_MULTIPLE_EXTENDS_FILE)
360+
354361
def test_validate(self):
355362
output = run_command('validate', ['--input', REPOS_FILE])
356363
expected = get_expected_output('validate')

vcs2l/commands/import_.py

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -128,27 +128,37 @@ def get_repositories(yaml_file, visited_files=None):
128128
combined_repos = {}
129129

130130
if 'extends' in root:
131-
parent_file = root['extends']
132-
133-
# If absolute path is not valid, try relative to current file
134-
if not os.path.isabs(parent_file):
135-
current_dir = os.path.dirname(current_file_path)
136-
parent_file = os.path.join(current_dir, parent_file)
137-
138-
if not os.path.exists(parent_file):
139-
raise RuntimeError(f'Parent file not found: {parent_file}')
140-
141-
try:
142-
# Recursively get repositories from parent file
143-
with open(parent_file, 'r', encoding='utf-8') as parent_f:
144-
parent_repos = get_repositories(parent_f, visited_files.copy())
145-
combined_repos.update(parent_repos)
146-
except Exception as e:
147-
if str(e).startswith('Circular import detected:'):
148-
raise
131+
parent_files = root['extends']
132+
# Convert single file to list for consistent processing
133+
if isinstance(parent_files, str):
134+
parent_files = [parent_files]
135+
136+
# Check for duplicate entries in extends
137+
if len(parent_files) != len(set(parent_files)):
149138
raise RuntimeError(
150-
f'Error reading parent file {parent_file}: \n{str(e)}'
151-
) from e
139+
f'Duplicate entries found in extends in file: {current_file_path}'
140+
)
141+
142+
for parent_file in parent_files:
143+
# If absolute path is not valid, try relative to current file
144+
if not os.path.isabs(parent_file):
145+
current_dir = os.path.dirname(current_file_path)
146+
parent_file = os.path.join(current_dir, parent_file)
147+
148+
if not os.path.exists(parent_file):
149+
raise RuntimeError(f'Parent file not found: {parent_file}')
150+
151+
try:
152+
# Recursively get repositories from parent file
153+
with open(parent_file, 'r', encoding='utf-8') as parent_f:
154+
parent_repos = get_repositories(parent_f, visited_files.copy())
155+
combined_repos.update(parent_repos)
156+
except Exception as e:
157+
if str(e).startswith('Circular import detected:'):
158+
raise
159+
raise RuntimeError(
160+
f'Error reading parent file {parent_file}: \n{str(e)}'
161+
) from e
152162

153163
current_repos = get_repositories_from_root(root)
154164
combined_repos.update(current_repos)
@@ -158,9 +168,7 @@ def get_repositories(yaml_file, visited_files=None):
158168
except FileNotFoundError as e:
159169
raise RuntimeError(f'File not found: {yaml_file}') from e
160170
except yaml.YAMLError as e:
161-
raise RuntimeError(
162-
f'Error parsing YAML file {yaml_file}: {str(e)}'
163-
) from e
171+
raise RuntimeError(f'Error parsing YAML file {yaml_file}: {str(e)}') from e
164172
finally:
165173
# Remove current file from visited set when leaving this call
166174
visited_files.discard(current_file_path)

0 commit comments

Comments
 (0)