Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
d93fe60
sync: Handle KeyboardInterrupt during checkout
sokcevicG Jan 8, 2025
41a27eb
gc: extract deletion from Execute method
sokcevicG Jan 14, 2025
3405446
gc: Add repack option
sokcevicG Jan 14, 2025
db111d3
sync: Recover from errors during read-tree
sokcevicG Jan 15, 2025
1711bc2
git_config: prefer XDG config location
Jan 23, 2025
747ec83
run_tests: update to python 3.11 & pytest 8.3.4
vapier Jan 30, 2025
dfdf577
docs: smart-sync: split out & expand details
vapier Jan 31, 2025
5ae8292
Revert "sync: skip network half on repo upgrade"
sokcevicG Jan 31, 2025
cf9a2a2
Update internal filesystem layout for submodules
quic-kaushikl Dec 17, 2024
66685f0
Use 'gitfile' in submodule checkouts
quic-kaushikl Dec 17, 2024
99eca45
Activate submodules
quic-kaushikl Dec 17, 2024
8d5f032
gc: Add tags to remote pack list
sokcevicG Feb 5, 2025
fc901b9
sync: Refresh index before updating repo
sokcevicG Mar 12, 2025
4b94e77
Sync: Fix full submodule sync while shallow specified
Feb 17, 2025
243df20
launcher: change RunResult to subprocess.CompletedProcess
vapier Mar 22, 2025
91f4280
run_tests: run all tests all the time
vapier Mar 25, 2025
d508739
run_tests: move CQ test skips here
vapier Mar 25, 2025
8310436
run_tests: move test filtering to pytest markers
vapier Mar 25, 2025
cd391e7
black: update to v25
vapier Mar 25, 2025
507d463
tox: sync black settings with run_tests
vapier Mar 27, 2025
59b81c8
launcher: change collections.namedtuple to typing.NamedTuple
vapier Mar 27, 2025
dc8185f
launcher: change RunError to subprocess.CalledProcessError
vapier Mar 27, 2025
9ecb80b
pager: drop unused global vars
vapier Apr 2, 2025
f070331
Fix EROFS error when root fs is mounted read-only
egor-duda Mar 6, 2025
85ee173
run_tests: enable Python 3.8 CI coverage
vapier Apr 2, 2025
3667de1
run_tests: fix running when cwd is not the root
vapier Apr 2, 2025
daebd6c
sync: Warn about excessive job counts
gavinmak Apr 9, 2025
0214730
launcher: switch command quoting to shlex.quote
vapier Apr 9, 2025
97dc5c1
project: use --netrc-optional instead of --netrc
gavinmak Apr 10, 2025
a94457d
Fallback to full sync when depth enabled fetch of a sha1 fails
quic-kaushikl Apr 8, 2025
c061593
manifest: Remove redundant re-raise of BaseExceptions
Apr 21, 2025
c8da28c
man: regenerate man pages
vapier Apr 22, 2025
0f200bb
flake8: Ignore .venv directory
Apr 23, 2025
21cbcc5
update-manpages: include in unittests
vapier Apr 22, 2025
c448ba9
run_tests: only allow help2man skipping in CI
vapier Apr 30, 2025
1acbc14
manifest: generalize --json as --format=<format>
vapier Apr 30, 2025
8d37f61
upload: Add superproject identifier as push option
gavinmak May 5, 2025
06338ab
subcmds: delete redundant dest= settings
vapier May 22, 2025
3c8bae2
info: print superproject revision
May 27, 2025
08815ad
upload: Add rev to rootRepo push option
gavinmak May 23, 2025
0cb88a8
git_superproject: Replace walrus operator
gavinmak Jun 4, 2025
044e52e
hooks: add internal check for external hook API
vapier Jun 5, 2025
b262d0e
info: fix mismatched format args and wrong symbol name
Jun 10, 2025
8535282
sync: Add scaffolding for interleaved sync
gavinmak Jun 11, 2025
f91f446
upload: fix FileNotFoundError when no superproject
gavinmak Jun 17, 2025
b4b323a
sync: Add orchestration logic for --interleaved
gavinmak Jun 17, 2025
7b6ffed
sync: Implement --interleaved sync worker
gavinmak Jun 14, 2025
6b8e9fc
sync: clarify job flags when using interleaved
gavinmak Jun 18, 2025
f7a3f99
sync: Share self-update logic between sync modes
gavinmak Jun 23, 2025
df3c401
sync: Share manifest list update logic between sync modes
gavinmak Jun 18, 2025
99b5a17
sync: Share final error handling logic between sync modes
gavinmak Jun 18, 2025
21269c3
init: Add environment variable for git-lfs
Sep 4, 2024
82d500e
sync: support post-sync hook in <repo-hooks>
Chao-Shun-Cheng Jun 2, 2025
5d95ba8
progress: Make end() idempotent
gavinmak Jun 26, 2025
74edacd
project: Use plumbing commands to manage HEAD
gavinmak Jul 17, 2025
2e6d088
sync: Improve UI and error reporting for interleaved mode
gavinmak Jul 17, 2025
52bab0b
project: Use git rev-parse to read HEAD
gavinmak Jul 21, 2025
25858c8
sync: Default to interleaved mode
gavinmak Jul 21, 2025
720bd1e
sync: Don't checkout if no worktree
gavinmak Jul 23, 2025
7f7d70e
project: Fix GetHead to handle detached HEADs
gavinmak Jul 25, 2025
d3eec0a
sync: fix connection error on macOS for interleaved sync
kcwu Jul 25, 2025
239fad7
hooks: verify hooks project has worktree before running
gavinmak Jul 25, 2025
8c3585f
project: fallback to reading HEAD when rev-parse fails
gavinmak Aug 4, 2025
d9cc0a1
Fix shallow clones when upstream attribute is present
kwesolowski-at-volvocars Jul 23, 2025
380bf95
sync: always show sync result stderr_text on error
gavinmak Aug 13, 2025
a6e1a59
sync: Avoid duplicate projects in error text
gavinmak Aug 13, 2025
3e6acf2
progress: Fix race condition causing fileno crash
gavinmak Aug 13, 2025
a64149a
sync: Record and propagate errors from deferred actions
gavinmak Aug 14, 2025
d534a55
sync: Fix missing error details in interleaved summary
gavinmak Aug 14, 2025
854fe44
git_superproject: fix AttributeError in Superproject logging
gavinmak Aug 14, 2025
38d2fe1
Revert "Fix shallow clones when upstream attribute is present"
gavinmak Aug 14, 2025
58a59fd
CONTRIBUTING: rename doc per Google OSS policies
vapier Aug 20, 2025
5ed12ec
standardize file header wrt licensing
vapier Aug 21, 2025
c615c96
man: regen after sync updates
vapier Aug 21, 2025
80d1a5a
run_tests: add file header checker for licensing blocks
vapier Aug 21, 2025
d30414b
forall: fix crash with no command
vapier Sep 15, 2025
67383bd
Follow up "Fix shallow clones when upstream attribute is present"
quic-kaushikl Sep 9, 2025
4623264
Fix submodule initialization in interleaved sync mode
quic-kaushikl Sep 11, 2025
e4872ac
sync: Use 'git rebase' during 'repo sync --rebase'
Oct 15, 2025
2719a8e
run_tests: log each command run
vapier Oct 15, 2025
1afe96a
sync: fix saving of fetch times and local state
gavinmak Oct 20, 2025
4ab2284
manifest: Make extend-project support copyfile, linkfile and annotation
Oct 16, 2025
877ef91
man: Regenerate after manifest update
Nov 6, 2025
5998c0b
tests: manifest_xml: convert most path usage to pathlib
vapier Nov 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/internal-fs-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ Instead, you should use standard Git workflows like [git worktree] or
(e.g. a local mirror & a public review server) while avoiding duplicating
the content. However, this can run into problems if different remotes use
the same path on their respective servers. Best to avoid that.
* `subprojects/`: Like `projects/`, but for git submodules.
* `modules/`: Like `projects/`, but for git submodules.
* `subproject-objects/`: Like `project-objects/`, but for git submodules.
* `worktrees/`: Bare checkouts of every project synced by the manifest. The
filesystem layout matches the `<project name=...` setting in the manifest
Expand Down
21 changes: 1 addition & 20 deletions docs/manifest-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,26 +231,7 @@ At most one manifest-server may be specified. The url attribute
is used to specify the URL of a manifest server, which is an
XML RPC service.

The manifest server should implement the following RPC methods:

GetApprovedManifest(branch, target)

Return a manifest in which each project is pegged to a known good revision
for the current branch and target. This is used by repo sync when the
--smart-sync option is given.

The target to use is defined by environment variables TARGET_PRODUCT
and TARGET_BUILD_VARIANT. These variables are used to create a string
of the form $TARGET_PRODUCT-$TARGET_BUILD_VARIANT, e.g. passion-userdebug.
If one of those variables or both are not present, the program will call
GetApprovedManifest without the target parameter and the manifest server
should choose a reasonable default target.

GetManifest(tag)

Return a manifest in which each project is pegged to the revision at
the specified tag. This is used by repo sync when the --smart-tag option
is given.
See the [smart sync documentation](./smart-sync.md) for more details.


### Element submanifest
Expand Down
129 changes: 129 additions & 0 deletions docs/smart-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# repo Smart Syncing

Repo normally fetches & syncs manifests from the same URL specified during
`repo init`, and that often fetches the latest revisions of all projects in
the manifest. This flow works well for tracking and developing with the
latest code, but often it's desirable to sync to other points. For example,
to get a local build matching a specific release or build to reproduce bugs
reported by other people.

Repo's sync subcommand has support for fetching manifests from a server over
an XML-RPC connection. The local configuration and network API are defined by
repo, but individual projects have to host their own server for the client to
communicate with.

This process is called "smart syncing" -- instead of blindly fetching the latest
revision of all projects and getting an unknown state to develop against, the
client passes a request to the server and is given a matching manifest that
typically specifies specific commits for every project to fetch a known source
state.

[TOC]

## Manifest Configuration

The manifest specifies the server to communicate with via the
the [`<manifest-server>` element](manifest-format.md#Element-manifest_server)
element. This is how the client knows what service to talk to.

```xml
<manifest-server url="https://example.com/your/manifest/server/url" />
```

If the URL starts with `persistent-`, then the
[`git-remote-persistent-https` helper](https://github.com/git/git/blob/HEAD/contrib/persistent-https/README)
is used to communicate with the server.

## Credentials

Credentials may be specified directly in typical `username:password`
[URI syntax](https://en.wikipedia.org/wiki/URI#Syntax) in the
`<manifest-server>` element directly in the manifest.

If they are not specified, `repo sync` has `--manifest-server-username=USERNAME`
and `--manifest-server-password=PASSWORD` options.

If those are not used, then repo will look up the host in your
[`~/.netrc`](https://docs.python.org/3/library/netrc.html) database.

When making the connection, cookies matching the host are automatically loaded
from the cookiejar specified in
[Git's `http.cookiefile` setting](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpcookieFile).

## Manifest Server

Unfortunately, there are no public reference implementations. Google has an
internal one for Android, but it is written using Google's internal systems,
so wouldn't be that helpful as a reference.

That said, the XML-RPC API is pretty simple, so any standard XML-RPC server
example would do. Google's internal server uses Python's
[xmlrpc.server.SimpleXMLRPCDispatcher](https://docs.python.org/3/library/xmlrpc.server.html).

## Network API

The manifest server should implement the following RPC methods.

### GetApprovedManifest

> `GetApprovedManifest(branch: str, target: Optional[str]) -> str`

The meaning of `branch` and `target` is not strictly defined. The server may
interpret them however it wants. The recommended interpretation is that the
`branch` matches the manifest branch, and `target` is an identifier for your
project that matches something users would build.

See the client section below for how repo typically generates these values.

The server will return a manifest or an error. If it's an error, repo will
show the output directly to the user to provide a limited feedback channel.

If the user's request is ambiguous and could match multiple manifests, the
server has to decide whether to pick one automatically (and silently such that
the user won't know there were multiple matches), or return an error and force
the user to be more specific.

### GetManifest

> `GetManifest(tag: str) -> str`

The meaning of `tag` is not strictly defined. Projects are encouraged to use
a system where the tag matches a unique source state.

See the client section below for how repo typically generates these values.

The server will return a manifest or an error. If it's an error, repo will
show the output directly to the user to provide a limited feedback channel.

If the user's request is ambiguous and could match multiple manifests, the
server has to decide whether to pick one automatically (and silently such that
the user won't know there were multiple matches), or return an error and force
the user to be more specific.

## Client Options

Once repo has successfully downloaded the manifest from the server, it saves a
copy into `.repo/manifests/smart_sync_override.xml` so users can examine it.
The next time `repo sync` is run, this file is automatically replaced or removed
based on the current set of options.

### --smart-sync

Repo will call `GetApprovedManifest(branch[, target])`.

The `branch` is determined by the current manifest branch as specified by
`--manifest-branch=BRANCH` when running `repo init`.

The `target` is defined by environment variables in the order below. If none
of them match, then `target` is omitted. These variables were decided as they
match the settings Android build environments automatically setup.

1. `${SYNC_TARGET}`: If defined, the value is used directly.
2. `${TARGET_PRODUCT}-${TARGET_RELEASE}-${TARGET_BUILD_VARIANT}`: If these
variables are all defined, then they are merged with `-` and used.
3. `${TARGET_PRODUCT}-${TARGET_BUILD_VARIANT}`: If these variables are all
defined, then they are merged with `-` and used.

### --smart-tag=TAG

Repo will call `GetManifest(TAG)`.
14 changes: 14 additions & 0 deletions git_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,20 @@ def ForUser(cls):

@staticmethod
def _getUserConfig():
"""Get the user-specific config file.

Prefers the XDG config location if available, with fallback to
~/.gitconfig

This matches git behavior:
https://git-scm.com/docs/git-config#FILES
"""
xdg_config_home = os.getenv(
"XDG_CONFIG_HOME", os.path.expanduser("~/.config")
)
xdg_config_file = os.path.join(xdg_config_home, "git", "config")
if os.path.exists(xdg_config_file):
return xdg_config_file
return os.path.expanduser("~/.gitconfig")

@classmethod
Expand Down
7 changes: 6 additions & 1 deletion manifest_xml.py
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CAUTION

LINT IS FLAMMABLE

Original file line number Diff line number Diff line change
Expand Up @@ -2056,7 +2056,12 @@ def GetSubprojectPaths(self, parent, name, path):
path = path.rstrip("/")
name = name.rstrip("/")
relpath = self._JoinRelpath(parent.relpath, path)
gitdir = os.path.join(parent.gitdir, "subprojects", "%s.git" % path)
subprojects = os.path.join(parent.gitdir, "subprojects", f"{path}.git")
modules = os.path.join(parent.gitdir, "modules", path)
if platform_utils.isdir(subprojects):
gitdir = subprojects
else:
gitdir = modules
objdir = os.path.join(
parent.gitdir, "subproject-objects", "%s.git" % name
)
Expand Down
115 changes: 103 additions & 12 deletions project.py
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of these file-handling routines seem a little fragile to me, although it might be the compromises necessary for cross-platform support.

Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,10 @@ def __init__(
# project containing repo hooks.
self.enabled_repo_hooks = []

# This will be updated later if the project has submodules and
# if they will be synced.
self.has_subprojects = False

def RelPath(self, local=True):
"""Return the path for the project relative to a manifest.

Expand Down Expand Up @@ -1560,6 +1564,11 @@ def fail(error: Exception):
return

self._InitWorkTree(force_sync=force_sync, submodules=submodules)
# TODO(https://git-scm.com/docs/git-worktree#_bugs): Re-evaluate if
# submodules can be init when using worktrees once its support is
# complete.
if self.has_subprojects and not self.use_git_worktrees:
self._InitSubmodules()
all_refs = self.bare_ref.all
self.CleanPublishedCache(all_refs)
revid = self.GetRevisionId(all_refs)
Expand Down Expand Up @@ -2347,6 +2356,8 @@ def GetDerivedSubprojects(self):
)
result.append(subproject)
result.extend(subproject.GetDerivedSubprojects())
if result:
self.has_subprojects = True
return result

def EnableRepositoryExtension(self, key, value="true", version=1):
Expand Down Expand Up @@ -2997,6 +3008,17 @@ def _SyncSubmodules(self, quiet=True):
project=self.name,
)

def _InitSubmodules(self, quiet=True):
"""Initialize the submodules for the project."""
cmd = ["submodule", "init"]
if quiet:
cmd.append("-q")
if GitCommand(self, cmd).Wait() != 0:
raise GitError(
f"{self.name} submodule init",
project=self.name,
)

def _Rebase(self, upstream, onto=None):
cmd = ["rebase"]
if onto is not None:
Expand Down Expand Up @@ -3415,6 +3437,11 @@ def _InitWorkTree(self, force_sync=False, submodules=False):
"""
dotgit = os.path.join(self.worktree, ".git")

# If bare checkout of the submodule is stored under the subproject dir,
# migrate it.
if self.parent:
self._MigrateOldSubmoduleDir()

# If using an old layout style (a directory), migrate it.
if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
Expand All @@ -3425,34 +3452,76 @@ def _InitWorkTree(self, force_sync=False, submodules=False):
self._InitGitWorktree()
self._CopyAndLinkFiles()
else:
# Remove old directory symbolic links for submodules.
if self.parent and platform_utils.islink(dotgit):
platform_utils.remove(dotgit)
init_dotgit = True

if not init_dotgit:
# See if the project has changed.
if os.path.realpath(self.gitdir) != os.path.realpath(dotgit):
platform_utils.remove(dotgit)
self._removeBadGitDirLink(dotgit)

if init_dotgit or not os.path.exists(dotgit):
os.makedirs(self.worktree, exist_ok=True)
platform_utils.symlink(
os.path.relpath(self.gitdir, self.worktree), dotgit
)
self._createDotGit(dotgit)

if init_dotgit:
_lwrite(
os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
os.path.join(self.gitdir, HEAD), f"{self.GetRevisionId()}\n"
)

# Finish checking out the worktree.
cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
if GitCommand(self, cmd).Wait() != 0:
raise GitError(
"Cannot initialize work tree for " + self.name,
project=self.name,
)
try:
if GitCommand(self, cmd).Wait() != 0:
raise GitError(
"Cannot initialize work tree for " + self.name,
project=self.name,
)
except Exception as e:
# Something went wrong with read-tree (perhaps fetching
# missing blobs), so remove .git to avoid half initialized
# workspace from which repo can't recover on its own.
platform_utils.remove(dotgit)
raise e

if submodules:
self._SyncSubmodules(quiet=True)
self._CopyAndLinkFiles()

def _createDotGit(self, dotgit):
"""Initialize .git path.

For submodule projects, create a '.git' file using the gitfile
mechanism, and for the rest, create a symbolic link.
"""
os.makedirs(self.worktree, exist_ok=True)
if self.parent:
_lwrite(
dotgit,
f"gitdir: {os.path.relpath(self.gitdir, self.worktree)}\n",
)
else:
platform_utils.symlink(
os.path.relpath(self.gitdir, self.worktree), dotgit
)

def _removeBadGitDirLink(self, dotgit):
"""Verify .git is initialized correctly, otherwise delete it."""
if self.parent and os.path.isfile(dotgit):
with open(dotgit) as fp:
setting = fp.read()
if not setting.startswith("gitdir:"):
raise GitError(
f"'.git' in {self.worktree} must start with 'gitdir:'",
project=self.name,
)
gitdir = setting.split(":", 1)[1].strip()
dotgit_path = os.path.normpath(os.path.join(self.worktree, gitdir))
else:
dotgit_path = os.path.realpath(dotgit)
if os.path.realpath(self.gitdir) != dotgit_path:
platform_utils.remove(dotgit)

@classmethod
def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
"""Migrate the old worktree .git/ dir style to a symlink.
Expand Down Expand Up @@ -3541,6 +3610,28 @@ def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
dotgit,
)

def _MigrateOldSubmoduleDir(self):
"""Move the old bare checkout in 'subprojects' to 'modules'
as bare checkouts of submodules are now in 'modules' dir.
"""
subprojects = os.path.join(self.parent.gitdir, "subprojects")
if not platform_utils.isdir(subprojects):
return

modules = os.path.join(self.parent.gitdir, "modules")
old = self.gitdir
new = os.path.splitext(self.gitdir.replace(subprojects, modules))[0]

if all(map(platform_utils.isdir, [old, new])):
platform_utils.rmtree(old, ignore_errors=True)
else:
os.makedirs(modules, exist_ok=True)
platform_utils.rename(old, new)
self.gitdir = new
self.UpdatePaths(self.relpath, self.worktree, self.gitdir, self.objdir)
if platform_utils.isdir(subprojects) and not os.listdir(subprojects):
platform_utils.rmtree(subprojects, ignore_errors=True)

def _get_symlink_error_message(self):
if platform_utils.isWindows():
return (
Expand Down
Loading