diff options
| -rw-r--r-- | docs/internal-fs-layout.md | 1 | ||||
| -rw-r--r-- | docs/repoconfig.md | 103 | ||||
| -rw-r--r-- | docs/smart-sync.md | 16 | ||||
| -rw-r--r-- | man/repo-sync.1 | 6 | ||||
| -rw-r--r-- | subcmds/sync.py | 27 | ||||
| -rw-r--r-- | tests/test_subcmds_sync.py | 167 |
6 files changed, 299 insertions, 21 deletions
diff --git a/docs/internal-fs-layout.md b/docs/internal-fs-layout.md index ea0070bfb..64fe74cbe 100644 --- a/docs/internal-fs-layout.md +++ b/docs/internal-fs-layout.md | |||
| @@ -253,6 +253,7 @@ setting `REPO_CONFIG_DIR`. | |||
| 253 | 253 | ||
| 254 | * `.repoconfig/`: Repo's per-user directory for all random config files/state. | 254 | * `.repoconfig/`: Repo's per-user directory for all random config files/state. |
| 255 | * `.repoconfig/config`: Per-user settings using [git-config] file format. | 255 | * `.repoconfig/config`: Per-user settings using [git-config] file format. |
| 256 | See [repoconfig.md](./repoconfig.md) for the available settings. | ||
| 256 | * `.repoconfig/keyring-version`: Cache file for checking if the gnupg subdir | 257 | * `.repoconfig/keyring-version`: Cache file for checking if the gnupg subdir |
| 257 | has all the same keys as the repo launcher. Used to avoid running gpg | 258 | has all the same keys as the repo launcher. Used to avoid running gpg |
| 258 | constantly as that can be quite slow. | 259 | constantly as that can be quite slow. |
diff --git a/docs/repoconfig.md b/docs/repoconfig.md new file mode 100644 index 000000000..aef92fdc6 --- /dev/null +++ b/docs/repoconfig.md | |||
| @@ -0,0 +1,103 @@ | |||
| 1 | # repoconfig: User Configuration | ||
| 2 | |||
| 3 | Repo keeps a small number of settings that apply to the current user rather | ||
| 4 | than to one checkout. They live in the per-user configuration file | ||
| 5 | `~/.repoconfig/config`, which is separate from both git's own `~/.gitconfig` | ||
| 6 | and the per-checkout settings stored in `.repo/manifests.git/config` (see | ||
| 7 | [internal-fs-layout.md](./internal-fs-layout.md#Global-settings) for those). | ||
| 8 | |||
| 9 | [TOC] | ||
| 10 | |||
| 11 | ## Location | ||
| 12 | |||
| 13 | The file is `.repoconfig/config` inside the user's home directory. The parent | ||
| 14 | directory can be changed by setting the `REPO_CONFIG_DIR` environment variable, | ||
| 15 | in which case repo reads `$REPO_CONFIG_DIR/.repoconfig/config` instead. | ||
| 16 | |||
| 17 | The file is created on demand; it's fine for it to be missing. | ||
| 18 | |||
| 19 | ## Format | ||
| 20 | |||
| 21 | The file uses the [git-config] syntax, so it can be edited by hand: | ||
| 22 | |||
| 23 | ```ini | ||
| 24 | [sync] | ||
| 25 | smartsync = true | ||
| 26 | ``` | ||
| 27 | |||
| 28 | or managed with `git config` by pointing it at the file explicitly: | ||
| 29 | |||
| 30 | ```sh | ||
| 31 | # Set a value. | ||
| 32 | git config --file ~/.repoconfig/config sync.smartsync true | ||
| 33 | |||
| 34 | # Read a value. | ||
| 35 | git config --file ~/.repoconfig/config sync.smartsync | ||
| 36 | |||
| 37 | # Remove a value. | ||
| 38 | git config --file ~/.repoconfig/config --unset sync.smartsync | ||
| 39 | ``` | ||
| 40 | |||
| 41 | Boolean settings accept `true`/`yes` and `false`/`no`. | ||
| 42 | |||
| 43 | Repo caches a parsed copy of the file in `.repoconfig/.repo_config.json`; it is | ||
| 44 | refreshed automatically whenever `config` changes and can be safely deleted. | ||
| 45 | |||
| 46 | ## Settings | ||
| 47 | |||
| 48 | ### alias.* | ||
| 49 | |||
| 50 | `alias.<name>` defines `repo <name>` as a shortcut for another subcommand, | ||
| 51 | optionally with arguments, in the same way as git aliases: | ||
| 52 | |||
| 53 | ```ini | ||
| 54 | [alias] | ||
| 55 | ss = sync -s | ||
| 56 | st = status -o | ||
| 57 | ``` | ||
| 58 | |||
| 59 | Aliases defined in a checkout's `.repo/config` take precedence over per-user | ||
| 60 | ones. Existing subcommands cannot be overridden by an alias. | ||
| 61 | |||
| 62 | ### sync | ||
| 63 | |||
| 64 | #### smartsync | ||
| 65 | |||
| 66 | `sync.smartsync` is a boolean that lets a user decide once, for all their | ||
| 67 | checkouts, whether `repo sync` should [Smart Sync](./smart-sync.md) by | ||
| 68 | default: | ||
| 69 | |||
| 70 | ```sh | ||
| 71 | # Always Smart Sync, as if -s/--smart-sync were passed. | ||
| 72 | git config --file ~/.repoconfig/config sync.smartsync true | ||
| 73 | |||
| 74 | # Never Smart Sync by default, even if the manifest sets sync-smartsync="true". | ||
| 75 | git config --file ~/.repoconfig/config sync.smartsync false | ||
| 76 | |||
| 77 | # Remove the preference and go back to following the manifest. | ||
| 78 | git config --file ~/.repoconfig/config --unset sync.smartsync | ||
| 79 | ``` | ||
| 80 | |||
| 81 | The effective behavior is resolved in this order; the first match wins: | ||
| 82 | |||
| 83 | 1. Command line: `-s`/`--smart-sync`, `--no-smart-sync`, or `-t`/`--smart-tag`. | ||
| 84 | 2. User config: `sync.smartsync` in `~/.repoconfig/config`. | ||
| 85 | 3. Manifest: `sync-smartsync="true"` on the `<default>` element. | ||
| 86 | 4. Otherwise, a regular ToT sync. | ||
| 87 | |||
| 88 | So `sync.smartsync=false` turns off the manifest-driven default, but an | ||
| 89 | explicit `repo sync -s`, `repo sync -t TAG` or `repo smartsync` still works. | ||
| 90 | Likewise `sync.smartsync=true` is overridden by `--no-smart-sync`. | ||
| 91 | |||
| 92 | A Smart Sync enabled by `sync.smartsync=true` is treated as an explicit | ||
| 93 | request: it is subject to the same restrictions as `-s` (it is skipped when | ||
| 94 | `-l`/`--local-only`, `-m`/`--manifest-name`, `-t`/`--smart-tag` or | ||
| 95 | `--superproject-revision` is used), and a failure to obtain a manifest from | ||
| 96 | the server fails the sync instead of silently falling back to ToT. | ||
| 97 | |||
| 98 | `sync.smartsync` does not affect `-l`/`--local-only`: a local-only sync keeps | ||
| 99 | whatever `smart_sync_override.xml` the previous network sync left behind. See | ||
| 100 | [smart-sync.md](./smart-sync.md#Client-Options) for details. | ||
| 101 | |||
| 102 | |||
| 103 | [git-config]: https://git-scm.com/docs/git-config | ||
diff --git a/docs/smart-sync.md b/docs/smart-sync.md index 24f37461b..905a004a0 100644 --- a/docs/smart-sync.md +++ b/docs/smart-sync.md | |||
| @@ -204,6 +204,10 @@ still fails the sync, as before. Options that specify an explicit target or | |||
| 204 | manifest source (such as `-t`/`--smart-tag`, `-m`/`--manifest-name` or | 204 | manifest source (such as `-t`/`--smart-tag`, `-m`/`--manifest-name` or |
| 205 | `--superproject-revision`) will also disable the default Smart Sync behavior. | 205 | `--superproject-revision`) will also disable the default Smart Sync behavior. |
| 206 | 206 | ||
| 207 | Users can override this manifest default on their workstation with the | ||
| 208 | `sync.smartsync` user setting; see | ||
| 209 | [Per-user default](#Per_user-default-sync_smartsync) below. | ||
| 210 | |||
| 207 | ### --smart-sync / -s | 211 | ### --smart-sync / -s |
| 208 | 212 | ||
| 209 | Explicitly enables Smart Sync. Repo will call | 213 | Explicitly enables Smart Sync. Repo will call |
| @@ -231,6 +235,18 @@ match the settings Android build environments automatically setup. | |||
| 231 | Explicitly disables Smart Sync, overriding any `sync-smartsync="true"` setting | 235 | Explicitly disables Smart Sync, overriding any `sync-smartsync="true"` setting |
| 232 | declared in the manifest `<default>` element. | 236 | declared in the manifest `<default>` element. |
| 233 | 237 | ||
| 238 | ### Per-user default (sync.smartsync) | ||
| 239 | |||
| 240 | A user can decide once, for all their checkouts, whether `repo sync` should | ||
| 241 | Smart Sync by default by setting the boolean `sync.smartsync` key in their | ||
| 242 | [per-user repo config](./repoconfig.md#smartsync). | ||
| 243 | |||
| 244 | Command line flags (`-s`, `--no-smart-sync`, `-t`) override the user setting, | ||
| 245 | which in turn overrides the manifest's `sync-smartsync` default. A Smart Sync | ||
| 246 | enabled this way is treated as an explicit request, so it is subject to the | ||
| 247 | same restrictions as `-s` and fails the sync if no manifest can be obtained | ||
| 248 | from the server instead of silently falling back to ToT. | ||
| 249 | |||
| 234 | ### --smart-tag=TAG / -t | 250 | ### --smart-tag=TAG / -t |
| 235 | 251 | ||
| 236 | Repo will call `GetManifest(TAG)`. | 252 | Repo will call `GetManifest(TAG)`. |
diff --git a/man/repo-sync.1 b/man/repo-sync.1 index b033a906f..2c7467e1f 100644 --- a/man/repo-sync.1 +++ b/man/repo-sync.1 | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man. | 1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man. |
| 2 | .TH REPO "1" "September 2026" "repo sync" "Repo Manual" | 2 | .TH REPO "1" "October 2026" "repo sync" "Repo Manual" |
| 3 | .SH NAME | 3 | .SH NAME |
| 4 | repo \- repo sync - manual page for repo sync | 4 | repo \- repo sync - manual page for repo sync |
| 5 | .SH SYNOPSIS | 5 | .SH SYNOPSIS |
| @@ -188,6 +188,10 @@ The \fB\-s\fR/\-\-smart\-sync option can be used to sync to a known good build a | |||
| 188 | specified by the manifest\-server element in the current manifest. The | 188 | specified by the manifest\-server element in the current manifest. The |
| 189 | \fB\-t\fR/\-\-smart\-tag option is similar and allows you to specify a custom tag/label. | 189 | \fB\-t\fR/\-\-smart\-tag option is similar and allows you to specify a custom tag/label. |
| 190 | .PP | 190 | .PP |
| 191 | Whether 'repo sync' smart syncs by default can also be controlled with the | ||
| 192 | sync.smartsync per\-user setting; see docs/repoconfig.md. Explicit | ||
| 193 | \fB\-s\fR/\-\-no\-smart\-sync/\-t flags always override it. | ||
| 194 | .PP | ||
| 191 | The \fB\-u\fR/\-\-manifest\-server\-username and \fB\-p\fR/\-\-manifest\-server\-password options can | 195 | The \fB\-u\fR/\-\-manifest\-server\-username and \fB\-p\fR/\-\-manifest\-server\-password options can |
| 192 | be used to specify a username and password to authenticate with the manifest | 196 | be used to specify a username and password to authenticate with the manifest |
| 193 | server when using the \fB\-s\fR or \fB\-t\fR option. | 197 | server when using the \fB\-s\fR or \fB\-t\fR option. |
diff --git a/subcmds/sync.py b/subcmds/sync.py index e83f42345..d6354d4a0 100644 --- a/subcmds/sync.py +++ b/subcmds/sync.py | |||
| @@ -69,6 +69,7 @@ import event_log | |||
| 69 | from git_command import git_require | 69 | from git_command import git_require |
| 70 | from git_command import GitCommand | 70 | from git_command import GitCommand |
| 71 | from git_config import GetUrlCookieFile | 71 | from git_config import GetUrlCookieFile |
| 72 | from git_config import RepoConfig | ||
| 72 | from git_refs import HEAD | 73 | from git_refs import HEAD |
| 73 | from git_refs import R_HEADS | 74 | from git_refs import R_HEADS |
| 74 | import git_superproject | 75 | import git_superproject |
| @@ -491,6 +492,10 @@ build as specified by the manifest-server element in the current | |||
| 491 | manifest. The -t/--smart-tag option is similar and allows you to | 492 | manifest. The -t/--smart-tag option is similar and allows you to |
| 492 | specify a custom tag/label. | 493 | specify a custom tag/label. |
| 493 | 494 | ||
| 495 | Whether '%prog' smart syncs by default can also be controlled with the | ||
| 496 | sync.smartsync per-user setting; see docs/repoconfig.md. Explicit | ||
| 497 | -s/--no-smart-sync/-t flags always override it. | ||
| 498 | |||
| 494 | The -u/--manifest-server-username and -p/--manifest-server-password | 499 | The -u/--manifest-server-username and -p/--manifest-server-password |
| 495 | options can be used to specify a username and password to authenticate | 500 | options can be used to specify a username and password to authenticate |
| 496 | with the manifest server when using the -s or -t option. | 501 | with the manifest server when using the -s or -t option. |
| @@ -2522,22 +2527,36 @@ later is required to fix a server side protocol bug. | |||
| 2522 | def _ResolveSmartSyncOption( | 2527 | def _ResolveSmartSyncOption( |
| 2523 | self, opt: optparse.Values, manifest: XmlManifest | 2528 | self, opt: optparse.Values, manifest: XmlManifest |
| 2524 | ) -> bool: | 2529 | ) -> bool: |
| 2525 | """Resolve opt.smart_sync from the CLI flags and manifest default. | 2530 | """Resolve opt.smart_sync from CLI flags, user config and manifest. |
| 2531 | |||
| 2532 | Precedence: CLI flags > per-user settings > per-manifest settings. | ||
| 2533 | See docs/repoconfig.md and docs/smart-sync.md for details. | ||
| 2526 | 2534 | ||
| 2527 | Returns: | 2535 | Returns: |
| 2528 | True if smart_sync was enabled implicitly by the manifest default, | 2536 | True if smart_sync was enabled implicitly by the manifest default, |
| 2529 | False otherwise. | 2537 | False otherwise. Smart sync enabled via per-user settings counts |
| 2538 | as an explicit user request, not implicit. | ||
| 2530 | """ | 2539 | """ |
| 2531 | implicit = ( | 2540 | user_pref = RepoConfig.ForUser().GetBoolean("sync.smartsync") |
| 2541 | default_allowed = ( | ||
| 2532 | opt.smart_sync is None | 2542 | opt.smart_sync is None |
| 2533 | and not opt.smart_tag | 2543 | and not opt.smart_tag |
| 2534 | and not opt.manifest_name | 2544 | and not opt.manifest_name |
| 2535 | and not opt.superproject_revision | 2545 | and not opt.superproject_revision |
| 2536 | and not opt.local_only | 2546 | and not opt.local_only |
| 2547 | ) | ||
| 2548 | implicit = ( | ||
| 2549 | default_allowed | ||
| 2550 | and user_pref is None | ||
| 2537 | and getattr(manifest.default, "sync_smartsync", False) is True | 2551 | and getattr(manifest.default, "sync_smartsync", False) is True |
| 2538 | ) | 2552 | ) |
| 2539 | if opt.smart_sync is None: | 2553 | if opt.smart_sync is None: |
| 2540 | opt.smart_sync = implicit | 2554 | if user_pref is False: |
| 2555 | opt.smart_sync = False | ||
| 2556 | elif user_pref is True: | ||
| 2557 | opt.smart_sync = default_allowed | ||
| 2558 | else: | ||
| 2559 | opt.smart_sync = implicit | ||
| 2541 | 2560 | ||
| 2542 | if ( | 2561 | if ( |
| 2543 | opt.manifest_server_username or opt.manifest_server_password | 2562 | opt.manifest_server_username or opt.manifest_server_password |
diff --git a/tests/test_subcmds_sync.py b/tests/test_subcmds_sync.py index e7e8013e0..32ff3cdd1 100644 --- a/tests/test_subcmds_sync.py +++ b/tests/test_subcmds_sync.py | |||
| @@ -501,27 +501,116 @@ def test_cli_smart_sync( | |||
| 501 | 501 | ||
| 502 | 502 | ||
| 503 | @pytest.mark.parametrize( | 503 | @pytest.mark.parametrize( |
| 504 | "argv, sync_smartsync_manifest, expected_exception", | 504 | "argv, user_pref, sync_smartsync_manifest, expected_smart_sync, " |
| 505 | "expected_implicit", | ||
| 505 | [ | 506 | [ |
| 506 | (["-u", "user", "-p", "pass"], False, sync.SmartSyncError), | 507 | # sync.smartsync=false: never smart sync unless -s/-t is given. |
| 507 | (["-u", "user", "-p", "pass"], True, None), | 508 | ([], False, True, False, False), |
| 508 | (["-s", "-u", "user", "-p", "pass"], False, None), | 509 | ([], False, False, False, False), |
| 509 | (["-t", "tag", "-u", "user", "-p", "pass"], False, None), | 510 | (["-s"], False, True, True, False), |
| 511 | (["--smart-sync"], False, False, True, False), | ||
| 512 | (["-t", "tag123"], False, True, False, False), | ||
| 513 | (["--nmu"], False, True, False, False), | ||
| 514 | # sync.smartsync=true: like -s, counted as explicit, not implicit. | ||
| 515 | ([], True, False, True, False), | ||
| 516 | ([], True, True, True, False), | ||
| 517 | (["--nmu"], True, False, True, False), | ||
| 518 | (["--no-smart-sync"], True, True, False, False), | ||
| 519 | (["-t", "tag123"], True, False, False, False), | ||
| 520 | (["-m", "other.xml"], True, False, False, False), | ||
| 521 | (["-l"], True, False, False, False), | ||
| 522 | (["--superproject-revision=abc"], True, False, False, False), | ||
| 523 | # sync.smartsync unset: manifest default applies as before. | ||
| 524 | ([], None, True, True, True), | ||
| 525 | ([], None, False, False, False), | ||
| 526 | ], | ||
| 527 | ) | ||
| 528 | def test_user_config_smart_sync( | ||
| 529 | argv: List[str], | ||
| 530 | user_pref: Optional[bool], | ||
| 531 | sync_smartsync_manifest: bool, | ||
| 532 | expected_smart_sync: bool, | ||
| 533 | expected_implicit: bool, | ||
| 534 | ) -> None: | ||
| 535 | """Tests the sync.smartsync user setting against CLI and manifest.""" | ||
| 536 | manifest = mock.MagicMock() | ||
| 537 | manifest.default.sync_smartsync = sync_smartsync_manifest | ||
| 538 | |||
| 539 | user_cfg = mock.MagicMock() | ||
| 540 | user_cfg.GetBoolean.return_value = user_pref | ||
| 541 | |||
| 542 | cmd = sync.Sync(manifest=manifest) | ||
| 543 | opts, args = cmd.OptionParser.parse_args(argv) | ||
| 544 | cmd.ValidateOptions(opts, args) | ||
| 545 | with mock.patch.object(sync.RepoConfig, "ForUser", return_value=user_cfg): | ||
| 546 | implicit = cmd._ResolveSmartSyncOption(opts, manifest) | ||
| 547 | user_cfg.GetBoolean.assert_called_once_with("sync.smartsync") | ||
| 548 | assert opts.smart_sync == expected_smart_sync | ||
| 549 | assert implicit == expected_implicit | ||
| 550 | |||
| 551 | |||
| 552 | @pytest.mark.parametrize( | ||
| 553 | "value, expected_smart_sync", | ||
| 554 | [("true", True), ("false", False), ("yes", True), ("no", False)], | ||
| 555 | ) | ||
| 556 | def test_user_config_smart_sync_from_file( | ||
| 557 | monkeypatch: pytest.MonkeyPatch, | ||
| 558 | tmp_home_dir: Path, | ||
| 559 | value: str, | ||
| 560 | expected_smart_sync: bool, | ||
| 561 | ) -> None: | ||
| 562 | """Tests that sync.smartsync is read from ~/.repoconfig/config.""" | ||
| 563 | # ForUser() caches its instance process-wide; make it pick up this HOME. | ||
| 564 | monkeypatch.setattr(sync.RepoConfig, "_ForUser", None) | ||
| 565 | monkeypatch.delenv("REPO_CONFIG_DIR", raising=False) | ||
| 566 | cfg_dir = tmp_home_dir / ".repoconfig" | ||
| 567 | cfg_dir.mkdir() | ||
| 568 | (cfg_dir / "config").write_text(f"[sync]\n\tsmartsync = {value}\n") | ||
| 569 | |||
| 570 | manifest = mock.MagicMock() | ||
| 571 | # Set the manifest default to the opposite so we know the user config won. | ||
| 572 | manifest.default.sync_smartsync = not expected_smart_sync | ||
| 573 | |||
| 574 | cmd = sync.Sync(manifest=manifest) | ||
| 575 | opts, args = cmd.OptionParser.parse_args([]) | ||
| 576 | cmd.ValidateOptions(opts, args) | ||
| 577 | assert cmd._ResolveSmartSyncOption(opts, manifest) is False | ||
| 578 | assert opts.smart_sync == expected_smart_sync | ||
| 579 | |||
| 580 | |||
| 581 | @pytest.mark.parametrize( | ||
| 582 | "argv, user_pref, sync_smartsync_manifest, expected_exception", | ||
| 583 | [ | ||
| 584 | (["-u", "user", "-p", "pass"], None, False, sync.SmartSyncError), | ||
| 585 | (["-u", "user", "-p", "pass"], None, True, None), | ||
| 586 | (["-s", "-u", "user", "-p", "pass"], None, False, None), | ||
| 587 | (["-t", "tag", "-u", "user", "-p", "pass"], None, False, None), | ||
| 510 | ( | 588 | ( |
| 511 | ["--no-smart-sync", "-u", "user", "-p", "pass"], | 589 | ["--no-smart-sync", "-u", "user", "-p", "pass"], |
| 590 | None, | ||
| 512 | True, | 591 | True, |
| 513 | sync.SmartSyncError, | 592 | sync.SmartSyncError, |
| 514 | ), | 593 | ), |
| 515 | (["-l", "-u", "user", "-p", "pass"], True, sync.SmartSyncError), | 594 | (["-l", "-u", "user", "-p", "pass"], None, True, sync.SmartSyncError), |
| 516 | (["--nmu", "-u", "user", "-p", "pass"], True, None), | 595 | (["--nmu", "-u", "user", "-p", "pass"], None, True, None), |
| 517 | (["--nmu", "-u", "user", "-p", "pass"], False, sync.SmartSyncError), | 596 | ( |
| 518 | (["-s", "-l", "-u", "user", "-p", "pass"], True, None), | 597 | ["--nmu", "-u", "user", "-p", "pass"], |
| 519 | (["-u", "user"], False, SystemExit), | 598 | None, |
| 520 | (["-p", "pass"], False, SystemExit), | 599 | False, |
| 600 | sync.SmartSyncError, | ||
| 601 | ), | ||
| 602 | (["-s", "-l", "-u", "user", "-p", "pass"], None, True, None), | ||
| 603 | (["-u", "user"], None, False, SystemExit), | ||
| 604 | (["-p", "pass"], None, False, SystemExit), | ||
| 605 | # -u/-p are accepted when smart sync comes from sync.smartsync=true... | ||
| 606 | (["-u", "user", "-p", "pass"], True, False, None), | ||
| 607 | # ...and rejected when sync.smartsync=false drops the manifest default. | ||
| 608 | (["-u", "user", "-p", "pass"], False, True, sync.SmartSyncError), | ||
| 521 | ], | 609 | ], |
| 522 | ) | 610 | ) |
| 523 | def test_cli_manifest_server_credentials( | 611 | def test_cli_manifest_server_credentials( |
| 524 | argv: List[str], | 612 | argv: List[str], |
| 613 | user_pref: Optional[bool], | ||
| 525 | sync_smartsync_manifest: bool, | 614 | sync_smartsync_manifest: bool, |
| 526 | expected_exception: Optional[Type[BaseException]], | 615 | expected_exception: Optional[Type[BaseException]], |
| 527 | ) -> None: | 616 | ) -> None: |
| @@ -529,15 +618,19 @@ def test_cli_manifest_server_credentials( | |||
| 529 | manifest = mock.MagicMock() | 618 | manifest = mock.MagicMock() |
| 530 | manifest.default.sync_smartsync = sync_smartsync_manifest | 619 | manifest.default.sync_smartsync = sync_smartsync_manifest |
| 531 | 620 | ||
| 621 | user_cfg = mock.MagicMock() | ||
| 622 | user_cfg.GetBoolean.return_value = user_pref | ||
| 623 | |||
| 532 | cmd = sync.Sync(manifest=manifest) | 624 | cmd = sync.Sync(manifest=manifest) |
| 533 | opts, args = cmd.OptionParser.parse_args(argv) | 625 | opts, args = cmd.OptionParser.parse_args(argv) |
| 534 | if expected_exception: | 626 | with mock.patch.object(sync.RepoConfig, "ForUser", return_value=user_cfg): |
| 535 | with pytest.raises(expected_exception): | 627 | if expected_exception: |
| 628 | with pytest.raises(expected_exception): | ||
| 629 | cmd.ValidateOptions(opts, args) | ||
| 630 | cmd._ResolveSmartSyncOption(opts, manifest) | ||
| 631 | else: | ||
| 536 | cmd.ValidateOptions(opts, args) | 632 | cmd.ValidateOptions(opts, args) |
| 537 | cmd._ResolveSmartSyncOption(opts, manifest) | 633 | cmd._ResolveSmartSyncOption(opts, manifest) |
| 538 | else: | ||
| 539 | cmd.ValidateOptions(opts, args) | ||
| 540 | cmd._ResolveSmartSyncOption(opts, manifest) | ||
| 541 | 634 | ||
| 542 | 635 | ||
| 543 | class LocalSyncState(unittest.TestCase): | 636 | class LocalSyncState(unittest.TestCase): |
| @@ -1699,6 +1792,48 @@ class SyncCommand(unittest.TestCase): | |||
| 1699 | mock_setup.assert_called_once() | 1792 | mock_setup.assert_called_once() |
| 1700 | self.assertFalse(mock_setup.call_args.kwargs.get("best_effort")) | 1793 | self.assertFalse(mock_setup.call_args.kwargs.get("best_effort")) |
| 1701 | 1794 | ||
| 1795 | def test_user_config_smart_sync_disabled(self) -> None: | ||
| 1796 | """Ensure sync.smartsync=false skips the manifest's default.""" | ||
| 1797 | self.manifest.default.sync_smartsync = True | ||
| 1798 | self.opt.smart_sync = None | ||
| 1799 | mock.patch.object(self.cmd, "_UpdateAllManifestProjects").start() | ||
| 1800 | user_cfg = mock.MagicMock() | ||
| 1801 | user_cfg.GetBoolean.return_value = False | ||
| 1802 | with mock.patch.object( | ||
| 1803 | sync.RepoConfig, "ForUser", return_value=user_cfg | ||
| 1804 | ): | ||
| 1805 | with mock.patch.object(self.cmd, "_SmartSyncSetup") as mock_setup: | ||
| 1806 | with mock.patch.object(self.cmd, "_UpdateRepoProject"): | ||
| 1807 | with mock.patch.object( | ||
| 1808 | self.cmd, "_ValidateOptionsWithManifest" | ||
| 1809 | ): | ||
| 1810 | with mock.patch.object(self.cmd, "_SyncInterleaved"): | ||
| 1811 | with mock.patch.object( | ||
| 1812 | self.cmd, "_RunPostSyncHook" | ||
| 1813 | ): | ||
| 1814 | self.cmd.Execute(self.opt, []) | ||
| 1815 | mock_setup.assert_not_called() | ||
| 1816 | self.assertFalse(self.opt.smart_sync) | ||
| 1817 | |||
| 1818 | def test_user_config_smart_sync_enabled_error(self) -> None: | ||
| 1819 | """Ensure sync.smartsync=true fails hard instead of falling back.""" | ||
| 1820 | self.manifest.default.sync_smartsync = False | ||
| 1821 | self.opt.smart_sync = None | ||
| 1822 | self.opt.mp_update = False | ||
| 1823 | user_cfg = mock.MagicMock() | ||
| 1824 | user_cfg.GetBoolean.return_value = True | ||
| 1825 | with mock.patch.object( | ||
| 1826 | sync.RepoConfig, "ForUser", return_value=user_cfg | ||
| 1827 | ): | ||
| 1828 | with mock.patch.object( | ||
| 1829 | self.cmd, | ||
| 1830 | "_SmartSyncSetup", | ||
| 1831 | side_effect=sync.SmartSyncError("unreachable"), | ||
| 1832 | ) as mock_setup: | ||
| 1833 | with self.assertRaises(sync.SmartSyncError): | ||
| 1834 | self.cmd.Execute(self.opt, []) | ||
| 1835 | mock_setup.assert_called_once() | ||
| 1836 | |||
| 1702 | def test_implicit_smart_sync_unparsable_manifest(self) -> None: | 1837 | def test_implicit_smart_sync_unparsable_manifest(self) -> None: |
| 1703 | """Ensure an unparsable smart sync manifest falls back to ToT.""" | 1838 | """Ensure an unparsable smart sync manifest falls back to ToT.""" |
| 1704 | self.manifest.default.sync_smartsync = True | 1839 | self.manifest.default.sync_smartsync = True |
