From f0124d0fd5f316330ce6c1513874a61c044ede3c Mon Sep 17 00:00:00 2001 From: LaMont Jones Date: Mon, 19 Oct 2026 19:53:02 +0000 Subject: [PATCH] sync, init: support repo.smartsync config and --head alias Allow users to configure Smart Sync as the default for a checkout: * Add `ManifestProject.smart_sync` backed by `self.config.GetBoolean("repo.smartsync")`. * Add `-s`/`--smart-sync` and `--no-smart-sync`/`--head` to `repo init` to persist `repo.smartsync` in the manifest project config. * Add `--head` as an alias for `--no-smart-sync` in `repo sync` and `repo init`. * Update `Sync._ResolveSmartSyncOption` so `ManifestProject.smart_sync` takes precedence over `` while remaining overridable by per-invocation `repo sync` CLI flags. Bug: 569927750 Test: pytest tests/test_project.py tests/test_subcmds_init.py tests/test_subcmds_sync.py Change-Id: Ib40efc530336efb59a28acfed3d582713e9d67b2 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/643303 Reviewed-by: Mike Frysinger Commit-Queue: LaMont Jones Tested-by: LaMont Jones --- completion.zsh | 4 +++ docs/internal-fs-layout.md | 1 + docs/smart-sync.md | 28 +++++++++++++++-- man/repo-init.1 | 6 ++++ man/repo-sync.1 | 2 +- project.py | 13 ++++++++ repo | 16 +++++++++- subcmds/init.py | 1 + subcmds/sync.py | 28 ++++++++++++----- tests/test_project.py | 3 ++ tests/test_subcmds_init.py | 8 ++++- tests/test_subcmds_sync.py | 62 ++++++++++++++++++++++++++++++++++++++ 12 files changed, 158 insertions(+), 14 deletions(-) diff --git a/completion.zsh b/completion.zsh index 23b7db149..217583e7e 100644 --- a/completion.zsh +++ b/completion.zsh @@ -236,6 +236,9 @@ _repo() { '--clone-filter=[Clone filter]:filter:' \ '--use-superproject[Use superproject]' \ '--no-use-superproject[Do not use superproject]' \ + '(-s --smart-sync)'{-s,--smart-sync}'[Default to smart sync]' \ + '--no-smart-sync[Do not default to smart sync]' \ + '--head[Do not default to smart sync]' \ '--clone-bundle[Use clone bundle]' \ '--no-clone-bundle[Do not use clone bundle]' \ '--git-lfs[Use git lfs]' \ @@ -338,6 +341,7 @@ _repo() { '--no-auto-gc[Do not run auto gc]' \ '(-s --smart-sync)'{-s,--smart-sync}'[Smart sync]' \ '--no-smart-sync[Do not smart sync]' \ + '--head[Do not smart sync]' \ '(-t --smart-tag)'{-t,--smart-tag=}'[Smart tag]:tag:' \ '--no-repo-verify[Do not verify repo]' \ '--no-verify[Do not verify]' \ diff --git a/docs/internal-fs-layout.md b/docs/internal-fs-layout.md index 64fe74cbe..10f173d5a 100644 --- a/docs/internal-fs-layout.md +++ b/docs/internal-fs-layout.md @@ -176,6 +176,7 @@ User controlled settings are initialized when running `repo init`. | repo.partialclone | `--partial-clone` | Create [partial git clones] | | repo.partialcloneexclude | `--partial-clone-exclude` | Comma-delimited list of project names (not paths) to exclude while using [partial git clones] | | repo.reference | `--reference` | Reference repo client checkout | +| repo.smartsync | `--smart-sync` | Default to [Smart Sync](./smart-sync.md) when syncing | | repo.submodules | `--submodules` | Sync git submodules | | repo.superproject | `--use-superproject` | Sync [superproject] | | repo.worktree | `--worktree` | Use [git worktree] for checkouts | diff --git a/docs/smart-sync.md b/docs/smart-sync.md index 905a004a0..aa5e52520 100644 --- a/docs/smart-sync.md +++ b/docs/smart-sync.md @@ -182,6 +182,28 @@ affect Smart Sync: the default Smart Sync from `sync-smartsync` (see below) still runs, and an existing `smart_sync_override.xml` is replaced or removed as in a regular sync. +### `repo init --[no-]smart-sync` + +Users can configure Smart Sync as the default for a checkout by passing +`-s`/`--smart-sync` (or `--no-smart-sync`/`--head`) to `repo init` (this can +also be re-run in an existing checkout to change the setting). + +When resolving whether to perform a Smart Sync by default, `repo sync` uses the +following precedence order (highest to lowest): + +1. Per-invocation CLI flags (`-s`/`--smart-sync`, `--no-smart-sync`/`--head`, + or target selection flags like `-t`, `-m`, `--superproject-revision`, `-l`). +2. Checkout `repo init` setting (`repo init --[no-]smart-sync`). +3. [Per-user `sync.smartsync` setting](#Per_user-default-sync_smartsync). +4. Manifest [`sync-smartsync`](#sync-smartsync-manifest-attribute) setting. + +Running `repo init --no-smart-sync` (or `repo init --head`) disables default +Smart Sync for the checkout even when enabled by the user setting or manifest. +As with the [manifest setting](#sync-smartsync-manifest-attribute), a +checkout-enabled default Smart Sync falls back to a regular ToT sync if the +manifest does not define a `` or the manifest server cannot +provide a usable manifest. + ### sync-smartsync (Manifest Attribute) The manifest can enable Smart Sync by default for all `repo sync` invocations @@ -230,10 +252,10 @@ match the settings Android build environments automatically setup. 3. `${TARGET_PRODUCT}-${TARGET_BUILD_VARIANT}`: If these variables are all defined, then they are merged with `-` and used. -### --no-smart-sync +### --no-smart-sync / --head -Explicitly disables Smart Sync, overriding any `sync-smartsync="true"` setting -declared in the manifest `` element. +Explicitly disables Smart Sync, overriding any `repo.smartsync` git config or +`sync-smartsync="true"` setting declared in the manifest `` element. ### Per-user default (sync.smartsync) diff --git a/man/repo-init.1 b/man/repo-init.1 index ba7b52d3f..32b4c33dd 100644 --- a/man/repo-init.1 +++ b/man/repo-init.1 @@ -103,6 +103,12 @@ use the manifest superproject to sync projects; implies \fB\-c\fR \fB\-\-no\-use\-superproject\fR disable use of manifest superprojects .TP +\fB\-s\fR, \fB\-\-smart\-sync\fR +default to smart sync when syncing projects +.TP +\fB\-\-no\-smart\-sync\fR, \fB\-\-head\fR +do not default to smart sync when syncing projects +.TP \fB\-\-clone\-bundle\fR enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if not \fB\-\-partial\-clone\fR) .TP diff --git a/man/repo-sync.1 b/man/repo-sync.1 index 2c7467e1f..835ae21f5 100644 --- a/man/repo-sync.1 +++ b/man/repo-sync.1 @@ -125,7 +125,7 @@ do not run garbage collection on any projects (default) \fB\-s\fR, \fB\-\-smart\-sync\fR smart sync using manifest from the latest known good build .TP -\fB\-\-no\-smart\-sync\fR +\fB\-\-no\-smart\-sync\fR, \fB\-\-head\fR disable smart sync and sync to ToT instead .TP \fB\-t\fR SMART_TAG, \fB\-\-smart\-tag\fR=\fI\,SMART_TAG\/\fR diff --git a/project.py b/project.py index d4c2307cd..f98d293e5 100644 --- a/project.py +++ b/project.py @@ -5607,6 +5607,11 @@ class ManifestProject(MetaProject): """Whether we use superproject.""" return self.config.GetBoolean("repo.superproject") + @property + def smart_sync(self) -> Optional[bool]: + """Whether we use smart sync by default.""" + return self.config.GetBoolean("repo.smartsync") + @property def partial_clone(self): """Whether this is a partial clone.""" @@ -5701,6 +5706,7 @@ class ManifestProject(MetaProject): clone_bundle=mp.clone_bundle, git_lfs=mp.git_lfs, use_superproject=mp.use_superproject, + smart_sync=mp.smart_sync, verbose=verbose, current_branch_only=current_branch_only, tags=tags, @@ -5732,6 +5738,7 @@ class ManifestProject(MetaProject): clone_bundle=None, git_lfs=None, use_superproject=None, + smart_sync: Optional[bool] = None, verbose=False, current_branch_only=False, git_event_log=None, @@ -5773,6 +5780,7 @@ class ManifestProject(MetaProject): git_lfs: a boolean, whether to enable git LFS support. use_superproject: a boolean, whether to use the manifest superproject to sync projects. + smart_sync: a boolean, whether to default to smart sync. verbose: a boolean, whether to show all output, rather than only errors. current_branch_only: a boolean, whether to only fetch the current @@ -5820,6 +5828,7 @@ class ManifestProject(MetaProject): clone_bundle=clone_bundle, git_lfs=git_lfs, use_superproject=use_superproject, + smart_sync=smart_sync, verbose=verbose, current_branch_only=current_branch_only, tags=tags, @@ -6090,6 +6099,9 @@ class ManifestProject(MetaProject): if use_superproject is not None: self.config.SetBoolean("repo.superproject", use_superproject) + if smart_sync is not None: + self.config.SetBoolean("repo.smartsync", smart_sync) + if not standalone_manifest: success = self.Sync_NetworkHalf( is_new=is_new, @@ -6173,6 +6185,7 @@ class ManifestProject(MetaProject): clone_bundle=clone_bundle, git_lfs=git_lfs, use_superproject=use_superproject, + smart_sync=smart_sync, verbose=verbose, current_branch_only=current_branch_only, tags=tags, diff --git a/repo b/repo index 47c30cec5..ce34ca6ec 100755 --- a/repo +++ b/repo @@ -129,7 +129,7 @@ if not REPO_REV: BUG_URL = "https://issues.gerritcodereview.com/issues/new?component=1370071" # increment this whenever we make important changes to this script -VERSION = (2, 69) +VERSION = (2, 71) # increment this if the MAINTAINER_KEYS block is modified KEYRING_VERSION = (2, 3) @@ -443,6 +443,20 @@ def InitParser(parser): dest="use_superproject", help="disable use of manifest superprojects", ) + group.add_option( + "-s", + "--smart-sync", + action="store_true", + default=None, + help="default to smart sync when syncing projects", + ) + group.add_option( + "--no-smart-sync", + "--head", + action="store_false", + dest="smart_sync", + help="do not default to smart sync when syncing projects", + ) group.add_option( "--clone-bundle", action="store_true", diff --git a/subcmds/init.py b/subcmds/init.py index 772002e4d..494a1b773 100644 --- a/subcmds/init.py +++ b/subcmds/init.py @@ -164,6 +164,7 @@ to update the working directory files. clone_bundle=opt.clone_bundle, git_lfs=opt.git_lfs, use_superproject=opt.use_superproject, + smart_sync=opt.smart_sync, verbose=opt.verbose, current_branch_only=opt.current_branch_only, tags=opt.tags, diff --git a/subcmds/sync.py b/subcmds/sync.py index d6354d4a0..f6181b7cc 100644 --- a/subcmds/sync.py +++ b/subcmds/sync.py @@ -839,6 +839,7 @@ later is required to fix a server side protocol bug. ) p.add_option( "--no-smart-sync", + "--head", dest="smart_sync", action="store_false", help="disable smart sync and sync to ToT instead", @@ -2527,16 +2528,20 @@ later is required to fix a server side protocol bug. def _ResolveSmartSyncOption( self, opt: optparse.Values, manifest: XmlManifest ) -> bool: - """Resolve opt.smart_sync from CLI flags, user config and manifest. + """Resolve opt.smart_sync from CLI flags, config, and manifest. - Precedence: CLI flags > per-user settings > per-manifest settings. + Precedence: CLI flags > per-checkout settings > per-user settings > + per-manifest settings. See docs/repoconfig.md and docs/smart-sync.md for details. Returns: - True if smart_sync was enabled implicitly by the manifest default, - False otherwise. Smart sync enabled via per-user settings counts - as an explicit user request, not implicit. + True if smart_sync was enabled implicitly by checkout or manifest + default, False otherwise. Smart sync enabled via per-user settings + counts as an explicit user request, not implicit. """ + checkout_pref = getattr(manifest.manifestProject, "smart_sync", None) + if not isinstance(checkout_pref, bool): + checkout_pref = None user_pref = RepoConfig.ForUser().GetBoolean("sync.smartsync") default_allowed = ( opt.smart_sync is None @@ -2545,13 +2550,20 @@ later is required to fix a server side protocol bug. and not opt.superproject_revision and not opt.local_only ) + default_smart_sync = ( + checkout_pref + if checkout_pref is not None + else getattr(manifest.default, "sync_smartsync", False) is True + ) implicit = ( default_allowed - and user_pref is None - and getattr(manifest.default, "sync_smartsync", False) is True + and (checkout_pref is not None or user_pref is None) + and default_smart_sync ) if opt.smart_sync is None: - if user_pref is False: + if checkout_pref is not None: + opt.smart_sync = implicit + elif user_pref is False: opt.smart_sync = False elif user_pref is True: opt.smart_sync = default_allowed diff --git a/tests/test_project.py b/tests/test_project.py index 21f30ba99..22732512d 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -1822,6 +1822,9 @@ class ManifestPropertiesFetchedCorrectly(unittest.TestCase): fakeproj.config.SetBoolean("repo.superproject", False) self.assertFalse(fakeproj.use_superproject) + fakeproj.config.SetBoolean("repo.smartsync", False) + self.assertFalse(fakeproj.smart_sync) + fakeproj.config.SetBoolean("repo.partialclone", False) self.assertFalse(fakeproj.partial_clone) diff --git a/tests/test_subcmds_init.py b/tests/test_subcmds_init.py index 960fb93fb..369f20dfc 100644 --- a/tests/test_subcmds_init.py +++ b/tests/test_subcmds_init.py @@ -23,7 +23,13 @@ from subcmds import init @pytest.mark.parametrize( "argv", - ([],), + ( + [], + ["-s"], + ["--smart-sync"], + ["--no-smart-sync"], + ["--head"], + ), ) def test_cli_parser_good(argv: List[str]) -> None: """Check valid command line options.""" diff --git a/tests/test_subcmds_sync.py b/tests/test_subcmds_sync.py index 32ff3cdd1..4612331df 100644 --- a/tests/test_subcmds_sync.py +++ b/tests/test_subcmds_sync.py @@ -465,6 +465,8 @@ def test_cli_jobs_sync_j_max( (["--smart-sync"], True, True, False), (["--no-smart-sync"], True, False, False), (["--no-smart-sync"], False, False, False), + (["--head"], True, False, False), + (["--head"], False, False, False), (["-t", "tag123"], True, False, False), (["--smart-tag=tag123"], True, False, False), (["-m", "other.xml"], True, False, False), @@ -491,6 +493,7 @@ def test_cli_smart_sync( """ manifest = mock.MagicMock() manifest.default.sync_smartsync = sync_smartsync_manifest + manifest.manifestProject.smart_sync = None cmd = sync.Sync(manifest=manifest) opts, args = cmd.OptionParser.parse_args(argv) @@ -500,6 +503,65 @@ def test_cli_smart_sync( assert implicit == expected_implicit +@pytest.mark.parametrize( + "argv, config_smart_sync, user_pref, sync_smartsync_manifest, " + "expected_smart_sync, expected_implicit", + [ + # Config True overrides manifest False/None and user_pref False. + ([], True, None, False, True, True), + ([], True, None, None, True, True), + ([], True, False, False, True, True), + # Config False overrides manifest True and user_pref True. + ([], False, None, True, False, False), + ([], False, True, True, False, False), + # Config None falls back to user_pref, then manifest. + ([], None, True, False, True, False), + ([], None, False, True, False, False), + ([], None, None, True, True, True), + ([], None, None, False, False, False), + # CLI flags override config. + (["-s"], False, None, False, True, False), + (["--smart-sync"], False, None, True, True, False), + (["--no-smart-sync"], True, None, True, False, False), + (["--no-smart-sync"], True, None, False, False, False), + (["--head"], True, None, True, False, False), + (["--head"], True, None, False, False, False), + # Conflicting flags suppress implicit smart sync from config. + (["-t", "tag123"], True, None, False, False, False), + (["-m", "other.xml"], True, None, False, False, False), + (["-l"], True, None, False, False, False), + (["--nmu"], True, None, False, True, True), + (["--superproject-revision=abc"], True, None, False, False, False), + (["-s", "-l"], True, None, False, True, False), + ], +) +def test_cli_smart_sync_with_config( + argv: List[str], + config_smart_sync: Optional[bool], + user_pref: Optional[bool], + sync_smartsync_manifest: Optional[bool], + expected_smart_sync: bool, + expected_implicit: bool, +) -> None: + """Tests repo.smartsync config precedence between CLI, user, and + manifest. + """ + manifest = mock.MagicMock() + manifest.default.sync_smartsync = sync_smartsync_manifest + manifest.manifestProject.smart_sync = config_smart_sync + + user_cfg = mock.MagicMock() + user_cfg.GetBoolean.return_value = user_pref + + cmd = sync.Sync(manifest=manifest) + opts, args = cmd.OptionParser.parse_args(argv) + cmd.ValidateOptions(opts, args) + with mock.patch.object(sync.RepoConfig, "ForUser", return_value=user_cfg): + implicit = cmd._ResolveSmartSyncOption(opts, manifest) + assert opts.smart_sync == expected_smart_sync + assert implicit == expected_implicit + + @pytest.mark.parametrize( "argv, user_pref, sync_smartsync_manifest, expected_smart_sync, " "expected_implicit",