Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package git-repo for openSUSE:Factory 
checked in at 2025-11-13 17:26:37
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/git-repo (Old)
 and      /work/SRC/openSUSE:Factory/.git-repo.new.2061 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "git-repo"

Thu Nov 13 17:26:37 2025 rev:12 rq:1317427 version:2.59

Changes:
--------
--- /work/SRC/openSUSE:Factory/git-repo/git-repo.changes        2025-10-07 
18:30:20.458459700 +0200
+++ /work/SRC/openSUSE:Factory/.git-repo.new.2061/git-repo.changes      
2025-11-13 17:28:49.290807492 +0100
@@ -1,0 +2,15 @@
+Wed Nov 05 18:38:43 UTC 2025 - BenoĆ®t Monin <[email protected]>
+
+- Update to version 2.59:
+  * sync: fix saving of fetch times and local state
+  * run_tests: log each command run
+  * sync: Use 'git rebase' during 'repo sync --rebase'
+  * Fix submodule initialization in interleaved sync mode
+  * Follow up "Fix shallow clones when upstream attribute is present"
+  * forall: fix crash with no command
+  * run_tests: add file header checker for licensing blocks
+  * man: regen after sync updates
+  * standardize file header wrt licensing
+  * CONTRIBUTING: rename doc per Google OSS policies
+
+-------------------------------------------------------------------

Old:
----
  git-repo-2.58.tar.xz

New:
----
  git-repo-2.59.tar.xz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ git-repo.spec ++++++
--- /var/tmp/diff_new_pack.zjke7k/_old  2025-11-13 17:28:50.042839412 +0100
+++ /var/tmp/diff_new_pack.zjke7k/_new  2025-11-13 17:28:50.046839581 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           git-repo
-Version:        2.58
+Version:        2.59
 Release:        0
 Summary:        The Multiple Git Repository Tool
 License:        Apache-2.0

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.zjke7k/_old  2025-11-13 17:28:50.150843995 +0100
+++ /var/tmp/diff_new_pack.zjke7k/_new  2025-11-13 17:28:50.166844675 +0100
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://gerrit.googlesource.com/git-repo</param>
-              <param 
name="changesrevision">38d2fe11b9df521727fcca23c9dac086ce8378d3</param></service></servicedata>
+              <param 
name="changesrevision">1afe96a7e997ce7748f066b206a85ac648f7a87c</param></service></servicedata>
 (No newline at EOF)
 

++++++ git-repo-2.58.tar.xz -> git-repo-2.59.tar.xz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/git-repo-2.58/.github/workflows/close-pull-request.yml 
new/git-repo-2.59/.github/workflows/close-pull-request.yml
--- old/git-repo-2.58/.github/workflows/close-pull-request.yml  2025-08-15 
01:35:26.000000000 +0200
+++ new/git-repo-2.59/.github/workflows/close-pull-request.yml  2025-10-20 
20:28:21.000000000 +0200
@@ -18,5 +18,5 @@
           Thanks for your contribution!
           Unfortunately, we don't use GitHub pull requests to manage code
           contributions to this repository.
-          Instead, please see [README.md](../blob/HEAD/SUBMITTING_PATCHES.md)
+          Instead, please see [README.md](../blob/HEAD/CONTRIBUTING.md)
           which provides full instructions on how to get involved.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/.isort.cfg new/git-repo-2.59/.isort.cfg
--- old/git-repo-2.58/.isort.cfg        2025-08-15 01:35:26.000000000 +0200
+++ new/git-repo-2.59/.isort.cfg        2025-10-20 20:28:21.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright 2023 The Android Open Source Project
+# Copyright (C) 2023 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/CONTRIBUTING.md 
new/git-repo-2.59/CONTRIBUTING.md
--- old/git-repo-2.58/CONTRIBUTING.md   1970-01-01 01:00:00.000000000 +0100
+++ new/git-repo-2.59/CONTRIBUTING.md   2025-10-20 20:28:21.000000000 +0200
@@ -0,0 +1,190 @@
+# Submitting Changes
+
+Here's a short overview of the process.
+
+*   Make small logical changes.
+*   [Provide a meaningful commit message][commit-message-style].
+*   Make sure all code is under the Apache License, 2.0.
+*   Publish your changes for review.
+    *   `git push origin HEAD:refs/for/main`
+*   Make corrections if requested.
+*   [Verify your changes on Gerrit.](#verify)
+*   [Send to the commit queue for testing & merging.](#cq)
+
+[TOC]
+
+## Long Version
+
+I wanted a file describing how to submit patches for repo,
+so I started with the one found in the core Git distribution
+(Documentation/SubmittingPatches), which itself was based on the
+patch submission guidelines for the Linux kernel.
+
+However there are some differences, so please review and familiarize
+yourself with the following relevant bits.
+
+
+## Make separate commits for logically separate changes.
+
+Unless your patch is really trivial, you should not be sending out a patch that
+was generated between your working tree and your commit head.
+Instead, always make a commit with a complete
+[commit message][commit-message-style] and generate a series of patches from
+your repository.
+It is a good discipline.
+
+Describe the technical detail of the change(s).
+
+If your description starts to get too long, that's a sign that you
+probably need to split up your commit to finer grained pieces.
+
+
+## Linting and formatting code
+
+Lint any changes by running:
+```sh
+$ tox -e lint -- file.py
+```
+
+And format with:
+```sh
+$ tox -e format -- file.py
+```
+
+Or format everything:
+```sh
+$ tox -e format
+```
+
+Repo uses [black](https://black.readthedocs.io/) with line length of 80 as its
+formatter and flake8 as its linter. Repo also follows
+[Google's Python Style Guide].
+
+There should be no new errors or warnings introduced.
+
+Warnings that cannot be avoided without going against the Google Style Guide
+may be suppressed inline individally using a `# noqa` comment as described
+in the [flake8 documentation].
+
+If there are many occurrences of the same warning, these may be suppressed for
+the entire project in the included `.flake8` file.
+
+[Google's Python Style Guide]: https://google.github.io/styleguide/pyguide.html
+[PEP 8]: https://www.python.org/dev/peps/pep-0008/
+[flake8 documentation]: 
https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html#in-line-ignoring-errors
+
+## Running tests
+
+We use [pytest](https://pytest.org/) and [tox](https://tox.readthedocs.io/) for
+running tests.  You should make sure to install those first.
+
+To run the full suite against all supported Python versions, simply execute:
+```sh
+$ tox -p auto
+```
+
+We have [`./run_tests`](./run_tests) which is a simple wrapper around `pytest`:
+```sh
+# Run the full suite against the default Python version.
+$ ./run_tests
+# List each test as it runs.
+$ ./run_tests -v
+
+# Run a specific unittest module (and all tests in it).
+$ ./run_tests tests/test_git_command.py
+
+# Run a specific testsuite in a specific unittest module.
+$ ./run_tests tests/test_editor.py::EditString
+
+# Run a single test.
+$ ./run_tests tests/test_editor.py::EditString::test_cat_editor
+
+# List all available tests.
+$ ./run_tests --collect-only
+
+# Run a single test using substring match.
+$ ./run_tests -k test_cat_editor
+```
+
+The coverage isn't great currently, but it should still be run for all commits.
+Adding more unittests for changes you make would be greatly appreciated :).
+Check out the [tests/](./tests/) subdirectory for more details.
+
+
+## Check the license
+
+repo is licensed under the Apache License, 2.0.
+
+Because of this licensing model *every* file within the project
+*must* list the license that covers it in the header of the file.
+Any new contributions to an existing file *must* be submitted under
+the current license of that file.  Any new files *must* clearly
+indicate which license they are provided under in the file header.
+
+Please verify that you are legally allowed and willing to submit your
+changes under the license covering each file *prior* to submitting
+your patch.  It is virtually impossible to remove a patch once it
+has been applied and pushed out.
+
+
+## Sending your patches.
+
+Do not email your patches to anyone.
+
+Instead, login to the Gerrit Code Review tool at:
+
+  https://gerrit-review.googlesource.com/
+
+Ensure you have completed one of the necessary contributor
+agreements, providing documentation to the project maintainers that
+they have right to redistribute your work under the Apache License:
+
+  https://gerrit-review.googlesource.com/#/settings/agreements
+
+Ensure you have obtained an HTTP password to authenticate:
+
+  https://gerrit-review.googlesource.com/new-password
+
+Ensure that you have the local commit hook installed to automatically
+add a ChangeId to your commits:
+
+    curl -Lo `git rev-parse --git-dir`/hooks/commit-msg 
https://gerrit-review.googlesource.com/tools/hooks/commit-msg
+    chmod +x `git rev-parse --git-dir`/hooks/commit-msg
+
+If you have already committed your changes you will need to amend the commit
+to get the ChangeId added.
+
+    git commit --amend
+
+Push your patches over HTTPS to the review server, possibly through
+a remembered remote to make this easier in the future:
+
+    git config remote.review.url 
https://gerrit-review.googlesource.com/git-repo
+    git config remote.review.push HEAD:refs/for/main
+
+    git push review
+
+You will be automatically emailed a copy of your commits, and any
+comments made by the project maintainers.
+
+
+## Make changes if requested
+
+The project maintainer who reviews your changes might request changes to your
+commit. If you make the requested changes you will need to amend your commit
+and push it to the review server again.
+
+
+## Verify your changes on Gerrit {#verify}
+
+After you receive a Code-Review+2 from the maintainer, select the Verified
+button on the Gerrit page for the change. This verifies that you have tested
+your changes and notifies the maintainer that they are ready to be submitted.
+
+## Merge your changes via the commit queue {#cq}
+
+Once a change is ready to be merged, select the Commit-Queue+2 setting on the
+Gerrit page for it. This tells the CI system to test the change, and if it
+passes all the checks, automatically merges it.
+
+[commit-message-style]: https://chris.beams.io/posts/git-commit/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/README.md new/git-repo-2.59/README.md
--- old/git-repo-2.58/README.md 2025-08-15 01:35:26.000000000 +0200
+++ new/git-repo-2.59/README.md 2025-10-20 20:28:21.000000000 +0200
@@ -14,7 +14,7 @@
 * Docs: <https://source.android.com/source/using-repo.html>
 * [repo Manifest Format](./docs/manifest-format.md)
 * [repo Hooks](./docs/repo-hooks.md)
-* [Submitting patches](./SUBMITTING_PATCHES.md)
+* [Contributing](./CONTRIBUTING.md)
 * Running Repo in [Microsoft Windows](./docs/windows.md)
 * GitHub mirror: <https://github.com/GerritCodeReview/git-repo>
 * Postsubmit tests: <https://github.com/GerritCodeReview/git-repo/actions>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/SUBMITTING_PATCHES.md 
new/git-repo-2.59/SUBMITTING_PATCHES.md
--- old/git-repo-2.58/SUBMITTING_PATCHES.md     2025-08-15 01:35:26.000000000 
+0200
+++ new/git-repo-2.59/SUBMITTING_PATCHES.md     1970-01-01 01:00:00.000000000 
+0100
@@ -1,190 +0,0 @@
-# Submitting Changes
-
-Here's a short overview of the process.
-
-*   Make small logical changes.
-*   [Provide a meaningful commit message][commit-message-style].
-*   Make sure all code is under the Apache License, 2.0.
-*   Publish your changes for review.
-    *   `git push origin HEAD:refs/for/main`
-*   Make corrections if requested.
-*   [Verify your changes on Gerrit.](#verify)
-*   [Send to the commit queue for testing & merging.](#cq)
-
-[TOC]
-
-## Long Version
-
-I wanted a file describing how to submit patches for repo,
-so I started with the one found in the core Git distribution
-(Documentation/SubmittingPatches), which itself was based on the
-patch submission guidelines for the Linux kernel.
-
-However there are some differences, so please review and familiarize
-yourself with the following relevant bits.
-
-
-## Make separate commits for logically separate changes.
-
-Unless your patch is really trivial, you should not be sending out a patch that
-was generated between your working tree and your commit head.
-Instead, always make a commit with a complete
-[commit message][commit-message-style] and generate a series of patches from
-your repository.
-It is a good discipline.
-
-Describe the technical detail of the change(s).
-
-If your description starts to get too long, that's a sign that you
-probably need to split up your commit to finer grained pieces.
-
-
-## Linting and formatting code
-
-Lint any changes by running:
-```sh
-$ tox -e lint -- file.py
-```
-
-And format with:
-```sh
-$ tox -e format -- file.py
-```
-
-Or format everything:
-```sh
-$ tox -e format
-```
-
-Repo uses [black](https://black.readthedocs.io/) with line length of 80 as its
-formatter and flake8 as its linter. Repo also follows
-[Google's Python Style Guide].
-
-There should be no new errors or warnings introduced.
-
-Warnings that cannot be avoided without going against the Google Style Guide
-may be suppressed inline individally using a `# noqa` comment as described
-in the [flake8 documentation].
-
-If there are many occurrences of the same warning, these may be suppressed for
-the entire project in the included `.flake8` file.
-
-[Google's Python Style Guide]: https://google.github.io/styleguide/pyguide.html
-[PEP 8]: https://www.python.org/dev/peps/pep-0008/
-[flake8 documentation]: 
https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html#in-line-ignoring-errors
-
-## Running tests
-
-We use [pytest](https://pytest.org/) and [tox](https://tox.readthedocs.io/) for
-running tests.  You should make sure to install those first.
-
-To run the full suite against all supported Python versions, simply execute:
-```sh
-$ tox -p auto
-```
-
-We have [`./run_tests`](./run_tests) which is a simple wrapper around `pytest`:
-```sh
-# Run the full suite against the default Python version.
-$ ./run_tests
-# List each test as it runs.
-$ ./run_tests -v
-
-# Run a specific unittest module (and all tests in it).
-$ ./run_tests tests/test_git_command.py
-
-# Run a specific testsuite in a specific unittest module.
-$ ./run_tests tests/test_editor.py::EditString
-
-# Run a single test.
-$ ./run_tests tests/test_editor.py::EditString::test_cat_editor
-
-# List all available tests.
-$ ./run_tests --collect-only
-
-# Run a single test using substring match.
-$ ./run_tests -k test_cat_editor
-```
-
-The coverage isn't great currently, but it should still be run for all commits.
-Adding more unittests for changes you make would be greatly appreciated :).
-Check out the [tests/](./tests/) subdirectory for more details.
-
-
-## Check the license
-
-repo is licensed under the Apache License, 2.0.
-
-Because of this licensing model *every* file within the project
-*must* list the license that covers it in the header of the file.
-Any new contributions to an existing file *must* be submitted under
-the current license of that file.  Any new files *must* clearly
-indicate which license they are provided under in the file header.
-
-Please verify that you are legally allowed and willing to submit your
-changes under the license covering each file *prior* to submitting
-your patch.  It is virtually impossible to remove a patch once it
-has been applied and pushed out.
-
-
-## Sending your patches.
-
-Do not email your patches to anyone.
-
-Instead, login to the Gerrit Code Review tool at:
-
-  https://gerrit-review.googlesource.com/
-
-Ensure you have completed one of the necessary contributor
-agreements, providing documentation to the project maintainers that
-they have right to redistribute your work under the Apache License:
-
-  https://gerrit-review.googlesource.com/#/settings/agreements
-
-Ensure you have obtained an HTTP password to authenticate:
-
-  https://gerrit-review.googlesource.com/new-password
-
-Ensure that you have the local commit hook installed to automatically
-add a ChangeId to your commits:
-
-    curl -Lo `git rev-parse --git-dir`/hooks/commit-msg 
https://gerrit-review.googlesource.com/tools/hooks/commit-msg
-    chmod +x `git rev-parse --git-dir`/hooks/commit-msg
-
-If you have already committed your changes you will need to amend the commit
-to get the ChangeId added.
-
-    git commit --amend
-
-Push your patches over HTTPS to the review server, possibly through
-a remembered remote to make this easier in the future:
-
-    git config remote.review.url 
https://gerrit-review.googlesource.com/git-repo
-    git config remote.review.push HEAD:refs/for/main
-
-    git push review
-
-You will be automatically emailed a copy of your commits, and any
-comments made by the project maintainers.
-
-
-## Make changes if requested
-
-The project maintainer who reviews your changes might request changes to your
-commit. If you make the requested changes you will need to amend your commit
-and push it to the review server again.
-
-
-## Verify your changes on Gerrit {#verify}
-
-After you receive a Code-Review+2 from the maintainer, select the Verified
-button on the Gerrit page for the change. This verifies that you have tested
-your changes and notifies the maintainer that they are ready to be submitted.
-
-## Merge your changes via the commit queue {#cq}
-
-Once a change is ready to be merged, select the Commit-Queue+2 setting on the
-Gerrit page for it. This tells the CI system to test the change, and if it
-passes all the checks, automatically merges it.
-
-[commit-message-style]: https://chris.beams.io/posts/git-commit/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/completion.bash 
new/git-repo-2.59/completion.bash
--- old/git-repo-2.58/completion.bash   2025-08-15 01:35:26.000000000 +0200
+++ new/git-repo-2.59/completion.bash   2025-10-20 20:28:21.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright 2021 The Android Open Source Project
+# Copyright (C) 2021 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/git_ssh new/git-repo-2.59/git_ssh
--- old/git-repo-2.58/git_ssh   2025-08-15 01:35:26.000000000 +0200
+++ new/git-repo-2.59/git_ssh   2025-10-20 20:28:21.000000000 +0200
@@ -1,5 +1,4 @@
 #!/bin/sh
-#
 # Copyright (C) 2009 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/git_trace2_event_log.py 
new/git-repo-2.59/git_trace2_event_log.py
--- old/git-repo-2.58/git_trace2_event_log.py   2025-08-15 01:35:26.000000000 
+0200
+++ new/git-repo-2.59/git_trace2_event_log.py   2025-10-20 20:28:21.000000000 
+0200
@@ -1,3 +1,19 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Event logging in the git trace2 EVENT format."""
+
 from git_command import GetEventTargetPath
 from git_command import RepoSourceVersion
 from git_trace2_event_log_base import BaseEventLog
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/main.py new/git-repo-2.59/main.py
--- old/git-repo-2.58/main.py   2025-08-15 01:35:26.000000000 +0200
+++ new/git-repo-2.59/main.py   2025-10-20 20:28:21.000000000 +0200
@@ -1,5 +1,4 @@
 #!/usr/bin/env python3
-#
 # Copyright (C) 2008 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/man/repo-smartsync.1 
new/git-repo-2.59/man/repo-smartsync.1
--- old/git-repo-2.58/man/repo-smartsync.1      2025-08-15 01:35:26.000000000 
+0200
+++ new/git-repo-2.59/man/repo-smartsync.1      2025-10-20 20:28:21.000000000 
+0200
@@ -1,5 +1,5 @@
 .\" DO NOT MODIFY THIS FILE!  It was generated by help2man.
-.TH REPO "1" "June 2025" "repo smartsync" "Repo Manual"
+.TH REPO "1" "August 2025" "repo smartsync" "Repo Manual"
 .SH NAME
 repo \- repo smartsync - manual page for repo smartsync
 .SH SYNOPSIS
@@ -20,12 +20,11 @@
 .TP
 \fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
 number of network jobs to run in parallel (defaults to
-\fB\-\-jobs\fR or 1). Ignored when \fB\-\-interleaved\fR is set
+\fB\-\-jobs\fR or 1). Ignored unless \fB\-\-no\-interleaved\fR is set
 .TP
 \fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
 number of local checkout jobs to run in parallel
-(defaults to \fB\-\-jobs\fR or 8). Ignored when \fB\-\-interleaved\fR
-is set
+(defaults to \fB\-\-jobs\fR or 8). Ignored unless \fB\-\-nointerleaved\fR is 
set
 .TP
 \fB\-f\fR, \fB\-\-force\-broken\fR
 obsolete option (to be deleted in the future)
@@ -60,7 +59,10 @@
 update to the latest revision)
 .TP
 \fB\-\-interleaved\fR
-fetch and checkout projects in parallel (experimental)
+fetch and checkout projects in parallel (default)
+.TP
+\fB\-\-no\-interleaved\fR
+fetch and checkout projects in phases
 .TP
 \fB\-n\fR, \fB\-\-network\-only\fR
 fetch only, don't update working tree
@@ -149,6 +151,16 @@
 .TP
 \fB\-\-no\-repo\-verify\fR
 do not verify repo source code
+.SS post\-sync hooks:
+.TP
+\fB\-\-no\-verify\fR
+Do not run the post\-sync hook.
+.TP
+\fB\-\-verify\fR
+Run the post\-sync hook without prompting.
+.TP
+\fB\-\-ignore\-hooks\fR
+Do not abort if post\-sync hooks fail.
 .PP
 Run `repo help smartsync` to view the detailed manual.
 .SH DETAILS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/man/repo-sync.1 
new/git-repo-2.59/man/repo-sync.1
--- old/git-repo-2.58/man/repo-sync.1   2025-08-15 01:35:26.000000000 +0200
+++ new/git-repo-2.59/man/repo-sync.1   2025-10-20 20:28:21.000000000 +0200
@@ -1,5 +1,5 @@
 .\" DO NOT MODIFY THIS FILE!  It was generated by help2man.
-.TH REPO "1" "June 2025" "repo sync" "Repo Manual"
+.TH REPO "1" "August 2025" "repo sync" "Repo Manual"
 .SH NAME
 repo \- repo sync - manual page for repo sync
 .SH SYNOPSIS
@@ -20,12 +20,11 @@
 .TP
 \fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
 number of network jobs to run in parallel (defaults to
-\fB\-\-jobs\fR or 1). Ignored when \fB\-\-interleaved\fR is set
+\fB\-\-jobs\fR or 1). Ignored unless \fB\-\-no\-interleaved\fR is set
 .TP
 \fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
 number of local checkout jobs to run in parallel
-(defaults to \fB\-\-jobs\fR or 8). Ignored when \fB\-\-interleaved\fR
-is set
+(defaults to \fB\-\-jobs\fR or 8). Ignored unless \fB\-\-nointerleaved\fR is 
set
 .TP
 \fB\-f\fR, \fB\-\-force\-broken\fR
 obsolete option (to be deleted in the future)
@@ -60,7 +59,10 @@
 update to the latest revision)
 .TP
 \fB\-\-interleaved\fR
-fetch and checkout projects in parallel (experimental)
+fetch and checkout projects in parallel (default)
+.TP
+\fB\-\-no\-interleaved\fR
+fetch and checkout projects in phases
 .TP
 \fB\-n\fR, \fB\-\-network\-only\fR
 fetch only, don't update working tree
@@ -156,6 +158,16 @@
 .TP
 \fB\-\-no\-repo\-verify\fR
 do not verify repo source code
+.SS post\-sync hooks:
+.TP
+\fB\-\-no\-verify\fR
+Do not run the post\-sync hook.
+.TP
+\fB\-\-verify\fR
+Run the post\-sync hook without prompting.
+.TP
+\fB\-\-ignore\-hooks\fR
+Do not abort if post\-sync hooks fail.
 .PP
 Run `repo help sync` to view the detailed manual.
 .SH DETAILS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/project.py new/git-repo-2.59/project.py
--- old/git-repo-2.58/project.py        2025-08-15 01:35:26.000000000 +0200
+++ new/git-repo-2.59/project.py        2025-10-20 20:28:21.000000000 +0200
@@ -642,10 +642,6 @@
         # project containing repo hooks.
         self.enabled_repo_hooks = []
 
-        # This will be updated later if the project has submodules and
-        # if they will be synced.
-        self.has_subprojects = False
-
     def RelPath(self, local=True):
         """Return the path for the project relative to a manifest.
 
@@ -1563,8 +1559,8 @@
         # TODO(https://git-scm.com/docs/git-worktree#_bugs): Re-evaluate if
         # submodules can be init when using worktrees once its support is
         # complete.
-        if self.has_subprojects and not self.use_git_worktrees:
-            self._InitSubmodules()
+        if self.parent and not self.use_git_worktrees:
+            self._InitSubmodule()
         all_refs = self.bare_ref.all
         self.CleanPublishedCache(all_refs)
         revid = self.GetRevisionId(all_refs)
@@ -1593,6 +1589,9 @@
             self._FastForward(revid)
             self._CopyAndLinkFiles()
 
+        def _dorebase():
+            self._Rebase(upstream="@{upstream}")
+
         def _dosubmodules():
             self._SyncSubmodules(quiet=True)
 
@@ -1684,19 +1683,24 @@
         if pub:
             not_merged = self._revlist(not_rev(revid), pub)
             if not_merged:
-                if upstream_gain and not force_rebase:
-                    # The user has published this branch and some of those
-                    # commits are not yet merged upstream.  We do not want
-                    # to rewrite the published commits so we punt.
-                    fail(
-                        LocalSyncFail(
-                            "branch %s is published (but not merged) and is "
-                            "now %d commits behind. Fix this manually or rerun 
"
-                            "with the --rebase option to force a rebase."
-                            % (branch.name, len(upstream_gain)),
-                            project=self.name,
+                if upstream_gain:
+                    if force_rebase:
+                        # Try to rebase local published but not merged changes
+                        # on top of the upstream changes.
+                        syncbuf.later1(self, _dorebase, not verbose)
+                    else:
+                        # The user has published this branch and some of those
+                        # commits are not yet merged upstream.  We do not want
+                        # to rewrite the published commits so we punt.
+                        fail(
+                            LocalSyncFail(
+                                "branch %s is published (but not merged) and "
+                                "is now %d commits behind. Fix this manually "
+                                "or rerun with the --rebase option to force a "
+                                "rebase." % (branch.name, len(upstream_gain)),
+                                project=self.name,
+                            )
                         )
-                    )
                     return
                 syncbuf.later1(self, _doff, not verbose)
                 return
@@ -2359,8 +2363,6 @@
             )
             result.append(subproject)
             result.extend(subproject.GetDerivedSubprojects())
-        if result:
-            self.has_subprojects = True
         return result
 
     def EnableRepositoryExtension(self, key, value="true", version=1):
@@ -2411,7 +2413,9 @@
             # throws an error.
             revs = [f"{self.revisionExpr}^0"]
             upstream_rev = None
-            if self.upstream:
+
+            # Only check upstream when using superproject.
+            if self.upstream and 
self.manifest.manifestProject.use_superproject:
                 upstream_rev = self.GetRemote().ToLocal(self.upstream)
                 revs.append(upstream_rev)
 
@@ -2423,7 +2427,9 @@
                 log_as_error=False,
             )
 
-            if self.upstream:
+            # Only verify upstream relationship for superproject scenarios
+            # without affecting plain usage.
+            if self.upstream and 
self.manifest.manifestProject.use_superproject:
                 self.bare_git.merge_base(
                     "--is-ancestor",
                     self.revisionExpr,
@@ -3026,16 +3032,39 @@
                 project=self.name,
             )
 
-    def _InitSubmodules(self, quiet=True):
-        """Initialize the submodules for the project."""
+    def _InitSubmodule(self, quiet=True):
+        """Initialize the submodule."""
         cmd = ["submodule", "init"]
         if quiet:
             cmd.append("-q")
-        if GitCommand(self, cmd).Wait() != 0:
-            raise GitError(
-                f"{self.name} submodule init",
-                project=self.name,
+        cmd.extend(["--", self.worktree])
+        max_retries = 3
+        base_delay_secs = 1
+        jitter_ratio = 1 / 3
+        for attempt in range(max_retries):
+            git_cmd = GitCommand(
+                None,
+                cmd,
+                cwd=self.parent.worktree,
+                capture_stdout=True,
+                capture_stderr=True,
             )
+            if git_cmd.Wait() == 0:
+                return
+            error = git_cmd.stderr or git_cmd.stdout
+            if "lock" in error:
+                delay = base_delay_secs * (2**attempt)
+                delay += random.uniform(0, delay * jitter_ratio)
+                logger.warning(
+                    f"Attempt {attempt+1}/{max_retries}: "
+                    + f"git {' '.join(cmd)} failed."
+                    + f" Error: {error}."
+                    + f" Sleeping {delay:.2f}s before retrying."
+                )
+                time.sleep(delay)
+            else:
+                break
+        git_cmd.VerifyCommand()
 
     def _Rebase(self, upstream, onto=None):
         cmd = ["rebase"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/pyproject.toml 
new/git-repo-2.59/pyproject.toml
--- old/git-repo-2.58/pyproject.toml    2025-08-15 01:35:26.000000000 +0200
+++ new/git-repo-2.59/pyproject.toml    2025-10-20 20:28:21.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright 2023 The Android Open Source Project
+# Copyright (C) 2023 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/release/check-metadata.py 
new/git-repo-2.59/release/check-metadata.py
--- old/git-repo-2.58/release/check-metadata.py 1970-01-01 01:00:00.000000000 
+0100
+++ new/git-repo-2.59/release/check-metadata.py 2025-10-20 20:28:21.000000000 
+0200
@@ -0,0 +1,152 @@
+#!/usr/bin/env python3
+# Copyright (C) 2025 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Helper tool to check various metadata (e.g. licensing) in source files."""
+
+import argparse
+from pathlib import Path
+import re
+import sys
+
+import util
+
+
+_FILE_HEADER_RE = re.compile(
+    r"""# Copyright \(C\) 20[0-9]{2} The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2\.0 \(the "License"\);
+# you may not use this file except in compliance with the License\.
+# You may obtain a copy of the License at
+#
+#      http://www\.apache\.org/licenses/LICENSE-2\.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.
+# See the License for the specific language governing permissions and
+# limitations under the License\.
+"""
+)
+
+
+def check_license(path: Path, lines: list[str]) -> bool:
+    """Check license header."""
+    # Enforce licensing on configs & scripts.
+    if not (
+        path.suffix in (".bash", ".cfg", ".ini", ".py", ".toml")
+        or lines[0] in ("#!/bin/bash", "#!/bin/sh", "#!/usr/bin/env python3")
+    ):
+        return True
+
+    # Extract the file header.
+    header_lines = []
+    for line in lines:
+        if line.startswith("#"):
+            header_lines.append(line)
+        else:
+            break
+    if not header_lines:
+        print(
+            f"error: {path.relative_to(util.TOPDIR)}: "
+            "missing file header (copyright+licensing)",
+            file=sys.stderr,
+        )
+        return False
+
+    # Skip the shebang.
+    if header_lines[0].startswith("#!"):
+        header_lines.pop(0)
+
+    # If this file is imported into the tree, then leave it be.
+    if header_lines[0] == "# DO NOT EDIT THIS FILE":
+        return True
+
+    header = "".join(f"{x}\n" for x in header_lines)
+    if not _FILE_HEADER_RE.match(header):
+        print(
+            f"error: {path.relative_to(util.TOPDIR)}: "
+            "file header incorrectly formatted",
+            file=sys.stderr,
+        )
+        print(
+            "".join(f"> {x}\n" for x in header_lines), end="", file=sys.stderr
+        )
+        return False
+
+    return True
+
+
+def check_path(opts: argparse.Namespace, path: Path) -> bool:
+    """Check a single path."""
+    data = path.read_text(encoding="utf-8")
+    lines = data.splitlines()
+    # NB: Use list comprehension and not a generator so we run all the checks.
+    return all(
+        [
+            check_license(path, lines),
+        ]
+    )
+
+
+def check_paths(opts: argparse.Namespace, paths: list[Path]) -> bool:
+    """Check all the paths."""
+    # NB: Use list comprehension and not a generator so we check all paths.
+    return all([check_path(opts, x) for x in paths])
+
+
+def find_files(opts: argparse.Namespace) -> list[Path]:
+    """Find all the files in the source tree."""
+    result = util.run(
+        opts,
+        ["git", "ls-tree", "-r", "-z", "--name-only", "HEAD"],
+        cwd=util.TOPDIR,
+        capture_output=True,
+        encoding="utf-8",
+    )
+    return [util.TOPDIR / x for x in result.stdout.split("\0")[:-1]]
+
+
+def get_parser() -> argparse.ArgumentParser:
+    """Get a CLI parser."""
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument(
+        "-n",
+        "--dry-run",
+        dest="dryrun",
+        action="store_true",
+        help="show everything that would be done",
+    )
+    parser.add_argument(
+        "paths",
+        nargs="*",
+        help="the paths to scan",
+    )
+    return parser
+
+
+def main(argv: list[str]) -> int:
+    """The main func!"""
+    parser = get_parser()
+    opts = parser.parse_args(argv)
+
+    paths = opts.paths
+    if not opts.paths:
+        paths = find_files(opts)
+
+    return 0 if check_paths(opts, paths) else 1
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/release/util.py 
new/git-repo-2.59/release/util.py
--- old/git-repo-2.58/release/util.py   2025-08-15 01:35:26.000000000 +0200
+++ new/git-repo-2.59/release/util.py   2025-10-20 20:28:21.000000000 +0200
@@ -14,7 +14,7 @@
 
 """Random utility code for release tools."""
 
-import os
+from pathlib import Path
 import re
 import shlex
 import subprocess
@@ -24,8 +24,9 @@
 assert sys.version_info >= (3, 6), "This module requires Python 3.6+"
 
 
-TOPDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-HOMEDIR = os.path.expanduser("~")
+THIS_FILE = Path(__file__).resolve()
+TOPDIR = THIS_FILE.parent.parent
+HOMEDIR = Path("~").expanduser()
 
 
 # These are the release keys we sign with.
@@ -54,7 +55,7 @@
 def import_release_key(opts):
     """Import the public key of the official release repo signing key."""
     # Extract the key from our repo launcher.
-    launcher = getattr(opts, "launcher", os.path.join(TOPDIR, "repo"))
+    launcher = getattr(opts, "launcher", TOPDIR / "repo")
     print(f'Importing keys from "{launcher}" launcher script')
     with open(launcher, encoding="utf-8") as fp:
         data = fp.read()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/repo new/git-repo-2.59/repo
--- old/git-repo-2.58/repo      2025-08-15 01:35:26.000000000 +0200
+++ new/git-repo-2.59/repo      2025-10-20 20:28:21.000000000 +0200
@@ -1,5 +1,4 @@
 #!/usr/bin/env python3
-#
 # Copyright (C) 2008 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/run_tests new/git-repo-2.59/run_tests
--- old/git-repo-2.58/run_tests 2025-08-15 01:35:26.000000000 +0200
+++ new/git-repo-2.59/run_tests 2025-10-20 20:28:21.000000000 +0200
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2019 The Android Open Source Project
+# Copyright (C) 2019 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
 
 import functools
 import os
+import shlex
 import shutil
 import subprocess
 import sys
@@ -26,6 +27,11 @@
 ROOT_DIR = os.path.dirname(os.path.realpath(__file__))
 
 
+def log_cmd(cmd: str, argv: list[str]) -> None:
+    """Log a debug message to make history easier to track."""
+    print("+", cmd, shlex.join(argv), file=sys.stderr)
+
+
 @functools.lru_cache()
 def is_ci() -> bool:
     """Whether we're running in our CI system."""
@@ -37,6 +43,7 @@
     if is_ci():
         argv = ["-m", "not skip_cq"] + argv
 
+    log_cmd("pytest", argv)
     return subprocess.run(
         [sys.executable, "-m", "pytest"] + argv,
         check=False,
@@ -49,6 +56,7 @@
     if is_ci():
         argv = ["-m", "not skip_cq"] + argv
 
+    log_cmd("[vpython 3.8] pytest", argv)
     try:
         return subprocess.run(
             [
@@ -77,8 +85,10 @@
         "release/update-hooks",
         "release/update-manpages",
     ]
+    argv = ["--diff", "--check", ROOT_DIR] + extra_programs
+    log_cmd("black", argv)
     return subprocess.run(
-        [sys.executable, "-m", "black", "--check", ROOT_DIR] + extra_programs,
+        [sys.executable, "-m", "black"] + argv,
         check=False,
         cwd=ROOT_DIR,
     ).returncode
@@ -86,8 +96,10 @@
 
 def run_flake8():
     """Returns the exit code from flake8."""
+    argv = [ROOT_DIR]
+    log_cmd("flake8", argv)
     return subprocess.run(
-        [sys.executable, "-m", "flake8", ROOT_DIR],
+        [sys.executable, "-m", "flake8"] + argv,
         check=False,
         cwd=ROOT_DIR,
     ).returncode
@@ -95,8 +107,21 @@
 
 def run_isort():
     """Returns the exit code from isort."""
+    argv = ["--check", ROOT_DIR]
+    log_cmd("isort", argv)
+    return subprocess.run(
+        [sys.executable, "-m", "isort"] + argv,
+        check=False,
+        cwd=ROOT_DIR,
+    ).returncode
+
+
+def run_check_metadata():
+    """Returns the exit code from check-metadata."""
+    argv = []
+    log_cmd("release/check-metadata.py", argv)
     return subprocess.run(
-        [sys.executable, "-m", "isort", "--check", ROOT_DIR],
+        [sys.executable, "release/check-metadata.py"] + argv,
         check=False,
         cwd=ROOT_DIR,
     ).returncode
@@ -109,8 +134,10 @@
         print("update-manpages: help2man not found; skipping test")
         return 0
 
+    argv = ["--check"]
+    log_cmd("release/update-manpages", argv)
     return subprocess.run(
-        [sys.executable, "release/update-manpages", "--check"],
+        [sys.executable, "release/update-manpages"] + argv,
         check=False,
         cwd=ROOT_DIR,
     ).returncode
@@ -124,6 +151,7 @@
         run_black,
         run_flake8,
         run_isort,
+        run_check_metadata,
         run_update_manpages,
     )
     # Run all the tests all the time to get full feedback.  Don't exit on the
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/setup.py new/git-repo-2.59/setup.py
--- old/git-repo-2.58/setup.py  2025-08-15 01:35:26.000000000 +0200
+++ new/git-repo-2.59/setup.py  2025-10-20 20:28:21.000000000 +0200
@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
-# Copyright 2019 The Android Open Source Project
+# Copyright (C) 2019 The Android Open Source Project
 #
-# Licensed under the Apache License, Version 2.0 (the 'License");
+# Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # You may obtain a copy of the License at
 #
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/subcmds/forall.py 
new/git-repo-2.59/subcmds/forall.py
--- old/git-repo-2.58/subcmds/forall.py 2025-08-15 01:35:26.000000000 +0200
+++ new/git-repo-2.59/subcmds/forall.py 2025-10-20 20:28:21.000000000 +0200
@@ -133,7 +133,7 @@
 
     @staticmethod
     def _cmd_option(option, _opt_str, _value, parser):
-        setattr(parser.values, option.dest or "command", list(parser.rargs))
+        setattr(parser.values, option.dest, list(parser.rargs))
         while parser.rargs:
             del parser.rargs[0]
 
@@ -161,6 +161,7 @@
         p.add_option(
             "-c",
             "--command",
+            dest="command",
             help="command (and arguments) to execute",
             action="callback",
             callback=self._cmd_option,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/subcmds/sync.py 
new/git-repo-2.59/subcmds/sync.py
--- old/git-repo-2.58/subcmds/sync.py   2025-08-15 01:35:26.000000000 +0200
+++ new/git-repo-2.59/subcmds/sync.py   2025-10-20 20:28:21.000000000 +0200
@@ -975,9 +975,6 @@
                 sync_event.set()
                 sync_progress_thread.join()
 
-        self._fetch_times.Save()
-        self._local_sync_state.Save()
-
         if not self.outer_client.manifest.IsArchive:
             self._GCProjects(projects, opt, err_event)
 
@@ -1003,53 +1000,58 @@
         to_fetch.extend(all_projects)
         to_fetch.sort(key=self._fetch_times.Get, reverse=True)
 
-        result = self._Fetch(to_fetch, opt, err_event, ssh_proxy, errors)
-        success = result.success
-        fetched = result.projects
-        if not success:
-            err_event.set()
-
-        if opt.network_only:
-            # Bail out now; the rest touches the working tree.
-            if err_event.is_set():
-                e = SyncError(
-                    "error: Exited sync due to fetch errors.",
-                    aggregate_errors=errors,
-                )
-
-                logger.error(e)
-                raise e
-            return _FetchMainResult([])
-
-        # Iteratively fetch missing and/or nested unregistered submodules.
-        previously_missing_set = set()
-        while True:
-            self._ReloadManifest(None, manifest)
-            all_projects = self.GetProjects(
-                args,
-                missing_ok=True,
-                submodules_ok=opt.fetch_submodules,
-                manifest=manifest,
-                all_manifests=not opt.this_manifest_only,
-            )
-            missing = []
-            for project in all_projects:
-                if project.gitdir not in fetched:
-                    missing.append(project)
-            if not missing:
-                break
-            # Stop us from non-stopped fetching actually-missing repos: If set
-            # of missing repos has not been changed from last fetch, we break.
-            missing_set = {p.name for p in missing}
-            if previously_missing_set == missing_set:
-                break
-            previously_missing_set = missing_set
-            result = self._Fetch(missing, opt, err_event, ssh_proxy, errors)
+        try:
+            result = self._Fetch(to_fetch, opt, err_event, ssh_proxy, errors)
             success = result.success
-            new_fetched = result.projects
+            fetched = result.projects
             if not success:
                 err_event.set()
-            fetched.update(new_fetched)
+
+            if opt.network_only:
+                # Bail out now; the rest touches the working tree.
+                if err_event.is_set():
+                    e = SyncError(
+                        "error: Exited sync due to fetch errors.",
+                        aggregate_errors=errors,
+                    )
+
+                    logger.error(e)
+                    raise e
+                return _FetchMainResult([])
+
+            # Iteratively fetch missing and/or nested unregistered submodules.
+            previously_missing_set = set()
+            while True:
+                self._ReloadManifest(None, manifest)
+                all_projects = self.GetProjects(
+                    args,
+                    missing_ok=True,
+                    submodules_ok=opt.fetch_submodules,
+                    manifest=manifest,
+                    all_manifests=not opt.this_manifest_only,
+                )
+                missing = []
+                for project in all_projects:
+                    if project.gitdir not in fetched:
+                        missing.append(project)
+                if not missing:
+                    break
+                # Stop us from non-stopped fetching actually-missing repos: If
+                # set of missing repos has not been changed from last fetch, we
+                # break.
+                missing_set = {p.name for p in missing}
+                if previously_missing_set == missing_set:
+                    break
+                previously_missing_set = missing_set
+                result = self._Fetch(missing, opt, err_event, ssh_proxy, 
errors)
+                success = result.success
+                new_fetched = result.projects
+                if not success:
+                    err_event.set()
+                fetched.update(new_fetched)
+        finally:
+            self._fetch_times.Save()
+            self._local_sync_state.Save()
 
         return _FetchMainResult(all_projects)
 
@@ -2491,107 +2493,120 @@
         sync_event = _threading.Event()
         sync_progress_thread = self._CreateSyncProgressThread(pm, sync_event)
 
-        with multiprocessing.Manager() as manager, ssh.ProxyManager(
-            manager
-        ) as ssh_proxy:
-            ssh_proxy.sock()
-            with self.ParallelContext():
-                self.get_parallel_context()["ssh_proxy"] = ssh_proxy
-                # TODO(gavinmak): Use multprocessing.Queue instead of dict.
-                self.get_parallel_context()[
-                    "sync_dict"
-                ] = multiprocessing.Manager().dict()
-                sync_progress_thread.start()
+        try:
+            with multiprocessing.Manager() as manager, ssh.ProxyManager(
+                manager
+            ) as ssh_proxy:
+                ssh_proxy.sock()
+                with self.ParallelContext():
+                    self.get_parallel_context()["ssh_proxy"] = ssh_proxy
+                    # TODO(gavinmak): Use multprocessing.Queue instead of dict.
+                    self.get_parallel_context()[
+                        "sync_dict"
+                    ] = multiprocessing.Manager().dict()
+                    sync_progress_thread.start()
 
-                try:
-                    # Outer loop for dynamic project discovery. This continues
-                    # until no unsynced projects remain.
-                    while True:
-                        projects_to_sync = [
-                            p
-                            for p in project_list
-                            if p.relpath not in finished_relpaths
-                        ]
-                        if not projects_to_sync:
-                            break
-
-                        pending_relpaths = {p.relpath for p in 
projects_to_sync}
-                        if previously_pending_relpaths == pending_relpaths:
-                            stalled_projects_str = "\n".join(
-                                f" - {path}"
-                                for path in sorted(list(pending_relpaths))
-                            )
-                            logger.error(
-                                "The following projects failed and could not "
-                                "be synced:\n%s",
-                                stalled_projects_str,
-                            )
-                            err_event.set()
-                            break
-                        previously_pending_relpaths = pending_relpaths
-
-                        self.get_parallel_context()[
-                            "projects"
-                        ] = projects_to_sync
-                        project_index_map = {
-                            p: i for i, p in enumerate(projects_to_sync)
-                        }
-
-                        # Inner loop to process projects in a hierarchical
-                        # order. This iterates through levels of project
-                        # dependencies (e.g. 'foo' then 'foo/bar'). All 
projects
-                        # in one level can be processed in parallel, but we 
must
-                        # wait for a level to complete before starting the 
next.
-                        for level_projects in _SafeCheckoutOrder(
-                            projects_to_sync
-                        ):
-                            if not level_projects:
-                                continue
-
-                            objdir_project_map = collections.defaultdict(list)
-                            for p in level_projects:
-                                objdir_project_map[p.objdir].append(
-                                    project_index_map[p]
+                    try:
+                        # Outer loop for dynamic project discovery. This
+                        # continues until no unsynced projects remain.
+                        while True:
+                            projects_to_sync = [
+                                p
+                                for p in project_list
+                                if p.relpath not in finished_relpaths
+                            ]
+                            if not projects_to_sync:
+                                break
+
+                            pending_relpaths = {
+                                p.relpath for p in projects_to_sync
+                            }
+                            if previously_pending_relpaths == pending_relpaths:
+                                stalled_projects_str = "\n".join(
+                                    f" - {path}"
+                                    for path in sorted(list(pending_relpaths))
+                                )
+                                logger.error(
+                                    "The following projects failed and could "
+                                    "not be synced:\n%s",
+                                    stalled_projects_str,
                                 )
-
-                            work_items = list(objdir_project_map.values())
-                            if not work_items:
-                                continue
-
-                            jobs = max(1, min(opt.jobs, len(work_items)))
-                            callback = functools.partial(
-                                self._ProcessSyncInterleavedResults,
-                                finished_relpaths,
-                                err_event,
-                                errors,
-                                opt,
-                            )
-                            if not self.ExecuteInParallel(
-                                jobs,
-                                functools.partial(self._SyncProjectList, opt),
-                                work_items,
-                                callback=callback,
-                                output=pm,
-                                chunksize=1,
-                                initializer=self.InitWorker,
-                            ):
                                 err_event.set()
+                                break
+                            previously_pending_relpaths = pending_relpaths
 
-                            if err_event.is_set() and opt.fail_fast:
-                                raise 
SyncFailFastError(aggregate_errors=errors)
+                            self.get_parallel_context()[
+                                "projects"
+                            ] = projects_to_sync
+                            project_index_map = {
+                                p: i for i, p in enumerate(projects_to_sync)
+                            }
+
+                            # Inner loop to process projects in a hierarchical
+                            # order. This iterates through levels of project
+                            # dependencies (e.g. 'foo' then 'foo/bar'). All
+                            # projects in one level can be processed in
+                            # parallel, but we must wait for a level to 
complete
+                            # before starting the next.
+                            for level_projects in _SafeCheckoutOrder(
+                                projects_to_sync
+                            ):
+                                if not level_projects:
+                                    continue
 
-                        self._ReloadManifest(None, manifest)
-                        project_list = self.GetProjects(
-                            args,
-                            missing_ok=True,
-                            submodules_ok=opt.fetch_submodules,
-                            manifest=manifest,
-                            all_manifests=not opt.this_manifest_only,
-                        )
-                        pm.update_total(len(project_list))
-                finally:
-                    sync_event.set()
-                    sync_progress_thread.join()
+                                objdir_project_map = collections.defaultdict(
+                                    list
+                                )
+                                for p in level_projects:
+                                    objdir_project_map[p.objdir].append(
+                                        project_index_map[p]
+                                    )
+
+                                work_items = list(objdir_project_map.values())
+                                if not work_items:
+                                    continue
+
+                                jobs = max(1, min(opt.jobs, len(work_items)))
+                                callback = functools.partial(
+                                    self._ProcessSyncInterleavedResults,
+                                    finished_relpaths,
+                                    err_event,
+                                    errors,
+                                    opt,
+                                )
+                                if not self.ExecuteInParallel(
+                                    jobs,
+                                    functools.partial(
+                                        self._SyncProjectList, opt
+                                    ),
+                                    work_items,
+                                    callback=callback,
+                                    output=pm,
+                                    chunksize=1,
+                                    initializer=self.InitWorker,
+                                ):
+                                    err_event.set()
+
+                                if err_event.is_set() and opt.fail_fast:
+                                    raise SyncFailFastError(
+                                        aggregate_errors=errors
+                                    )
+
+                            self._ReloadManifest(None, manifest)
+                            project_list = self.GetProjects(
+                                args,
+                                missing_ok=True,
+                                submodules_ok=opt.fetch_submodules,
+                                manifest=manifest,
+                                all_manifests=not opt.this_manifest_only,
+                            )
+                            pm.update_total(len(project_list))
+                    finally:
+                        sync_event.set()
+                        sync_progress_thread.join()
+        finally:
+            self._fetch_times.Save()
+            self._local_sync_state.Save()
 
         pm.end()
 
@@ -2695,17 +2710,19 @@
                 self._saved = {}
 
     def Save(self):
-        if self._saved is None:
+        if not self._seen:
             return
 
+        self._Load()
+
         for name, t in self._seen.items():
             # Keep a moving average across the previous/current sync runs.
             old = self._saved.get(name, t)
-            self._seen[name] = (self._ALPHA * t) + ((1 - self._ALPHA) * old)
+            self._saved[name] = (self._ALPHA * t) + ((1 - self._ALPHA) * old)
 
         try:
             with open(self._path, "w") as f:
-                json.dump(self._seen, f, indent=2)
+                json.dump(self._saved, f, indent=2)
         except (OSError, TypeError):
             platform_utils.remove(self._path, missing_ok=True)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/tests/conftest.py 
new/git-repo-2.59/tests/conftest.py
--- old/git-repo-2.58/tests/conftest.py 2025-08-15 01:35:26.000000000 +0200
+++ new/git-repo-2.59/tests/conftest.py 2025-10-20 20:28:21.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright 2022 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/tests/test_error.py 
new/git-repo-2.59/tests/test_error.py
--- old/git-repo-2.58/tests/test_error.py       2025-08-15 01:35:26.000000000 
+0200
+++ new/git-repo-2.59/tests/test_error.py       2025-10-20 20:28:21.000000000 
+0200
@@ -1,4 +1,4 @@
-# Copyright 2021 The Android Open Source Project
+# Copyright (C) 2021 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/tests/test_git_command.py 
new/git-repo-2.59/tests/test_git_command.py
--- old/git-repo-2.58/tests/test_git_command.py 2025-08-15 01:35:26.000000000 
+0200
+++ new/git-repo-2.59/tests/test_git_command.py 2025-10-20 20:28:21.000000000 
+0200
@@ -1,4 +1,4 @@
-# Copyright 2019 The Android Open Source Project
+# Copyright (C) 2019 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/tests/test_platform_utils.py 
new/git-repo-2.59/tests/test_platform_utils.py
--- old/git-repo-2.58/tests/test_platform_utils.py      2025-08-15 
01:35:26.000000000 +0200
+++ new/git-repo-2.59/tests/test_platform_utils.py      2025-10-20 
20:28:21.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright 2021 The Android Open Source Project
+# Copyright (C) 2021 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/tests/test_repo_trace.py 
new/git-repo-2.59/tests/test_repo_trace.py
--- old/git-repo-2.58/tests/test_repo_trace.py  2025-08-15 01:35:26.000000000 
+0200
+++ new/git-repo-2.59/tests/test_repo_trace.py  2025-10-20 20:28:21.000000000 
+0200
@@ -1,4 +1,4 @@
-# Copyright 2022 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/tests/test_ssh.py 
new/git-repo-2.59/tests/test_ssh.py
--- old/git-repo-2.58/tests/test_ssh.py 2025-08-15 01:35:26.000000000 +0200
+++ new/git-repo-2.59/tests/test_ssh.py 2025-10-20 20:28:21.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright 2019 The Android Open Source Project
+# Copyright (C) 2019 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/tests/test_subcmds.py 
new/git-repo-2.59/tests/test_subcmds.py
--- old/git-repo-2.58/tests/test_subcmds.py     2025-08-15 01:35:26.000000000 
+0200
+++ new/git-repo-2.59/tests/test_subcmds.py     2025-10-20 20:28:21.000000000 
+0200
@@ -94,7 +94,12 @@
         """Block redundant dest= arguments."""
 
         def _check_dest(opt):
-            if opt.dest is None or not opt._long_opts:
+            """Check the dest= setting."""
+            # If the destination is not set, nothing to check.
+            # If long options are not set, then there's no implicit 
destination.
+            # If callback is used, then a destination might be needed because
+            # optparse cannot assume a value is always stored.
+            if opt.dest is None or not opt._long_opts or opt.callback:
                 return
 
             long = opt._long_opts[0]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/tests/test_subcmds_sync.py 
new/git-repo-2.59/tests/test_subcmds_sync.py
--- old/git-repo-2.58/tests/test_subcmds_sync.py        2025-08-15 
01:35:26.000000000 +0200
+++ new/git-repo-2.59/tests/test_subcmds_sync.py        2025-10-20 
20:28:21.000000000 +0200
@@ -681,6 +681,9 @@
         # Mock _GetCurrentBranchOnly for worker tests.
         mock.patch.object(sync.Sync, "_GetCurrentBranchOnly").start()
 
+        self.cmd._fetch_times = mock.Mock()
+        self.cmd._local_sync_state = mock.Mock()
+
     def tearDown(self):
         """Clean up resources."""
         shutil.rmtree(self.repodir)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/tests/test_update_manpages.py 
new/git-repo-2.59/tests/test_update_manpages.py
--- old/git-repo-2.58/tests/test_update_manpages.py     2025-08-15 
01:35:26.000000000 +0200
+++ new/git-repo-2.59/tests/test_update_manpages.py     2025-10-20 
20:28:21.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright 2022 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-repo-2.58/tox.ini new/git-repo-2.59/tox.ini
--- old/git-repo-2.58/tox.ini   2025-08-15 01:35:26.000000000 +0200
+++ new/git-repo-2.59/tox.ini   2025-10-20 20:28:21.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright 2019 The Android Open Source Project
+# Copyright (C) 2019 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.

++++++ git-repo.obsinfo ++++++
--- /var/tmp/diff_new_pack.zjke7k/_old  2025-11-13 17:28:50.474857748 +0100
+++ /var/tmp/diff_new_pack.zjke7k/_new  2025-11-13 17:28:50.482858088 +0100
@@ -1,5 +1,5 @@
 name: git-repo
-version: 2.58
-mtime: 1755214526
-commit: 38d2fe11b9df521727fcca23c9dac086ce8378d3
+version: 2.59
+mtime: 1760984901
+commit: 1afe96a7e997ce7748f066b206a85ac648f7a87c
 

Reply via email to