mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-11-10 22:41:07 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7c431b996 | ||
|
|
020767b9d3 | ||
|
|
0f83e19bbb | ||
|
|
221a31cd0a | ||
|
|
ccddf51633 | ||
|
|
72e3c8c277 | ||
|
|
4cef373d98 | ||
|
|
95d7c8c0df | ||
|
|
616396317c | ||
|
|
4978203b07 | ||
|
|
cc3f52cd6c | ||
|
|
e045aa19d8 | ||
|
|
c382d3807a | ||
|
|
9d01d216e0 | ||
|
|
52f01a18da | ||
|
|
3a3d1fc7cd | ||
|
|
2029997aef | ||
|
|
caa4cd6564 | ||
|
|
e15b3e0298 | ||
|
|
a4b44c5026 | ||
|
|
8f2c0f1483 | ||
|
|
7ed9700a95 | ||
|
|
d621aa8ee4 | ||
|
|
b16e5f49d6 | ||
|
|
6596cb6499 | ||
|
|
ecb91f465e | ||
|
|
3656290156 | ||
|
|
b90c7cd8f9 | ||
|
|
c5340585b8 | ||
|
|
86a7bec83d |
No files matched your search
@@ -1,8 +1,54 @@
|
||||
# https://flake8.pycqa.org/en/latest/user/configuration.html
|
||||
[flake8]
|
||||
max-line-length = 80
|
||||
per-file-ignores =
|
||||
# E501: line too long
|
||||
tests/test_git_superproject.py: E501
|
||||
tests/test_git_superproject.py: E501,
|
||||
# IMR241: When using the from syntax, only submodules are imported, not module elements.
|
||||
# NB: Do not add any more here.
|
||||
command.py: IMR241,
|
||||
git_status.py: IMR241,
|
||||
main.py: IMR241,
|
||||
manifest_xml.py: IMR241,
|
||||
subcmds/abandon.py: IMR241,
|
||||
subcmds/branches.py: IMR241,
|
||||
subcmds/checkout.py: IMR241,
|
||||
subcmds/cherry_pick.py: IMR241,
|
||||
subcmds/diffmanifests.py: IMR241,
|
||||
subcmds/diff.py: IMR241,
|
||||
subcmds/download.py: IMR241,
|
||||
subcmds/forall.py: IMR241,
|
||||
subcmds/gc.py: IMR241,
|
||||
subcmds/grep.py: IMR241,
|
||||
subcmds/help.py: IMR241,
|
||||
subcmds/info.py: IMR241,
|
||||
subcmds/init.py: IMR241,
|
||||
subcmds/list.py: IMR241,
|
||||
subcmds/manifest.py: IMR241,
|
||||
subcmds/overview.py: IMR241,
|
||||
subcmds/prune.py: IMR241,
|
||||
subcmds/rebase.py: IMR241,
|
||||
subcmds/selfupdate.py: IMR241,
|
||||
subcmds/smartsync.py: IMR241,
|
||||
subcmds/stage.py: IMR241,
|
||||
subcmds/start.py: IMR241,
|
||||
subcmds/status.py: IMR241,
|
||||
subcmds/sync.py: IMR241,
|
||||
subcmds/upload.py: IMR241,
|
||||
subcmds/version.py: IMR241,
|
||||
subcmds/wipe.py: IMR241,
|
||||
tests/e2e/test_failures.py: IMR241,
|
||||
tests/e2e/test_groups.py: IMR241,
|
||||
tests/e2e/test_init_sync.py: IMR241,
|
||||
tests/e2e/test_manifest_regressions.py: IMR241,
|
||||
tests/e2e/test_partial_clone.py: IMR241,
|
||||
tests/e2e/test_sync_modes.py: IMR241,
|
||||
tests/e2e/test_workflows.py: IMR241,
|
||||
tests/test_command.py: IMR241,
|
||||
tests/test_error.py: IMR241,
|
||||
tests/test_main.py: IMR241,
|
||||
tests/test_subcmds.py: IMR241,
|
||||
tests/test_subcmds_sync.py: IMR241,
|
||||
extend-ignore =
|
||||
# E203: Whitespace before ':'
|
||||
# See https://github.com/PyCQA/pycodestyle/issues/373
|
||||
@@ -15,3 +61,11 @@ exclude =
|
||||
.venv,
|
||||
venv,
|
||||
.tox,
|
||||
# IMR241: When using the from syntax, only submodules are imported, not module elements.
|
||||
# NB: Do not add more modules here without strong discussion. This is aligned
|
||||
# with Google's Python style guide.
|
||||
imr241_exclude =
|
||||
collections.abc,
|
||||
pathlib,
|
||||
typing,
|
||||
typing_extensions,
|
||||
+2
-2
@@ -20,7 +20,7 @@ import re
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
|
||||
import error
|
||||
from event_log import EventLog
|
||||
import event_log
|
||||
import progress
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ class Command:
|
||||
# provide event summary to callers. Only used by sync subcommand currently.
|
||||
#
|
||||
# NB: This is being replaced by git trace2 events. See git_trace2_event_log.
|
||||
event_log = EventLog()
|
||||
event_log = event_log.EventLog(enabled=False)
|
||||
|
||||
# Whether this command is a "common" one, i.e. whether the user would
|
||||
# commonly use it or it's a more uncommon command. This is used by the help
|
||||
|
||||
@@ -411,6 +411,7 @@ _repo() {
|
||||
'(-y --yes)'{-y,--yes}'[Answer yes to all prompts]' \
|
||||
'--ignore-untracked-files[Ignore untracked files]' \
|
||||
'--no-ignore-untracked-files[Do not ignore untracked files]' \
|
||||
'--ignore-remote-commits[Ignore commits already in remote branches]' \
|
||||
'--no-cert-checks[Do not check certificates]' \
|
||||
'--no-verify[Do not verify]' \
|
||||
'--verify[Verify]' \
|
||||
|
||||
+14
-9
@@ -42,13 +42,13 @@ postconditions to be met:
|
||||
|
||||
1. When the manifest revision names a branch or tag, the mapped local tracking
|
||||
ref (e.g. `refs/remotes/REPO_REMOTE/<branch>` for a branch, or
|
||||
`refs/tags/<tag>` for a tag) must resolve to a valid commit in the object
|
||||
store (`git rev-parse --verify <ref>^{commit}`), and must equal `REPO_TREV`
|
||||
if `REPO_TREV` was non-empty.
|
||||
2. `FETCH_HEAD` must resolve to the same target commit in the object store
|
||||
(`git rev-parse --verify FETCH_HEAD^{commit}`).
|
||||
3. The commit graph from the target commit must be reachable far enough to
|
||||
compute merge bases with local branches.
|
||||
`refs/tags/<tag>` for a tag) must name the target commit, and must equal
|
||||
`REPO_TREV` if `REPO_TREV` was non-empty. A tag ref may name an annotated
|
||||
tag of the target commit.
|
||||
2. `FETCH_HEAD` must name the same target commit.
|
||||
3. The target commit, and the commit graph from it far enough to compute
|
||||
merge bases with local branches, must be readable from the object store.
|
||||
They may be materialized lazily, when first read.
|
||||
|
||||
### Invariants
|
||||
|
||||
@@ -66,5 +66,10 @@ postconditions to be met:
|
||||
|
||||
* A non-zero exit status aborts the project's sync, and the command's stderr
|
||||
is surfaced to the user.
|
||||
* `repo` verifies the tracking ref and target reachability after exit 0. Any
|
||||
mismatch is treated as a failure.
|
||||
* After exit 0, `repo` checks postconditions 1 and 2 by comparing the object
|
||||
IDs that the refs record (`git rev-parse --verify <ref>`), so it does not
|
||||
read the object store during the network half of the sync. It peels the
|
||||
IDs to commits only when they differ, as for an annotated tag. Any mismatch
|
||||
is treated as a failure.
|
||||
* A target commit missing from the object store fails the checkout that reads
|
||||
it.
|
||||
+69
-6
@@ -74,7 +74,8 @@ following DTD:
|
||||
<!ELEMENT project (annotation*,
|
||||
project*,
|
||||
copyfile*,
|
||||
linkfile*)>
|
||||
linkfile*,
|
||||
sparse-path*)>
|
||||
<!ATTLIST project name CDATA #REQUIRED>
|
||||
<!ATTLIST project path CDATA #IMPLIED>
|
||||
<!ATTLIST project remote IDREF #IMPLIED>
|
||||
@@ -102,9 +103,13 @@ following DTD:
|
||||
<!ATTLIST linkfile src CDATA #REQUIRED>
|
||||
<!ATTLIST linkfile dest CDATA #REQUIRED>
|
||||
|
||||
<!ELEMENT sparse-path EMPTY>
|
||||
<!ATTLIST sparse-path path CDATA #REQUIRED>
|
||||
|
||||
<!ELEMENT extend-project (annotation*,
|
||||
copyfile*,
|
||||
linkfile*)>
|
||||
linkfile*,
|
||||
sparse-path*)>
|
||||
<!ATTLIST extend-project name CDATA #REQUIRED>
|
||||
<!ATTLIST extend-project path CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project dest-path CDATA #IMPLIED>
|
||||
@@ -424,10 +429,11 @@ attributes of an existing project without completely replacing the
|
||||
existing project definition. This makes the local manifest more robust
|
||||
against changes to the original manifest.
|
||||
|
||||
The `extend-project` element can also contain `annotation`, `copyfile`, and
|
||||
`linkfile` child elements. These are added to the project's definition. A
|
||||
`copyfile` or `linkfile` with a `dest` that already exists in the project
|
||||
will overwrite the original.
|
||||
The `extend-project` element can also contain `annotation`, `copyfile`,
|
||||
`linkfile`, and `sparse-path` child elements. These are added to the project's
|
||||
definition. A `copyfile` or `linkfile` with a `dest` that already exists in the
|
||||
project will overwrite the original. There is no way to remove a `sparse-path`
|
||||
that the original project definition declared.
|
||||
|
||||
Attribute `path`: If specified, limit the change to projects checked out
|
||||
at the specified path, rather than all projects with the given name.
|
||||
@@ -471,6 +477,63 @@ attribute "keep" which accepts the case insensitive values "true"
|
||||
(default) or "false". This attribute determines whether or not the
|
||||
annotation will be kept when exported with the manifest subcommand.
|
||||
|
||||
### Element sparse-path
|
||||
|
||||
Zero or more sparse-path elements may be specified as children of a
|
||||
project element or an extend-project element. Each element specifies a
|
||||
path to include in the working directory.
|
||||
|
||||
Requires Git 2.25.0 or later, which is when `git sparse-checkout` and cone mode
|
||||
were introduced. Declaring a sparse-path with an older Git fails the sync for
|
||||
that project.
|
||||
|
||||
Declaring at least one sparse-path is what enables Git sparse-checkout for
|
||||
the project: the listed paths are checked out, and other directories are left
|
||||
out of the working directory. Cone mode also checks out the files sitting
|
||||
directly in the project's top-level directory and in each leading directory of
|
||||
a sparse-path, so `src/backend` brings along the top-level files and the files
|
||||
directly under `src/` too. A project with no sparse-path elements is checked
|
||||
out in full.
|
||||
The checkout uses [cone mode](https://git-scm.com/docs/git-sparse-checkout#_internalscone_mode_handling)
|
||||
for better performance, which means only directory paths are supported;
|
||||
individual files and globs are not. Cone mode does no pattern matching, so `*`,
|
||||
`?`, `[`, `]`, and `\` are not glob syntax in a `path`. In fact, Git 2.36.0 and
|
||||
later reject them with `specify directories rather than patterns`, which
|
||||
surfaces as a sync failure for the project. Earlier versions starting with
|
||||
2.25.0 have no such check and are treated as literals.
|
||||
|
||||
Sparse-checkout is configured only when the project's working directory is
|
||||
first created. Changes to `sparse-path` in the manifest (added, changed, or
|
||||
removed) are not applied to a project that is already checked out. To change
|
||||
the paths in an existing checkout, run `git sparse-checkout` in the project
|
||||
directly, or remove the project's working directory and sync again.
|
||||
|
||||
Because sparse-checkout only prunes the working directory, a project with at
|
||||
least one sparse-path is fetched with `--filter=blob:none` so that blobs which
|
||||
will never be checked out are not downloaded. Without the filter Git still
|
||||
downloads every blob in the project and sparse-checkout merely declines to
|
||||
write most of them to disk, so the filter is what turns sparse-checkout into a
|
||||
saving in transfer size and server load. The filter from
|
||||
`repo init --partial-clone --clone-filter` takes precedence, and projects listed
|
||||
in `repo init --partial-clone-exclude` are fetched in full as usual. Like
|
||||
sparse-checkout itself, the implied filter only applies when the project is
|
||||
first fetched (or is already a partial clone), so adding a sparse-path to an
|
||||
existing full checkout does not convert it to a partial clone. Mirrors are never
|
||||
filtered.
|
||||
|
||||
Because that filter is only useful if the objects it excludes are not fetched
|
||||
by some other route, a project that takes the implied `--filter=blob:none` also
|
||||
skips the `/clone.bundle` download: the bundle is an unfiltered pack of every
|
||||
object, so fetching it would undo the filter. Projects that set the filter
|
||||
explicitly, or that are listed in `repo init --partial-clone-exclude`, keep
|
||||
using the clone bundle.
|
||||
|
||||
Attribute `path`: A path relative to the project root to include in the
|
||||
sparse-checkout. The path is validated like `<copyfile>` and `<linkfile>`
|
||||
paths: it must stay inside the project, so absolute paths, `.` or `..`
|
||||
components, `~`, and `.git` or `.repo*` components are all rejected. A trailing
|
||||
`/` is allowed.
|
||||
|
||||
### Element copyfile
|
||||
|
||||
Zero or more copyfile elements may be specified as children of a
|
||||
|
||||
@@ -45,10 +45,10 @@ that are critical to us.
|
||||
That said, let's assume we need functionality that only exists in Python 3.7.
|
||||
How do we decide when it's acceptable to drop Python 3.6?
|
||||
|
||||
1. Review the [Project References](./release-process.md#project-references) to
|
||||
see what major distros are using the previous version of Python, and when
|
||||
they go EOL. Generally we care about Ubuntu LTS & current/previous Debian
|
||||
stable versions.
|
||||
1. Review what major distros are using the previous version of Python (e.g.
|
||||
via [Repology Python versions]), and when they go EOL (e.g. via
|
||||
[Ubuntu releases] & [Debian releases]). Generally we care about Ubuntu LTS
|
||||
& current/previous Debian stable versions.
|
||||
* If they're all EOL already, then go for it, drop support.
|
||||
* If they aren't EOL, start a thread on [repo-discuss] to see how the user
|
||||
base feels about the proposal.
|
||||
@@ -88,5 +88,8 @@ $ repo-2.32 init --repo-rev=repo-1 ...
|
||||
|
||||
|
||||
[repo-discuss]: https://groups.google.com/forum/#!forum/repo-discuss
|
||||
[Debian releases]: https://wiki.debian.org/DebianReleases#Versions_of_Debian_stable
|
||||
[Repology Python versions]: https://repology.org/project/python/versions
|
||||
[repo hooks]: ./repo-hooks.md
|
||||
[repo launcher]: ../repo
|
||||
[Ubuntu releases]: https://wiki.ubuntu.com/Releases
|
||||
+54
-200
@@ -5,11 +5,19 @@ related topics and flows.
|
||||
|
||||
[TOC]
|
||||
|
||||
## Schedule
|
||||
## Schedule {#schedule}
|
||||
|
||||
Releases are automated.
|
||||
Every Tuesday morning [US PT], a pipeline checks if any new commits have landed
|
||||
since last release.
|
||||
If so, it tags & tests the latest `main`, pushes & announces the tag, and then
|
||||
promotes it to `stable` a day later.
|
||||
The release is skipped if there's a holiday or [production freeze](#freeze)
|
||||
that week.
|
||||
Maintainers can also trigger it by hand.
|
||||
The [manual steps](#manual) below are only needed as a fallback, or to roll
|
||||
back a release.
|
||||
|
||||
There is no specific schedule for when releases are made.
|
||||
Usually it's more along the lines of "enough minor changes have been merged",
|
||||
or "there's a known issue the maintainers know should get fixed".
|
||||
If you find a fix has been merged for an issue important to you, but hasn't been
|
||||
released after a week or so, feel free to [contact] us to request a new release.
|
||||
|
||||
@@ -48,6 +56,11 @@ Whenever major changes are made to the launcher script, you should increment the
|
||||
At runtime, repo will check this to see if it needs to be updated (and notify
|
||||
the user automatically).
|
||||
|
||||
When `VERSION` changes, the release pipeline publishes the new launcher to the
|
||||
`git-repo-downloads` Google Cloud Storage bucket.
|
||||
To publish it by hand, run `./release/sign-launcher.py`, which signs the
|
||||
launcher and prints the commands to upload it.
|
||||
|
||||
## Key management
|
||||
|
||||
Every release has a git tag that is signed with a key that repo recognizes.
|
||||
@@ -96,8 +109,8 @@ If that tag is valid, then repo will warn and use that commit instead.
|
||||
|
||||
If that tag cannot be verified, it gives up and forces the user to resolve.
|
||||
|
||||
If env variable `REPO_SKIP_SELF_UPDATE` is defined, this will
|
||||
bypass the self update algorithm.
|
||||
If the env variable `REPO_SKIP_SELF_UPDATE` is set to anything other than `0`,
|
||||
`repo sync` will bypass the self update algorithm.
|
||||
|
||||
### Force an update
|
||||
|
||||
@@ -110,29 +123,20 @@ All development happens on the `main` branch and should generally be stable.
|
||||
|
||||
Since the repo launcher defaults to tracking the `stable` branch, it is not
|
||||
normally updated until a new release is available.
|
||||
If something goes wrong with a new release, an older release can be force pushed
|
||||
and clients will automatically downgrade.
|
||||
|
||||
The `maint` branch is used to track the previous major release of repo.
|
||||
It is not normally meant to be used by people as `stable` should be good enough.
|
||||
Once a new major release is pushed to the `stable` branch, then the previous
|
||||
major release can be pushed to `maint`.
|
||||
For example, when `stable` moves from `v1.10.x` to `v1.11.x`, then the `maint`
|
||||
branch will be updated from `v1.9.x` to `v1.10.x`.
|
||||
If something goes wrong with a new release, `stable` can be pointed back at an
|
||||
older release and clients will automatically downgrade.
|
||||
|
||||
We don't have parallel release branches/series.
|
||||
Typically all tags are made against the `main` branch and then pushed to the
|
||||
Typically all tags are made against the `main` branch and then promoted to the
|
||||
`stable` branch to make it available to the rest of the world.
|
||||
Since repo doesn't typically see a lot of changes, this tends to be OK.
|
||||
|
||||
## Creating a new release
|
||||
## Creating a new release {#manual}
|
||||
|
||||
When you want to create a new release, you'll need to select a good version and
|
||||
Releases are normally created automatically (see [Schedule](#schedule)).
|
||||
If you need to create one by hand, you'll need to select a good version and
|
||||
create a signed tag using a key registered in repo itself.
|
||||
Typically we just tag the latest version of the `main` branch.
|
||||
The tag could be pushed now, but it won't be used by clients normally (since the
|
||||
default `repo-rev` setting is `stable`).
|
||||
This would allow some early testing on systems who explicitly select `main`.
|
||||
|
||||
### Creating a signed tag
|
||||
|
||||
@@ -147,10 +151,10 @@ internal processes for accessing the restricted keys.
|
||||
|
||||
```sh
|
||||
# Pick the new version.
|
||||
$ t=v2.30
|
||||
$ t=v2.XX
|
||||
|
||||
# Create a new signed tag with the current HEAD.
|
||||
$ ./release/sign-tag.py $t
|
||||
# Create a new signed tag of the latest main branch.
|
||||
$ ./release/sign-tag.py $t origin/main
|
||||
|
||||
# Verify the signed tag.
|
||||
$ git show $t
|
||||
@@ -158,199 +162,49 @@ $ git show $t
|
||||
|
||||
### Push the new release
|
||||
|
||||
Once you're ready to make the release available to everyone, push it to the
|
||||
`stable` branch.
|
||||
|
||||
Make sure you never push the tag itself to the stable branch!
|
||||
Only push the commit -- note the use of `^0` below.
|
||||
First push the tag, [announce](#announce) it, and wait a day to get feedback
|
||||
from people testing new releases early.
|
||||
|
||||
```sh
|
||||
$ git push https://gerrit-review.googlesource.com/git-repo $t
|
||||
$ git push https://gerrit-review.googlesource.com/git-repo $t^0:stable
|
||||
```
|
||||
|
||||
If something goes horribly wrong, you can force push the previous version to the
|
||||
`stable` branch and people should automatically recover.
|
||||
Again, make sure you never push the tag itself!
|
||||
Then make the release available to everyone by updating the `stable` branch.
|
||||
Direct pushes to it are rejected, so it has to be deleted & recreated via the
|
||||
authenticated [Gerrit REST API] (see [go/repo-release] for maintainers).
|
||||
|
||||
Make sure you never point the stable branch at the tag itself!
|
||||
Only use the commit -- note the use of `^0` below.
|
||||
The commit is resolved before deleting anything so that a bad `$t` can't leave
|
||||
`stable` missing.
|
||||
|
||||
```sh
|
||||
$ oldrev="whatever-old-commit"
|
||||
$ git push https://gerrit-review.googlesource.com/git-repo $oldrev:stable --force
|
||||
$ api=https://gerrit-review.googlesource.com/a/projects/git-repo/branches/stable
|
||||
$ rev=$(git rev-parse --verify "$t^0") && \
|
||||
curl --fail -X DELETE "$api" && \
|
||||
curl --fail -X PUT -H 'Content-Type: application/json' \
|
||||
-d "{\"revision\":\"$rev\"}" "$api"
|
||||
```
|
||||
|
||||
### Announce the release
|
||||
If something goes horribly wrong, run the same commands with the previous
|
||||
release (e.g. `t=$(git describe --abbrev=0 $t^0~)`) and people should
|
||||
automatically recover.
|
||||
|
||||
Once you do push a new release to `stable`, make sure to announce it on the
|
||||
[repo-discuss@googlegroups.com] group.
|
||||
### Announce the release {#announce}
|
||||
|
||||
Once you push a new release, make sure to announce it on the
|
||||
[repo-discuss@googlegroups.com] group, and follow up once it's on `stable`.
|
||||
Here is an [example announcement].
|
||||
|
||||
You can create a short changelog using the command:
|
||||
You can create a short changelog since the previous release using the command:
|
||||
|
||||
```sh
|
||||
# If you haven't pushed to the stable branch yet, you can use origin/stable.
|
||||
# If you have pushed, change origin/stable to the previous release tag.
|
||||
# This assumes "main" is the current tagged release. If it's newer, change it
|
||||
# to the current release tag too.
|
||||
$ git log --format="%h (%aN) %s" --no-merges origin/stable..main
|
||||
$ git log --format="%h (%aN) %s" --no-merges \
|
||||
$(git describe --abbrev=0 $t^0~)..$t
|
||||
```
|
||||
|
||||
## Project References
|
||||
|
||||
Here's a table showing the relationship of major tools, their EOL dates, and
|
||||
their status in Ubuntu & Debian.
|
||||
Those distros tend to be good indicators of how long we need to support things.
|
||||
|
||||
Things in bold indicate stuff to take note of, but does not guarantee that we
|
||||
still support them.
|
||||
Things in italics are things we used to care about but probably don't anymore.
|
||||
|
||||
| Date | EOL | [Git][rel-g] | [Python][rel-p] | [SSH][rel-o] | [Ubuntu][rel-u] / [Debian][rel-d] | Git | Python | SSH |
|
||||
|:--------:|:------------:|:------------:|:---------------:|:------------:|-----------------------------------|:---:|:------:|:---:|
|
||||
| Apr 2008 | | | | 5.0 |
|
||||
| Jun 2008 | | | | 5.1 |
|
||||
| Oct 2008 | *Oct 2013* | | 2.6.0 | | *10.04 Lucid* - 10.10 Maverick / *Squeeze* |
|
||||
| Dec 2008 | *Feb 2009* | | 3.0.0 |
|
||||
| Feb 2009 | | | | 5.2 |
|
||||
| Feb 2009 | *Mar 2012* | | | | Debian 5 Lenny | 1.5.6.5 | 2.5.2 |
|
||||
| Jun 2009 | *Jun 2016* | | 3.1.0 | | *10.04 Lucid* - 10.10 Maverick / *Squeeze* |
|
||||
| Sep 2009 | | | | 5.3 | *10.04 Lucid* |
|
||||
| Feb 2010 | *Oct 2012* | 1.7.0 | | | *10.04 Lucid* - *12.04 Precise* - 12.10 Quantal |
|
||||
| Mar 2010 | | | | 5.4 |
|
||||
| Apr 2010 | | | | 5.5 | 10.10 Maverick |
|
||||
| Apr 2010 | *Apr 2015* | | | | *10.04 Lucid* | 1.7.0.4 | 2.6.5 3.1.2 | 5.3 |
|
||||
| Jul 2010 | *Dec 2019* | | *2.7.0* | | 11.04 Natty - *<current>* |
|
||||
| Aug 2010 | | | | 5.6 |
|
||||
| Oct 2010 | | | | | 10.10 Maverick | 1.7.1 | 2.6.6 3.1.3 | 5.5 |
|
||||
| Jan 2011 | | | | 5.7 |
|
||||
| Feb 2011 | | | | 5.8 | 11.04 Natty |
|
||||
| Feb 2011 | *Feb 2016* | | | | Debian 6 Squeeze | 1.7.2.5 | 2.6.6 3.1.3 |
|
||||
| Apr 2011 | | | | | 11.04 Natty | 1.7.4 | 2.7.1 3.2.0 | 5.8 |
|
||||
| Sep 2011 | | | | 5.9 | *12.04 Precise* |
|
||||
| Oct 2011 | *Feb 2016* | | 3.2.0 | | 11.04 Natty - 12.10 Quantal |
|
||||
| Oct 2011 | | | | | 11.10 Ocelot | 1.7.5.4 | 2.7.2 3.2.2 | 5.8 |
|
||||
| Apr 2012 | | | | 6.0 | 12.10 Quantal |
|
||||
| Apr 2012 | *Apr 2019* | | | | *12.04 Precise* | 1.7.9.5 | 2.7.3 3.2.3 | 5.9 |
|
||||
| Aug 2012 | | | | 6.1 | 13.04 Raring |
|
||||
| Sep 2012 | *Sep 2017* | | 3.3.0 | | 13.04 Raring - 13.10 Saucy |
|
||||
| Oct 2012 | *Dec 2014* | 1.8.0 | | | 13.04 Raring - 13.10 Saucy |
|
||||
| Oct 2012 | | | | | 12.10 Quantal | 1.7.10.4 | 2.7.3 3.2.3 | 6.0 |
|
||||
| Mar 2013 | | | | 6.2 | 13.10 Saucy |
|
||||
| Apr 2013 | | | | | 13.04 Raring | 1.8.1.2 | 2.7.4 3.3.1 | 6.1 |
|
||||
| May 2013 | *May 2018* | | | | Debian 7 Wheezy | 1.7.10.4 | 2.7.3 3.2.3 |
|
||||
| Sep 2013 | | | | 6.3 |
|
||||
| Oct 2013 | | | | | 13.10 Saucy | 1.8.3.2 | 2.7.5 3.3.2 | 6.2 |
|
||||
| Nov 2013 | | | | 6.4 |
|
||||
| Jan 2014 | | | | 6.5 |
|
||||
| Feb 2014 | *Dec 2014* | **1.9.0** | | | *14.04 Trusty* |
|
||||
| Mar 2014 | *Mar 2019* | | *3.4.0* | | *14.04 Trusty* - 15.10 Wily / *Jessie* |
|
||||
| Mar 2014 | | | | 6.6 | *14.04 Trusty* - 14.10 Utopic |
|
||||
| Apr 2014 | *Apr 2024* | | | | *14.04 Trusty* | 1.9.1 | 2.7.5 3.4.0 | 6.6 |
|
||||
| May 2014 | *Dec 2014* | 2.0.0 |
|
||||
| Aug 2014 | *Dec 2014* | *2.1.0* | | | 14.10 Utopic - 15.04 Vivid / *Jessie* |
|
||||
| Oct 2014 | | | | 6.7 | 15.04 Vivid |
|
||||
| Oct 2014 | | | | | 14.10 Utopic | 2.1.0 | 2.7.8 3.4.2 | 6.6 |
|
||||
| Nov 2014 | *Sep 2015* | 2.2.0 |
|
||||
| Feb 2015 | *Sep 2015* | 2.3.0 |
|
||||
| Mar 2015 | | | | 6.8 |
|
||||
| Apr 2015 | *May 2017* | 2.4.0 |
|
||||
| Apr 2015 | *Jun 2020* | | | | *Debian 8 Jessie* | 2.1.4 | 2.7.9 3.4.2 |
|
||||
| Apr 2015 | | | | | 15.04 Vivid | 2.1.4 | 2.7.9 3.4.3 | 6.7 |
|
||||
| Jul 2015 | *May 2017* | 2.5.0 | | | 15.10 Wily |
|
||||
| Jul 2015 | | | | 6.9 | 15.10 Wily |
|
||||
| Aug 2015 | | | | 7.0 |
|
||||
| Aug 2015 | | | | 7.1 |
|
||||
| Sep 2015 | *May 2017* | 2.6.0 |
|
||||
| Sep 2015 | *Sep 2020* | | *3.5.0* | | *16.04 Xenial* - 17.04 Zesty / *Stretch* |
|
||||
| Oct 2015 | | | | | 15.10 Wily | 2.5.0 | 2.7.9 3.4.3 | 6.9 |
|
||||
| Jan 2016 | *Jul 2017* | *2.7.0* | | | *16.04 Xenial* |
|
||||
| Feb 2016 | | | | 7.2 | *16.04 Xenial* |
|
||||
| Mar 2016 | *Jul 2017* | 2.8.0 |
|
||||
| Apr 2016 | *Apr 2026* | | | | *16.04 Xenial* | 2.7.4 | 2.7.11 3.5.1 | 7.2 |
|
||||
| Jun 2016 | *Jul 2017* | 2.9.0 | | | 16.10 Yakkety |
|
||||
| Jul 2016 | | | | 7.3 | 16.10 Yakkety |
|
||||
| Sep 2016 | *Sep 2017* | 2.10.0 |
|
||||
| Oct 2016 | | | | | 16.10 Yakkety | 2.9.3 | 2.7.11 3.5.1 | 7.3 |
|
||||
| Nov 2016 | *Sep 2017* | *2.11.0* | | | 17.04 Zesty / *Stretch* |
|
||||
| Dec 2016 | **Dec 2021** | | **3.6.0** | | 17.10 Artful - **18.04 Bionic** - 18.10 Cosmic |
|
||||
| Dec 2016 | | | | 7.4 | 17.04 Zesty / *Debian 9 Stretch* |
|
||||
| Feb 2017 | *Sep 2017* | 2.12.0 |
|
||||
| Mar 2017 | | | | 7.5 | 17.10 Artful |
|
||||
| Apr 2017 | | | | | 17.04 Zesty | 2.11.0 | 2.7.13 3.5.3 | 7.4 |
|
||||
| May 2017 | *May 2018* | 2.13.0 |
|
||||
| Jun 2017 | *Jun 2022* | | | | *Debian 9 Stretch* | 2.11.0 | 2.7.13 3.5.3 | 7.4 |
|
||||
| Aug 2017 | *Dec 2019* | 2.14.0 | | | 17.10 Artful |
|
||||
| Oct 2017 | *Dec 2019* | 2.15.0 |
|
||||
| Oct 2017 | | | | 7.6 | **18.04 Bionic** |
|
||||
| Oct 2017 | | | | | 17.10 Artful | 2.14.1 | 2.7.14 3.6.3 | 7.5 |
|
||||
| Jan 2018 | *Dec 2019* | 2.16.0 |
|
||||
| Apr 2018 | *Mar 2021* | **2.17.0** | | | **18.04 Bionic** |
|
||||
| Apr 2018 | | | | 7.7 | 18.10 Cosmic |
|
||||
| Apr 2018 | **Apr 2028** | | | | **18.04 Bionic** | 2.17.0 | 2.7.15 3.6.5 | 7.6 |
|
||||
| Jun 2018 | *Mar 2021* | 2.18.0 |
|
||||
| Jun 2018 | **Jun 2023** | | 3.7.0 | | 19.04 Disco - **Buster** |
|
||||
| Aug 2018 | | | | 7.8 |
|
||||
| Sep 2018 | *Mar 2021* | 2.19.0 | | | 18.10 Cosmic |
|
||||
| Oct 2018 | | | | 7.9 | 19.04 Disco / **Buster** |
|
||||
| Oct 2018 | | | | | 18.10 Cosmic | 2.19.1 | 2.7.15 3.6.6 | 7.7 |
|
||||
| Dec 2018 | *Mar 2021* | **2.20.0** | | | 19.04 Disco - 19.10 Eoan / **Buster** |
|
||||
| Feb 2019 | *Mar 2021* | 2.21.0 |
|
||||
| Apr 2019 | | | | 8.0 | 19.10 Eoan |
|
||||
| Apr 2019 | | | | | 19.04 Disco | 2.20.1 | 2.7.16 3.7.3 | 7.9 |
|
||||
| Jun 2019 | | 2.22.0 |
|
||||
| Jul 2019 | **Jul 2024** | | | | **Debian 10 Buster** | 2.20.1 | 2.7.16 3.7.3 | 7.9 |
|
||||
| Aug 2019 | *Mar 2021* | 2.23.0 |
|
||||
| Oct 2019 | **Oct 2024** | | 3.8.0 | | **20.04 Focal** - 20.10 Groovy |
|
||||
| Oct 2019 | | | | 8.1 |
|
||||
| Oct 2019 | | | | | 19.10 Eoan | 2.20.1 | 2.7.17 3.7.5 | 8.0 |
|
||||
| Nov 2019 | *Mar 2021* | 2.24.0 |
|
||||
| Jan 2020 | *Mar 2021* | 2.25.0 | | | **20.04 Focal** |
|
||||
| Feb 2020 | | | | 8.2 | **20.04 Focal** |
|
||||
| Mar 2020 | *Mar 2021* | 2.26.0 |
|
||||
| Apr 2020 | **Apr 2030** | | | | **20.04 Focal** | 2.25.1 | 2.7.17 3.8.2 | 8.2 |
|
||||
| May 2020 | *Mar 2021* | 2.27.0 | | | 20.10 Groovy |
|
||||
| May 2020 | | | | 8.3 |
|
||||
| Jul 2020 | *Mar 2021* | 2.28.0 |
|
||||
| Sep 2020 | | | | 8.4 | 21.04 Hirsute / **Bullseye** |
|
||||
| Oct 2020 | *Mar 2021* | 2.29.0 |
|
||||
| Oct 2020 | | | | | 20.10 Groovy | 2.27.0 | 2.7.18 3.8.6 | 8.3 |
|
||||
| Oct 2020 | **Oct 2025** | | 3.9.0 | | 21.04 Hirsute / **Bullseye** |
|
||||
| Dec 2020 | *Mar 2021* | 2.30.0 | | | 21.04 Hirsute / **Bullseye** |
|
||||
| Mar 2021 | | 2.31.0 | | 8.5 |
|
||||
| Apr 2021 | | | | 8.6 |
|
||||
| Apr 2021 | *Jan 2022* | | | | 21.04 Hirsute | 2.30.2 | 2.7.18 3.9.4 | 8.4 |
|
||||
| Jun 2021 | | 2.32.0 |
|
||||
| Aug 2021 | | 2.33.0 | | 8.7 |
|
||||
| Aug 2021 | **Aug 2026** | | | | **Debian 11 Bullseye** | 2.30.2 | 2.7.18 3.9.2 | 8.4 |
|
||||
| Sep 2021 | | | | 8.8 |
|
||||
| Oct 2021 | | 2.34.0 | 3.10.0 | | **22.04 Jammy** |
|
||||
| Jan 2022 | | 2.35.0 |
|
||||
| Feb 2022 | | | | 8.9 | **22.04 Jammy** |
|
||||
| Apr 2022 | | 2.36.0 | | 9.0 |
|
||||
| Apr 2022 | **Apr 2032** | | | | **22.04 Jammy** | 2.34.1 | 2.7.18 3.10.6 | 8.9 |
|
||||
| Jun 2022 | | 2.37.0 |
|
||||
| Oct 2022 | | 2.38.0 | | 9.1 |
|
||||
| Oct 2022 | | | 3.11.0 | | **Bookworm** |
|
||||
| Dec 2022 | | 2.39.0 | | | **Bookworm** |
|
||||
| Feb 2023 | | | | 9.2 | **Bookworm** |
|
||||
| Mar 2023 | | 2.40.0 | | 9.3 |
|
||||
| Jun 2023 | | 2.41.0 |
|
||||
| Jun 2023 | **Jun 2028** | | | | **Debian 12 Bookworm** | 2.39.2 | 3.11.2 | 9.2 |
|
||||
| Aug 2023 | | 2.42.0 | | 9.4 |
|
||||
| Oct 2023 | | | 3.12.0 | 9.5 |
|
||||
| Nov 2022 | | 2.43.0 |
|
||||
| Dec 2023 | | | | 9.6 |
|
||||
| Feb 2024 | | 2.44.0 |
|
||||
| Mar 2024 | | | | 9.7 |
|
||||
| Oct 2024 | | | 3.13.0 |
|
||||
| **Date** | **EOL** | **[Git][rel-g]** | **[Python][rel-p]** | **[SSH][rel-o]** | **[Ubuntu][rel-u] / [Debian][rel-d]** | **Git** | **Python** | **SSH** |
|
||||
|
||||
|
||||
[contact]: ../README.md#contact
|
||||
[rel-d]: https://en.wikipedia.org/wiki/Debian_version_history
|
||||
[rel-g]: https://en.wikipedia.org/wiki/Git#Releases
|
||||
[rel-o]: https://www.openssh.com/releasenotes.html
|
||||
[rel-p]: https://en.wikipedia.org/wiki/History_of_Python#Table_of_versions
|
||||
[rel-u]: https://wiki.ubuntu.com/Releases
|
||||
[example announcement]: https://groups.google.com/d/topic/repo-discuss/UGBNismWo1M/discussion
|
||||
[repo-discuss@googlegroups.com]: https://groups.google.com/forum/#!forum/repo-discuss
|
||||
[go/repo-release]: https://goto.google.com/repo-release
|
||||
[Gerrit REST API]: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#branch-endpoints
|
||||
@@ -91,8 +91,9 @@ Detecting collisions with untracked files or unstaged working-tree modifications
|
||||
After the command exits with status 0, `repo` expects the following
|
||||
postconditions to be met:
|
||||
|
||||
1. `git diff-index --quiet --cached REPO_TREV^{tree}` exits 0 (the index
|
||||
matches the target tree).
|
||||
1. The index matches `REPO_TREV`'s tree (`repo` trusts the command's exit
|
||||
status and does not run `git diff-index` against `REPO_TREV^{tree}`, which
|
||||
would read every tree object from `.git/objects`).
|
||||
2. `HEAD` still names what it did before the command, and its resolved commit
|
||||
object ID has not changed.
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -89,8 +90,6 @@ least one of these before using this command.""", # noqa: E501
|
||||
|
||||
if platform_utils.isWindows():
|
||||
# Split on spaces, respecting quoted strings
|
||||
import shlex
|
||||
|
||||
args = shlex.split(editor)
|
||||
shell = False
|
||||
elif re.compile("^.*[$ \t'].*$").match(editor):
|
||||
|
||||
+11
-3
@@ -47,10 +47,15 @@ class EventLog:
|
||||
Specific tasks may include additional informational properties.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initializes the event log."""
|
||||
def __init__(self, enabled: bool = True) -> None:
|
||||
"""Initializes the event log.
|
||||
|
||||
Args:
|
||||
enabled: Whether event logging is enabled.
|
||||
"""
|
||||
self._log = []
|
||||
self._parent = None
|
||||
self.enabled = enabled
|
||||
|
||||
def Add(
|
||||
self,
|
||||
@@ -74,8 +79,11 @@ class EventLog:
|
||||
kind: The kind of the object for the unique identifier.
|
||||
|
||||
Returns:
|
||||
A dictionary of the event added to the log.
|
||||
A dictionary of the event added to the log, or None if disabled.
|
||||
"""
|
||||
if not self.enabled:
|
||||
return None
|
||||
|
||||
event = {
|
||||
"id": (kind, _NextEventId()),
|
||||
"name": name,
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import urlopen
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
import error
|
||||
|
||||
@@ -32,7 +32,7 @@ def fetch_file(url, verbose=False):
|
||||
Returns:
|
||||
The contents of the file as bytes.
|
||||
"""
|
||||
scheme = urlparse(url).scheme
|
||||
scheme = urllib.parse.urlparse(url).scheme
|
||||
if scheme == "gs":
|
||||
cmd = ["gcloud", "storage", "cat", url]
|
||||
errors = []
|
||||
@@ -54,5 +54,5 @@ def fetch_file(url, verbose=False):
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise FetchFileError(aggregate_errors=errors)
|
||||
with urlopen(url) as f:
|
||||
with urllib.request.urlopen(url) as f:
|
||||
return f.read()
|
||||
+4
-4
@@ -21,11 +21,11 @@ import sys
|
||||
from typing import Any, Optional
|
||||
|
||||
import error
|
||||
from git_trace2_event_log_base import BaseEventLog
|
||||
import git_trace2_event_log_base
|
||||
import platform_utils
|
||||
import repo_logging
|
||||
import repo_trace
|
||||
from wrapper import Wrapper
|
||||
import wrapper
|
||||
|
||||
|
||||
GIT = "git"
|
||||
@@ -46,7 +46,7 @@ logger = repo_logging.RepoLogger(__file__)
|
||||
class _GitCall:
|
||||
@functools.lru_cache(maxsize=None) # noqa: B019
|
||||
def version_tuple(self):
|
||||
ret = Wrapper().ParseGitVersion()
|
||||
ret = wrapper.Wrapper().ParseGitVersion()
|
||||
if ret is None:
|
||||
msg = "fatal: unable to detect git version"
|
||||
logger.error(msg)
|
||||
@@ -347,7 +347,7 @@ class GitCommand:
|
||||
command.extend(cmdv[1:])
|
||||
|
||||
event_log = (
|
||||
BaseEventLog(env=env, add_init_count=True)
|
||||
git_trace2_event_log_base.BaseEventLog(env=env, add_init_count=True)
|
||||
if add_event_log
|
||||
else None
|
||||
)
|
||||
|
||||
+169
-32
@@ -15,6 +15,7 @@
|
||||
import contextlib
|
||||
import datetime
|
||||
import errno
|
||||
import hashlib
|
||||
import http.client
|
||||
import json
|
||||
import os
|
||||
@@ -22,7 +23,8 @@ import re
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Union
|
||||
import tempfile
|
||||
from typing import Optional, Tuple, Union
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
@@ -41,6 +43,9 @@ ID_RE = re.compile(r"^(?:[0-9a-f]{40}|[0-9a-f]{64})$")
|
||||
|
||||
REVIEW_CACHE = {}
|
||||
|
||||
# Metadata key stored in .repo_config.json to detect same-tick config edits.
|
||||
_JSON_META_KEY = "__repo_meta__"
|
||||
|
||||
|
||||
def IsChange(rev):
|
||||
return rev.startswith(git_refs.R_CHANGES)
|
||||
@@ -161,6 +166,7 @@ class GitConfig:
|
||||
self.file = str(configfile)
|
||||
self.defaults = defaults
|
||||
self._cache_dict = None
|
||||
self._cache_fingerprint = None
|
||||
self._section_dict = None
|
||||
self._remotes = {}
|
||||
self._branches = {}
|
||||
@@ -175,6 +181,20 @@ class GitConfig:
|
||||
def ClearCache(self):
|
||||
"""Clear the in-memory cache of config."""
|
||||
self._cache_dict = None
|
||||
self._cache_fingerprint = None
|
||||
self._section_dict = None
|
||||
self._remotes = {}
|
||||
self._branches = {}
|
||||
|
||||
def _RemoveJson(self) -> None:
|
||||
with contextlib.suppress(OSError):
|
||||
platform_utils.remove(self._json, missing_ok=True)
|
||||
|
||||
def Reload(self) -> None:
|
||||
"""Clear cached state and reload configuration from disk."""
|
||||
self.ClearCache()
|
||||
self._RemoveJson()
|
||||
_ = self._cache
|
||||
|
||||
def Has(self, name, include_defaults=True):
|
||||
"""Return true if this configuration file has the key."""
|
||||
@@ -292,6 +312,13 @@ class GitConfig:
|
||||
"""
|
||||
key = _key(name)
|
||||
|
||||
if (
|
||||
self._cache_dict is not None
|
||||
and self._ReadFileFingerprint() != self._cache_fingerprint
|
||||
):
|
||||
self.ClearCache()
|
||||
self._RemoveJson()
|
||||
|
||||
try:
|
||||
old = self._cache[key]
|
||||
except KeyError:
|
||||
@@ -306,8 +333,9 @@ class GitConfig:
|
||||
|
||||
if value is None:
|
||||
if old:
|
||||
self._do("--unset-all", name, allow_missing=True)
|
||||
del self._cache[key]
|
||||
self._do("--unset-all", name)
|
||||
self._SyncCacheAfterSet(key)
|
||||
|
||||
elif isinstance(value, list):
|
||||
if len(value) == 0:
|
||||
@@ -317,14 +345,48 @@ class GitConfig:
|
||||
self.SetString(name, value[0])
|
||||
|
||||
elif old != value:
|
||||
self._cache[key] = list(value)
|
||||
self._do("--replace-all", name, value[0])
|
||||
for i in range(1, len(value)):
|
||||
self._do("--add", name, value[i])
|
||||
try:
|
||||
for i in range(1, len(value)):
|
||||
self._do("--add", name, value[i])
|
||||
except BaseException:
|
||||
self.ClearCache()
|
||||
self._RemoveJson()
|
||||
raise
|
||||
self._cache[key] = list(value)
|
||||
self._SyncCacheAfterSet(key)
|
||||
|
||||
elif len(old) != 1 or old[0] != value:
|
||||
self._cache[key] = [value]
|
||||
self._do("--replace-all", name, value)
|
||||
self._cache[key] = [value]
|
||||
self._SyncCacheAfterSet(key)
|
||||
|
||||
def _SyncCacheAfterSet(self, key: str) -> None:
|
||||
"""Keep the in-memory section index and JSON cache coherent.
|
||||
|
||||
Args:
|
||||
key: Canonicalized configuration key that was modified.
|
||||
"""
|
||||
self._section_dict = None
|
||||
if key.startswith("remote."):
|
||||
self._remotes = {}
|
||||
self._branches = {}
|
||||
elif key.startswith("branch."):
|
||||
self._branches = {}
|
||||
|
||||
has_includes = self._cache_dict is not None and (
|
||||
"include.path" in self._cache_dict
|
||||
or any(k.startswith("includeif.") for k in self._cache_dict)
|
||||
)
|
||||
if (
|
||||
key == "include.path"
|
||||
or key.startswith("includeif.")
|
||||
or has_includes
|
||||
):
|
||||
self.ClearCache()
|
||||
self._RemoveJson()
|
||||
else:
|
||||
self._SaveJson(self._cache)
|
||||
|
||||
def GetRemote(self, name):
|
||||
"""Get the remote.$name.* configuration values as an object."""
|
||||
@@ -417,27 +479,109 @@ class GitConfig:
|
||||
self._SaveJson(d)
|
||||
return d
|
||||
|
||||
def _ReadFileFingerprint(self) -> Optional[Tuple[int, int, int, int, str]]:
|
||||
"""Return (mtime_ns, ctime_ns, size, ino, sha1_hex) for self.file."""
|
||||
try:
|
||||
with open(self.file, "rb") as fd:
|
||||
raw_cfg = fd.read()
|
||||
st = os.fstat(fd.fileno())
|
||||
except OSError:
|
||||
return None
|
||||
return (
|
||||
st.st_mtime_ns,
|
||||
st.st_ctime_ns,
|
||||
st.st_size,
|
||||
st.st_ino,
|
||||
hashlib.sha1(raw_cfg).hexdigest(),
|
||||
)
|
||||
|
||||
def _ReadJson(self):
|
||||
try:
|
||||
if os.path.getmtime(self._json) <= os.path.getmtime(self.file):
|
||||
platform_utils.remove(self._json)
|
||||
return None
|
||||
cfg_stat = os.stat(self.file)
|
||||
json_stat = os.stat(self._json)
|
||||
except OSError:
|
||||
return None
|
||||
if json_stat.st_mtime_ns < cfg_stat.st_mtime_ns:
|
||||
self._RemoveJson()
|
||||
return None
|
||||
try:
|
||||
with repo_trace.Trace(": parsing %s", self.file):
|
||||
with open(self._json) as fd:
|
||||
return json.load(fd)
|
||||
with open(self._json, encoding="utf-8") as fd:
|
||||
data = json.load(fd)
|
||||
except (OSError, ValueError):
|
||||
platform_utils.remove(self._json, missing_ok=True)
|
||||
self._RemoveJson()
|
||||
return None
|
||||
if not isinstance(data, dict):
|
||||
self._RemoveJson()
|
||||
return None
|
||||
|
||||
meta = data.pop(_JSON_META_KEY, None)
|
||||
if (
|
||||
isinstance(meta, list)
|
||||
and len(meta) == 2
|
||||
and isinstance(meta[1], str)
|
||||
):
|
||||
if (
|
||||
json_stat.st_mtime_ns == cfg_stat.st_mtime_ns
|
||||
or cfg_stat.st_size != meta[0]
|
||||
):
|
||||
if cfg_stat.st_size != meta[0]:
|
||||
self._RemoveJson()
|
||||
return None
|
||||
fp = self._ReadFileFingerprint()
|
||||
if fp is None:
|
||||
return None
|
||||
if fp[4] != meta[1]:
|
||||
self._RemoveJson()
|
||||
return None
|
||||
self._cache_fingerprint = fp
|
||||
else:
|
||||
self._cache_fingerprint = (
|
||||
cfg_stat.st_mtime_ns,
|
||||
cfg_stat.st_ctime_ns,
|
||||
cfg_stat.st_size,
|
||||
cfg_stat.st_ino,
|
||||
meta[1],
|
||||
)
|
||||
elif json_stat.st_mtime_ns <= cfg_stat.st_mtime_ns:
|
||||
self._RemoveJson()
|
||||
return None
|
||||
else:
|
||||
fp = self._ReadFileFingerprint()
|
||||
if fp is None:
|
||||
return None
|
||||
self._cache_fingerprint = fp
|
||||
|
||||
return data
|
||||
|
||||
def _SaveJson(self, cache):
|
||||
fp = self._ReadFileFingerprint()
|
||||
if fp is None:
|
||||
self._cache_fingerprint = None
|
||||
self._RemoveJson()
|
||||
return
|
||||
self._cache_fingerprint = fp
|
||||
|
||||
payload = dict(cache)
|
||||
payload[_JSON_META_KEY] = [fp[2], fp[4]]
|
||||
json_dir = os.path.dirname(self._json) or "."
|
||||
tmp_path = None
|
||||
try:
|
||||
with open(self._json, "w") as fd:
|
||||
json.dump(cache, fd, indent=2)
|
||||
with tempfile.NamedTemporaryFile(
|
||||
"w",
|
||||
dir=json_dir,
|
||||
prefix=f".tmp{os.path.basename(self._json)}.",
|
||||
delete=False,
|
||||
encoding="utf-8",
|
||||
) as fd:
|
||||
tmp_path = fd.name
|
||||
json.dump(payload, fd, indent=2)
|
||||
os.replace(tmp_path, self._json)
|
||||
except (OSError, TypeError):
|
||||
platform_utils.remove(self._json, missing_ok=True)
|
||||
if tmp_path is not None:
|
||||
with contextlib.suppress(OSError):
|
||||
platform_utils.remove(tmp_path, missing_ok=True)
|
||||
self._RemoveJson()
|
||||
|
||||
def _ReadGit(self):
|
||||
"""
|
||||
@@ -448,6 +592,7 @@ class GitConfig:
|
||||
"""
|
||||
c = {}
|
||||
if not os.path.exists(self.file):
|
||||
self._cache_fingerprint = None
|
||||
return c
|
||||
|
||||
d = self._do("--null", "--list")
|
||||
@@ -463,9 +608,10 @@ class GitConfig:
|
||||
else:
|
||||
c[key] = [val]
|
||||
|
||||
self._cache_fingerprint = self._ReadFileFingerprint()
|
||||
return c
|
||||
|
||||
def _do(self, *args):
|
||||
def _do(self, *args, allow_missing: bool = False) -> str:
|
||||
if self.file == self._SYSTEM_CONFIG:
|
||||
command = ["config", "--system", "--includes"]
|
||||
else:
|
||||
@@ -475,10 +621,10 @@ class GitConfig:
|
||||
p = git_command.GitCommand(
|
||||
None, command, capture_stdout=True, capture_stderr=True
|
||||
)
|
||||
if p.Wait() == 0:
|
||||
rc = p.Wait()
|
||||
if rc == 0 or (allow_missing and rc == 5):
|
||||
return p.stdout
|
||||
else:
|
||||
raise error.GitError(f"git {' '.join(command)}: {p.stderr}")
|
||||
raise error.GitError(f"git {' '.join(command)}: {p.stderr}")
|
||||
|
||||
|
||||
class RepoConfig(GitConfig):
|
||||
@@ -814,20 +960,11 @@ class Branch:
|
||||
|
||||
def Save(self):
|
||||
"""Save this branch back into the configuration."""
|
||||
if self._config.HasSection("branch", self.name):
|
||||
if self.remote:
|
||||
self._Set("remote", self.remote.name)
|
||||
else:
|
||||
self._Set("remote", None)
|
||||
self._Set("merge", self.merge)
|
||||
|
||||
if self.remote:
|
||||
self._Set("remote", self.remote.name)
|
||||
else:
|
||||
with open(self._config.file, "a") as fd:
|
||||
fd.write('[branch "%s"]\n' % self.name)
|
||||
if self.remote:
|
||||
fd.write("\tremote = %s\n" % self.remote.name)
|
||||
if self.merge:
|
||||
fd.write("\tmerge = %s\n" % self.merge)
|
||||
self._Set("remote", None)
|
||||
self._Set("merge", self.merge)
|
||||
|
||||
def _Set(self, key, value):
|
||||
key = f"branch.{self.name}.{key}"
|
||||
|
||||
+123
-16
@@ -27,6 +27,8 @@ R_PUB = "refs/published/"
|
||||
R_WORKTREE = "refs/worktree/"
|
||||
R_WORKTREE_M = R_WORKTREE + "m/"
|
||||
R_M = "refs/remotes/m/"
|
||||
# Maximum symbolic-ref indirection depth (matches Git SYMREF_MAXDEPTH).
|
||||
_SYMREF_MAXDEPTH = 5
|
||||
|
||||
|
||||
class GitRefs:
|
||||
@@ -76,6 +78,101 @@ class GitRefs:
|
||||
except KeyError:
|
||||
return ""
|
||||
|
||||
def UpdateSymref(self, name: str, target: str) -> None:
|
||||
"""Update a symbolic reference in an already-loaded ref snapshot.
|
||||
|
||||
Args:
|
||||
name: Full symbolic reference name (e.g. "refs/remotes/m/main").
|
||||
target: Target reference name that the symref points to.
|
||||
"""
|
||||
if self._phyref is None:
|
||||
return
|
||||
if self._HasExternalRefChanges(name):
|
||||
self._phyref = None
|
||||
return
|
||||
|
||||
self._symref[name] = target
|
||||
self._ResolveSymrefs()
|
||||
|
||||
parts = name.strip("/").split("/")
|
||||
for idx in range(1, len(parts)):
|
||||
anc = os.path.join(*parts[:idx])
|
||||
self._TrackMtime(anc)
|
||||
self._TrackMtime(os.path.join(*parts))
|
||||
|
||||
def _HasExternalRefChanges(self, updated_symref: str) -> bool:
|
||||
"""Check whether refs outside |updated_symref| changed on disk.
|
||||
|
||||
Args:
|
||||
updated_symref: Symbolic ref path just updated on disk.
|
||||
|
||||
Returns:
|
||||
True if reftable is in use or any unrelated ref path changed.
|
||||
"""
|
||||
if os.path.exists(os.path.join(self._gitdir, "reftable")):
|
||||
return True
|
||||
if "packed-refs" not in self._mtime and os.path.exists(
|
||||
os.path.join(self._gitdir, "packed-refs")
|
||||
):
|
||||
return True
|
||||
|
||||
parts = updated_symref.strip("/").split("/")
|
||||
expected_child = {
|
||||
"/".join(parts[:idx]): parts[idx] for idx in range(1, len(parts))
|
||||
}
|
||||
|
||||
for tracked, mtime in self._mtime.items():
|
||||
norm = tracked.replace(os.sep, "/")
|
||||
if norm == updated_symref:
|
||||
continue
|
||||
full_path = os.path.join(self._gitdir, tracked)
|
||||
try:
|
||||
curr_mtime = os.path.getmtime(full_path)
|
||||
except OSError:
|
||||
return True
|
||||
if norm in expected_child:
|
||||
if curr_mtime != mtime:
|
||||
try:
|
||||
children = platform_utils.listdir(full_path)
|
||||
except OSError:
|
||||
return True
|
||||
next_comp = expected_child[norm]
|
||||
for child in children:
|
||||
if child == next_comp:
|
||||
continue
|
||||
if os.path.join(tracked, child) not in self._mtime:
|
||||
return True
|
||||
elif curr_mtime != mtime:
|
||||
return True
|
||||
|
||||
for anc, next_comp in expected_child.items():
|
||||
anc_os = anc.replace("/", os.sep)
|
||||
if anc_os not in self._mtime:
|
||||
anc_full = os.path.join(self._gitdir, anc_os)
|
||||
try:
|
||||
if platform_utils.listdir(anc_full) != [next_comp]:
|
||||
return True
|
||||
except OSError:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _ResolveSymrefs(self) -> None:
|
||||
"""Resolve all symbolic references into _phyref up to max depth."""
|
||||
for name in self._symref:
|
||||
self._phyref.pop(name, None)
|
||||
|
||||
scan = self._symref
|
||||
attempts = 0
|
||||
while scan and attempts < _SYMREF_MAXDEPTH:
|
||||
scan_next = {}
|
||||
for name, dest in scan.items():
|
||||
if dest in self._phyref:
|
||||
self._phyref[name] = self._phyref[dest]
|
||||
else:
|
||||
scan_next[name] = dest
|
||||
scan = scan_next
|
||||
attempts += 1
|
||||
|
||||
def _EnsureLoaded(self):
|
||||
if self._phyref is None or self._NeedUpdate():
|
||||
self._LoadAll()
|
||||
@@ -105,23 +202,17 @@ class GitRefs:
|
||||
# --include-root-refs does not report an unborn HEAD.
|
||||
self._ReadSymbolicRef(HEAD)
|
||||
|
||||
scan = self._symref
|
||||
attempts = 0
|
||||
while scan and attempts < 5:
|
||||
scan_next = {}
|
||||
for name, dest in scan.items():
|
||||
if dest in self._phyref:
|
||||
self._phyref[name] = self._phyref[dest]
|
||||
else:
|
||||
scan_next[name] = dest
|
||||
scan = scan_next
|
||||
attempts += 1
|
||||
self._ResolveSymrefs()
|
||||
self._RefreshMtimes()
|
||||
|
||||
self._TrackMtime(HEAD)
|
||||
self._TrackMtime("config")
|
||||
self._TrackMtime("packed-refs")
|
||||
self._TrackTreeMtimes("refs")
|
||||
self._TrackTreeMtimes("reftable")
|
||||
def _RefreshMtimes(self) -> None:
|
||||
"""Record current modification times for ref storage paths."""
|
||||
self._mtime = {}
|
||||
self._TrackMtime(HEAD)
|
||||
self._TrackMtime("config")
|
||||
self._TrackMtime("packed-refs")
|
||||
self._TrackTreeMtimes("refs")
|
||||
self._TrackTreeMtimes("reftable")
|
||||
|
||||
@staticmethod
|
||||
def _IsNullRef(ref_id: str) -> bool:
|
||||
@@ -165,6 +256,22 @@ class GitRefs:
|
||||
|
||||
def _ReadSymbolicRef(self, name: str) -> None:
|
||||
"""Read a symbolic reference."""
|
||||
if name == HEAD:
|
||||
path = os.path.join(self._gitdir, HEAD)
|
||||
try:
|
||||
if not platform_utils.islink(path):
|
||||
with open(
|
||||
path, "r", encoding="utf-8", errors="replace"
|
||||
) as fd:
|
||||
line = fd.readline().strip()
|
||||
if line.startswith("ref:"):
|
||||
ref = line[4:].strip()
|
||||
if ref and ref != R_HEADS + ".invalid":
|
||||
self._symref[name] = ref
|
||||
return
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
p = git_command.GitCommand(
|
||||
None,
|
||||
["symbolic-ref", "-q", name],
|
||||
|
||||
+3
-3
@@ -14,7 +14,7 @@
|
||||
|
||||
"""Read a worktree's state from one machine-readable git status snapshot."""
|
||||
|
||||
from collections import OrderedDict
|
||||
import collections
|
||||
import os
|
||||
from typing import Iterator, List, Optional, TYPE_CHECKING
|
||||
|
||||
@@ -45,8 +45,8 @@ class StatusSnapshot:
|
||||
"""A consistent view of worktree, index, and branch state."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.index_changes = OrderedDict()
|
||||
self.worktree_changes = OrderedDict()
|
||||
self.index_changes = collections.OrderedDict()
|
||||
self.worktree_changes = collections.OrderedDict()
|
||||
self.untracked = []
|
||||
self.branch_oid = None
|
||||
self.branch_head = None
|
||||
|
||||
+5
-6
@@ -33,8 +33,7 @@ from typing import NamedTuple
|
||||
import urllib.parse
|
||||
|
||||
import git_command
|
||||
from git_config import IsId
|
||||
from git_config import RepoConfig
|
||||
import git_config
|
||||
import platform_utils
|
||||
|
||||
|
||||
@@ -324,7 +323,7 @@ class Superproject:
|
||||
# creating a local branch of the same name.
|
||||
refspec = (
|
||||
self.revision
|
||||
if IsId(self.revision)
|
||||
if git_config.IsId(self.revision)
|
||||
else f"{self.revision}:{self.revision}"
|
||||
)
|
||||
cmd.append(refspec)
|
||||
@@ -429,7 +428,7 @@ class Superproject:
|
||||
|
||||
if not self._Init():
|
||||
return SyncResult(False, should_exit)
|
||||
if IsId(self.revision) and self.commit_id:
|
||||
if git_config.IsId(self.revision) and self.commit_id:
|
||||
return SyncResult(True, False)
|
||||
if not self._Fetch():
|
||||
return SyncResult(False, should_exit)
|
||||
@@ -568,7 +567,7 @@ class Superproject:
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def _UseSuperprojectFromConfiguration():
|
||||
"""Returns the user choice of whether to use superproject."""
|
||||
user_cfg = RepoConfig.ForUser()
|
||||
user_cfg = git_config.RepoConfig.ForUser()
|
||||
time_now = int(time.time())
|
||||
|
||||
user_value = user_cfg.GetBoolean("repo.superprojectChoice")
|
||||
@@ -602,7 +601,7 @@ def _UseSuperprojectFromConfiguration():
|
||||
return user_value
|
||||
|
||||
# We don't have an unexpired choice, ask for one.
|
||||
system_cfg = RepoConfig.ForSystem()
|
||||
system_cfg = git_config.RepoConfig.ForSystem()
|
||||
system_value = system_cfg.GetBoolean("repo.superprojectChoice")
|
||||
if system_value:
|
||||
# The system configuration is proposing that we should enable the
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
"""Event logging in the git trace2 EVENT format."""
|
||||
|
||||
import git_command
|
||||
from git_trace2_event_log_base import BaseEventLog
|
||||
import git_trace2_event_log_base
|
||||
|
||||
|
||||
class EventLog(BaseEventLog):
|
||||
class EventLog(git_trace2_event_log_base.BaseEventLog):
|
||||
"""Event log that records events that occurred during a repo invocation.
|
||||
|
||||
Events are written to the log as a consecutive JSON entries, one per line.
|
||||
|
||||
@@ -41,23 +41,21 @@ try:
|
||||
except ImportError:
|
||||
kerberos = None
|
||||
|
||||
from color import SetDefaultColoring
|
||||
import color
|
||||
from command import InteractiveCommand
|
||||
from command import MirrorSafeCommand
|
||||
from editor import Editor
|
||||
import editor
|
||||
import error
|
||||
import event_log
|
||||
import git_command
|
||||
from git_config import RepoConfig
|
||||
from git_trace2_event_log import EventLog
|
||||
from manifest_xml import RepoClient
|
||||
from pager import RunPager
|
||||
from pager import TerminatePager
|
||||
import git_config
|
||||
import git_trace2_event_log
|
||||
import manifest_xml
|
||||
import pager
|
||||
import repo_trace
|
||||
from subcmds import all_commands
|
||||
from subcmds.version import Version
|
||||
from wrapper import Wrapper
|
||||
from wrapper import WrapperPath
|
||||
import wrapper
|
||||
|
||||
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
@@ -195,7 +193,7 @@ class _Repo:
|
||||
help_commands = "".join(f"\n {x}" for x in wrapped_commands)
|
||||
print(f"Available commands:{help_commands}")
|
||||
print("\nRun `repo help <command>` for command-specific details.")
|
||||
print("Bug reports:", Wrapper().BUG_URL)
|
||||
print("Bug reports:", wrapper.Wrapper().BUG_URL)
|
||||
else:
|
||||
cmd = self.commands["help"]()
|
||||
if all_commands:
|
||||
@@ -230,9 +228,9 @@ class _Repo:
|
||||
return name, []
|
||||
|
||||
key = f"alias.{name}"
|
||||
alias = RepoConfig.ForRepository(self.repodir).GetString(key)
|
||||
alias = git_config.RepoConfig.ForRepository(self.repodir).GetString(key)
|
||||
if alias is None:
|
||||
alias = RepoConfig.ForUser().GetString(key)
|
||||
alias = git_config.RepoConfig.ForUser().GetString(key)
|
||||
if alias is None:
|
||||
return name, []
|
||||
|
||||
@@ -263,18 +261,18 @@ class _Repo:
|
||||
self._PrintHelp(short=True)
|
||||
return 1
|
||||
|
||||
git_trace2_event_log = EventLog()
|
||||
run = (
|
||||
lambda: self._RunLong(name, gopts, argv, git_trace2_event_log) or 0
|
||||
)
|
||||
git_event_log = git_trace2_event_log.EventLog()
|
||||
run = lambda: self._RunLong(name, gopts, argv, git_event_log) or 0
|
||||
with repo_trace.Trace(
|
||||
"starting new command: %s [sid=%s]",
|
||||
", ".join([name] + argv),
|
||||
git_trace2_event_log.full_sid,
|
||||
git_event_log.full_sid,
|
||||
first_trace=True,
|
||||
):
|
||||
if gopts.trace_python:
|
||||
import trace
|
||||
# This is a debug module that is not otherwise used, so don't
|
||||
# import it all the time.
|
||||
import trace # noqa: IMR200
|
||||
|
||||
tracer = trace.Trace(
|
||||
count=False,
|
||||
@@ -288,7 +286,7 @@ class _Repo:
|
||||
return result
|
||||
|
||||
def _autocorrect_command_name(
|
||||
self, name: str, config: RepoConfig
|
||||
self, name: str, config: git_config.RepoConfig
|
||||
) -> Optional[str]:
|
||||
"""Autocorrect command name based on user's git config."""
|
||||
|
||||
@@ -386,12 +384,12 @@ class _Repo:
|
||||
def _RunLong(self, name, gopts, argv, git_trace2_event_log):
|
||||
"""Execute the (longer running) requested subcommand."""
|
||||
result = 0
|
||||
SetDefaultColoring(gopts.color)
|
||||
color.SetDefaultColoring(gopts.color)
|
||||
|
||||
outer_client = RepoClient(self.repodir)
|
||||
outer_client = manifest_xml.RepoClient(self.repodir)
|
||||
repo_client = outer_client
|
||||
if gopts.submanifest_path:
|
||||
repo_client = RepoClient(
|
||||
repo_client = manifest_xml.RepoClient(
|
||||
self.repodir,
|
||||
submanifest_path=gopts.submanifest_path,
|
||||
outer_client=outer_client,
|
||||
@@ -413,8 +411,9 @@ class _Repo:
|
||||
outer_manifest=outer_client.manifest,
|
||||
git_event_log=git_trace2_event_log,
|
||||
)
|
||||
cmd.event_log.enabled = bool(gopts.event_log)
|
||||
|
||||
Editor.globalConfig = cmd.client.globalConfig
|
||||
editor.Editor.globalConfig = cmd.client.globalConfig
|
||||
|
||||
if not isinstance(cmd, MirrorSafeCommand) and cmd.manifest.IsMirror:
|
||||
logger.error("fatal: '%s' requires a working directory", name)
|
||||
@@ -442,7 +441,7 @@ class _Repo:
|
||||
if use_pager is None:
|
||||
use_pager = cmd.WantPager(copts)
|
||||
if use_pager:
|
||||
RunPager(config)
|
||||
pager.RunPager(config)
|
||||
|
||||
start = time.time()
|
||||
cmd_event = cmd.event_log.Add(name, event_log.TASK_COMMAND, start)
|
||||
@@ -588,9 +587,10 @@ class _Repo:
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
cmd.event_log.FinishEvent(
|
||||
cmd_event, finish, result is None or result == 0
|
||||
)
|
||||
if cmd_event is not None:
|
||||
cmd.event_log.FinishEvent(
|
||||
cmd_event, finish, result is None or result == 0
|
||||
)
|
||||
git_trace2_event_log.DefParamRepoEvents(
|
||||
cmd.manifest.manifestProject.config.DumpConfigDict()
|
||||
)
|
||||
@@ -627,7 +627,7 @@ def _CheckWrapperVersion(ver_str, repo_path):
|
||||
sys.exit(1)
|
||||
|
||||
# Pull out the version of the repo launcher we know about to compare.
|
||||
exp = Wrapper().VERSION
|
||||
exp = wrapper.Wrapper().VERSION
|
||||
ver = tuple(map(int, ver_str.split(".")))
|
||||
|
||||
exp_str = ".".join(map(str, exp))
|
||||
@@ -645,7 +645,7 @@ repo: error:
|
||||
ver_str,
|
||||
min_str,
|
||||
exp_str,
|
||||
WrapperPath(),
|
||||
wrapper.WrapperPath(),
|
||||
repo_path,
|
||||
)
|
||||
sys.exit(1)
|
||||
@@ -660,7 +660,7 @@ repo: error:
|
||||
... You should upgrade soon:
|
||||
cp %s %s
|
||||
""",
|
||||
WrapperPath(),
|
||||
wrapper.WrapperPath(),
|
||||
repo_path,
|
||||
)
|
||||
else:
|
||||
@@ -671,7 +671,7 @@ repo: error:
|
||||
!!! The launcher is not writable. Please talk to your sysadmin or distro
|
||||
!!! to get an update installed.
|
||||
""",
|
||||
WrapperPath(),
|
||||
wrapper.WrapperPath(),
|
||||
repo_path,
|
||||
)
|
||||
|
||||
@@ -955,7 +955,7 @@ def _Main(argv):
|
||||
print("fatal: %s" % e, file=sys.stderr)
|
||||
result = 128
|
||||
|
||||
TerminatePager()
|
||||
pager.TerminatePager()
|
||||
sys.exit(result)
|
||||
|
||||
|
||||
|
||||
+68
-7
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "August 2026" "repo manifest" "Repo Manual"
|
||||
.TH REPO "1" "October 2026" "repo manifest" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo manifest - manual page for repo manifest
|
||||
.SH SYNOPSIS
|
||||
@@ -149,7 +149,8 @@ include*)>
|
||||
<!ELEMENT project (annotation*,
|
||||
project*,
|
||||
copyfile*,
|
||||
linkfile*)>
|
||||
linkfile*,
|
||||
sparse\-path*)>
|
||||
.TP
|
||||
<!ATTLIST project name
|
||||
CDATA #REQUIRED>
|
||||
@@ -201,10 +202,14 @@ CDATA #IMPLIED>
|
||||
<!ELEMENT linkfile EMPTY>
|
||||
<!ATTLIST linkfile src CDATA #REQUIRED>
|
||||
<!ATTLIST linkfile dest CDATA #REQUIRED>
|
||||
.IP
|
||||
<!ELEMENT sparse\-path EMPTY>
|
||||
<!ATTLIST sparse\-path path CDATA #REQUIRED>
|
||||
.TP
|
||||
<!ELEMENT extend\-project (annotation*,
|
||||
copyfile*,
|
||||
linkfile*)>
|
||||
linkfile*,
|
||||
sparse\-path*)>
|
||||
.TP
|
||||
<!ATTLIST extend\-project name
|
||||
CDATA #REQUIRED>
|
||||
@@ -514,10 +519,11 @@ of an existing project without completely replacing the existing project
|
||||
definition. This makes the local manifest more robust against changes to the
|
||||
original manifest.
|
||||
.PP
|
||||
The `extend\-project` element can also contain `annotation`, `copyfile`, and
|
||||
`linkfile` child elements. These are added to the project's definition. A
|
||||
`copyfile` or `linkfile` with a `dest` that already exists in the project will
|
||||
overwrite the original.
|
||||
The `extend\-project` element can also contain `annotation`, `copyfile`,
|
||||
`linkfile`, and `sparse\-path` child elements. These are added to the project's
|
||||
definition. A `copyfile` or `linkfile` with a `dest` that already exists in the
|
||||
project will overwrite the original. There is no way to remove a `sparse\-path`
|
||||
that the original project definition declared.
|
||||
.PP
|
||||
Attribute `path`: If specified, limit the change to projects checked out at the
|
||||
specified path, rather than all projects with the given name.
|
||||
@@ -561,6 +567,61 @@ insensitive values "true" (default) or "false". This attribute determines
|
||||
whether or not the annotation will be kept when exported with the manifest
|
||||
subcommand.
|
||||
.PP
|
||||
Element sparse\-path
|
||||
.PP
|
||||
Zero or more sparse\-path elements may be specified as children of a project
|
||||
element or an extend\-project element. Each element specifies a path to include
|
||||
in the working directory.
|
||||
.PP
|
||||
Requires Git 2.25.0 or later, which is when `git sparse\-checkout` and cone mode
|
||||
were introduced. Declaring a sparse\-path with an older Git fails the sync for
|
||||
that project.
|
||||
.PP
|
||||
Declaring at least one sparse\-path is what enables Git sparse\-checkout for the
|
||||
project: the listed paths are checked out, and other directories are left out of
|
||||
the working directory. Cone mode also checks out the files sitting directly in
|
||||
the project's top\-level directory and in each leading directory of a
|
||||
sparse\-path, so `src/backend` brings along the top\-level files and the files
|
||||
directly under `src/` too. A project with no sparse\-path elements is checked out
|
||||
in full. The checkout uses [cone
|
||||
mode](https://git\-scm.com/docs/git\-sparse\-checkout#_internalscone_mode_handling)
|
||||
for better performance, which means only directory paths are supported;
|
||||
individual files and globs are not. Cone mode does no pattern matching, so `*`,
|
||||
`?`, `[`, `]`, and `\e` are not glob syntax in a `path`. In fact, Git 2.36.0 and
|
||||
later reject them with `specify directories rather than patterns`, which
|
||||
surfaces as a sync failure for the project. Earlier versions starting with
|
||||
2.25.0 have no such check and are treated as literals.
|
||||
.PP
|
||||
Sparse\-checkout is configured only when the project's working directory is first
|
||||
created. Changes to `sparse\-path` in the manifest (added, changed, or removed)
|
||||
are not applied to a project that is already checked out. To change the paths in
|
||||
an existing checkout, run `git sparse\-checkout` in the project directly, or
|
||||
remove the project's working directory and sync again.
|
||||
.PP
|
||||
Because sparse\-checkout only prunes the working directory, a project with at
|
||||
least one sparse\-path is fetched with `\-\-filter=blob:none` so that blobs which
|
||||
will never be checked out are not downloaded. Without the filter Git still
|
||||
downloads every blob in the project and sparse\-checkout merely declines to write
|
||||
most of them to disk, so the filter is what turns sparse\-checkout into a saving
|
||||
in transfer size and server load. The filter from `repo init \fB\-\-partial\-clone\fR
|
||||
\fB\-\-clone\-filter\fR` takes precedence, and projects listed in `repo init
|
||||
\fB\-\-partial\-clone\-exclude\fR` are fetched in full as usual. Like sparse\-checkout
|
||||
itself, the implied filter only applies when the project is first fetched (or is
|
||||
already a partial clone), so adding a sparse\-path to an existing full checkout
|
||||
does not convert it to a partial clone. Mirrors are never filtered.
|
||||
.PP
|
||||
Because that filter is only useful if the objects it excludes are not fetched by
|
||||
some other route, a project that takes the implied `\-\-filter=blob:none` also
|
||||
skips the `/clone.bundle` download: the bundle is an unfiltered pack of every
|
||||
object, so fetching it would undo the filter. Projects that set the filter
|
||||
explicitly, or that are listed in `repo init \fB\-\-partial\-clone\-exclude\fR`, keep
|
||||
using the clone bundle.
|
||||
.PP
|
||||
Attribute `path`: A path relative to the project root to include in the
|
||||
sparse\-checkout. The path is validated like `<copyfile>` and `<linkfile>` paths:
|
||||
it must stay inside the project, so absolute paths, `.` or `..` components, `~`,
|
||||
and `.git` or `.repo*` components are all rejected. A trailing `/` is allowed.
|
||||
.PP
|
||||
Element copyfile
|
||||
.PP
|
||||
Zero or more copyfile elements may be specified as children of a project
|
||||
|
||||
+4
-1
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "August 2026" "repo upload" "Repo Manual"
|
||||
.TH REPO "1" "October 2026" "repo upload" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo upload - manual page for repo upload
|
||||
.SH SYNOPSIS
|
||||
@@ -80,6 +80,9 @@ ignore untracked files in the working copy
|
||||
\fB\-\-no\-ignore\-untracked\-files\fR
|
||||
always ask about untracked files in the working copy
|
||||
.TP
|
||||
\fB\-\-ignore\-remote\-commits\fR
|
||||
ignore commits already in remote branches
|
||||
.TP
|
||||
\fB\-\-no\-cert\-checks\fR
|
||||
disable verifying ssl certs (unsafe)
|
||||
.SS Logging options:
|
||||
|
||||
+40
-7
@@ -22,16 +22,16 @@ import urllib.parse
|
||||
import xml.dom.minidom
|
||||
|
||||
import error
|
||||
from git_config import GitConfig
|
||||
import git_config
|
||||
import git_refs
|
||||
from git_superproject import Superproject
|
||||
import git_superproject
|
||||
import platform_utils
|
||||
from project import Annotation
|
||||
from project import ManifestProject
|
||||
from project import Project
|
||||
from project import RemoteSpec
|
||||
from project import RepoProject
|
||||
from wrapper import Wrapper
|
||||
import wrapper
|
||||
|
||||
|
||||
MANIFEST_FILE_NAME = "manifest.xml"
|
||||
@@ -414,7 +414,7 @@ class XmlManifest:
|
||||
default_groups: a string, the default manifest groups to use.
|
||||
"""
|
||||
# TODO(vapier): Move this out of this class.
|
||||
self.globalConfig = GitConfig.ForUser()
|
||||
self.globalConfig = git_config.GitConfig.ForUser()
|
||||
|
||||
self.repodir = os.path.abspath(repodir)
|
||||
self._CheckLocalPath(submanifest_path)
|
||||
@@ -754,6 +754,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
le.setAttribute("dest", lf.dest)
|
||||
e.appendChild(le)
|
||||
|
||||
for sp in p.sparse_paths:
|
||||
se = doc.createElement("sparse-path")
|
||||
se.setAttribute("path", sp)
|
||||
e.appendChild(se)
|
||||
|
||||
groups = p.groups - {"all", f"name:{p.name}", f"path:{p.relpath}"}
|
||||
if groups:
|
||||
e.setAttribute("groups", ",".join(sorted(groups)))
|
||||
@@ -815,7 +820,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
e.setAttribute("revision", self._superproject.revision)
|
||||
root.appendChild(e)
|
||||
|
||||
if self._contactinfo.bugurl != Wrapper().BUG_URL:
|
||||
if self._contactinfo.bugurl != wrapper.Wrapper().BUG_URL:
|
||||
root.appendChild(doc.createTextNode(""))
|
||||
e = doc.createElement("contactinfo")
|
||||
e.setAttribute("bugurl", self._contactinfo.bugurl)
|
||||
@@ -846,6 +851,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
"annotation",
|
||||
"copyfile",
|
||||
"linkfile",
|
||||
"sparse-path",
|
||||
}
|
||||
|
||||
doc = self.ToXml(**kwargs)
|
||||
@@ -1176,7 +1182,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
self._submanifests = {}
|
||||
self._repo_hooks_project = None
|
||||
self._superproject = None
|
||||
self._contactinfo = ContactInfo(Wrapper().BUG_URL)
|
||||
self._contactinfo = ContactInfo(wrapper.Wrapper().BUG_URL)
|
||||
self._notice = None
|
||||
self.branch = None
|
||||
self._manifest_server = None
|
||||
@@ -1569,6 +1575,8 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
self._ParseLinkFile(p, n)
|
||||
elif n.nodeName == "annotation":
|
||||
self._ParseAnnotation(p, n)
|
||||
elif n.nodeName == "sparse-path":
|
||||
self._ParseSparsePath(p, n)
|
||||
|
||||
if node.nodeName == "repo-hooks":
|
||||
# Only one project can be the hooks project
|
||||
@@ -1608,7 +1616,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
"no revision for superproject %s within %s"
|
||||
% (name, self.manifestFile)
|
||||
)
|
||||
self._superproject = Superproject(
|
||||
self._superproject = git_superproject.Superproject(
|
||||
self,
|
||||
name=name,
|
||||
remote=remote.ToRemoteSpec(name),
|
||||
@@ -2033,6 +2041,8 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
self._ParseLinkFile(project, n)
|
||||
elif n.nodeName == "annotation":
|
||||
self._ParseAnnotation(project, n)
|
||||
elif n.nodeName == "sparse-path":
|
||||
self._ParseSparsePath(project, n)
|
||||
elif n.nodeName == "project":
|
||||
project.subprojects.append(
|
||||
self._ParseProject(n, parent=project)
|
||||
@@ -2307,6 +2317,29 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
)
|
||||
element.AddAnnotation(name, value, keep)
|
||||
|
||||
def _ParseSparsePath(
|
||||
self, project: Project, node: xml.dom.minidom.Element
|
||||
) -> None:
|
||||
"""Parses a <sparse-path> element and adds it to the project.
|
||||
|
||||
Args:
|
||||
project: The Project object to add the sparse path to.
|
||||
node: The XML node representing the <sparse-path> element.
|
||||
|
||||
Raises:
|
||||
ManifestInvalidPathError: |path| is not usable as a project
|
||||
relative path.
|
||||
"""
|
||||
path = self._reqatt(node, "path")
|
||||
# Sparse paths name directories inside the project checkout, so they
|
||||
# get the same scrutiny as <copyfile> & <linkfile> sources.
|
||||
msg = self._CheckLocalPath(path, dir_ok=True)
|
||||
if msg:
|
||||
raise error.ManifestInvalidPathError(
|
||||
f'<sparse-path> invalid "path": {path}: {msg}'
|
||||
)
|
||||
project.sparse_paths.append(path)
|
||||
|
||||
def _get_remote(self, node):
|
||||
name = node.getAttribute("remote")
|
||||
if not name:
|
||||
|
||||
+4
-6
@@ -28,6 +28,10 @@ def isWindows():
|
||||
return platform.system() == "Windows"
|
||||
|
||||
|
||||
if isWindows():
|
||||
import platform_utils_win32
|
||||
|
||||
|
||||
def symlink(source, link_name):
|
||||
"""Creates a symbolic link pointing to source named link_name.
|
||||
|
||||
@@ -35,8 +39,6 @@ def symlink(source, link_name):
|
||||
to know whether to create a "File" or a "Directory" symbolic link.
|
||||
"""
|
||||
if isWindows():
|
||||
import platform_utils_win32
|
||||
|
||||
source = _validate_winpath(source)
|
||||
link_name = _validate_winpath(link_name)
|
||||
target = os.path.join(os.path.dirname(link_name), source)
|
||||
@@ -273,8 +275,6 @@ def islink(path):
|
||||
Availability: Windows, Unix.
|
||||
"""
|
||||
if isWindows():
|
||||
import platform_utils_win32
|
||||
|
||||
return platform_utils_win32.islink(_makelongpath(path))
|
||||
else:
|
||||
return os.path.islink(path)
|
||||
@@ -289,8 +289,6 @@ def readlink(path):
|
||||
Availability: Windows, Unix.
|
||||
"""
|
||||
if isWindows():
|
||||
import platform_utils_win32
|
||||
|
||||
return platform_utils_win32.readlink(_makelongpath(path))
|
||||
else:
|
||||
return os.readlink(path)
|
||||
+532
-191
File diff suppressed because it is too large.
Load diff
+2
-2
@@ -16,7 +16,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
from color import Coloring
|
||||
import color
|
||||
import error
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class _ConfigMock:
|
||||
return self.default_values.get(x, None)
|
||||
|
||||
|
||||
class _LogColoring(Coloring):
|
||||
class _LogColoring(color.Coloring):
|
||||
"""Coloring outstream for logging."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
||||
+8
-3
@@ -19,9 +19,14 @@
|
||||
# Hard versions are when we refuse to work the tool. Users will be shown an
|
||||
# error message before we abort entirely.
|
||||
|
||||
# When deciding whether to upgrade a version requirement, check out the distro
|
||||
# lists to see who will be impacted:
|
||||
# https://gerrit.googlesource.com/git-repo/+/HEAD/docs/release-process.md#Project-References
|
||||
# When deciding whether to upgrade a version requirement, check out what
|
||||
# versions major distros (e.g. Ubuntu LTS & Debian stable) ship and when they
|
||||
# go EOL to see who will be impacted:
|
||||
# https://wiki.ubuntu.com/Releases
|
||||
# https://wiki.debian.org/DebianReleases#Versions_of_Debian_stable
|
||||
# https://repology.org/project/python/versions
|
||||
# https://repology.org/project/git/versions
|
||||
# https://repology.org/project/openssh/versions
|
||||
|
||||
{
|
||||
# The repo launcher itself. This allows us to force people to upgrade as some
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
# dependencies = [
|
||||
# 'pytest==8.3.4',
|
||||
# 'black==25.1.0',
|
||||
# 'flake8==6.0.0',
|
||||
# 'flake8-bugbear==24.12.12',
|
||||
# 'flake8-comprehensions==3.16.0',
|
||||
# 'flake8==7.4.1',
|
||||
# 'flake8-bugbear==26.9.30',
|
||||
# 'flake8-comprehensions==3.17.0',
|
||||
# 'flake8-import-restrictions==2.1',
|
||||
# 'isort==5.10.1'
|
||||
# ]
|
||||
# ///
|
||||
|
||||
Generated
+41
-64
@@ -1,101 +1,78 @@
|
||||
attrs==24.2.0 \
|
||||
--hash=sha256:37c4653e51383dc1249dba15635d59011ca11714a8e61db8cd6dc73011e4b603 \
|
||||
--hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2
|
||||
attrs==25.4.0 ; python_full_version < '3.12' \
|
||||
--hash=sha256:3e2be3ccc931d6c0cda7d37122b992d6d4b905968a437da7843cff016e2c41e8 \
|
||||
--hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373
|
||||
# via flake8-bugbear
|
||||
black==25.1.0 \
|
||||
black==25.1.0 ; python_full_version < '3.12' \
|
||||
--hash=sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717 \
|
||||
--hash=sha256:c9ed033e41998dd45d6f19bf9923e2444cb401185ccb26d1a2101a9049637c1f
|
||||
click==8.0.3 \
|
||||
--hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \
|
||||
--hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \
|
||||
--hash=sha256:b0a0492d07c8e7e4a88ee153ca6553d357a0746792d6868bfca79dc1a344bb1b
|
||||
click==8.4.1 ; python_full_version < '3.12' \
|
||||
--hash=sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2
|
||||
# via black
|
||||
colorama==0.4.6 ; sys_platform == 'win32' \
|
||||
colorama==0.4.6 ; python_full_version < '3.12' and sys_platform == 'win32' \
|
||||
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 \
|
||||
--hash=sha256:8a5a9494346d7ae2d8f599e4661738007d2df56dbbf4ccbbf7a6a23679a3a302
|
||||
# via
|
||||
# click
|
||||
# pytest
|
||||
flake8==6.0.0 \
|
||||
flake8==6.0.0 ; python_full_version < '3.12' \
|
||||
--hash=sha256:9b5bf0ebd06aff1601e38dfc4e91f9b99ba3cdf446e74ee25a9cbbe01aef81ee \
|
||||
--hash=sha256:f34b5cb8bf908ff49e4673d3dbc557b2381a14132b637d53e9d3a59816f4b6ba
|
||||
# via
|
||||
# flake8-bugbear
|
||||
# flake8-comprehensions
|
||||
flake8-bugbear==24.12.12 \
|
||||
flake8-bugbear==24.12.12 ; python_full_version < '3.12' \
|
||||
--hash=sha256:1b6967436f65ca22a42e5373aaa6f2d87966ade9aa38d4baf2a1be550767545e
|
||||
flake8-comprehensions==3.16.0 \
|
||||
flake8-comprehensions==3.16.0 ; python_full_version < '3.12' \
|
||||
--hash=sha256:7c1eadc9d22e765f39857798febe7766b4d9c519793c6c149e3e13bf99693f70
|
||||
iniconfig==1.1.1 \
|
||||
--hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \
|
||||
--hash=sha256:9d81514370059186276e9a1799e5a9752e4c060233c254d6954b53fad2ff8a5e
|
||||
flake8-import-restrictions==2.1 ; python_full_version < '3.12' \
|
||||
--hash=sha256:77522e76b82a6ccb25820b1e195825a695094ceded23a18c1b5ce026c23e1071
|
||||
iniconfig==2.3.0 ; python_full_version < '3.12' \
|
||||
--hash=sha256:e6ae3625a739878eeb4baf25f17a0b4e9d985421e45b897d1332ed7478f822d9 \
|
||||
--hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12
|
||||
# via pytest
|
||||
isort==5.10.1 \
|
||||
isort==5.10.1 ; python_full_version < '3.12' \
|
||||
--hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
|
||||
--hash=sha256:ec0da0d88470e89ade75bf1e2ea7b4349ec906f95c84aff08e4d41dea6864d25
|
||||
mccabe==0.7.0 \
|
||||
mccabe==0.7.0 ; python_full_version < '3.12' \
|
||||
--hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e \
|
||||
--hash=sha256:b85abd0a321cdd357d0be34adbf49cf01453a857a23e94d44612ec6f159cb261
|
||||
# via flake8
|
||||
mypy-extensions==0.4.3 \
|
||||
--hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \
|
||||
--hash=sha256:38cbac35a502c4f13b9d9b1d06258fa51e01da4f5828b171b4d1200d00378e7c
|
||||
mypy-extensions==1.1.0 ; python_full_version < '3.12' \
|
||||
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
|
||||
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
|
||||
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
|
||||
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
|
||||
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
|
||||
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
|
||||
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
|
||||
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
|
||||
--hash=sha256:4ec363d206d93f233330fa7f08eefe9a382dd1e326112a94244f95ec2f22f10e
|
||||
# via black
|
||||
packaging==23.0 \
|
||||
--hash=sha256:37cb5b4759d17ae6bad78ee3347f0ee1057452895fcd1bbff48fb12908ce00a1 \
|
||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2
|
||||
packaging==26.2 ; python_full_version < '3.12' \
|
||||
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e
|
||||
# via
|
||||
# black
|
||||
# pytest
|
||||
pathspec==0.9.0 \
|
||||
--hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \
|
||||
--hash=sha256:aaf90f4ad38a16bd3ebe0fa64da7f6ee01819c881120db68cc6440941a9adc08
|
||||
pathspec==1.0.4 ; python_full_version < '3.12' \
|
||||
--hash=sha256:eeaf9fc7cbc0a4492b666be276f38956baa67cd3faca745462db83d98f1cef87 \
|
||||
--hash=sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723
|
||||
# via black
|
||||
platformdirs==2.5.2 \
|
||||
--hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \
|
||||
--hash=sha256:e257a04b5d0528cb9836b766c8a16a85368e249cba7ae42888a403976ccd7d2d
|
||||
platformdirs==4.9.2 ; python_full_version < '3.12' \
|
||||
--hash=sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd \
|
||||
--hash=sha256:ef245e78eb7a2ba57599bdb39c4ff323cfae3de356a47207b58bb29e7f479f46
|
||||
# via black
|
||||
pluggy==1.5.0 \
|
||||
--hash=sha256:40da5bf96b15e359e280fd760a80c11d7a8ea20c3c296c3f3e09f8be67cec10b \
|
||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669
|
||||
pluggy==1.6.0 ; python_full_version < '3.12' \
|
||||
--hash=sha256:88349c39e440567519b15b27a781bcd3430de6bae17c44c37e758795be96dc5a \
|
||||
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
|
||||
# via pytest
|
||||
py==1.11.0 \
|
||||
--hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 \
|
||||
--hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378
|
||||
pycodestyle==2.10.0 \
|
||||
pycodestyle==2.10.0 ; python_full_version < '3.12' \
|
||||
--hash=sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610 \
|
||||
--hash=sha256:aacf8d28ecea10fad31b2d14943a42d85d007f4d2fee8f212fc05751c8460647
|
||||
# via flake8
|
||||
pyflakes==3.0.1 \
|
||||
pyflakes==3.0.1 ; python_full_version < '3.12' \
|
||||
--hash=sha256:a968b8baa43926124b692c7e76279bc5fa8c6ef9a2281e4a2a475be69f954bdf \
|
||||
--hash=sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf
|
||||
# via flake8
|
||||
pyparsing==3.0.7 \
|
||||
--hash=sha256:943c220ca64e93940b74dd070152ed9ef1402bc3b28fd1f899843bee34e83708 \
|
||||
--hash=sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484
|
||||
pytest==8.3.4 \
|
||||
pytest==8.3.4 ; python_full_version < '3.12' \
|
||||
--hash=sha256:00cba7845d2dccfe5403025a688d60ceae228b1ddaa866d2eb86b2edb6923e73 \
|
||||
--hash=sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6
|
||||
toml==0.10.1 \
|
||||
--hash=sha256:4099063d9b50219bf09bfa693ab8dcd96ce585cee9fdadce5a48a08ee23b0646 \
|
||||
--hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88
|
||||
tomli==2.0.1 \
|
||||
--hash=sha256:864f310e021d68415d6ba45b23e29808f99703b471d884267d6942eecdf8d6c5 \
|
||||
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc
|
||||
typing-extensions==4.3.0 \
|
||||
--hash=sha256:09126dab54422810a541e078c37e62024e7260a022d4f67c075a6707d9553649 \
|
||||
--hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02
|
||||
+2
-2
@@ -20,7 +20,7 @@ from command import Command
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
import error
|
||||
import git_command
|
||||
from progress import Progress
|
||||
import progress
|
||||
import repo_logging
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ It is equivalent to "git branch -D <branchname>".
|
||||
functools.partial(self._ExecuteOne, opt.all, nb),
|
||||
range(len(all_projects)),
|
||||
callback=_ProcessResults,
|
||||
output=Progress(
|
||||
output=progress.Progress(
|
||||
f"Abandon {nb}", len(all_projects), quiet=opt.quiet
|
||||
),
|
||||
chunksize=1,
|
||||
|
||||
+3
-3
@@ -15,14 +15,14 @@
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
from color import Coloring
|
||||
import color
|
||||
from command import Command
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
|
||||
|
||||
class BranchColoring(Coloring):
|
||||
class BranchColoring(color.Coloring):
|
||||
def __init__(self, config):
|
||||
Coloring.__init__(self, config, "branch")
|
||||
super().__init__(config, "branch")
|
||||
self.current = self.printer("current", fg="green")
|
||||
self.local = self.printer("local")
|
||||
self.notinproject = self.printer("notinproject", fg="red")
|
||||
|
||||
+2
-2
@@ -18,7 +18,7 @@ from typing import NamedTuple
|
||||
from command import Command
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
import error
|
||||
from progress import Progress
|
||||
import progress
|
||||
import repo_logging
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ The command is equivalent to:
|
||||
functools.partial(self._ExecuteOne, nb),
|
||||
range(len(all_projects)),
|
||||
callback=_ProcessResults,
|
||||
output=Progress(
|
||||
output=progress.Progress(
|
||||
f"Checkout {nb}", len(all_projects), quiet=opt.quiet
|
||||
),
|
||||
)
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from color import Coloring
|
||||
import color
|
||||
from command import PagedCommand
|
||||
from manifest_xml import RepoClient
|
||||
import manifest_xml
|
||||
|
||||
|
||||
class _Coloring(Coloring):
|
||||
class _Coloring(color.Coloring):
|
||||
def __init__(self, config):
|
||||
Coloring.__init__(self, config, "status")
|
||||
super().__init__(config, "status")
|
||||
|
||||
|
||||
class Diffmanifests(PagedCommand):
|
||||
@@ -238,12 +238,12 @@ synced and their revisions won't be found.
|
||||
self.printRevision
|
||||
) = self.printText
|
||||
|
||||
manifest1 = RepoClient(self.repodir)
|
||||
manifest1 = manifest_xml.RepoClient(self.repodir)
|
||||
manifest1.Override(args[0], load_local_manifests=False)
|
||||
if len(args) == 1:
|
||||
manifest2 = self.manifest
|
||||
else:
|
||||
manifest2 = RepoClient(self.repodir)
|
||||
manifest2 = manifest_xml.RepoClient(self.repodir)
|
||||
manifest2.Override(args[1], load_local_manifests=False)
|
||||
|
||||
diff = manifest1.projectsDiff(manifest2)
|
||||
|
||||
+5
-5
@@ -21,7 +21,7 @@ import signal
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from color import Coloring
|
||||
import color
|
||||
from command import Command
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
from command import MirrorSafeCommand
|
||||
@@ -37,9 +37,9 @@ _CAN_COLOR = [
|
||||
]
|
||||
|
||||
|
||||
class ForallColoring(Coloring):
|
||||
class ForallColoring(color.Coloring):
|
||||
def __init__(self, config):
|
||||
Coloring.__init__(self, config, "forall")
|
||||
super().__init__(config, "forall")
|
||||
self.project = self.printer("project", attr="bold")
|
||||
|
||||
|
||||
@@ -234,9 +234,9 @@ without iterating through the remaining projects.
|
||||
cn = None
|
||||
if cn and cn in _CAN_COLOR:
|
||||
|
||||
class ColorCmd(Coloring):
|
||||
class ColorCmd(color.Coloring):
|
||||
def __init__(self, config, cmd):
|
||||
Coloring.__init__(self, config, cmd)
|
||||
super().__init__(config, cmd)
|
||||
|
||||
if ColorCmd(self.manifest.manifestProject.config, cn).is_on:
|
||||
cmd.insert(cmd.index(cn) + 1, "--color")
|
||||
|
||||
+3
-3
@@ -18,7 +18,7 @@ from typing import List, Set
|
||||
from command import Command
|
||||
import git_command
|
||||
import platform_utils
|
||||
from progress import Progress
|
||||
import progress
|
||||
from project import Project
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ are still in the manifests.
|
||||
if ask.lower() != "y":
|
||||
return 1
|
||||
|
||||
pm = Progress(
|
||||
pm = progress.Progress(
|
||||
"Deleting",
|
||||
len(to_delete),
|
||||
delay=False,
|
||||
@@ -180,7 +180,7 @@ are still in the manifests.
|
||||
print(f"Would have repacked {len(repack_projects)} projects.")
|
||||
return 0
|
||||
|
||||
pm = Progress(
|
||||
pm = progress.Progress(
|
||||
"Repacking (this will take a while)",
|
||||
len(repack_projects),
|
||||
delay=False,
|
||||
|
||||
+3
-3
@@ -16,7 +16,7 @@ import functools
|
||||
import sys
|
||||
from typing import NamedTuple
|
||||
|
||||
from color import Coloring
|
||||
import color
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
from command import PagedCommand
|
||||
import error
|
||||
@@ -27,9 +27,9 @@ import repo_logging
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
|
||||
|
||||
class GrepColoring(Coloring):
|
||||
class GrepColoring(color.Coloring):
|
||||
def __init__(self, config):
|
||||
Coloring.__init__(self, config, "grep")
|
||||
super().__init__(config, "grep")
|
||||
self.project = self.printer("project", attr="bold")
|
||||
self.fail = self.printer("fail", fg="red")
|
||||
|
||||
|
||||
+6
-6
@@ -16,12 +16,12 @@ import re
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
from color import Coloring
|
||||
import color
|
||||
from command import MirrorSafeCommand
|
||||
from command import PagedCommand
|
||||
import error
|
||||
from subcmds import all_commands
|
||||
from wrapper import Wrapper
|
||||
import wrapper
|
||||
|
||||
|
||||
class InvalidHelpCommand(error.RepoExitError):
|
||||
@@ -65,7 +65,7 @@ Displays detailed usage information about a command.
|
||||
"See 'repo help <command>' for more information on a "
|
||||
"specific command."
|
||||
)
|
||||
print("Bug reports:", Wrapper().BUG_URL)
|
||||
print("Bug reports:", wrapper.Wrapper().BUG_URL)
|
||||
|
||||
def _PrintCommonCommands(self):
|
||||
print("usage: repo COMMAND [ARGS]")
|
||||
@@ -84,12 +84,12 @@ Displays detailed usage information about a command.
|
||||
"command.\nSee 'repo help --all' for a complete list of recognized "
|
||||
"commands."
|
||||
)
|
||||
print("Bug reports:", Wrapper().BUG_URL)
|
||||
print("Bug reports:", wrapper.Wrapper().BUG_URL)
|
||||
|
||||
def _PrintCommandHelp(self, cmd, header_prefix=""):
|
||||
class _Out(Coloring):
|
||||
class _Out(color.Coloring):
|
||||
def __init__(self, gc):
|
||||
Coloring.__init__(self, gc, "help")
|
||||
super().__init__(gc, "help")
|
||||
self.heading = self.printer("heading", attr="bold")
|
||||
self._first = True
|
||||
|
||||
|
||||
+3
-3
@@ -20,7 +20,7 @@ import optparse
|
||||
import sys
|
||||
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
|
||||
|
||||
from color import Coloring
|
||||
import color
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
from command import PagedCommand
|
||||
import git_refs
|
||||
@@ -46,9 +46,9 @@ class OutputFormat(enum.Enum):
|
||||
JSON = enum.auto()
|
||||
|
||||
|
||||
class _Coloring(Coloring):
|
||||
class _Coloring(color.Coloring):
|
||||
def __init__(self, config):
|
||||
Coloring.__init__(self, config, "status")
|
||||
super().__init__(config, "status")
|
||||
|
||||
|
||||
class Info(PagedCommand):
|
||||
|
||||
+8
-11
@@ -15,14 +15,13 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from color import Coloring
|
||||
import color
|
||||
from command import InteractiveCommand
|
||||
from command import MirrorSafeCommand
|
||||
import error
|
||||
import git_command
|
||||
import repo_logging
|
||||
from wrapper import Wrapper
|
||||
from wrapper import WrapperDir
|
||||
import wrapper
|
||||
|
||||
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
@@ -95,7 +94,7 @@ to update the working directory files.
|
||||
"""Disable due to re-use of Wrapper()."""
|
||||
|
||||
def _Options(self, p):
|
||||
Wrapper().InitParser(p)
|
||||
wrapper.Wrapper().InitParser(p)
|
||||
m = p.add_option_group("Multi-manifest")
|
||||
m.add_option(
|
||||
"--outer-manifest",
|
||||
@@ -244,9 +243,9 @@ to update the working directory files.
|
||||
if self._HasColorSet(gc):
|
||||
return
|
||||
|
||||
class _Test(Coloring):
|
||||
class _Test(color.Coloring):
|
||||
def __init__(self):
|
||||
Coloring.__init__(self, gc, "test color display")
|
||||
super().__init__(gc, "test color display")
|
||||
self._on = True
|
||||
|
||||
out = _Test()
|
||||
@@ -344,9 +343,7 @@ to update the working directory files.
|
||||
self.OptionParser.error("too many arguments to init")
|
||||
|
||||
def Execute(self, opt, args):
|
||||
wrapper = Wrapper()
|
||||
|
||||
reqs = wrapper.Requirements.from_dir(WrapperDir())
|
||||
reqs = wrapper.Wrapper().Requirements.from_dir(wrapper.WrapperDir())
|
||||
git_command.git_require(reqs.get_hard_ver("git"), fail=True)
|
||||
min_git_version_soft = reqs.get_soft_ver("git")
|
||||
if not git_command.git_require(min_git_version_soft):
|
||||
@@ -368,13 +365,13 @@ to update the working directory files.
|
||||
# Handle new --repo-rev requests.
|
||||
if opt.repo_rev:
|
||||
try:
|
||||
remote_ref, rev = wrapper.check_repo_rev(
|
||||
remote_ref, rev = wrapper.Wrapper().check_repo_rev(
|
||||
rp.worktree,
|
||||
opt.repo_rev,
|
||||
repo_verify=opt.repo_verify,
|
||||
quiet=opt.quiet,
|
||||
)
|
||||
except wrapper.CloneFailure as e:
|
||||
except wrapper.Wrapper().CloneFailure as e:
|
||||
err_msg = "fatal: double check your --repo-rev setting."
|
||||
logger.error(err_msg)
|
||||
self.git_event_log.ErrorEvent(err_msg)
|
||||
|
||||
+3
-3
@@ -14,7 +14,7 @@
|
||||
|
||||
import optparse
|
||||
|
||||
from color import Coloring
|
||||
import color
|
||||
from command import PagedCommand
|
||||
|
||||
|
||||
@@ -73,9 +73,9 @@ are displayed.
|
||||
if not all_branches:
|
||||
return
|
||||
|
||||
class Report(Coloring):
|
||||
class Report(color.Coloring):
|
||||
def __init__(self, config):
|
||||
Coloring.__init__(self, config, "status")
|
||||
super().__init__(config, "status")
|
||||
self.project = self.printer("header", attr="bold")
|
||||
self.text = self.printer("text")
|
||||
|
||||
|
||||
+3
-3
@@ -14,7 +14,7 @@
|
||||
|
||||
import itertools
|
||||
|
||||
from color import Coloring
|
||||
import color
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
from command import PagedCommand
|
||||
|
||||
@@ -56,9 +56,9 @@ class Prune(PagedCommand):
|
||||
if not all_branches:
|
||||
return
|
||||
|
||||
class Report(Coloring):
|
||||
class Report(color.Coloring):
|
||||
def __init__(self, config):
|
||||
Coloring.__init__(self, config, "status")
|
||||
super().__init__(config, "status")
|
||||
self.project = self.printer("header", attr="bold")
|
||||
|
||||
out = Report(all_branches[0].project.config)
|
||||
|
||||
+3
-3
@@ -14,7 +14,7 @@
|
||||
|
||||
import sys
|
||||
|
||||
from color import Coloring
|
||||
import color
|
||||
from command import Command
|
||||
import error
|
||||
import git_command
|
||||
@@ -25,9 +25,9 @@ import repo_logging
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
|
||||
|
||||
class RebaseColoring(Coloring):
|
||||
class RebaseColoring(color.Coloring):
|
||||
def __init__(self, config):
|
||||
Coloring.__init__(self, config, "rebase")
|
||||
super().__init__(config, "rebase")
|
||||
self.project = self.printer("project", attr="bold")
|
||||
self.fail = self.printer("fail", fg="red")
|
||||
|
||||
|
||||
+3
-3
@@ -14,7 +14,7 @@
|
||||
|
||||
import sys
|
||||
|
||||
from color import Coloring
|
||||
import color
|
||||
from command import InteractiveCommand
|
||||
import git_command
|
||||
import repo_logging
|
||||
@@ -23,9 +23,9 @@ import repo_logging
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
|
||||
|
||||
class _ProjectList(Coloring):
|
||||
class _ProjectList(color.Coloring):
|
||||
def __init__(self, gc):
|
||||
Coloring.__init__(self, gc, "interactive")
|
||||
super().__init__(gc, "interactive")
|
||||
self.prompt = self.printer("prompt", fg="blue", attr="bold")
|
||||
self.header = self.printer("header", attr="bold")
|
||||
self.help = self.printer("help", fg="red", attr="bold")
|
||||
|
||||
+4
-4
@@ -19,8 +19,8 @@ from command import Command
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
import error
|
||||
import git_command
|
||||
from git_config import IsImmutable
|
||||
from progress import Progress
|
||||
import git_config
|
||||
import progress
|
||||
import repo_logging
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ revision specified in the manifest.
|
||||
branch_merge = ""
|
||||
err = None
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
if IsImmutable(project.revisionExpr):
|
||||
if git_config.IsImmutable(project.revisionExpr):
|
||||
if project.dest_branch:
|
||||
branch_merge = project.dest_branch
|
||||
else:
|
||||
@@ -137,7 +137,7 @@ revision specified in the manifest.
|
||||
),
|
||||
range(len(all_projects)),
|
||||
callback=_ProcessResults,
|
||||
output=Progress(
|
||||
output=progress.Progress(
|
||||
f"Starting {nb}", len(all_projects), quiet=opt.quiet
|
||||
),
|
||||
chunksize=1,
|
||||
|
||||
+3
-3
@@ -17,7 +17,7 @@ import glob
|
||||
import io
|
||||
import os
|
||||
|
||||
from color import Coloring
|
||||
import color
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
from command import PagedCommand
|
||||
import platform_utils
|
||||
@@ -180,9 +180,9 @@ the following meanings:
|
||||
(head, _tail) = os.path.split(head)
|
||||
proj_dirs.add(".repo")
|
||||
|
||||
class StatusColoring(Coloring):
|
||||
class StatusColoring(color.Coloring):
|
||||
def __init__(self, config):
|
||||
Coloring.__init__(self, config, "status")
|
||||
super().__init__(config, "status")
|
||||
self.project = self.printer("header", attr="bold")
|
||||
self.untracked = self.printer("untracked", fg="red")
|
||||
|
||||
|
||||
+31
-32
@@ -60,16 +60,13 @@ from command import WORKER_BATCH_SIZE
|
||||
import error
|
||||
import event_log
|
||||
import git_command
|
||||
from git_config import GetUrlCookieFile
|
||||
from git_config import RepoConfig
|
||||
import git_config
|
||||
import git_refs
|
||||
import git_superproject
|
||||
from hooks import RepoHook
|
||||
from manifest_xml import XmlManifest
|
||||
import manifest_xml
|
||||
import platform_utils
|
||||
from progress import elapsed_str
|
||||
from progress import jobs_str
|
||||
from progress import Progress
|
||||
import progress
|
||||
from project import DeleteWorktreeError
|
||||
from project import Project
|
||||
from project import RemoteSpec
|
||||
@@ -77,7 +74,7 @@ from project import SyncBuffer
|
||||
import repo_logging
|
||||
import repo_trace
|
||||
import ssh
|
||||
from wrapper import Wrapper
|
||||
import wrapper
|
||||
|
||||
|
||||
_ONE_DAY_S = 24 * 60 * 60
|
||||
@@ -888,7 +885,7 @@ later is required to fix a server side protocol bug.
|
||||
)
|
||||
|
||||
def _GetSuperprojectRevision(
|
||||
self, opt: optparse.Values, manifest: XmlManifest
|
||||
self, opt: optparse.Values, manifest: manifest_xml.XmlManifest
|
||||
) -> Optional[str]:
|
||||
"""Returns the --superproject-revision to apply to manifest.
|
||||
|
||||
@@ -907,7 +904,7 @@ later is required to fix a server side protocol bug.
|
||||
return None
|
||||
|
||||
def _ConfigureSuperproject(
|
||||
self, opt: optparse.Values, manifest: XmlManifest
|
||||
self, opt: optparse.Values, manifest: manifest_xml.XmlManifest
|
||||
) -> bool:
|
||||
"""Configure superproject with options.
|
||||
|
||||
@@ -1143,8 +1140,8 @@ later is required to fix a server side protocol bug.
|
||||
return "..working.."
|
||||
|
||||
elapsed = time.time() - earliest_time
|
||||
jobs = jobs_str(len(items))
|
||||
return f"{jobs} | {elapsed_str(elapsed)} {earliest_proj}"
|
||||
jobs = progress.jobs_str(len(items))
|
||||
return f"{jobs} | {progress.elapsed_str(elapsed)} {earliest_proj}"
|
||||
|
||||
@classmethod
|
||||
def InitWorker(cls):
|
||||
@@ -1161,7 +1158,7 @@ later is required to fix a server side protocol bug.
|
||||
|
||||
fetched = set()
|
||||
remote_fetched = set()
|
||||
pm = Progress(
|
||||
pm = progress.Progress(
|
||||
"Fetching",
|
||||
len(projects),
|
||||
delay=False,
|
||||
@@ -1485,7 +1482,7 @@ later is required to fix a server side protocol bug.
|
||||
),
|
||||
range(len(projects)),
|
||||
callback=_ProcessResults,
|
||||
output=Progress(
|
||||
output=progress.Progress(
|
||||
"Checking out", len(all_projects), quiet=opt.quiet
|
||||
),
|
||||
# Use chunksize=1 to avoid the chance that some workers are
|
||||
@@ -1617,7 +1614,7 @@ later is required to fix a server side protocol bug.
|
||||
cls._SetPreciousObjectsState(project, opt)
|
||||
return
|
||||
|
||||
pm = Progress(
|
||||
pm = progress.Progress(
|
||||
"Garbage collecting", len(projects), delay=False, quiet=opt.quiet
|
||||
)
|
||||
pm.update(inc=0, msg="prescan")
|
||||
@@ -1748,7 +1745,9 @@ later is required to fix a server side protocol bug.
|
||||
return project.name
|
||||
return None
|
||||
|
||||
def _CheckForBloatedProjects(self, projects, opt):
|
||||
def _CheckForBloatedProjects(
|
||||
self, projects: List[Project], opt: optparse.Values
|
||||
) -> None:
|
||||
"""Check for shallow projects that are accumulating unoptimized data.
|
||||
|
||||
For projects with clone-depth="1" that are dirty (have local changes),
|
||||
@@ -1766,12 +1765,14 @@ later is required to fix a server side protocol bug.
|
||||
if not git_command.git_require((2, 23, 0)):
|
||||
return
|
||||
|
||||
# Skip projects with no worktree on disk, e.g. ones only ever synced
|
||||
# with --network-only.
|
||||
# Skip projects fetched via repo.fetchcmd (which bypasses _RemoteFetch
|
||||
# and manages object storage externally) or with no worktree on disk,
|
||||
# e.g. ones only ever synced with --network-only.
|
||||
projects = [
|
||||
p
|
||||
for p in projects
|
||||
if p.clone_depth
|
||||
and not p.UseFetchCmd
|
||||
and not p.stateless_prune_needed
|
||||
and p.worktree
|
||||
and p.Exists
|
||||
@@ -1780,7 +1781,7 @@ later is required to fix a server side protocol bug.
|
||||
if not projects:
|
||||
return
|
||||
|
||||
pm = Progress(
|
||||
pm = progress.Progress(
|
||||
"Checking for bloat", len(projects), delay=False, quiet=opt.quiet
|
||||
)
|
||||
|
||||
@@ -2153,7 +2154,7 @@ later is required to fix a server side protocol bug.
|
||||
self,
|
||||
opt: optparse.Values,
|
||||
smart_sync_manifest_path: str,
|
||||
manifest: XmlManifest,
|
||||
manifest: manifest_xml.XmlManifest,
|
||||
best_effort: bool = False,
|
||||
) -> str:
|
||||
if not manifest.manifest_server:
|
||||
@@ -2471,7 +2472,7 @@ later is required to fix a server side protocol bug.
|
||||
def _SyncToSuperprojectRev(
|
||||
self,
|
||||
opt: optparse.Values,
|
||||
manifest: XmlManifest,
|
||||
manifest: manifest_xml.XmlManifest,
|
||||
mp: Project,
|
||||
manifest_name: Optional[str],
|
||||
errors: List[Exception],
|
||||
@@ -2517,7 +2518,7 @@ later is required to fix a server side protocol bug.
|
||||
)
|
||||
|
||||
def _ResolveSmartSyncOption(
|
||||
self, opt: optparse.Values, manifest: XmlManifest
|
||||
self, opt: optparse.Values, manifest: manifest_xml.XmlManifest
|
||||
) -> bool:
|
||||
"""Resolve opt.smart_sync from CLI flags, config, and manifest.
|
||||
|
||||
@@ -2533,7 +2534,7 @@ later is required to fix a server side protocol bug.
|
||||
checkout_pref = getattr(manifest.manifestProject, "smart_sync", None)
|
||||
if not isinstance(checkout_pref, bool):
|
||||
checkout_pref = None
|
||||
user_pref = RepoConfig.ForUser().GetBoolean("sync.smartsync")
|
||||
user_pref = git_config.RepoConfig.ForUser().GetBoolean("sync.smartsync")
|
||||
default_allowed = (
|
||||
opt.smart_sync is None
|
||||
and not opt.smart_tag
|
||||
@@ -2770,7 +2771,7 @@ later is required to fix a server side protocol bug.
|
||||
self,
|
||||
opt: optparse.Values,
|
||||
args: List[str],
|
||||
manifest: XmlManifest,
|
||||
manifest: manifest_xml.XmlManifest,
|
||||
all_projects: List[Project],
|
||||
) -> None:
|
||||
"""Fail when repo.reprojectcmd is used on a manifest nesting projects.
|
||||
@@ -2807,7 +2808,7 @@ later is required to fix a server side protocol bug.
|
||||
raise e
|
||||
|
||||
def _CreateSyncProgressThread(
|
||||
self, pm: Progress, stop_event: _threading.Event
|
||||
self, pm: progress.Progress, stop_event: _threading.Event
|
||||
) -> _threading.Thread:
|
||||
"""Creates and returns a daemon thread to update a Progress object.
|
||||
|
||||
@@ -3191,7 +3192,7 @@ later is required to fix a server side protocol bug.
|
||||
errors: List[Exception],
|
||||
opt: optparse.Values,
|
||||
pool: Optional[multiprocessing.Pool],
|
||||
pm: Progress,
|
||||
pm: progress.Progress,
|
||||
results_sets: List[_InterleavedSyncResult],
|
||||
):
|
||||
"""Callback to process results from interleaved sync workers."""
|
||||
@@ -3290,7 +3291,7 @@ later is required to fix a server side protocol bug.
|
||||
err_event = multiprocessing.Event()
|
||||
finished_relpaths = set()
|
||||
project_list = list(all_projects)
|
||||
pm = Progress(
|
||||
pm = progress.Progress(
|
||||
"Syncing",
|
||||
len(project_list),
|
||||
delay=False,
|
||||
@@ -3460,9 +3461,8 @@ def _PostRepoUpgrade(manifest, quiet=False):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
wrapper = Wrapper()
|
||||
if wrapper.NeedSetupGnuPG():
|
||||
wrapper.SetupGnuPG(quiet)
|
||||
if wrapper.Wrapper().NeedSetupGnuPG():
|
||||
wrapper.Wrapper().SetupGnuPG(quiet)
|
||||
for project in manifest.projects:
|
||||
if project.Exists:
|
||||
project.PostRepoUpgrade()
|
||||
@@ -3471,12 +3471,11 @@ def _PostRepoUpgrade(manifest, quiet=False):
|
||||
def _PostRepoFetch(rp, repo_verify=True, verbose=False):
|
||||
if rp.HasChanges:
|
||||
logger.warning("info: A new version of repo is available")
|
||||
wrapper = Wrapper()
|
||||
try:
|
||||
rev = rp.bare_git.describe(rp.GetRevisionId())
|
||||
except error.GitError:
|
||||
rev = None
|
||||
_, new_rev = wrapper.check_repo_rev(
|
||||
_, new_rev = wrapper.Wrapper().check_repo_rev(
|
||||
rp.gitdir, rev, repo_verify=repo_verify
|
||||
)
|
||||
# See if we're held back due to missing signed tag.
|
||||
@@ -3652,7 +3651,7 @@ class PersistentTransport(xmlrpc.client.Transport):
|
||||
self.proxy = proxy
|
||||
|
||||
def request(self, host, handler, request_body, verbose=False):
|
||||
with GetUrlCookieFile(self.orig_host, not verbose) as (
|
||||
with git_config.GetUrlCookieFile(self.orig_host, not verbose) as (
|
||||
cookiefile,
|
||||
cookie_proxy,
|
||||
):
|
||||
|
||||
+14
-4
@@ -21,7 +21,7 @@ from typing import List
|
||||
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
from command import InteractiveCommand
|
||||
from editor import Editor
|
||||
import editor
|
||||
import error
|
||||
import git_refs
|
||||
import git_superproject
|
||||
@@ -369,6 +369,12 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
action="store_false",
|
||||
help="always ask about untracked files in the working copy",
|
||||
)
|
||||
p.add_option(
|
||||
"--ignore-remote-commits",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="ignore commits already in remote branches",
|
||||
)
|
||||
p.add_option(
|
||||
"--no-cert-checks",
|
||||
dest="validate_certs",
|
||||
@@ -488,7 +494,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
branches[project_path] = b
|
||||
script.append("")
|
||||
|
||||
script = Editor.EditString("\n".join(script)).split("\n")
|
||||
script = editor.Editor.EditString("\n".join(script)).split("\n")
|
||||
|
||||
project_re = re.compile(r"^#?\s*project\s*([^\s]+)/:$")
|
||||
branch_re = re.compile(r"^\s*branch\s*([^\s(]+)\s*\(.*")
|
||||
@@ -738,10 +744,14 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
cbr = None
|
||||
if opt.current_branch:
|
||||
cbr = project.CurrentBranch
|
||||
up_branch = project.GetUploadableBranch(cbr)
|
||||
up_branch = project.GetUploadableBranch(
|
||||
cbr, ignore_remote_commits=opt.ignore_remote_commits
|
||||
)
|
||||
avail = [up_branch] if up_branch else None
|
||||
else:
|
||||
avail = project.GetUploadableBranches(opt.branch)
|
||||
avail = project.GetUploadableBranches(
|
||||
opt.branch, ignore_remote_commits=opt.ignore_remote_commits
|
||||
)
|
||||
return (project_idx, avail, cbr)
|
||||
|
||||
def ValidateOptions(self, opt: optparse.Values, args: List[str]) -> None:
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ from command import Command
|
||||
from command import MirrorSafeCommand
|
||||
import git_command
|
||||
import git_refs
|
||||
from wrapper import Wrapper
|
||||
import wrapper
|
||||
|
||||
|
||||
class Version(Command, MirrorSafeCommand):
|
||||
@@ -81,4 +81,4 @@ class Version(Command, MirrorSafeCommand):
|
||||
print(f"OS {uname.system} {uname.release} ({uname.version})")
|
||||
processor = uname.processor if uname.processor else "unknown"
|
||||
print(f"CPU {uname.machine} ({processor})")
|
||||
print("Bug reports:", Wrapper().BUG_URL)
|
||||
print("Bug reports:", wrapper.Wrapper().BUG_URL)
|
||||
@@ -16,30 +16,30 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from editor import Editor
|
||||
import editor
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_editor() -> None:
|
||||
"""Take care of resetting Editor state across tests."""
|
||||
Editor._editor = None
|
||||
editor.Editor._editor = None
|
||||
yield
|
||||
Editor._editor = None
|
||||
editor.Editor._editor = None
|
||||
|
||||
|
||||
def test_basic() -> None:
|
||||
"""Basic checking of _GetEditor."""
|
||||
Editor._editor = ":"
|
||||
assert Editor._GetEditor() == ":"
|
||||
editor.Editor._editor = ":"
|
||||
assert editor.Editor._GetEditor() == ":"
|
||||
|
||||
|
||||
def test_no_editor() -> None:
|
||||
"""Check behavior when no editor is available."""
|
||||
Editor._editor = ":"
|
||||
assert Editor.EditString("foo") == "foo"
|
||||
editor.Editor._editor = ":"
|
||||
assert editor.Editor.EditString("foo") == "foo"
|
||||
|
||||
|
||||
def test_cat_editor() -> None:
|
||||
"""Check behavior when editor is `cat`."""
|
||||
Editor._editor = "cat"
|
||||
assert Editor.EditString("foo") == "foo"
|
||||
editor.Editor._editor = "cat"
|
||||
assert editor.Editor.EditString("foo") == "foo"
|
||||
@@ -14,12 +14,15 @@
|
||||
|
||||
"""Unittests for the git_config.py module."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
import pytest
|
||||
import utils_for_test
|
||||
|
||||
import error
|
||||
import git_config
|
||||
|
||||
|
||||
@@ -346,3 +349,214 @@ def test_get_boolean_git_spellings(
|
||||
config_file.write_text(f"[section]\n\tkey = {value}\n")
|
||||
config = git_config.GitConfig(config_file)
|
||||
assert config.GetBoolean("section.key") is expected
|
||||
|
||||
|
||||
def test_set_string_updates_json_and_section_caches(
|
||||
rw_config_file: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Test SetString keeps .repo_config.json and _sections coherent."""
|
||||
config = git_config.GitConfig(str(rw_config_file))
|
||||
assert not config.HasSection("remote", "origin")
|
||||
|
||||
config.SetString("remote.origin.url", "https://example.com/repo")
|
||||
assert config.HasSection("remote", "origin")
|
||||
|
||||
json_file = rw_config_file.parent / ".repo_config.json"
|
||||
raw = json.loads(json_file.read_text(encoding="utf-8"))
|
||||
assert git_config._JSON_META_KEY in raw
|
||||
raw.pop(git_config._JSON_META_KEY)
|
||||
assert raw == {"remote.origin.url": ["https://example.com/repo"]}
|
||||
|
||||
reader = git_config.GitConfig(str(rw_config_file))
|
||||
|
||||
def fail_read_git() -> None:
|
||||
raise AssertionError("Expected _ReadJson cache hit without git config")
|
||||
|
||||
monkeypatch.setattr(reader, "_ReadGit", fail_read_git)
|
||||
assert reader.GetString("remote.origin.url") == "https://example.com/repo"
|
||||
assert git_config._JSON_META_KEY not in reader.DumpConfigDict()
|
||||
|
||||
|
||||
def test_reload_refreshes_external_edits(rw_config_file: Path) -> None:
|
||||
"""Test Reload() discards stale JSON cache and reloads external edits."""
|
||||
config = git_config.GitConfig(str(rw_config_file))
|
||||
config.SetString("abfs.initialized", "false")
|
||||
assert config.GetBoolean("abfs.initialized") is False
|
||||
|
||||
rw_config_file.write_text(
|
||||
"[abfs]\n\tinitialized = true\n", encoding="utf-8"
|
||||
)
|
||||
config.Reload()
|
||||
|
||||
assert config.GetBoolean("abfs.initialized") is True
|
||||
json_file = rw_config_file.parent / ".repo_config.json"
|
||||
raw = json.loads(json_file.read_text(encoding="utf-8"))
|
||||
raw.pop(git_config._JSON_META_KEY, None)
|
||||
assert raw == {"abfs.initialized": ["true"]}
|
||||
|
||||
|
||||
def test_same_tick_external_edit_invalidates_json_cache(
|
||||
rw_config_file: Path,
|
||||
) -> None:
|
||||
"""External edit with identical mtime_ns and size invalidates JSON."""
|
||||
config = git_config.GitConfig(str(rw_config_file))
|
||||
config.SetString("foo.bar", "val1")
|
||||
|
||||
json_file = rw_config_file.parent / ".repo_config.json"
|
||||
json_stat = os.stat(json_file)
|
||||
|
||||
# Rewrite with identical byte length and force identical mtime_ns.
|
||||
updated = rw_config_file.read_text(encoding="utf-8").replace("val1", "val2")
|
||||
rw_config_file.write_text(updated, encoding="utf-8")
|
||||
os.utime(rw_config_file, ns=(json_stat.st_atime_ns, json_stat.st_mtime_ns))
|
||||
|
||||
reader = git_config.GitConfig(str(rw_config_file))
|
||||
assert reader.GetString("foo.bar") == "val2"
|
||||
|
||||
|
||||
def test_clear_cache_and_set_string_refresh_remotes_and_branches(
|
||||
rw_config_file: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""ClearCache, Reload, SetString, and Branch.Save keep caches coherent."""
|
||||
config = git_config.GitConfig(str(rw_config_file))
|
||||
remote = config.GetRemote("origin")
|
||||
remote.url = "https://old.example.com/repo"
|
||||
remote.ResetFetch()
|
||||
remote.Save()
|
||||
|
||||
branch = config.GetBranch("main")
|
||||
branch.remote = remote
|
||||
branch.merge = "refs/heads/main"
|
||||
branch.Save()
|
||||
|
||||
assert config.HasSection("branch", "main")
|
||||
assert config.GetString("branch.main.remote") == "origin"
|
||||
assert config.GetString("branch.main.merge") == "refs/heads/main"
|
||||
|
||||
# Subsequent SetString must not clobber branch.* keys in .repo_config.json.
|
||||
config.SetString("custom.flag", "1")
|
||||
reader = git_config.GitConfig(str(rw_config_file))
|
||||
monkeypatch.setattr(
|
||||
reader,
|
||||
"_ReadGit",
|
||||
lambda: (_ for _ in ()).throw(AssertionError("expected JSON hit")),
|
||||
)
|
||||
assert reader.GetString("branch.main.remote") == "origin"
|
||||
assert reader.GetString("branch.main.merge") == "refs/heads/main"
|
||||
|
||||
# Direct SetString on remote.* invalidates cached Remote and Branch objects.
|
||||
config.SetString(
|
||||
"remote.origin.fetch", "+refs/heads/dev:refs/remotes/origin/dev"
|
||||
)
|
||||
assert (
|
||||
config.GetRemote("origin").ToLocal("dev") == "refs/remotes/origin/dev"
|
||||
)
|
||||
assert config.GetBranch("main").remote is config.GetRemote("origin")
|
||||
|
||||
# External edit + Reload() refreshes both GetRemote() and GetBranch().
|
||||
rw_config_file.write_text(
|
||||
'[remote "origin"]\n'
|
||||
"\turl = https://new.example.com/repo\n"
|
||||
"\tfetch = +refs/heads/*:refs/remotes/origin/*\n"
|
||||
'[branch "main"]\n'
|
||||
"\tremote = origin\n"
|
||||
"\tmerge = refs/heads/next\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
config.Reload()
|
||||
assert config.GetRemote("origin").url == "https://new.example.com/repo"
|
||||
assert config.GetBranch("main").merge == "refs/heads/next"
|
||||
|
||||
|
||||
def test_set_string_with_include_invalidates_instead_of_corrupting(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""SetString with [include] avoids freezing or dropping included values."""
|
||||
inc_file = tmp_path / "extra.gitconfig"
|
||||
inc_file.write_text("[inc]\n\tkey = v1\n", encoding="utf-8")
|
||||
|
||||
cfg_file = tmp_path / "config"
|
||||
config = git_config.GitConfig(str(cfg_file))
|
||||
config.SetString("include.path", str(inc_file))
|
||||
assert config.GetString("inc.key") == "v1"
|
||||
|
||||
config.SetString("local.only", "yes")
|
||||
json_file = tmp_path / ".repo_config.json"
|
||||
assert not json_file.exists()
|
||||
|
||||
inc_file.write_text("[inc]\n\tkey = v2\n", encoding="utf-8")
|
||||
assert config.GetString("inc.key") == "v2"
|
||||
assert config.GetString("local.only") == "yes"
|
||||
|
||||
# Unsetting an included-only key does not fail with exit code 5 or hide it.
|
||||
config.SetString("inc.key", None)
|
||||
assert config.GetString("inc.key") == "v2"
|
||||
|
||||
|
||||
def test_set_string_failure_preserves_cache(
|
||||
rw_config_file: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Failed _do() must not leave mutated values in _cache."""
|
||||
config = git_config.GitConfig(str(rw_config_file))
|
||||
config.SetString("foo.bar", "good")
|
||||
|
||||
def fail_do(*_args: Any, **_kwargs: Any) -> str:
|
||||
raise error.GitError("lock contention")
|
||||
|
||||
monkeypatch.setattr(config, "_do", fail_do)
|
||||
with pytest.raises(error.GitError):
|
||||
config.SetString("foo.bar", "bad")
|
||||
assert config.GetString("foo.bar") == "good"
|
||||
|
||||
with pytest.raises(error.GitError):
|
||||
config.SetString("foo.bar", None)
|
||||
assert config.GetString("foo.bar") == "good"
|
||||
|
||||
|
||||
def test_set_string_after_external_edit_preserves_external_keys(
|
||||
rw_config_file: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Warm SetString reloads external edits instead of overwriting JSON."""
|
||||
config = git_config.GitConfig(str(rw_config_file))
|
||||
config.SetString("init.key", "v0")
|
||||
assert config._cache_dict is not None
|
||||
|
||||
# External edit adds other.key while config._cache_dict is warm in memory.
|
||||
rw_config_file.write_text(
|
||||
"[init]\n\tkey = v0\n[other]\n\tkey = ext1\n", encoding="utf-8"
|
||||
)
|
||||
config.SetString("user.name", "Alice")
|
||||
|
||||
assert config.GetString("other.key") == "ext1"
|
||||
assert config.GetString("user.name") == "Alice"
|
||||
|
||||
reader = git_config.GitConfig(str(rw_config_file))
|
||||
monkeypatch.setattr(
|
||||
reader,
|
||||
"_ReadGit",
|
||||
lambda: (_ for _ in ()).throw(AssertionError("expected JSON hit")),
|
||||
)
|
||||
assert reader.GetString("other.key") == "ext1"
|
||||
assert reader.GetString("user.name") == "Alice"
|
||||
|
||||
# Same-mtime_ns and same-size external edit while warm is also detected.
|
||||
prev_stat = os.stat(rw_config_file)
|
||||
same_len = rw_config_file.read_text(encoding="utf-8").replace(
|
||||
"ext1", "ext2"
|
||||
)
|
||||
rw_config_file.write_text(same_len, encoding="utf-8")
|
||||
os.utime(rw_config_file, ns=(prev_stat.st_atime_ns, prev_stat.st_mtime_ns))
|
||||
|
||||
config.SetString("user.email", "alice@example.com")
|
||||
assert config.GetString("other.key") == "ext2"
|
||||
assert config.GetString("user.email") == "alice@example.com"
|
||||
|
||||
reader2 = git_config.GitConfig(str(rw_config_file))
|
||||
monkeypatch.setattr(
|
||||
reader2,
|
||||
"_ReadGit",
|
||||
lambda: (_ for _ in ()).throw(AssertionError("expected JSON hit")),
|
||||
)
|
||||
assert reader2.GetString("other.key") == "ext2"
|
||||
assert reader2.GetString("user.name") == "Alice"
|
||||
assert reader2.GetString("user.email") == "alice@example.com"
|
||||
@@ -244,3 +244,165 @@ def test_updates_when_storage_backend_toggles(tmp_path):
|
||||
_run(repo, "refs", "migrate", "--ref-format=files")
|
||||
_run(repo, "branch", "files-branch")
|
||||
assert refs.get("refs/heads/files-branch") == head
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reftable", [False, True])
|
||||
def test_unborn_head_resolution(
|
||||
tmp_path: Path, reftable: bool, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Unborn HEAD reads files backend directly and uses git on reftable."""
|
||||
if reftable and not utils_for_test.supports_reftable():
|
||||
pytest.skip("reftable not supported")
|
||||
|
||||
repo = os.path.join(tmp_path, "unborn")
|
||||
ref_format = "reftable" if reftable else "files"
|
||||
utils_for_test.init_git_tree(repo, ref_format=ref_format)
|
||||
_run(repo, "symbolic-ref", "HEAD", "refs/heads/unborn-main")
|
||||
|
||||
refs = git_refs.GitRefs(os.path.join(repo, ".git"))
|
||||
seen_symbolic_ref = []
|
||||
|
||||
class SpyGitCommand(git_command.GitCommand):
|
||||
def __init__(
|
||||
self,
|
||||
proj: Any,
|
||||
cmdv: List[str],
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
if cmdv and cmdv[0] == "symbolic-ref":
|
||||
seen_symbolic_ref.append(cmdv)
|
||||
super().__init__(proj, cmdv, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(git_command, "GitCommand", SpyGitCommand)
|
||||
assert refs.symref("HEAD") == "refs/heads/unborn-main"
|
||||
if not reftable:
|
||||
assert seen_symbolic_ref == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reftable", [False, True])
|
||||
def test_update_symref_keeps_snapshot_warm(
|
||||
tmp_path: Path, reftable: bool
|
||||
) -> None:
|
||||
"""UpdateSymref stays warm on files backend and reloads on reftable."""
|
||||
if reftable and not utils_for_test.supports_reftable():
|
||||
pytest.skip("reftable not supported")
|
||||
|
||||
repo = _init_repo(tmp_path, reftable=reftable)
|
||||
branch = _run(repo, "symbolic-ref", "--short", "HEAD")
|
||||
head = _run(repo, "rev-parse", "HEAD")
|
||||
refs = git_refs.GitRefs(os.path.join(repo, ".git"))
|
||||
assert refs.get(f"refs/heads/{branch}") == head
|
||||
|
||||
_run(repo, "symbolic-ref", "refs/remotes/m/main", f"refs/heads/{branch}")
|
||||
refs.UpdateSymref("refs/remotes/m/main", f"refs/heads/{branch}")
|
||||
|
||||
if reftable:
|
||||
assert not refs.is_loaded
|
||||
assert refs.symref("refs/remotes/m/main") == f"refs/heads/{branch}"
|
||||
assert refs.get("refs/remotes/m/main") == head
|
||||
else:
|
||||
with mock.patch.object(
|
||||
refs,
|
||||
"_LoadAll",
|
||||
side_effect=AssertionError("expected warm GitRefs snapshot"),
|
||||
):
|
||||
assert refs.symref("refs/remotes/m/main") == f"refs/heads/{branch}"
|
||||
assert refs.get("refs/remotes/m/main") == head
|
||||
|
||||
|
||||
def test_update_symref_invalidates_when_external_ref_changed(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""External ref writes before UpdateSymref invalidate instead of masking."""
|
||||
repo = _init_repo(tmp_path, reftable=False)
|
||||
branch = _run(repo, "symbolic-ref", "--short", "HEAD")
|
||||
head = _run(repo, "rev-parse", "HEAD")
|
||||
refs = git_refs.GitRefs(os.path.join(repo, ".git"))
|
||||
assert refs.get(f"refs/heads/{branch}") == head
|
||||
|
||||
# External branch write happens before symbolic-ref + UpdateSymref.
|
||||
_run(repo, "branch", "external-topic")
|
||||
_run(repo, "symbolic-ref", "refs/remotes/m/main", f"refs/heads/{branch}")
|
||||
refs.UpdateSymref("refs/remotes/m/main", f"refs/heads/{branch}")
|
||||
|
||||
assert not refs.is_loaded
|
||||
assert refs.get("refs/heads/external-topic") == head
|
||||
assert refs.get("refs/remotes/m/main") == head
|
||||
|
||||
# External sibling ref in the same directory is also detected.
|
||||
_run(repo, "update-ref", "refs/remotes/m/sibling", head)
|
||||
_run(
|
||||
repo,
|
||||
"symbolic-ref",
|
||||
"refs/remotes/m/main",
|
||||
"refs/heads/external-topic",
|
||||
)
|
||||
refs.UpdateSymref("refs/remotes/m/main", "refs/heads/external-topic")
|
||||
|
||||
assert not refs.is_loaded
|
||||
assert refs.get("refs/remotes/m/sibling") == head
|
||||
|
||||
|
||||
def test_update_symref_self_or_cyclic_clears_phyref(tmp_path: Path) -> None:
|
||||
"""Self or cyclic symrefs clear any prior physical SHA from _phyref."""
|
||||
repo = _init_repo(tmp_path, reftable=False)
|
||||
head = _run(repo, "rev-parse", "HEAD")
|
||||
gitdir = os.path.join(repo, ".git")
|
||||
_run(repo, "update-ref", "refs/remotes/m/main", head)
|
||||
_run(repo, "update-ref", "refs/remotes/m/other", head)
|
||||
|
||||
refs = git_refs.GitRefs(gitdir)
|
||||
assert refs.get("refs/remotes/m/main") == head
|
||||
assert refs.get("refs/remotes/m/other") == head
|
||||
|
||||
_run(repo, "symbolic-ref", "refs/remotes/m/main", "refs/remotes/m/main")
|
||||
refs.UpdateSymref("refs/remotes/m/main", "refs/remotes/m/main")
|
||||
assert refs.get("refs/remotes/m/main") == ""
|
||||
|
||||
_run(repo, "symbolic-ref", "refs/remotes/m/main", "refs/remotes/m/other")
|
||||
refs.UpdateSymref("refs/remotes/m/main", "refs/remotes/m/other")
|
||||
assert refs.get("refs/remotes/m/main") == head
|
||||
|
||||
_run(repo, "symbolic-ref", "refs/remotes/m/other", "refs/remotes/m/main")
|
||||
refs.UpdateSymref("refs/remotes/m/other", "refs/remotes/m/main")
|
||||
assert refs.get("refs/remotes/m/main") == ""
|
||||
assert refs.get("refs/remotes/m/other") == ""
|
||||
|
||||
|
||||
def test_update_symref_propagates_chained_symrefs(tmp_path: Path) -> None:
|
||||
"""Re-pointing an intermediate symref updates chained symrefs cleanly."""
|
||||
repo = _init_repo(tmp_path, reftable=False)
|
||||
branch = _run(repo, "symbolic-ref", "--short", "HEAD")
|
||||
c1 = _run(repo, "rev-parse", "HEAD")
|
||||
|
||||
Path(os.path.join(repo, "a")).write_text("2", encoding="utf-8")
|
||||
_run(repo, "add", "a")
|
||||
_run(repo, "commit", "-q", "-m", "second")
|
||||
c2 = _run(repo, "rev-parse", "HEAD")
|
||||
_run(repo, "update-ref", "refs/heads/first", c1)
|
||||
|
||||
# Point HEAD at unborn refs/remotes/m/main before warming GitRefs.
|
||||
_run(repo, "symbolic-ref", "HEAD", "refs/remotes/m/main")
|
||||
refs = git_refs.GitRefs(os.path.join(repo, ".git"))
|
||||
assert refs.symref("HEAD") == "refs/remotes/m/main"
|
||||
assert refs.get("HEAD") == ""
|
||||
|
||||
_run(repo, "symbolic-ref", "refs/remotes/m/main", "refs/heads/first")
|
||||
refs.UpdateSymref("refs/remotes/m/main", "refs/heads/first")
|
||||
_run(repo, "symbolic-ref", "refs/remotes/m/alias", "refs/remotes/m/main")
|
||||
refs.UpdateSymref("refs/remotes/m/alias", "refs/remotes/m/main")
|
||||
assert refs.get("HEAD") == c1
|
||||
assert refs.get("refs/remotes/m/alias") == c1
|
||||
|
||||
_run(repo, "symbolic-ref", "refs/remotes/m/main", f"refs/heads/{branch}")
|
||||
refs.UpdateSymref("refs/remotes/m/main", f"refs/heads/{branch}")
|
||||
|
||||
with mock.patch.object(
|
||||
refs,
|
||||
"_LoadAll",
|
||||
side_effect=AssertionError("expected warm GitRefs snapshot"),
|
||||
):
|
||||
assert refs.get("refs/remotes/m/main") == c2
|
||||
assert refs.get("refs/remotes/m/alias") == c2
|
||||
assert refs.get("HEAD") == c2
|
||||
@@ -22,7 +22,7 @@ import unittest
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from test_manifest_xml import sort_attributes
|
||||
import test_manifest_xml
|
||||
|
||||
import git_superproject
|
||||
import git_trace2_event_log
|
||||
@@ -255,7 +255,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
||||
with open(manifest_path) as fp:
|
||||
manifest_xml_data = fp.read()
|
||||
self.assertEqual(
|
||||
sort_attributes(manifest_xml_data),
|
||||
test_manifest_xml.sort_attributes(manifest_xml_data),
|
||||
'<?xml version="1.0" ?><manifest>'
|
||||
'<remote fetch="http://localhost" name="default-remote"/>'
|
||||
'<default remote="default-remote" revision="refs/heads/main"/>'
|
||||
@@ -290,7 +290,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
||||
with open(update_result.manifest_path) as fp:
|
||||
manifest_xml_data = fp.read()
|
||||
self.assertEqual(
|
||||
sort_attributes(manifest_xml_data),
|
||||
test_manifest_xml.sort_attributes(manifest_xml_data),
|
||||
'<?xml version="1.0" ?><manifest>'
|
||||
'<remote fetch="http://localhost" name="default-remote"/>'
|
||||
'<default remote="default-remote" revision="refs/heads/main"/>'
|
||||
@@ -317,7 +317,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
||||
self.maxDiff = None
|
||||
self.assertIsNone(manifest.superproject)
|
||||
self.assertEqual(
|
||||
sort_attributes(manifest.ToXml().toxml()),
|
||||
test_manifest_xml.sort_attributes(manifest.ToXml().toxml()),
|
||||
'<?xml version="1.0" ?><manifest>'
|
||||
'<remote fetch="http://localhost" name="default-remote"/>'
|
||||
'<default remote="default-remote" revision="refs/heads/main"/>'
|
||||
@@ -379,7 +379,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
||||
# Verify platform/vendor/x's project revision hasn't
|
||||
# changed.
|
||||
self.assertEqual(
|
||||
sort_attributes(manifest_xml_data),
|
||||
test_manifest_xml.sort_attributes(manifest_xml_data),
|
||||
'<?xml version="1.0" ?><manifest>'
|
||||
'<remote fetch="http://localhost" name="default-remote"/>'
|
||||
'<remote fetch="http://localhost2" name="goog"/>'
|
||||
@@ -444,7 +444,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
||||
# Verify platform/vendor/x's project revision hasn't
|
||||
# changed.
|
||||
self.assertEqual(
|
||||
sort_attributes(manifest_xml_data),
|
||||
test_manifest_xml.sort_attributes(manifest_xml_data),
|
||||
'<?xml version="1.0" ?><manifest>'
|
||||
'<remote fetch="http://localhost" name="default-remote"/>'
|
||||
'<default remote="default-remote" revision="refs/heads/main"/>'
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@
|
||||
|
||||
"""Unittests for the hooks.py module."""
|
||||
|
||||
from io import StringIO
|
||||
import io
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
@@ -82,7 +82,7 @@ def test_post_sync_argument_validation() -> None:
|
||||
)
|
||||
|
||||
old_stderr = sys.stderr
|
||||
sys.stderr = StringIO()
|
||||
sys.stderr = io.StringIO()
|
||||
|
||||
try:
|
||||
# Call with missing arg `sync_duration_seconds`
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
|
||||
"""Tests for the main repo script and subcommand routing."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
import event_log
|
||||
from main import _Repo
|
||||
|
||||
|
||||
@@ -164,3 +166,52 @@ def test_autocorrect_prompt_interrupt(mock_input, repo, mock_config):
|
||||
res = repo._autocorrect_command_name("tart", mock_config)
|
||||
|
||||
assert res is None
|
||||
|
||||
|
||||
def test_event_log_disabled_skips_add_and_git_hash() -> None:
|
||||
"""Test disabled EventLog returns None and skips GetCommitRevisionId."""
|
||||
log = event_log.EventLog(enabled=False)
|
||||
project = mock.MagicMock()
|
||||
|
||||
assert log.Add("sync", event_log.TASK_COMMAND, 1.0) is None
|
||||
assert (
|
||||
log.AddSync(project, event_log.TASK_SYNC_NETWORK, 1.0, 2.0, True)
|
||||
is None
|
||||
)
|
||||
project.GetCommitRevisionId.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch("manifest_xml.RepoClient")
|
||||
def test_run_long_gates_event_log_on_flag(
|
||||
mock_client_cls: mock.MagicMock, tmp_path: Path
|
||||
) -> None:
|
||||
"""Test _RunLong enables cmd.event_log only when --event-log is set."""
|
||||
client = mock_client_cls.return_value
|
||||
client.manifest.IsMirror = False
|
||||
client.manifest.manifestProject.config.DumpConfigDict.return_value = {}
|
||||
repo = _Repo("repodir")
|
||||
|
||||
for log_path, expected in (
|
||||
(None, False),
|
||||
(str(tmp_path / "events.log"), True),
|
||||
):
|
||||
cmd = mock.MagicMock()
|
||||
cmd.event_log = event_log.EventLog(enabled=False)
|
||||
cmd.manifest = client.manifest
|
||||
cmd.MULTI_MANIFEST_SUPPORT = True
|
||||
cmd.OptionParser.parse_args.return_value = (mock.MagicMock(), [])
|
||||
cmd.ReadEnvironmentOptions.side_effect = lambda copts: copts
|
||||
cmd.Execute.return_value = 0
|
||||
repo.commands = {"sync": mock.Mock(return_value=cmd)}
|
||||
gopts = mock.MagicMock(
|
||||
color=None,
|
||||
submanifest_path="",
|
||||
pager=False,
|
||||
time=False,
|
||||
event_log=log_path,
|
||||
git_trace2_event_log=None,
|
||||
)
|
||||
|
||||
assert repo._RunLong("sync", gopts, [], mock.MagicMock()) == 0
|
||||
assert cmd.event_log.enabled is expected
|
||||
assert bool(log_path and Path(log_path).exists()) is expected
|
||||
@@ -933,6 +933,106 @@ class TestProjectElement:
|
||||
with pytest.raises(error.ManifestInvalidPathError):
|
||||
parse("ok", path)
|
||||
|
||||
def test_sparse_checkout(self, repo_client: RepoClient) -> None:
|
||||
"""Check sparse-checkout settings."""
|
||||
manifest = repo_client.get_xml_manifest(
|
||||
"""
|
||||
<manifest>
|
||||
<remote name="test-remote" fetch="http://localhost" />
|
||||
<default remote="test-remote" revision="refs/heads/main" />
|
||||
<project name="test-name" path="test-path">
|
||||
<sparse-path path="src/main" />
|
||||
<sparse-path path="docs" />
|
||||
</project>
|
||||
<project name="no-sparse" path="no-sparse-path" />
|
||||
</manifest>
|
||||
"""
|
||||
)
|
||||
assert len(manifest.projects) == 2
|
||||
result = {p.name: p for p in manifest.projects}
|
||||
|
||||
assert result["test-name"].sparse_paths == ["src/main", "docs"]
|
||||
assert result["no-sparse"].sparse_paths == []
|
||||
|
||||
def test_sparse_checkout_bad_paths(self, repo_client: RepoClient) -> None:
|
||||
"""Check <sparse-path> rejects paths that escape the checkout."""
|
||||
|
||||
def parse(path: str) -> None:
|
||||
path = repo_client.encode_xml_attr(path)
|
||||
manifest = repo_client.get_xml_manifest(
|
||||
f"""
|
||||
<manifest>
|
||||
<remote name="test-remote" fetch="http://localhost" />
|
||||
<default remote="test-remote" revision="refs/heads/main" />
|
||||
<project name="test-name" path="test-path">
|
||||
<sparse-path path="{path}" />
|
||||
</project>
|
||||
</manifest>
|
||||
"""
|
||||
)
|
||||
# Force the manifest to be parsed.
|
||||
manifest.ToXml()
|
||||
|
||||
# Verify the parser is valid by default to avoid buggy tests below.
|
||||
parse("src/main")
|
||||
|
||||
# Sparse paths name directories, so a trailing slash is fine.
|
||||
parse("src/main/")
|
||||
|
||||
# A missing path is rejected by a different codepath.
|
||||
with pytest.raises(error.ManifestParseError):
|
||||
parse("")
|
||||
|
||||
for path in INVALID_FS_PATHS:
|
||||
if not path or path.endswith("/") or path.endswith(os.path.sep):
|
||||
continue
|
||||
|
||||
with pytest.raises(error.ManifestInvalidPathError):
|
||||
parse(path)
|
||||
|
||||
def test_sparse_checkout_extend_project(
|
||||
self, repo_client: RepoClient
|
||||
) -> None:
|
||||
"""Check sparse-path elements added via extend-project."""
|
||||
manifest = repo_client.get_xml_manifest(
|
||||
"""
|
||||
<manifest>
|
||||
<remote name="test-remote" fetch="http://localhost" />
|
||||
<default remote="test-remote" revision="refs/heads/main" />
|
||||
<project name="test-name" path="test-path">
|
||||
<sparse-path path="src/main" />
|
||||
</project>
|
||||
<extend-project name="test-name">
|
||||
<sparse-path path="docs" />
|
||||
</extend-project>
|
||||
</manifest>
|
||||
"""
|
||||
)
|
||||
assert len(manifest.projects) == 1
|
||||
assert manifest.projects[0].sparse_paths == ["src/main", "docs"]
|
||||
|
||||
def test_sparse_checkout_roundtrip(self, repo_client: RepoClient) -> None:
|
||||
"""Check sparse-path elements survive a manifest round-trip."""
|
||||
manifest = repo_client.get_xml_manifest(
|
||||
"""
|
||||
<manifest>
|
||||
<remote name="test-remote" fetch="http://localhost" />
|
||||
<default remote="test-remote" revision="refs/heads/main" />
|
||||
<project name="test-name" path="test-path">
|
||||
<sparse-path path="src/main" />
|
||||
<sparse-path path="docs" />
|
||||
</project>
|
||||
</manifest>
|
||||
"""
|
||||
)
|
||||
roundtrip = repo_client.get_xml_manifest(manifest.ToXml().toxml())
|
||||
assert roundtrip.projects[0].sparse_paths == ["src/main", "docs"]
|
||||
|
||||
assert manifest.ToDict()["project"][0]["sparse-path"] == [
|
||||
{"path": "src/main"},
|
||||
{"path": "docs"},
|
||||
]
|
||||
|
||||
|
||||
class TestSuperProjectElement:
|
||||
"""Tests for <superproject>."""
|
||||
|
||||
+1377
-31
File diff suppressed because it is too large.
Load diff
@@ -16,7 +16,7 @@
|
||||
|
||||
import runpy
|
||||
import sys
|
||||
from types import ModuleType
|
||||
import types
|
||||
|
||||
import pytest
|
||||
import utils_for_test
|
||||
@@ -31,7 +31,7 @@ def test_wrapper_does_not_run_main_for_multiprocessing_child(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Do not rerun main when multiprocessing re-executes the wrapper."""
|
||||
fake_update_manpages = ModuleType("update_manpages")
|
||||
fake_update_manpages = types.ModuleType("update_manpages")
|
||||
|
||||
# Mock this because the real module requires newer Python versions than
|
||||
# our unittest framework does.
|
||||
|
||||
@@ -22,7 +22,7 @@ from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from color import SetDefaultColoring
|
||||
import color
|
||||
import error
|
||||
import repo_logging
|
||||
|
||||
@@ -86,7 +86,7 @@ def test_log_with_format_string(level: int) -> None:
|
||||
# Set color output to "always" for consistent test results.
|
||||
# This ensures the logger's behavior is uniform across different
|
||||
# environments and git configurations.
|
||||
SetDefaultColoring("always")
|
||||
color.SetDefaultColoring("always")
|
||||
|
||||
# Regex pattern to match optional ANSI color codes.
|
||||
# \033 - Escape character
|
||||
|
||||
@@ -21,6 +21,7 @@ import pytest
|
||||
|
||||
from command import Command
|
||||
import subcmds
|
||||
from subcmds import sync
|
||||
|
||||
|
||||
# NB: We don't test all subcommands as we want to avoid "change detection"
|
||||
@@ -164,9 +165,7 @@ def test_common_validate_options(name: str, cls: Type[Command]) -> None:
|
||||
|
||||
def test_attribute_error_repro() -> None:
|
||||
"""Confirm that accessing verbose before CommonValidateOptions fails."""
|
||||
from subcmds.sync import Sync
|
||||
|
||||
cmd = Sync()
|
||||
cmd = sync.Sync()
|
||||
opts, args = cmd.OptionParser.parse_args([])
|
||||
|
||||
# This confirms that without the fix in main.py, an AttributeError
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
from types import SimpleNamespace
|
||||
import types
|
||||
from unittest import mock
|
||||
|
||||
import error
|
||||
@@ -66,7 +66,7 @@ def test_execute_delegates_autostash_to_rebase() -> None:
|
||||
branch.LocalMerge = "refs/remotes/origin/main"
|
||||
project.GetBranch.return_value = branch
|
||||
cmd.GetProjects = mock.MagicMock(return_value=[project])
|
||||
opt = SimpleNamespace(
|
||||
opt = types.SimpleNamespace(
|
||||
interactive=False,
|
||||
fail_fast=False,
|
||||
whitespace=None,
|
||||
|
||||
+41
-15
@@ -19,6 +19,7 @@ import optparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
@@ -33,7 +34,9 @@ import pytest
|
||||
import command
|
||||
import error
|
||||
import git_command
|
||||
import git_config
|
||||
import manifest_xml
|
||||
import progress
|
||||
from project import SyncNetworkHalfResult
|
||||
from subcmds import sync
|
||||
|
||||
@@ -555,7 +558,9 @@ def test_cli_smart_sync_with_config(
|
||||
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):
|
||||
with mock.patch.object(
|
||||
git_config.RepoConfig, "ForUser", return_value=user_cfg
|
||||
):
|
||||
implicit = cmd._ResolveSmartSyncOption(opts, manifest)
|
||||
assert opts.smart_sync == expected_smart_sync
|
||||
assert implicit == expected_implicit
|
||||
@@ -603,7 +608,9 @@ def test_user_config_smart_sync(
|
||||
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):
|
||||
with mock.patch.object(
|
||||
git_config.RepoConfig, "ForUser", return_value=user_cfg
|
||||
):
|
||||
implicit = cmd._ResolveSmartSyncOption(opts, manifest)
|
||||
user_cfg.GetBoolean.assert_called_once_with("sync.smartsync")
|
||||
assert opts.smart_sync == expected_smart_sync
|
||||
@@ -622,7 +629,7 @@ def test_user_config_smart_sync_from_file(
|
||||
) -> None:
|
||||
"""Tests that sync.smartsync is read from ~/.repoconfig/config."""
|
||||
# ForUser() caches its instance process-wide; make it pick up this HOME.
|
||||
monkeypatch.setattr(sync.RepoConfig, "_ForUser", None)
|
||||
monkeypatch.setattr(git_config.RepoConfig, "_ForUser", None)
|
||||
monkeypatch.delenv("REPO_CONFIG_DIR", raising=False)
|
||||
cfg_dir = tmp_home_dir / ".repoconfig"
|
||||
cfg_dir.mkdir()
|
||||
@@ -684,7 +691,9 @@ def test_cli_manifest_server_credentials(
|
||||
|
||||
cmd = sync.Sync(manifest=manifest)
|
||||
opts, args = cmd.OptionParser.parse_args(argv)
|
||||
with mock.patch.object(sync.RepoConfig, "ForUser", return_value=user_cfg):
|
||||
with mock.patch.object(
|
||||
git_config.RepoConfig, "ForUser", return_value=user_cfg
|
||||
):
|
||||
if expected_exception:
|
||||
with pytest.raises(expected_exception):
|
||||
cmd.ValidateOptions(opts, args)
|
||||
@@ -979,6 +988,7 @@ class FakeProject:
|
||||
|
||||
self.use_git_worktrees = False
|
||||
self.UseAlternates = False
|
||||
self.UseFetchCmd = False
|
||||
self.UseReprojectCmd = False
|
||||
self.manifest = mock.MagicMock()
|
||||
self.manifest.GetProjectsWithName.return_value = [self]
|
||||
@@ -1425,6 +1435,7 @@ class CheckForBloatedProjects(unittest.TestCase):
|
||||
self.project.name = "project"
|
||||
self.project.Exists = True
|
||||
self.project.worktree = self.tempdirobj.name
|
||||
self.project.UseFetchCmd = False
|
||||
self.project.stateless_prune_needed = False
|
||||
self.cmd.git_event_log = mock.MagicMock()
|
||||
self.cmd._bloated_projects = []
|
||||
@@ -1492,7 +1503,7 @@ class CheckForBloatedProjects(unittest.TestCase):
|
||||
self.assertFalse(self.cmd.git_event_log.ErrorEvent.called)
|
||||
|
||||
@mock.patch.object(git_command, "git_require", return_value=True)
|
||||
@mock.patch("subcmds.sync.Progress")
|
||||
@mock.patch.object(progress, "Progress")
|
||||
def test_network_only_skips_check(
|
||||
self, mock_progress: mock.Mock, mock_git_require: mock.Mock
|
||||
) -> None:
|
||||
@@ -1506,7 +1517,7 @@ class CheckForBloatedProjects(unittest.TestCase):
|
||||
self.cmd.ExecuteInParallel.assert_not_called()
|
||||
|
||||
@mock.patch.object(git_command, "git_require", return_value=True)
|
||||
@mock.patch("subcmds.sync.Progress")
|
||||
@mock.patch.object(progress, "Progress")
|
||||
def test_projects_without_worktree_excluded(
|
||||
self, mock_progress: mock.Mock, mock_git_require: mock.Mock
|
||||
) -> None:
|
||||
@@ -1528,7 +1539,7 @@ class CheckForBloatedProjects(unittest.TestCase):
|
||||
self.cmd.ExecuteInParallel.assert_not_called()
|
||||
|
||||
@mock.patch.object(git_command, "git_require")
|
||||
@mock.patch("subcmds.sync.Progress")
|
||||
@mock.patch.object(progress, "Progress")
|
||||
def test_bloated_project_found(self, mock_progress, mock_git_require):
|
||||
"""Test that it adds project to _bloated_projects."""
|
||||
mock_git_require.return_value = True
|
||||
@@ -1551,7 +1562,7 @@ class CheckForBloatedProjects(unittest.TestCase):
|
||||
self.assertEqual(self.cmd._bloated_projects, ["project"])
|
||||
|
||||
@mock.patch.object(git_command, "git_require")
|
||||
@mock.patch("subcmds.sync.Progress")
|
||||
@mock.patch.object(progress, "Progress")
|
||||
def test_stateless_prune_excluded(self, mock_progress, mock_git_require):
|
||||
"""Test that projects pruned for stateless sync are excluded."""
|
||||
mock_git_require.return_value = True
|
||||
@@ -1565,6 +1576,22 @@ class CheckForBloatedProjects(unittest.TestCase):
|
||||
self.assertFalse(self.cmd.ExecuteInParallel.called)
|
||||
self.assertEqual(self.cmd._bloated_projects, [])
|
||||
|
||||
@mock.patch.object(git_command, "git_require", return_value=True)
|
||||
@mock.patch.object(progress, "Progress")
|
||||
def test_fetch_cmd_excluded(
|
||||
self, mock_progress: mock.Mock, mock_git_require: mock.Mock
|
||||
) -> None:
|
||||
"""Projects using repo.fetchcmd are excluded from the bloat check."""
|
||||
self.project.UseFetchCmd = True
|
||||
self.cmd.ExecuteInParallel = mock.Mock()
|
||||
|
||||
with mock.patch.object(self.cmd, "ParallelContext"):
|
||||
self.cmd._CheckForBloatedProjects([self.project], self.opt)
|
||||
|
||||
mock_progress.assert_not_called()
|
||||
self.cmd.ExecuteInParallel.assert_not_called()
|
||||
self.assertEqual(self.cmd._bloated_projects, [])
|
||||
|
||||
|
||||
class GCProjectsTest(unittest.TestCase):
|
||||
"""Tests for Sync._GCProjects."""
|
||||
@@ -1583,7 +1610,7 @@ class GCProjectsTest(unittest.TestCase):
|
||||
self.project.bare_git._project = self.project
|
||||
self.cmd.git_event_log = mock.MagicMock()
|
||||
|
||||
@mock.patch("subcmds.sync.Progress")
|
||||
@mock.patch.object(progress, "Progress")
|
||||
def test_GCProjects_skip_gc(self, mock_progress):
|
||||
"""Test that it skips GC if opt.auto_gc is False."""
|
||||
self.opt.auto_gc = False
|
||||
@@ -1594,7 +1621,7 @@ class GCProjectsTest(unittest.TestCase):
|
||||
mock_set_state.assert_called_once_with(self.project, self.opt)
|
||||
self.assertFalse(self.project.bare_git.gc.called)
|
||||
|
||||
@mock.patch("subcmds.sync.Progress")
|
||||
@mock.patch.object(progress, "Progress")
|
||||
def test_GCProjects_sequential(self, mock_progress):
|
||||
"""Test sequential GC (jobs < 2)."""
|
||||
with mock.patch.object(sync.Sync, "_SetPreciousObjectsState"):
|
||||
@@ -1606,7 +1633,7 @@ class GCProjectsTest(unittest.TestCase):
|
||||
for call in self.project.config.SetString.call_args_list:
|
||||
self.assertNotEqual(call.args[0], "gc.autoDetach")
|
||||
|
||||
@mock.patch("subcmds.sync.Progress")
|
||||
@mock.patch.object(progress, "Progress")
|
||||
def test_GCProjects_parallel(self, mock_progress):
|
||||
"""Test parallel GC (jobs >= 2)."""
|
||||
self.opt.jobs = 2
|
||||
@@ -1654,6 +1681,7 @@ class SyncCommand(unittest.TestCase):
|
||||
self.project = p = mock.MagicMock(
|
||||
use_git_worktrees=False,
|
||||
UseAlternates=False,
|
||||
UseFetchCmd=False,
|
||||
UseReprojectCmd=False,
|
||||
name="project",
|
||||
relpath="rel_path",
|
||||
@@ -1861,7 +1889,7 @@ class SyncCommand(unittest.TestCase):
|
||||
user_cfg = mock.MagicMock()
|
||||
user_cfg.GetBoolean.return_value = False
|
||||
with mock.patch.object(
|
||||
sync.RepoConfig, "ForUser", return_value=user_cfg
|
||||
git_config.RepoConfig, "ForUser", return_value=user_cfg
|
||||
):
|
||||
with mock.patch.object(self.cmd, "_SmartSyncSetup") as mock_setup:
|
||||
with mock.patch.object(self.cmd, "_UpdateRepoProject"):
|
||||
@@ -1884,7 +1912,7 @@ class SyncCommand(unittest.TestCase):
|
||||
user_cfg = mock.MagicMock()
|
||||
user_cfg.GetBoolean.return_value = True
|
||||
with mock.patch.object(
|
||||
sync.RepoConfig, "ForUser", return_value=user_cfg
|
||||
git_config.RepoConfig, "ForUser", return_value=user_cfg
|
||||
):
|
||||
with mock.patch.object(
|
||||
self.cmd,
|
||||
@@ -3012,8 +3040,6 @@ class TestSmartSyncSetupRemoteHelper(unittest.TestCase):
|
||||
self, mock_transport_class, mock_server_class, mock_popen, mock_which
|
||||
):
|
||||
"""Test _SmartSyncSetup when a helper is present and succeeds."""
|
||||
import subprocess
|
||||
|
||||
self.manifest.manifest_server = (
|
||||
"persistent-https://android-smartsync.corp.google.com/"
|
||||
"manifestserver"
|
||||
|
||||
@@ -116,11 +116,46 @@ def test_GatherOne_returns_resolved_current_branch(
|
||||
"get_parallel_context",
|
||||
lambda: {"projects": [project]},
|
||||
)
|
||||
opt = mock.MagicMock(current_branch=True)
|
||||
opt = mock.MagicMock(current_branch=True, ignore_remote_commits=False)
|
||||
|
||||
assert upload.Upload._GatherOne(opt, 0) == (0, [branch], "topic")
|
||||
|
||||
project.GetUploadableBranch.assert_called_once_with("topic")
|
||||
project.GetUploadableBranch.assert_called_once_with(
|
||||
"topic", ignore_remote_commits=False
|
||||
)
|
||||
|
||||
|
||||
def test_GatherOne_forwards_ignore_remote_commits(
|
||||
cmd: upload.Upload,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""_GatherOne forwards --ignore-remote-commits to project methods."""
|
||||
project = mock.MagicMock()
|
||||
project.CurrentBranch = "topic"
|
||||
branch = mock.sentinel.branch
|
||||
project.GetUploadableBranch.return_value = branch
|
||||
project.GetUploadableBranches.return_value = [branch]
|
||||
monkeypatch.setattr(
|
||||
upload.Upload,
|
||||
"get_parallel_context",
|
||||
lambda: {"projects": [project]},
|
||||
)
|
||||
|
||||
opt_cbr, _ = cmd.OptionParser.parse_args(
|
||||
["--cbr", "--ignore-remote-commits"]
|
||||
)
|
||||
assert upload.Upload._GatherOne(opt_cbr, 0) == (0, [branch], "topic")
|
||||
project.GetUploadableBranch.assert_called_once_with(
|
||||
"topic", ignore_remote_commits=True
|
||||
)
|
||||
|
||||
opt_br, _ = cmd.OptionParser.parse_args(
|
||||
["--br", "topic", "--ignore-remote-commits"]
|
||||
)
|
||||
assert upload.Upload._GatherOne(opt_br, 0) == (0, [branch], None)
|
||||
project.GetUploadableBranches.assert_called_once_with(
|
||||
"topic", ignore_remote_commits=True
|
||||
)
|
||||
|
||||
|
||||
def _create_mock_branch(
|
||||
|
||||
Reference in new issue
Block a user