summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/internal-fs-layout.md1
-rw-r--r--docs/repoconfig.md103
-rw-r--r--docs/smart-sync.md16
-rw-r--r--man/repo-sync.16
-rw-r--r--subcmds/sync.py27
-rw-r--r--tests/test_subcmds_sync.py167
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
3Repo keeps a small number of settings that apply to the current user rather
4than to one checkout. They live in the per-user configuration file
5`~/.repoconfig/config`, which is separate from both git's own `~/.gitconfig`
6and 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
13The file is `.repoconfig/config` inside the user's home directory. The parent
14directory can be changed by setting the `REPO_CONFIG_DIR` environment variable,
15in which case repo reads `$REPO_CONFIG_DIR/.repoconfig/config` instead.
16
17The file is created on demand; it's fine for it to be missing.
18
19## Format
20
21The file uses the [git-config] syntax, so it can be edited by hand:
22
23```ini
24[sync]
25 smartsync = true
26```
27
28or managed with `git config` by pointing it at the file explicitly:
29
30```sh
31# Set a value.
32git config --file ~/.repoconfig/config sync.smartsync true
33
34# Read a value.
35git config --file ~/.repoconfig/config sync.smartsync
36
37# Remove a value.
38git config --file ~/.repoconfig/config --unset sync.smartsync
39```
40
41Boolean settings accept `true`/`yes` and `false`/`no`.
42
43Repo caches a parsed copy of the file in `.repoconfig/.repo_config.json`; it is
44refreshed 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,
51optionally with arguments, in the same way as git aliases:
52
53```ini
54[alias]
55 ss = sync -s
56 st = status -o
57```
58
59Aliases defined in a checkout's `.repo/config` take precedence over per-user
60ones. 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
67checkouts, whether `repo sync` should [Smart Sync](./smart-sync.md) by
68default:
69
70```sh
71# Always Smart Sync, as if -s/--smart-sync were passed.
72git config --file ~/.repoconfig/config sync.smartsync true
73
74# Never Smart Sync by default, even if the manifest sets sync-smartsync="true".
75git config --file ~/.repoconfig/config sync.smartsync false
76
77# Remove the preference and go back to following the manifest.
78git config --file ~/.repoconfig/config --unset sync.smartsync
79```
80
81The effective behavior is resolved in this order; the first match wins:
82
831. Command line: `-s`/`--smart-sync`, `--no-smart-sync`, or `-t`/`--smart-tag`.
842. User config: `sync.smartsync` in `~/.repoconfig/config`.
853. Manifest: `sync-smartsync="true"` on the `<default>` element.
864. Otherwise, a regular ToT sync.
87
88So `sync.smartsync=false` turns off the manifest-driven default, but an
89explicit `repo sync -s`, `repo sync -t TAG` or `repo smartsync` still works.
90Likewise `sync.smartsync=true` is overridden by `--no-smart-sync`.
91
92A Smart Sync enabled by `sync.smartsync=true` is treated as an explicit
93request: 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
96the 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
99whatever `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
204manifest source (such as `-t`/`--smart-tag`, `-m`/`--manifest-name` or 204manifest 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
207Users 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
209Explicitly enables Smart Sync. Repo will call 213Explicitly enables Smart Sync. Repo will call
@@ -231,6 +235,18 @@ match the settings Android build environments automatically setup.
231Explicitly disables Smart Sync, overriding any `sync-smartsync="true"` setting 235Explicitly disables Smart Sync, overriding any `sync-smartsync="true"` setting
232declared in the manifest `<default>` element. 236declared in the manifest `<default>` element.
233 237
238### Per-user default (sync.smartsync)
239
240A user can decide once, for all their checkouts, whether `repo sync` should
241Smart Sync by default by setting the boolean `sync.smartsync` key in their
242[per-user repo config](./repoconfig.md#smartsync).
243
244Command line flags (`-s`, `--no-smart-sync`, `-t`) override the user setting,
245which in turn overrides the manifest's `sync-smartsync` default. A Smart Sync
246enabled this way is treated as an explicit request, so it is subject to the
247same restrictions as `-s` and fails the sync if no manifest can be obtained
248from the server instead of silently falling back to ToT.
249
234### --smart-tag=TAG / -t 250### --smart-tag=TAG / -t
235 251
236Repo will call `GetManifest(TAG)`. 252Repo 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
4repo \- repo sync - manual page for repo sync 4repo \- 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
188specified by the manifest\-server element in the current manifest. The 188specified 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
191Whether 'repo sync' smart syncs by default can also be controlled with the
192sync.smartsync per\-user setting; see docs/repoconfig.md. Explicit
193\fB\-s\fR/\-\-no\-smart\-sync/\-t flags always override it.
194.PP
191The \fB\-u\fR/\-\-manifest\-server\-username and \fB\-p\fR/\-\-manifest\-server\-password options can 195The \fB\-u\fR/\-\-manifest\-server\-username and \fB\-p\fR/\-\-manifest\-server\-password options can
192be used to specify a username and password to authenticate with the manifest 196be used to specify a username and password to authenticate with the manifest
193server when using the \fB\-s\fR or \fB\-t\fR option. 197server 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
69from git_command import git_require 69from git_command import git_require
70from git_command import GitCommand 70from git_command import GitCommand
71from git_config import GetUrlCookieFile 71from git_config import GetUrlCookieFile
72from git_config import RepoConfig
72from git_refs import HEAD 73from git_refs import HEAD
73from git_refs import R_HEADS 74from git_refs import R_HEADS
74import git_superproject 75import git_superproject
@@ -491,6 +492,10 @@ build as specified by the manifest-server element in the current
491manifest. The -t/--smart-tag option is similar and allows you to 492manifest. The -t/--smart-tag option is similar and allows you to
492specify a custom tag/label. 493specify a custom tag/label.
493 494
495Whether '%prog' smart syncs by default can also be controlled with the
496sync.smartsync per-user setting; see docs/repoconfig.md. Explicit
497-s/--no-smart-sync/-t flags always override it.
498
494The -u/--manifest-server-username and -p/--manifest-server-password 499The -u/--manifest-server-username and -p/--manifest-server-password
495options can be used to specify a username and password to authenticate 500options can be used to specify a username and password to authenticate
496with the manifest server when using the -s or -t option. 501with 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)
528def 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)
556def 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)
523def test_cli_manifest_server_credentials( 611def 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
543class LocalSyncState(unittest.TestCase): 636class 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