Commit 13818a2
authored
## Summary
- **`--with-shared`** — controls whether GitLab's `/groups/:id/projects`
endpoint returns projects that have been shared into the group from other
namespaces. GitLab's API default is `with_shared=true`; vcspull now sends
`with_shared=false` by default so only the group's own repos appear, with
`--with-shared` to opt back in.
- **`--skip-group GROUP`** — client-side filter that excludes all repositories
whose owner namespace path contains `GROUP` as a path segment
(case-insensitive). Repeatable: `--skip-group bots --skip-group archived`.
Matches `my-group/bots` and `my-group/bots/subteam` but not
`my-group/robotics`.
Both flags are GitLab-only; all other service importers (GitHub, Gitea,
CodeCommit, Codeberg, Forgejo) are unaffected — new params carry safe defaults.
## What changed
| Layer | File | Change |
|-------|------|--------|
| Model | `_internal/remotes/base.py` | `ImportOptions`: new `with_shared: bool = False` and `skip_groups: list[str]` fields; `filter_repo()`: segment-matching skip guard before min-stars check |
| API | `_internal/remotes/gitlab.py` | `_paginate_repos()`: emit `with_shared=true/false` inside `if include_subgroups:` block (group mode only) |
| CLI | `cli/import_cmd/gitlab.py` | `--with-shared` (`store_true`) and `--skip-group` (`action=append`, `default=None`) |
| Plumbing | `cli/import_cmd/_common.py` | `_run_import()`: new `with_shared`/`skip_groups` kwargs; normalises `None → []` before `ImportOptions` construction |
| Docs | `docs/cli/import/gitlab.md` | "Including shared repositories" and "Skipping subgroups" sections |
**GitLab API reference:**
- [`GET /groups/:id/projects`](https://docs.gitlab.com/api/groups/) — `with_shared` (boolean, optional, **default `true`**)
- [Sharing projects and groups](https://docs.gitlab.com/user/project/members/sharing_projects_groups/)
## Test plan
### Automated (run with `uv run pytest`)
All 1158 existing + new tests pass. New tests added:
#### URL parameter verification (`tests/_internal/remotes/test_gitlab.py`)
These tests monkeypatch `urllib.request.urlopen`, capture the actual URL
sent to the GitLab API, and assert using `urllib.parse.parse_qs` (not
substring search, so query-string ordering cannot cause false failures).
- [ ] `test_gitlab_with_shared_false_by_default` — group mode, no flag →
`parse_qs` confirms `with_shared=["false"]` in the request URL
- [ ] `test_gitlab_with_shared_true_when_flag_set` — group mode,
`ImportOptions(with_shared=True)` → `parse_qs` confirms
`with_shared=["true"]`
- [ ] `test_gitlab_with_shared_not_sent_in_user_mode` — user mode,
`with_shared=True` in options (should be ignored) → `with_shared` key
must be **absent** from the URL (GitLab `/users/:id/projects` does not
accept this param)
#### Filter integration (`tests/_internal/remotes/test_gitlab.py`)
These tests inject a multi-repo mock response and assert which repos survive
after the full `filter_repo()` pass inside `fetch_repos()`.
- [ ] `test_gitlab_skip_groups_filters_repos` — response contains repos in
`testgroup`, `testgroup/bots`, `testgroup/bots/subteam`; `skip_groups=["bots"]`
→ only the `testgroup` repo survives (segment match eliminates both bots tiers)
- [ ] `test_gitlab_skip_groups_case_insensitive` — repo owner is `ORG/Bots`,
`skip_groups=["bots"]` (lowercase) → repo is excluded (case-insensitive match)
#### CLI parser roundtrip (`tests/cli/test_import_repos.py`)
- [ ] `test_import_with_shared_flag_via_cli` — `parser.parse_args([...,
"--with-shared"])` → `args.with_shared is True`
- [ ] `test_import_skip_group_flag_via_cli` — `parser.parse_args([...,
"--skip-group", "bots", "--skip-group", "archived"])` →
`args.skip_groups == ["bots", "archived"]`
#### End-to-end plumbing (`tests/cli/test_import_repos.py`)
- [ ] `test_run_import_forwards_with_shared_and_skip_groups` — uses
`CapturingMockImporter` (new helper) to record the `ImportOptions` object
that `_run_import()` constructs and passes to `fetch_repos()`; asserts
`opts.with_shared is True` and `opts.skip_groups == ["bots", "archived"]`
### Manual smoke tests (requires `GITLAB_TOKEN` with `api` scope)
**E2E fixture setup (one-time):** `tony/external-shared-repo` (public) has been
shared into `vcs-python-group-test` at Reporter (20) access. This makes
`--with-shared` observably different from the default. The group now has
15 own repos + 1 externally-shared repo visible with `--with-shared`.
All 5 permutations verified on 2026-02-21:
| Flags | Expected | Actual | Status |
|-------|----------|--------|--------|
| (none) | 15 own repos | ✓ 15 | ✅ |
| `--with-shared` | 15 + 1 shared | ✓ 16 | ✅ |
| `--skip-group vcs-python-subgroup-test` | top-level only | ✓ 5 | ✅ |
| `--with-shared --skip-group vcs-python-subgroup-test` | top-level + shared | ✓ 6 | ✅ |
| `--skip-group subgroup` (segment guard) | all 15 | ✓ 15 | ✅ |
The combined case (row 4) is the key proof: `skip_groups` does **not**
accidentally drop `external-shared-repo` (whose namespace is `tony`, with
no overlap with `vcs-python-subgroup-test`).
**`--with-shared` default (shared repos excluded):**
```console
$ vcspull import gl vcs-python-group-test \
--mode org \
--workspace /tmp/test-import \
--dry-run \
--yes
```
Expected: 15 repos (group's own repos only; `external-shared-repo` absent).
**`--with-shared` enabled:**
```console
$ vcspull import gl vcs-python-group-test \
--mode org \
--workspace /tmp/test-import \
--with-shared \
--dry-run \
--yes
```
Expected: 16 repos (`external-shared-repo` from `tony` namespace appears).
**`--skip-group` skipping the subgroup:**
```console
$ vcspull import gl vcs-python-group-test \
--mode org \
--workspace /tmp/test-import \
--skip-group vcs-python-subgroup-test \
--dry-run \
--yes
```
Expected: only top-level repos (5 repos). `sub-test-repo-{1-4}` and
`subsub-test-repo-{1-4}` must be absent.
**`--skip-group` skipping only the sub-subgroup:**
```console
$ vcspull import gl vcs-python-group-test \
--mode org \
--workspace /tmp/test-import \
--skip-group vcs-python-subsubgroup-test \
--dry-run \
--yes
```
Expected: top-level + subgroup repos. `subsub-test-repo-{1-4}` must be absent.
**Segment-not-substring guard:**
```console
$ vcspull import gl vcs-python-group-test \
--mode org \
--workspace /tmp/test-import \
--skip-group subgroup \
--dry-run \
--yes
```
Expected: all 15 own repos returned — `vcs-python-subgroup-test` contains the
string "subgroup" but the segment is `vcs-python-subgroup-test`, not `subgroup`,
so no match.
**`--with-shared` has no effect in user mode:**
```console
$ vcspull import gl <your-gitlab-username> \
--mode user \
--workspace /tmp/test-import \
--with-shared \
--dry-run \
--yes
```
Expected: same result as without `--with-shared` (param not sent to API).
8 files changed
Lines changed: 1128 additions & 11 deletions
File tree
- docs/cli/import
- src/vcspull
- _internal/remotes
- cli/import_cmd
- tests
- _internal/remotes
- cli
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
36 | 55 | | |
37 | 56 | | |
38 | 57 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
61 | 61 | | |
62 | 62 | | |
63 | 63 | | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
64 | 108 | | |
65 | 109 | | |
66 | 110 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
232 | 232 | | |
233 | 233 | | |
234 | 234 | | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
235 | 244 | | |
236 | 245 | | |
237 | 246 | | |
| |||
244 | 253 | | |
245 | 254 | | |
246 | 255 | | |
| 256 | + | |
| 257 | + | |
247 | 258 | | |
248 | 259 | | |
249 | 260 | | |
| |||
263 | 274 | | |
264 | 275 | | |
265 | 276 | | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
266 | 289 | | |
267 | 290 | | |
268 | 291 | | |
| |||
606 | 629 | | |
607 | 630 | | |
608 | 631 | | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
609 | 663 | | |
610 | 664 | | |
611 | 665 | | |
| |||
628 | 682 | | |
629 | 683 | | |
630 | 684 | | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
631 | 692 | | |
632 | 693 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
241 | 241 | | |
242 | 242 | | |
243 | 243 | | |
244 | | - | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
245 | 256 | | |
246 | 257 | | |
247 | 258 | | |
| |||
283 | 294 | | |
284 | 295 | | |
285 | 296 | | |
| 297 | + | |
286 | 298 | | |
287 | 299 | | |
288 | 300 | | |
| |||
324 | 336 | | |
325 | 337 | | |
326 | 338 | | |
327 | | - | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
328 | 351 | | |
329 | 352 | | |
330 | 353 | | |
331 | 354 | | |
332 | 355 | | |
333 | 356 | | |
334 | 357 | | |
| 358 | + | |
| 359 | + | |
335 | 360 | | |
336 | 361 | | |
337 | 362 | | |
338 | 363 | | |
339 | 364 | | |
340 | 365 | | |
341 | | - | |
| 366 | + | |
342 | 367 | | |
343 | 368 | | |
344 | 369 | | |
345 | | - | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
346 | 373 | | |
347 | 374 | | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
348 | 379 | | |
349 | | - | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
350 | 386 | | |
351 | 387 | | |
352 | 388 | | |
353 | | - | |
354 | | - | |
355 | | - | |
356 | | - | |
357 | | - | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
358 | 404 | | |
359 | 405 | | |
360 | 406 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
281 | 281 | | |
282 | 282 | | |
283 | 283 | | |
| 284 | + | |
| 285 | + | |
284 | 286 | | |
285 | 287 | | |
286 | 288 | | |
| |||
329 | 331 | | |
330 | 332 | | |
331 | 333 | | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
332 | 338 | | |
333 | 339 | | |
334 | 340 | | |
| |||
347 | 353 | | |
348 | 354 | | |
349 | 355 | | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
350 | 371 | | |
351 | 372 | | |
352 | 373 | | |
| |||
357 | 378 | | |
358 | 379 | | |
359 | 380 | | |
| 381 | + | |
| 382 | + | |
360 | 383 | | |
361 | 384 | | |
362 | 385 | | |
| |||
384 | 407 | | |
385 | 408 | | |
386 | 409 | | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
387 | 420 | | |
388 | 421 | | |
389 | 422 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
57 | 78 | | |
58 | 79 | | |
59 | 80 | | |
| |||
98 | 119 | | |
99 | 120 | | |
100 | 121 | | |
| 122 | + | |
| 123 | + | |
101 | 124 | | |
0 commit comments