Compare commits

...

112 Commits

Author SHA1 Message Date
13f323b2c2 event_log: turn id generation from a generator to a func call
Running lots of sync processes in parallel can hit the failure:
Fetching projects:  23% (124/523)Exception in thread Thread-201:
Traceback (most recent call last):
  File "/usr/lib/python2.7/threading.py", line 801, in __bootstrap_inner
    self.run()
  File "/usr/lib/python2.7/threading.py", line 754, in run
    self.__target(*self.__args, **self.__kwargs)
  File "/usr/local/src/repo/subcmds/sync.py", line 278, in _FetchProjectList
    success = self._FetchHelper(opt, project, *args, **kwargs)
  File "/usr/local/src/repo/subcmds/sync.py", line 357, in _FetchHelper
    start, finish, success)
  File "/usr/local/src/repo/event_log.py", line 104, in AddSync
    event = self.Add(project.relpath, task_name, start, finish, success)
  File "/usr/local/src/repo/event_log.py", line 74, in Add
    'id': (kind, next(self._next_id)),
ValueError: generator already executing

It looks like, while we lock the multiprocessing value correctly, the
generator that wraps the value isn't parallel safe.  Since we don't
have a way of doing that (as it's part of the language), turn it into
a plain function call instead.

Bug: https://crbug.com/gerrit/10293
Change-Id: I0db03601986ca0370a1699bab32adb03e7b2910a
2019-01-14 16:11:08 -05:00
12ee5446e9 init: Remove -c short option for --current-branch
This option conflicts with the gitc-init -c short option.

Bug: https://crbug.com/gerrit/10200
Change-Id: I06f37564429ca0bd4c0bbea6066daae4f663c838
2018-12-20 19:55:02 +00:00
e158e3802d Merge "README: link in new bug tracker" 2018-12-20 07:21:02 +00:00
3bbbcaf99d README: link in new bug tracker
Change-Id: I043afc0b77e709919e49ce548dff47776fddaddf
2018-12-20 02:11:46 -05:00
d4b13c280b Leverage the next keyword from python 2.7
This is literally what the next keyword is for.
https://www.python.org/dev/peps/pep-3114/

Change-Id: I843755910b847737b077ff2361ba3e04409db0f0
2018-12-19 11:06:35 -08:00
6e53844f1e Allow clobbering of existing tags from remote.
Bug: 120778183
Change-Id: Id44e2b68abc410a3afd4e07a3c943b0936347e38
2018-12-10 11:33:16 -08:00
d26146de7f platform_utils: Fix exception handling in _walk_windows_impl
Change-Id: I6b79cbc4c1bbbe17ffe8361fe1544434beaa9059
2018-11-06 09:25:35 +09:00
bd8f658823 Add option for git-repo to support 'silent' uploads
When --ne/--no-emails is added to 'repo upload' command line, gerrit
server will not generate notification emails.

project.py:Project.UploadForReview method is modified to accept a
string recognizable by gerrit to indicate different sets of destination
email addressees, but the upload command line allows only one option -
disable sending emails completely.

Default repo upload behavior is not being changed.

TEST=tried in the Chrome OS repo, observed that patches uploaded with
     --ne or --no-emails indeed do not trigger any emails, while
     patches uploaded without these command line options still trigger
     email notifications.

Change-Id: I0301edec984907aedac277d883bd0e6d3099aedc
2018-11-05 14:01:05 -08:00
713c5872fb upload: Unify option passing in ssh and other transports
Pass options through the refspec for all transports, including ssh.
This means the behavior will be more consistent between the ssh and
https cases.

A downside is that this prevents passing special characters in
reviewer options.  That already didn't work over https, so it seems
okay.  It could be fixed by using push options instead.

Change-Id: Ia38d16e350cb8cb0de14463bfb3d9724e13bc4bf
2018-11-05 13:32:22 -08:00
36391bf5ca Merge "init: --dissociate option to copy objects borrowed with --reference" 2018-10-28 23:30:50 +00:00
bed8b62345 Add support for long paths
* Add more file i/o wrappers in platform_utils to allow using
  long paths (length > MAX_PATH) on Windows.

* Paths using the long path syntax ("\\?\" prefix) should never
  escape the platform_utils API surface area, so that this
  specific syntax is not visible to the rest of the repo code base.

* Forward many calls from os.xxx to platform_utils.xxx in various place
  to ensure long paths support, specifically when repo decides to delete
  obsolete directories.

* There are more places that need to be converted to support long paths,
  this commit is an initial effort to unblock a few common use cases.

* Also, fix remove function to handle directory symlinks

Change-Id: If82ccc408e516e96ff7260be25f8fd2fe3f9571a
2018-10-22 08:16:35 -07:00
09f0abb0ef init: --dissociate option to copy objects borrowed with --reference
"repo init --reference" has two purposes: to decrease bandwidth used
at clone time, and to decrease disk usage afterward, by using the
reference repositories as an alternate store of objects even after
the clone. The downside is that it makes the borrowing repositories
dependent on the reference repositories, so it is easy to end up
with missing objects by mistake after a cleanup operation like "git
gc".

To prevent that, v2.3.0-rc0~30^2 (clone: --dissociate option to mark
that reference is only temporary, 2014-10-14), "git clone" gained a
--dissociate option that makes --reference reuse objects from the
reference repository at clone time but copy them over instead of
using the reference as an alternate. This is more straightforward to
use than plain --reference, at the cost of higher disk usage.

Introduce a --dissociate to "repo init" that brings the same benefits
to repo. The option is simply passed on to "git clone".

Change-Id: Ib50a549eb71e0a2b3e234aea57537923962a80d4
2018-10-19 23:51:23 +05:00
b3133a3164 Merge "update markdown/help header format" 2018-10-10 05:54:59 +00:00
3b24e7b557 update homepage URIs
Change-Id: I482a72bf296978625b1e82ef580b0e0d4d57ff25
2018-10-10 01:35:58 -04:00
b8f7bb04d0 update markdown/help header format
Since gitiles recommends using # headers over ---/=== underlines,
change the manifest-format.md over and all our help texts.

Change-Id: I96391d41fba769e9f26870d497cf7cf01c8d8ab3
2018-10-10 01:28:43 -04:00
3891b7519d manifest-format: convert to markdown
The gitiles system doesn't render .txt files, so convert this to .md
for better display online.

Change-Id: Ie12e46daf008dd8c97ae2ffd21fb68bd948fe625
2018-10-05 19:32:51 -04:00
2b42d288c0 Windows: Add support for creating symlinks as an unprivileged user
See https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10/
for announcement of new flag.

This change follow the same pattern as what was done in "go":
https://github.com/golang/go/pull/24307/files#diff-b87bc12e4da2497308f9ef746086e4f0

Change-Id: If1e99fefdd3f787598e695731019c34b9bfcd1c2
2018-10-03 09:41:09 -07:00
e469a0c741 fix some sync error while using python3
Change-Id: I70925e48756c356d48359679d8ad1b9e33a68595
2018-07-24 22:20:08 +08:00
65b0ba5aa0 Remove unused pylint suppressions
pylint is not used since bb5b1a0. The pyflakes cleanup mentioned in that
commit has not been done, but given that this project is no longer being
actively developed I don't think it's worth spending time doing it.

Leaving the pylint suppressions causes confusion because it leads people
to think that we are still using pylint.

Change-Id: If7d9f280a0f408c780f15915ffdb80579ae21f69
2018-07-24 22:20:08 +08:00
a6515fb952 Merge "Flush stderr on Windows" 2018-07-13 15:48:36 +00:00
993dcacd17 Fix the initial existence check for "repo"
Commit 27226e742d introduced a warning if
"repo" is not part of the bootstrapped REPO_URL. However, that check was
done too early, directly after the call to _Clone. As the _Clone function
does not actually clone but it only initializes and fetches, the check
needs to be moved to after the call to _Checkout.

To reproduce, call

repo init --no-clone-bundle --repo-branch=master -u https://android.googlesource.com/platform/manifest

which will currently always show the (bogus) warning message. With this
fix, the warning will only be shown if "repo" indeed does not exist.

While at it, also slightly improve the code by using os.path.join().

Change-Id: Ied89e24231addabab6075005065748df1ffa74c4
2018-07-13 17:21:47 +02:00
a9399846fa Flush stderr on Windows
While on Linux stderr is unbuffered, it is buffered on Windows. Always
flush stderr on Windows to ensure any error messages appear in the right
order to ease diagnosing.

Change-Id: I37300e384ecd3a51a321a48818f0114d6f3357a0
2018-07-13 16:23:50 +02:00
b10f0e5b9a hooks/pre-auto-gc-battery: allow gc to run on non-laptops
Desktops and servers tend to have no power sensor, thus on_ac_power returns
255 ("unknown").  Thus, let's take any answer other than 1 ("battery") as
no contraindication to run gc.

If that tool returns "unknown", there's no point in querying other sources
as it already queried them, and is smarter than us (can handle multiple
adapters).

Reported by: Xin Li <delphij@google.com>
Signed-off-by: Adam Borowski <kilobyte@angband.pl>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
(cherry picked from git.git commit 781262c5e7ad4a7813c528803117ed0d2e8c5172)
Signed-off-by: Fredrik Roubert <roubert@google.com>
Signed-off-by: Jonathan Nieder <jrn@google.com>
Change-Id: I51fe2eb1eb879492a61e8e09c86ee34d049036c1
2018-07-11 13:45:58 -07:00
da40341a3e manifest: Support a default upstream value
It's convenient to set upstream for all projects in a manifest instead of
repeating the same value for each project.

Change-Id: I946b1de4efb01b351c332dfad108fa7d4f443cba
2018-05-09 14:58:18 -06:00
8d4b106642 Merge "docs: repo-hooks: fix cwd details" 2018-04-26 07:57:46 +00:00
ed429c9f6f docs: repo-hooks: fix cwd details
The hooks are run from the top of the manifest checkout, not from the
individual git repos.  It's up to individual hooks to chdir as needed.

Change-Id: I53325e0c3dcaa9c250b02b223e78d238d2cbd36d
2018-04-25 00:14:06 -04:00
0f2e45a3a6 Pass refs to ls-remote
This will fix the issue of parsing large output locally

Change-Id: I9a5cf1238147a02c92a3fca53eab9bd57f9d16b4
2018-03-24 13:00:08 +05:30
cf7c0834cf Download latest patch when no patch is specified
When someone does "repo download -c <project> <change>"
without specifying a patch number, by default patch 1 is
downloaded. An alternative is to look for the latest patch
and download the same when no explicit patch is given.
This commit does the same by identifying the latest patch
using "git ls-remote".

Change-Id: Ia5fa7364415f53a3d9436df4643e38f3c90ded58
2018-03-17 16:29:23 +05:30
4ea1f0cabd Merge changes I9c1ab65f,I7b2027ae
* changes:
  init: Remove string concat in no-op os.path.join
  Support relative paths in --reference
2018-03-16 02:06:10 +00:00
7d52585ec4 Add a way to override the revision of an <extend-project>
This change adds support for the 'revision' attribute in
<extend-project>. This avoids the need to perform a <remove-project>
followed by a <project> in local manifests.

Change-Id: Id2834fcfc1ae0d74b3347bed3618f250bf696b1f
2018-03-15 09:55:54 -07:00
1f365701b3 Merge "implement optional 'sync-tags' in the manifest file" 2018-02-26 06:50:53 +00:00
ce7e02601c Take care of a tilde on cookie file path
This handles cookie file path like "~/.gitcookies".

Change-Id: I87ba120a940fff38073d520f83b70654e6a239ba
2018-02-26 08:53:08 +09:00
a32c92c206 implement optional 'sync-tags' in the manifest file
Allow the 'default' and 'project' element in the manifest
file to apply "--no-tags" option equivalent.

Change-Id: I7e0f8c17a0e25cca744d45df049076d203c52ff5
Signed-off-by: YOUNG HO CHA <ganadist@gmail.com>
2018-02-14 16:57:41 +09:00
5f0e57d2ca init: Remove string concat in no-op os.path.join
This also fixes a line length warning.

Change-Id: I9c1ab65f83a35581dd657a707c7bc3c69db2b1dc
2018-01-22 11:00:24 -06:00
baa0009355 Support relative paths in --reference
Put the correctly-expanded relative paths in objects/info/alternates.
From gitrepository-layout(5), this path should be "relative to the
object database, not to the repository".

Change-Id: I7b2027ae23cf7d367b80f5a187603c4cbacdb2de
2018-01-22 10:57:29 -06:00
685320b000 event_log: Fix order of parameters to Add method call
Change-Id: I5add20eadfde39806ef4b2cc819da0ae0bfec2f5
2018-01-10 11:05:14 +09:00
02c0ee6ae6 Sync correctly when subproject url is a relative url to its parent url
Issue: when subproject url is a relative in .gitmodules
repo tool cannot handle this and cause:
"fatal: '***' does not appear to be a git repository
 fatal: Could not read from remote repository."
issue.

Signed-off-by: Shouheng Zhang <shouheng.zhang@intel.com>
Change-Id: I2a24c291ea0074ba13a740b32a11c0c25975e72b
2017-12-21 09:20:25 +08:00
1dc36600ef Merge "Update commit-msg hook to version from Gerrit 2.14.6" 2017-12-07 01:13:17 +00:00
cbe8aeb52b Update commit-msg hook to version from Gerrit 2.14.6
Change-Id: I14403fea4d017b97be5131e695803f121d404af2
2017-12-06 10:42:46 -08:00
305a2d029f Support --push-option in upload subcommand
Change-Id: I44836f8c66ded5a96cbf5431912e027e681f6529
2017-11-13 15:48:49 -08:00
84e7e16d35 document repo hooks mechanism
Change-Id: I9e25b92c846f887f515efcc706cf5a869645e0ec
2017-11-10 21:53:59 -05:00
f46902a800 forall: Clarify expansion of REPO_ environment values with -c
If a user executes:

  repo forall -c echo $REPO_PROJECT

then $REPO_NAME is expanded by the user's shell first, and passed
as $1 to the shell that executes echo. This will either result in
no output, or output of whatever REPO_NAME is set to in the user's
shell. Either way, this is an unexpected result.

The correct way to do it is:

  repo forall -c 'echo $REPO_PROJECT'

such that $REPO_NAME is passed in to the shell literally, and then
expanded to the value set in the environment that was passed to
the shell.

Update the documentation to make this clearer.

Change-Id: I713caee914172ad8d8f0fafacd27026502436f0d
2017-10-31 13:07:55 +09:00
c00d28b767 Set GIT_SSH_VARIANT when setting GIT_SSH
Make it explicit that the ssh wrapper we use for control master
support accepts OpenSSH-compatible command line arguments instead of
asking Git to guess.

The GIT_SSH_VARIANT setting was introduced in Git v2.13.0-rc0~3^2~2
(2017-02-01) as a more reliable detection method than relying on the
ssh command name.  Fortunately the default variant was 'ssh' (i.e.,
OpenSSH-compatible) so this wasn't initially required.

Now Git wants to start using more OpenSSH features
(-o SendEnv=GIT_PROTOCOL), and in order to do so its ssh variant
detection will need to be tweaked.  Set GIT_SSH_VARIANT explicitly
so this helper can continue to work regardless of how Git modifies
its autodetection.

Reported-by: William Yan <wyan@google.com>
Change-Id: I6bf2c53b4eb5303a429eae6cb68e0a5ccce89064
2017-10-19 14:39:26 -07:00
788e9626cc Provide more specific error message for symlinks errors on Windows
Change-Id: Ia6099beef37ae6b6143eba243fe7fbe02b74a9bb
2017-08-31 13:49:59 -07:00
cd892a38a6 Allow quotes in editor command on Windows
This change allows setting the EDITOR env. variable to point to a
program location that contains quotes and spaces.

For example:

> set EDITOR="C:\Program Files (x86)\Notepad++\notepad++.exe" -multiInst -nosession
> repo upload

Change-Id: Ic95b00f7443982b1956a2992d0220e50b1cf6bbb
2017-08-31 13:49:49 -07:00
010fed7711 Replace all os.remove calls
os.remove raises an exception when deleting read-only files on
Windows. Replace all calls with calls to platform_utils.remove,
which deals with deals with that issue.

Change-Id: I4dc9e0c9a36b4238880520c69f5075eca40f3e66
2017-08-31 13:49:36 -07:00
e8595e9df7 Support pager on Windows
Windows does not support pipe|fork, but we can simulate by creating
the pager as a child process, redirecting stdout/in/err appropriately
and then waiting for the child process to terminate after we are
done executing the repo command.

Change-Id: I5dd2bdeb4095e4d93bc678802e53c6d4eda0235b
2017-08-31 13:49:26 -07:00
227ad2ef42 Implement islink, readlink and realpath using Win32 api
Change-Id: I18452cbb32d24db73601ad10485dbe6bb278731c
2017-08-31 13:49:01 -07:00
2a4be94878 Handle Windows line endings when reading binary files
Without this change, '.git\HEAD' files, for examples, are sometime
read incorrectly resulting in the current branch to be reset to
"master" when running a "repo init -b xxx" on an already initialized
repository.

Change-Id: I48c7ef85ff81626edf156914329a560e14252f2a
2017-08-31 12:13:52 -07:00
9d743397bf Merge "Fixed upload to remotes with the url ssh://hostname" 2017-08-30 15:11:22 +00:00
2c57d619bc Merge "Add option '--no-cert-checks' for 'upload' sub command." 2017-08-30 15:11:10 +00:00
d1ebc89a08 Merge changes from topic "windows-support"
* changes:
  Port os.rename calls to work on Windows
  Workaround shutil.rmtree limitation on Windows
  Add support for creating symbolic links on Windows
  Make "git command" and "forall" work on Windows
2017-08-30 10:24:03 +00:00
2ec2a5d64c Fixed upload to remotes with the url ssh://hostname
Change-Id: I1d0dd4d3f90eac45205f6f4ca98a29b0babdbc3f
2017-08-29 20:16:08 +00:00
9ead97bb51 When starting a branch, do not use a tag or change value for branch.merge
When starting a branch, branch.merge is set to project revision unless
the revision is a SHA1. In that case, branch.merge is set to dest_branch
if defined or manifest default revision otherwise. This special handling
allows repo upload to work when the project revision is a SHA1.

Extend the special handling to also happen when the project revision
is a tag value or a change value so that repo upload will work in those
case as well.

Change-Id: Iff81ece40e770cd02535e80dcb023564d42dcf47
2017-08-25 09:10:29 +09:00
e43322625a Print a message when fetching is skipped for an immutable ref
The output indicates that fetching happens even when it is skipped.

To avoid confusion, print a message when fetching is skipped for
an immutable ref so that the user knows when and why a fetch is skipped.

Change-Id: Id6e4812cebc5e57d379feb76a9d034af0b93043b
2017-08-25 00:00:02 +00:00
bed59cec5e Add option '--no-cert-checks' for 'upload' sub command.
This option allow to bypass verification ssl certification while
establishing connection with Gerrit to upload review.

Change-Id: If2e15f5a273c18a700eb5093ca8a4d5a4cbf80cd
2017-08-23 14:06:14 +02:00
c94d6eb902 Revert "Migrate git-repo to create private changes rather than drafts"
This reverts commit d88f53e2b9. I merged
it too hastily without paying enough attention to compatibility with
released Gerrit versions.

Change-Id: I4028d4737df1255f11e217da183a19a010597d5b
2017-08-08 18:34:53 +00:00
d88f53e2b9 Migrate git-repo to create private changes rather than drafts
Considering that some users might expect changes created with
'-d' option are not public. Private changes may be a better
choice here than work-in-progress changes.

Change-Id: I46a8fb9ae38beb41cf96d6abe82bea6db2439669
2017-08-07 15:08:18 +02:00
87984c6db4 Add options for git-repo to support private and wip changes
This change adds options for git-repo tool to support private
changes and work-in-progress changes.

Change-Id: I343491f5949f06f1580d53f9cc0dee2dca09130f
2017-08-07 15:02:39 +02:00
ffc1401327 Merge "download: try to choose . as default project if none" 2017-08-02 07:19:45 +00:00
8a6eeed7f5 Merge "Always print percentage when syncing quietly" 2017-08-02 07:01:08 +00:00
7be072efa6 Always print percentage when syncing quietly
Change-Id: I574396e63520781067ed1e991c41caf7640e5731
2017-07-15 16:44:55 +00:00
7482a96443 download: try to choose . as default project if none
Change-Id: I28b5e3be5f3c9a4c077af87d6a3e0cc3b96a1b9d
2017-07-12 10:15:06 +02:00
3bcd30545e Fix "list comprehension redefines 'x'" warnings from pyflakes
$ git ls-files | grep py$ | xargs pyflakes
  subcmds/stage.py:101: list comprehension redefines 'p' from line 63
  subcmds/sync.py:784: list comprehension redefines 'p' from line 664
  subcmds/upload.py:467: list comprehension redefines 'avail' from line 454

Change-Id: Ia65d1a72ed185ab3357e1a91ed4450c719e75a7c
2017-07-10 23:26:04 +00:00
224a31a765 init: add missing submodule arg
The submodule argument to Sync_LocalHalf was missing in
MetaBranchSwitch, causing submodules not to get synced when the
-b/--manifest-branch argument to init is used.

Change-Id: Ie86d271abac2020725770be36ead83be3326e64b
Signed-off-by: Martin Kelly <mkelly@xevo.com>
2017-07-10 14:50:52 -07:00
b54343d9fd Tell the user if it will upload a draft
Change-Id: Ie004ec9d61603f3f618c47597947b82c59f2839c
2017-07-10 13:20:01 +00:00
259f16520a Merge "Add a newline after "Fetching projects" progress output" 2017-06-28 04:24:03 +00:00
8419ab22d6 sync: Continue job if some fetchs failed but force-broken is set
With --force-broken it continue to fetch other projects but nothing
is added in directory because it abort some lines later.

Change-Id: I32c4a4619b3028893dc4f98e8d4e5bc5c09adb27
2017-06-16 12:23:26 +02:00
913327f10c Add a newline after "Fetching projects" progress output
Output before change:

    Fetching project platform/packages/providers/UserDictionaryProvider
    Fetching projects:  66% (773/1171)  Fetching project platform/external/regex-re2
    Fetching project device/generic/mini-emulator-x86_64

Output after change:

    Fetching project platform/packages/providers/UserDictionaryProvider
    Fetching projects:  66% (773/1171)
    Fetching project platform/external/regex-re2
    Fetching project device/generic/mini-emulator-x86_64

Change-Id: I4da84da58316c69294e4da2792f83885dc942701
2017-06-13 13:03:39 +02:00
ad1abcb556 Port os.rename calls to work on Windows
os.rename fails on Windows if the destination exists, so replace
os.rename to platform_utils.rename which handles the platform
differences.

Change-Id: I15a86f10f65eedee5b003b80f88a0c28a3e1aa48
2017-05-29 19:33:07 +09:00
a65adf74f9 Workaround shutil.rmtree limitation on Windows
By default, shutil.rmtree raises an exception when deleting readonly
files on Windows.

Replace all shutil.rmtree with platform_utils.rmtree, which adds an
error handler to make files read-write when they can't be deleted.

Change-Id: I9cfea9a7b3703fb16a82cf69331540c2c179ed53
2017-05-29 19:32:31 +09:00
d5cec5e752 Add support for creating symbolic links on Windows
Replace all calls to os.symlink with platform_utils.symlink.

The Windows implementation calls into the CreateSymbolicLinkW Win32
API, as os.symlink is not supported.

Separate the Win32 API definitions into a separate module
platform_utils_win32 for clarity.

Change-Id: I0714c598664c2df93383734e609d948692c17ec5
2017-05-29 19:30:34 +09:00
2e70291162 Make "git command" and "forall" work on Windows
Python on Windows does not support non blocking file operations.
To workaround this issue, we instead use Threads and a Queue to
simulate non-blocking calls. This is happens only when running
with the native Windows version of Python, meaning Linux and Cygwin
are not affected by this change.

Change-Id: I4ce23827b096c5138f67a85c721f58a12279bb6f
2017-05-29 19:29:30 +09:00
35d22217a5 Ensure repo waits for child process to terminate
See http://stackoverflow.com/questions/7004687/os-exec-on-windows:

execv on Windows does not behave as on Linux, i.e. a new process is
spawned and the parent process terminates right away, which makes the
shell prompt come back too soon.

Change-Id: I1f8d23208765988629f081e9b949c67cf71c08ae
2017-05-29 13:56:18 +09:00
a24671f661 Merge "init: allow relative path on --reference argument" 2017-05-29 04:54:12 +00:00
e0684addee sync: Add support to dump a JSON event log of all sync events.
Change-Id: Id4852968ac1b2bf0093007cf2e5ca951ddab8b3b
2017-05-29 13:39:54 +09:00
fef9f21b28 Fix misplaced file separator string.replace call
Project names are stored as path using the '/' file separator, and
stored in a dictionary as keys.

Change-Id: Ide40dfe840958ac0d46caae5f77f1a49d71c9d90
2017-05-28 21:18:13 +09:00
6a470be220 Use OS file separator
Change-Id: I46b15bc1c1b4f2300a6fd98fe16c755da4910e7a
2017-05-28 21:16:15 +09:00
169d8ae93c Merge "pre-auto-gc: Add support for Windows" 2017-05-28 00:37:59 +00:00
c79d3b8fd1 init: allow relative path on --reference argument
Change-Id: I41d6be6bc035fdddb5a27c072994439986d58d58
Signed-off-by: YOUNG HO CHA <ganadist@gmail.com>
2017-05-28 00:33:25 +00:00
aa90021fbc Set result if sys.exit() is called by subcommand.
Allows the finally branch to make sure of the return code.

Change-Id: I7a796da5b60269cbd71aad953f1b9bb762b8eef8
2017-05-27 13:32:00 +09:00
fddfa6fbac Adding include element into the top-level element
The documentation of the XML file format contains DTD which contains
definition of all allowed elements and attributes. The "include" element
is defined but it's not referenced from the top-level "manifest"
element.

This patch is adding the "include" element into the list of elements of
the top-level "manifest" element.

Change-Id: I33beb8ef2846bbf42ffd42e6ae6888828566d604
2017-05-27 04:26:15 +00:00
997a92bd58 Merge "Add option REPO_IGNORE_SSH_INFO to ignore ssh_info" 2017-05-27 04:25:39 +00:00
fbcbcabe98 Merge "init: add --submodules to sync manifest submodules" 2017-05-27 04:24:58 +00:00
eec726c6d8 Add option REPO_IGNORE_SSH_INFO to ignore ssh_info
This is required for setups, where Gerrit access using ssh is only available
for some networks.
For network without ssh access, repo will get ssh_info from Gerrit and
use ssh for communications - which will fail. To support this setup
we need to have an option to ignore the ssh_info provided by Gerrit and
use http(s).

Using git insteadOf as alternative results in the inability to add
reviewers using "repo upload --re=...", since the syntax of adding
reviewers differs for ssh and https. repo is assuming an ssh
connection and uses "git push --receive-pack=...", which will fail
since git silently uses https for push operation. repo must be aware
that https is used so it uses "git push remote ...:refs/for/...%r=..."
for upload.

Change-Id: Idd83baef0fb26ffcc9ac65e204b68d323ce177a1
2017-05-26 15:11:11 +02:00
666debc518 gitc_delete: Remove unused imports
Change-Id: I672189ba99e18dca3956e2396c921d1ef0ca2ddd
2017-05-26 21:53:34 +09:00
c354a9b922 abandon: fix usage of undefined variable
As reported by pyflakes:

  subcmds/abandon.py:84: undefined name 'p'

The name of the variable should be 'proj'.

Change-Id: Ic09eb92e8db6b510e99efce010bd0bb094d7cbfe
2017-05-26 21:52:23 +09:00
06848b2415 Update .mailmap
Change-Id: Icabc7fb2161b661c2df9290a1ca6f75b9b1b8e1b
2017-05-26 21:44:57 +09:00
e4e94d26ae init: add --submodules to sync manifest submodules
repo sync can sync submodules via the --fetch-submodules option.
However, if the manifest repo has submodules, those will not be synced.
Having submodules in the manifest repo -- while not commonly done -- can
be useful for inheriting a manifest from another project using <include>
and layering changes on top of it.  In this way, you can avoid having to
deal with merge conflicts between your own manifests and the other
project's manifests (for example, if you're managing an Android fork).

Add a --submodule option to init that automatically syncs the submodules
in the manifest repo whenever the manifest repo changes.

Change-Id: I45d34f04517774c1462d7f233f482d1d81a332a8
Signed-off-by: Martin Kelly <mkelly@xevo.com>
2017-05-23 16:51:31 -07:00
c9439facdd pre-auto-gc: Add support for Windows
Previously, this would always have exited with 1 on Windows, causing "git
gc --auto" to abort. Fix this by adding support for Windows.

Change-Id: Ie519b366a11b6b18b2d465e892e738de3f4bbc99
2017-05-03 11:26:21 +00:00
3d7bbc9edf project.py: fix performance issue with --reference when the mirrored repository has many refs
Change-Id: Id0183903597f872eee80ca32a8050125b187a3d4
2017-04-12 20:04:47 +08:00
ffb4b89099 sync.py: report the remote URL on fatal git remote errors
repo can be configured to download from any number of remote git repos.
However when one fails repo doesn't report which one. Example:
Fatal: remote error: Daily ls-remote rate limit exceeded for IP xx.xx.xx.xx

TEST=repo init -q -u https://chromium.googlesource.com/chromiumos/manifest.git
  # Apply patch in ./.repo/repo/
  # Simulate a git remote error:
  sed -i -e 's#chromiumos/docs#chromiumos/XXdocs#' .repo/manifests/full.xml
  repo sync --quiet --force-sync docs
  # error message now shows the remote URL

Optional test tip: reduce the time.sleep(random(...)) in ./.repo/repo/project.py

Change-Id: I4509383b6a43a8e66064778e8ed612d8a735c8b6
2017-04-04 22:10:34 -07:00
04071c1c72 manifest-format: fix EMPTY keyword usage
The keyword EMPTY doesn't use parens.

BUG=git-repo:140

Change-Id: I7cd28a09c401520a72e5c244a77d9d70385f1b61
2016-12-28 16:07:16 -05:00
7de8c5db78 Merge "init: Add no-tags and current branch options" 2016-12-09 02:33:42 +00:00
bb9c42cf1d Merge "Fix removing broken symlink in reference dir" 2016-12-06 07:51:01 +00:00
f4dda9a1be init: Add no-tags and current branch options
This avoids fetching tags and branches for huge manifests

Change-Id: I19c9724d75364440b881b297d42b906f541f73ff
2016-12-01 19:03:41 -05:00
b881d227f3 Merge "Add a check and more output to protect against invalid REPO_URLs" 2016-10-29 07:28:35 +00:00
8e2d1d521e Merge "Fix checkout error when depth passed to repo init and revision is a sha1" 2016-10-28 19:30:45 +00:00
27226e742d Add a check and more output to protect against invalid REPO_URLs
If you don't know that the url to git-repo itself can be overridden via
REPO_URL, it's hard to debug cases where REPO_URL is accidentally set to
another repository, e.g. inside a Jenkins CI job. What makes is even
harder is that the ".repo/repo" directory gets silently removed in such
cases as verifications fails, which makes it impossible to look at the
cloned files to understand the problem.

To better protect against such an issue, warn if the cloned git-repo
repository does not contain a top-level "repo" file, and state that the
".repo/repo" directory will be removed in case of a clone failure.

Change-Id: I697b4999205a5967910c0237772ccaada01e74d4
2016-10-28 14:43:02 +02:00
6c5944606a Fix checkout error when depth passed to repo init and revision is a sha1
Currently, if direct fetch of a sha1 is not supported by git server and
depth option is used, we fallback on syncing the upstream branch by
ignoring depth option.

This fallback doesn't work in next 2 cases:
(1) upstream attribute is not specified in manifest
(2) depth option is passed to repo init command
    (not with clone-depth attribute in manifest)

This commit do the following:
- fixes (1) by updating condition used to apply fallback
  first we retry with depth set to None, then by syncing all branches
- fixes (2) by passing depth as argument of _RemoteFetch() method
  thus, its value is not set again to depth value passed to repo init
  command when applying fallback

Change-Id: Ifd6fffafc49ba229df624b0d7b64c83d47619d17
2016-10-28 14:29:57 +02:00
ae81c964b6 Merge "implement optional '--all' in the abandon command" 2016-10-28 07:52:31 +00:00
e02c17c9ea Merge "_CheckDirReference: log actual error before suggesting --force-sync" 2016-10-28 06:49:07 +00:00
6e31079033 Add sso to list of known schemes for relative URLs
repo already special-cases sso:// URLs to behave similarly to https://
and rpc:// elsewhere in repo, but it forgot to do so here.

Noticed when trying to use relative URLs in a manifest obtained using
an sso:// URL.

Change-Id: Ia11469a09bbd6e444dbc4f22c82f9bbe9f5fd083
2016-10-27 15:56:38 -07:00
ec287902e6 _CheckDirReference: log actual error before suggesting --force-sync
A recent backward incompatible change created confusion and loss of
productivity and highlighted the very limited amount of information
provided when repo sync fails; merely recommending to --force-sync
and blow-up git repos without any hint as to why. The addition of
this basic _error(...) call would have provided a clue and will in
the future.

BUG=Issue 232
TEST=simulate a breakage similar to the ones reported at
  https://groups.google.com/a/chromium.org/forum/#!topic/chromium-os-dev/2-0oCy_CX5s
  cd .repo/projects/src/third_party/libapps.git/
  file info; rm info; ln -s wronglink info
  cd -
  repo sync src/third_party/libapps/
  # error message now shows the failure

Change-Id: Idd2f177a096f1ad686caa8c67cb361d594ccaa57
2016-10-27 12:58:26 -07:00
4d5bb68d58 status: add -q/--quiet option
The --quiet option reduces the output to just
a list of projects with modified workspaces (and
orphans if -o is specified)

A common use case is when performing a full-workspace
merge.  The integrator will kick-off a merge via:

    repo forall -c git merge <some tag>

And then produce a short list of conflicted projects via:

    repo status -q

The integrator can then iteratively fix and clean up all conficted
components.  The merge is complete when:

    repo status -q

    returns no output.

Change-Id: Ibbba8713eac35befd8287c95948874e23fd5c7e2
2016-10-17 15:24:09 -05:00
2e14792a94 implement optional '--all' in the abandon command
when you want to delete all local branches, you should be find
all branches' name, and type them behind 'repo abandon' command.

Usage:
    repo abandon --all [<project>...]

Change-Id: I4d391f37fb9d89b8095488c585468eafc1a35f31
2016-10-17 02:29:42 +00:00
82f67987a3 Merge "sync: Fix semaphore release bug that causes thread 'leaks'" 2016-10-17 01:10:16 +00:00
699bcd40be Removed duplication code in abandon.py
code about getting argument is duplicated.
so this line is removed

Change-Id: Id321b999c7dacdb403cd986cbf35f8db62efc157
2016-10-12 10:02:30 +09:00
7f1ccfbb7b sync: Fix semaphore release bug that causes thread 'leaks'
When repo syncs a manifest that utilizes multiple branches
in the same project, then the sync will use an extra
thread for each "duplicate".  For example, if
the manifest includes the project "foo" and "bar"
twice, then "repo sync -jN" will fetch with N+2 threads.

This is caused by _FetchHelper() releasing the thread semaphore
object each time it's called, even though _FetchProjectList()
may call this function multiple times within the scope of a
single thread.

Fix by moving the thread semaphore release to
_FetchProjectList(), which is only called once per thread
instance.

Change-Id: I1da78b145e09524d40457db5ca5c37d315432bd8
2016-10-11 14:10:34 -05:00
eceeb1b1f5 Support broken symlinks when cleaning obsolete paths
When there's a symlink to a directory, os.walk still lists the symlink
in dirs, even if it isn't configured to follow symlinks. This will fail
the listdirs check if the symlink is broken (either before or during the
cleanup). So instead, check for directory symlinks and remove them using
os.remove.

Bug: Issue 231
Change-Id: I0ec45a26be566613a4a39bf694a3d9c6328481c2
2016-09-27 03:05:11 +00:00
16889ba43d Revert "Repo: fall back to http, if ssh connection fails for http repos"
This reverts commit 488bf092d5.

Issue 230

Change-Id: I3a5725301f576e1a2ac499cb6daa631895115640
2016-09-22 16:40:27 +00:00
8ac0c96537 Fix removing broken symlink in reference dir
Re-ordered to first create the symlink before checking the source
file and remove the destination if the source does not exists.

Change-Id: Iae923ba2ef0ba5a8dc1b8e42d8cc3f3708f773af
2016-06-29 05:43:01 +00:00
35 changed files with 1768 additions and 477 deletions

View File

@ -1,4 +1,5 @@
Anthony Newnam <anthony.newnam@garmin.com> Anthony <anthony@bnovc.com>
He Ping <tdihp@hotmail.com> heping <tdihp@hotmail.com>
Hu Xiuyun <xiuyun.hu@hisilicon.com> Hu xiuyun <xiuyun.hu@hisilicon.com>
Hu Xiuyun <xiuyun.hu@hisilicon.com> Hu Xiuyun <clouds08@qq.com>
Jelly Chen <chenguodong@huawei.com> chenguodong <chenguodong@huawei.com>

View File

@ -5,6 +5,6 @@
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/git-repo</path>
</pydev_pathproperty>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.6</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
</pydev_project>

View File

@ -6,9 +6,11 @@ development workflow. Repo is not meant to replace Git, only to make it
easier to work with Git. The repo command is an executable Python script
that you can put anywhere in your path.
* Homepage: https://code.google.com/p/git-repo/
* Bug reports: https://code.google.com/p/git-repo/issues/
* Source: https://code.google.com/p/git-repo/
* Homepage: https://gerrit.googlesource.com/git-repo/
* Bug reports: https://bugs.chromium.org/p/gerrit/issues/list?q=component:repo
* Source: https://gerrit.googlesource.com/git-repo/
* Overview: https://source.android.com/source/developing.html
* 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)

View File

@ -19,6 +19,7 @@ import platform
import re
import sys
from event_log import EventLog
from error import NoSuchProjectError
from error import InvalidProjectGroupsError
@ -28,6 +29,7 @@ class Command(object):
"""
common = False
event_log = EventLog()
manifest = None
_optparse = None
@ -216,11 +218,6 @@ class Command(object):
return result
# pylint: disable=W0223
# Pylint warns that the `InteractiveCommand` and `PagedCommand` classes do not
# override method `Execute` which is abstract in `Command`. Since that method
# is always implemented in classes derived from `InteractiveCommand` and
# `PagedCommand`, this warning can be suppressed.
class InteractiveCommand(Command):
"""Command which requires user interaction on the tty and
must not run within a pager, even if the user asks to.
@ -236,8 +233,6 @@ class PagedCommand(Command):
def WantPager(self, _opt):
return True
# pylint: enable=W0223
class MirrorSafeCommand(object):
"""Command permits itself to run within a mirror,

View File

@ -1,111 +1,116 @@
repo Manifest Format
====================
# repo Manifest Format
A repo manifest describes the structure of a repo client; that is
the directories that are visible and where they should be obtained
from with git.
The basic structure of a manifest is a bare Git repository holding
a single 'default.xml' XML file in the top level directory.
a single `default.xml` XML file in the top level directory.
Manifests are inherently version controlled, since they are kept
within a Git repository. Updates to manifests are automatically
obtained by clients during `repo sync`.
[TOC]
XML File Format
---------------
A manifest XML file (e.g. 'default.xml') roughly conforms to the
## XML File Format
A manifest XML file (e.g. `default.xml`) roughly conforms to the
following DTD:
<!DOCTYPE manifest [
<!ELEMENT manifest (notice?,
remote*,
default?,
manifest-server?,
remove-project*,
project*,
extend-project*,
repo-hooks?)>
```xml
<!DOCTYPE manifest [
<!ELEMENT manifest (notice?,
remote*,
default?,
manifest-server?,
remove-project*,
project*,
extend-project*,
repo-hooks?,
include*)>
<!ELEMENT notice (#PCDATA)>
<!ELEMENT notice (#PCDATA)>
<!ELEMENT remote (EMPTY)>
<!ATTLIST remote name ID #REQUIRED>
<!ATTLIST remote alias CDATA #IMPLIED>
<!ATTLIST remote fetch CDATA #REQUIRED>
<!ATTLIST remote pushurl CDATA #IMPLIED>
<!ATTLIST remote review CDATA #IMPLIED>
<!ATTLIST remote revision CDATA #IMPLIED>
<!ELEMENT remote EMPTY>
<!ATTLIST remote name ID #REQUIRED>
<!ATTLIST remote alias CDATA #IMPLIED>
<!ATTLIST remote fetch CDATA #REQUIRED>
<!ATTLIST remote pushurl CDATA #IMPLIED>
<!ATTLIST remote review CDATA #IMPLIED>
<!ATTLIST remote revision CDATA #IMPLIED>
<!ELEMENT default (EMPTY)>
<!ATTLIST default remote IDREF #IMPLIED>
<!ATTLIST default revision CDATA #IMPLIED>
<!ATTLIST default dest-branch CDATA #IMPLIED>
<!ATTLIST default sync-j CDATA #IMPLIED>
<!ATTLIST default sync-c CDATA #IMPLIED>
<!ATTLIST default sync-s CDATA #IMPLIED>
<!ELEMENT default EMPTY>
<!ATTLIST default remote IDREF #IMPLIED>
<!ATTLIST default revision CDATA #IMPLIED>
<!ATTLIST default dest-branch CDATA #IMPLIED>
<!ATTLIST default upstream CDATA #IMPLIED>
<!ATTLIST default sync-j CDATA #IMPLIED>
<!ATTLIST default sync-c CDATA #IMPLIED>
<!ATTLIST default sync-s CDATA #IMPLIED>
<!ATTLIST default sync-tags CDATA #IMPLIED>
<!ELEMENT manifest-server (EMPTY)>
<!ATTLIST manifest-server url CDATA #REQUIRED>
<!ELEMENT manifest-server EMPTY>
<!ATTLIST manifest-server url CDATA #REQUIRED>
<!ELEMENT project (annotation*,
project*,
copyfile*,
linkfile*)>
<!ATTLIST project name CDATA #REQUIRED>
<!ATTLIST project path CDATA #IMPLIED>
<!ATTLIST project remote IDREF #IMPLIED>
<!ATTLIST project revision CDATA #IMPLIED>
<!ATTLIST project dest-branch CDATA #IMPLIED>
<!ATTLIST project groups CDATA #IMPLIED>
<!ATTLIST project sync-c CDATA #IMPLIED>
<!ATTLIST project sync-s CDATA #IMPLIED>
<!ATTLIST project upstream CDATA #IMPLIED>
<!ATTLIST project clone-depth CDATA #IMPLIED>
<!ATTLIST project force-path CDATA #IMPLIED>
<!ELEMENT project (annotation*,
project*,
copyfile*,
linkfile*)>
<!ATTLIST project name CDATA #REQUIRED>
<!ATTLIST project path CDATA #IMPLIED>
<!ATTLIST project remote IDREF #IMPLIED>
<!ATTLIST project revision CDATA #IMPLIED>
<!ATTLIST project dest-branch CDATA #IMPLIED>
<!ATTLIST project groups CDATA #IMPLIED>
<!ATTLIST project sync-c CDATA #IMPLIED>
<!ATTLIST project sync-s CDATA #IMPLIED>
<!ATTLIST default sync-tags CDATA #IMPLIED>
<!ATTLIST project upstream CDATA #IMPLIED>
<!ATTLIST project clone-depth CDATA #IMPLIED>
<!ATTLIST project force-path CDATA #IMPLIED>
<!ELEMENT annotation (EMPTY)>
<!ATTLIST annotation name CDATA #REQUIRED>
<!ATTLIST annotation value CDATA #REQUIRED>
<!ATTLIST annotation keep CDATA "true">
<!ELEMENT annotation EMPTY>
<!ATTLIST annotation name CDATA #REQUIRED>
<!ATTLIST annotation value CDATA #REQUIRED>
<!ATTLIST annotation keep CDATA "true">
<!ELEMENT copyfile (EMPTY)>
<!ATTLIST copyfile src CDATA #REQUIRED>
<!ATTLIST copyfile dest CDATA #REQUIRED>
<!ELEMENT copyfile EMPTY>
<!ATTLIST copyfile src CDATA #REQUIRED>
<!ATTLIST copyfile dest CDATA #REQUIRED>
<!ELEMENT linkfile (EMPTY)>
<!ATTLIST linkfile src CDATA #REQUIRED>
<!ATTLIST linkfile dest CDATA #REQUIRED>
<!ELEMENT linkfile EMPTY>
<!ATTLIST linkfile src CDATA #REQUIRED>
<!ATTLIST linkfile dest CDATA #REQUIRED>
<!ELEMENT extend-project (EMPTY)>
<!ATTLIST extend-project name CDATA #REQUIRED>
<!ATTLIST extend-project path CDATA #IMPLIED>
<!ATTLIST extend-project groups CDATA #IMPLIED>
<!ELEMENT extend-project EMPTY>
<!ATTLIST extend-project name CDATA #REQUIRED>
<!ATTLIST extend-project path CDATA #IMPLIED>
<!ATTLIST extend-project groups CDATA #IMPLIED>
<!ATTLIST extend-project revision CDATA #IMPLIED>
<!ELEMENT remove-project (EMPTY)>
<!ATTLIST remove-project name CDATA #REQUIRED>
<!ELEMENT remove-project EMPTY>
<!ATTLIST remove-project name CDATA #REQUIRED>
<!ELEMENT repo-hooks (EMPTY)>
<!ATTLIST repo-hooks in-project CDATA #REQUIRED>
<!ATTLIST repo-hooks enabled-list CDATA #REQUIRED>
<!ELEMENT repo-hooks EMPTY>
<!ATTLIST repo-hooks in-project CDATA #REQUIRED>
<!ATTLIST repo-hooks enabled-list CDATA #REQUIRED>
<!ELEMENT include (EMPTY)>
<!ATTLIST include name CDATA #REQUIRED>
]>
<!ELEMENT include EMPTY>
<!ATTLIST include name CDATA #REQUIRED>
]>
```
A description of the elements and their attributes follows.
Element manifest
----------------
### Element manifest
The root element of the file.
Element remote
--------------
### Element remote
One or more remote elements may be specified. Each remote element
specifies a Git URL shared by one or more projects and (optionally)
@ -140,8 +145,7 @@ Attribute `revision`: Name of a Git branch (e.g. `master` or
`refs/heads/master`). Remotes with their own revision will override
the default revision.
Element default
---------------
### Element default
At most one default element may be specified. Its remote and
revision attributes are used when a project element does not
@ -160,6 +164,11 @@ Project elements not setting their own `dest-branch` will inherit
this value. If this value is not set, projects will use `revision`
by default instead.
Attribute `upstream`: Name of the Git ref in which a sha1
can be found. Used when syncing a revision locked manifest in
-c mode to avoid having to sync the entire ref space. Project elements
not setting their own `upstream` will inherit this value.
Attribute `sync-j`: Number of parallel jobs to use when synching.
Attribute `sync-c`: Set to true to only sync the given Git
@ -169,9 +178,12 @@ their own will use this value.
Attribute `sync-s`: Set to true to also sync sub-projects.
Attribute `sync-tags`: Set to false to only sync the given Git
branch (specified in the `revision` attribute) rather than
the other ref tags.
Element manifest-server
-----------------------
### Element manifest-server
At most one manifest-server may be specified. The url attribute
is used to specify the URL of a manifest server, which is an
@ -179,7 +191,7 @@ XML RPC service.
The manifest server should implement the following RPC methods:
GetApprovedManifest(branch, target)
GetApprovedManifest(branch, target)
Return a manifest in which each project is pegged to a known good revision
for the current branch and target. This is used by repo sync when the
@ -192,15 +204,14 @@ If one of those variables or both are not present, the program will call
GetApprovedManifest without the target parameter and the manifest server
should choose a reasonable default target.
GetManifest(tag)
GetManifest(tag)
Return a manifest in which each project is pegged to the revision at
the specified tag. This is used by repo sync when the --smart-tag option
is given.
Element project
---------------
### Element project
One or more project elements may be specified. Each element
describes a single Git repository to be cloned into the repo
@ -213,7 +224,7 @@ Attribute `name`: A unique name for this project. The project's
name is appended onto its remote's fetch URL to generate the actual
URL to configure the Git remote with. The URL gets formed as:
${remote_fetch}/${project_name}.git
${remote_fetch}/${project_name}.git
where ${remote_fetch} is the remote's fetch attribute and
${project_name} is the project's name attribute. The suffix ".git"
@ -277,8 +288,7 @@ rather than the `name` attribute. This attribute only applies to the
local mirrors syncing, it will be ignored when syncing the projects in a
client working directory.
Element extend-project
----------------------
### Element extend-project
Modify the attributes of the named project.
@ -293,8 +303,10 @@ at the specified path, rather than all projects with the given name.
Attribute `groups`: List of additional groups to which this project
belongs. Same syntax as the corresponding element of `project`.
Element annotation
------------------
Attribute `revision`: If specified, overrides the revision of the original
project. Same syntax as the corresponding element of `project`.
### Element annotation
Zero or more annotation elements may be specified as children of a
project element. Each element describes a name-value pair that will be
@ -304,23 +316,20 @@ prefixed with REPO__. In addition, there is an optional attribute
"false". This attribute determines whether or not the annotation will
be kept when exported with the manifest subcommand.
Element copyfile
----------------
### Element copyfile
Zero or more copyfile elements may be specified as children of a
project element. Each element describes a src-dest pair of files;
the "src" file will be copied to the "dest" place during 'repo sync'
the "src" file will be copied to the "dest" place during `repo sync`
command.
"src" is project relative, "dest" is relative to the top of the tree.
Element linkfile
----------------
### Element linkfile
It's just like copyfile and runs at the same time as copyfile but
instead of copying it creates a symlink.
Element remove-project
----------------------
### Element remove-project
Deletes the named project from the internal manifest table, possibly
allowing a subsequent project element in the same manifest file to
@ -330,8 +339,7 @@ This element is mostly useful in a local manifest file, where
the user can remove a project, and possibly replace it with their
own definition.
Element include
---------------
### Element include
This element provides the capability of including another manifest
file into the originating manifest. Normal rules apply for the
@ -341,26 +349,25 @@ Attribute `name`: the manifest to include, specified relative to
the manifest repository's root.
Local Manifests
===============
## Local Manifests
Additional remotes and projects may be added through local manifest
files stored in `$TOP_DIR/.repo/local_manifests/*.xml`.
For example:
$ ls .repo/local_manifests
local_manifest.xml
another_local_manifest.xml
$ ls .repo/local_manifests
local_manifest.xml
another_local_manifest.xml
$ cat .repo/local_manifests/local_manifest.xml
<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<project path="manifest"
name="tools/manifest" />
<project path="platform-manifest"
name="platform/manifest" />
</manifest>
$ cat .repo/local_manifests/local_manifest.xml
<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<project path="manifest"
name="tools/manifest" />
<project path="platform-manifest"
name="platform/manifest" />
</manifest>
Users may add projects to the local manifest(s) prior to a `repo sync`
invocation, instructing repo to automatically download and manage

110
docs/repo-hooks.md Normal file
View File

@ -0,0 +1,110 @@
# repo hooks
[TOC]
Repo provides a mechanism to hook specific stages of the runtime with custom
python modules. All the hooks live in one git project which is checked out by
the manifest (specified during `repo init`), and the manifest itself defines
which hooks are registered.
These are useful to run linters, check formatting, and run quick unittests
before allowing a step to proceed (e.g. before uploading a commit to Gerrit).
A complete example can be found in the Android project. It can be easily
re-used by any repo based project and is not specific to Android.<br>
https://android.googlesource.com/platform/tools/repohooks
## Approvals
When a hook is processed the first time, the user is prompted for approval.
We don't want to execute arbitrary code without explicit consent. For manifests
fetched via secure protocols (e.g. https://), the user is prompted once. For
insecure protocols (e.g. http://), the user is prompted whenever the registered
repohooks project is updated and a hook is triggered.
## Manifest Settings
For the full syntax, see the [repo manifest format](./manifest-format.md).
Here's a short example from
[Android](https://android.googlesource.com/platform/manifest/+/master/default.xml).
The `<project>` line checks out the repohooks git repo to the local
`tools/repohooks/` path. The `<repo-hooks>` line says to look in the project
with the name `platform/tools/repohooks` for hooks to run during the
`pre-upload` phase.
```xml
<project path="tools/repohooks" name="platform/tools/repohooks" />
<repo-hooks in-project="platform/tools/repohooks" enabled-list="pre-upload" />
```
## Source Layout
The repohooks git repo should have a python file with the same name as the hook.
So if you want to support the `pre-upload` hook, you'll need to create a file
named `pre-upload.py`. Repo will dynamically load that module when processing
the hook and then call the `main` function in it.
Hooks should have their `main` accept `**kwargs` for future compatibility.
## Runtime
Hook return values are ignored.
Any uncaught exceptions from the hook will cause the step to fail. This is
intended as a fallback safety check though rather than the normal flow. If
you want your hook to trigger a failure, it should call `sys.exit()` (after
displaying relevant diagnostics).
Output (stdout & stderr) are not filtered in any way. Hooks should generally
not be too verbose. A short summary is nice, and some status information when
long running operations occur, but long/verbose output should be used only if
the hook ultimately fails.
The hook runs from the top level of the repo client where the operation is
started.
For example, if the repo client is under `~/tree/`, then that is where the hook
runs, even if you ran repo in a git repository at `~/tree/src/foo/`, or in a
subdirectory of that git repository in `~/tree/src/foo/bar/`.
Hooks frequently start off by doing a `os.chdir` to the specific project they're
called on (see below) and then changing back to the original dir when they're
finished.
Python's `sys.path` is modified so that the top of repohooks directory comes
first. This should help simplify the hook logic to easily allow importing of
local modules.
Repo does not modify the state of the git checkout. This means that the hooks
might be running in a dirty git repo with many commits and checked out to the
latest one. If the hook wants to operate on specific git commits, it needs to
manually discover the list of pending commits, extract the diff/commit, and
then check it directly. Hooks should not normally modify the active git repo
(such as checking out a specific commit to run checks) without first prompting
the user. Although user interaction is discouraged in the common case, it can
be useful when deploying automatic fixes.
## Hooks
Here are all the points available for hooking.
### pre-upload
This hook runs when people run `repo upload`.
The `pre-upload.py` file should be defined like:
```py
def main(project_list, worktree_list=None, **kwargs):
"""Main function invoked directly by repo.
We must use the name "main" as that is what repo requires.
Args:
project_list: List of projects to run on.
worktree_list: A list of directories. It should be the same length as
project_list, so that each entry in project_list matches with a
directory in worktree_list. If None, we will attempt to calculate
the directories automatically.
kwargs: Leave this here for forward-compatibility.
"""
```

View File

@ -21,6 +21,7 @@ import subprocess
import tempfile
from error import EditorError
import platform_utils
class Editor(object):
"""Manages the user's preferred text editor."""
@ -82,7 +83,12 @@ least one of these before using this command.""", file=sys.stderr)
os.close(fd)
fd = None
if re.compile("^.*[$ \t'].*$").match(editor):
if platform_utils.isWindows():
# Split on spaces, respecting quoted strings
import shlex
args = shlex.split(editor)
shell = False
elif re.compile("^.*[$ \t'].*$").match(editor):
args = [editor + ' "$@"', 'sh']
shell = True
else:
@ -107,4 +113,4 @@ least one of these before using this command.""", file=sys.stderr)
finally:
if fd:
os.close(fd)
os.remove(path)
platform_utils.remove(path)

176
event_log.py Normal file
View File

@ -0,0 +1,176 @@
#
# Copyright (C) 2017 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.
from __future__ import print_function
import json
import multiprocessing
TASK_COMMAND = 'command'
TASK_SYNC_NETWORK = 'sync-network'
TASK_SYNC_LOCAL = 'sync-local'
class EventLog(object):
"""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.
Each entry contains the following keys:
- id: A ('RepoOp', ID) tuple, suitable for storing in a datastore.
The ID is only unique for the invocation of the repo command.
- name: Name of the object being operated upon.
- task_name: The task that was performed.
- start: Timestamp of when the operation started.
- finish: Timestamp of when the operation finished.
- success: Boolean indicating if the operation was successful.
- try_count: A counter indicating the try count of this task.
Optionally:
- parent: A ('RepoOp', ID) tuple indicating the parent event for nested
events.
Valid task_names include:
- command: The invocation of a subcommand.
- sync-network: The network component of a sync command.
- sync-local: The local component of a sync command.
Specific tasks may include additional informational properties.
"""
def __init__(self):
"""Initializes the event log."""
self._log = []
self._parent = None
def Add(self, name, task_name, start, finish=None, success=None,
try_count=1, kind='RepoOp'):
"""Add an event to the log.
Args:
name: Name of the object being operated upon.
task_name: A sub-task that was performed for name.
start: Timestamp of when the operation started.
finish: Timestamp of when the operation finished.
success: Boolean indicating if the operation was successful.
try_count: A counter indicating the try count of this task.
kind: The kind of the object for the unique identifier.
Returns:
A dictionary of the event added to the log.
"""
event = {
'id': (kind, _NextEventId()),
'name': name,
'task_name': task_name,
'start_time': start,
'try': try_count,
}
if self._parent:
event['parent'] = self._parent['id']
if success is not None or finish is not None:
self.FinishEvent(event, finish, success)
self._log.append(event)
return event
def AddSync(self, project, task_name, start, finish, success):
"""Add a event to the log for a sync command.
Args:
project: Project being synced.
task_name: A sub-task that was performed for name.
One of (TASK_SYNC_NETWORK, TASK_SYNC_LOCAL)
start: Timestamp of when the operation started.
finish: Timestamp of when the operation finished.
success: Boolean indicating if the operation was successful.
Returns:
A dictionary of the event added to the log.
"""
event = self.Add(project.relpath, task_name, start, finish, success)
if event is not None:
event['project'] = project.name
if project.revisionExpr:
event['revision'] = project.revisionExpr
if project.remote.url:
event['project_url'] = project.remote.url
if project.remote.fetchUrl:
event['remote_url'] = project.remote.fetchUrl
try:
event['git_hash'] = project.GetCommitRevisionId()
except Exception:
pass
return event
def GetStatusString(self, success):
"""Converst a boolean success to a status string.
Args:
success: Boolean indicating if the operation was successful.
Returns:
status string.
"""
return 'pass' if success else 'fail'
def FinishEvent(self, event, finish, success):
"""Finishes an incomplete event.
Args:
event: An event that has been added to the log.
finish: Timestamp of when the operation finished.
success: Boolean indicating if the operation was successful.
Returns:
A dictionary of the event added to the log.
"""
event['status'] = self.GetStatusString(success)
event['finish_time'] = finish
return event
def SetParent(self, event):
"""Set a parent event for all new entities.
Args:
event: The event to use as a parent.
"""
self._parent = event
def Write(self, filename):
"""Writes the log out to a file.
Args:
filename: The file to write the log to.
"""
with open(filename, 'w+') as f:
for e in self._log:
json.dump(e, f, sort_keys=True)
f.write('\n')
# An integer id that is unique across this invocation of the program.
_EVENT_ID = multiprocessing.Value('i', 1)
def _NextEventId():
"""Helper function for grabbing the next unique id.
Returns:
A unique, to this invocation of the program, integer id.
"""
with _EVENT_ID.get_lock():
val = _EVENT_ID.value
_EVENT_ID.value += 1
return val

View File

@ -14,14 +14,14 @@
# limitations under the License.
from __future__ import print_function
import fcntl
import os
import select
import sys
import subprocess
import tempfile
from signal import SIGTERM
from error import GitError
import platform_utils
from trace import REPO_TRACE, IsTrace, Trace
from wrapper import Wrapper
@ -78,16 +78,6 @@ def terminate_ssh_clients():
_git_version = None
class _sfd(object):
"""select file descriptor class"""
def __init__(self, fd, dest, std_name):
assert std_name in ('stdout', 'stderr')
self.fd = fd
self.dest = dest
self.std_name = std_name
def fileno(self):
return self.fd.fileno()
class _GitCall(object):
def version(self):
p = GitCommand(None, ['--version'], capture_stdout=True)
@ -162,6 +152,7 @@ class GitCommand(object):
if ssh_proxy:
_setenv(env, 'REPO_SSH_SOCK', ssh_sock())
_setenv(env, 'GIT_SSH', _ssh_proxy())
_setenv(env, 'GIT_SSH_VARIANT', 'ssh')
if 'http_proxy' in env and 'darwin' == sys.platform:
s = "'http.proxy=%s'" % (env['http_proxy'],)
p = env.get('GIT_CONFIG_PARAMETERS')
@ -253,19 +244,16 @@ class GitCommand(object):
def _CaptureOutput(self):
p = self.process
s_in = [_sfd(p.stdout, sys.stdout, 'stdout'),
_sfd(p.stderr, sys.stderr, 'stderr')]
s_in = platform_utils.FileDescriptorStreams.create()
s_in.add(p.stdout, sys.stdout, 'stdout')
s_in.add(p.stderr, sys.stderr, 'stderr')
self.stdout = ''
self.stderr = ''
for s in s_in:
flags = fcntl.fcntl(s.fd, fcntl.F_GETFL)
fcntl.fcntl(s.fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
while s_in:
in_ready, _, _ = select.select(s_in, [], [])
while not s_in.is_done:
in_ready = s_in.select()
for s in in_ready:
buf = s.fd.read(4096)
buf = s.read()
if not buf:
s_in.remove(s)
continue

View File

@ -20,6 +20,7 @@ import errno
import json
import os
import re
import ssl
import subprocess
import sys
try:
@ -41,6 +42,7 @@ else:
from signal import SIGTERM
from error import GitError, UploadError
import platform_utils
from trace import Trace
if is_python3():
from http.client import HTTPException
@ -50,16 +52,24 @@ else:
from git_command import GitCommand
from git_command import ssh_sock
from git_command import terminate_ssh_clients
from git_refs import R_CHANGES, R_HEADS, R_TAGS
R_HEADS = 'refs/heads/'
R_TAGS = 'refs/tags/'
ID_RE = re.compile(r'^[0-9a-f]{40}$')
REVIEW_CACHE = dict()
def IsChange(rev):
return rev.startswith(R_CHANGES)
def IsId(rev):
return ID_RE.match(rev)
def IsTag(rev):
return rev.startswith(R_TAGS)
def IsImmutable(rev):
return IsChange(rev) or IsId(rev) or IsTag(rev)
def _key(name):
parts = name.split('.')
if len(parts) < 2:
@ -259,7 +269,7 @@ class GitConfig(object):
try:
if os.path.getmtime(self._json) \
<= os.path.getmtime(self.file):
os.remove(self._json)
platform_utils.remove(self._json)
return None
except OSError:
return None
@ -271,7 +281,7 @@ class GitConfig(object):
finally:
fd.close()
except (IOError, ValueError):
os.remove(self._json)
platform_utils.remove(self._json)
return None
def _SaveJson(self, cache):
@ -283,7 +293,7 @@ class GitConfig(object):
fd.close()
except (IOError, TypeError):
if os.path.exists(self._json):
os.remove(self._json)
platform_utils.remove(self._json)
def _ReadGit(self):
"""
@ -296,8 +306,9 @@ class GitConfig(object):
d = self._do('--null', '--list')
if d is None:
return c
for line in d.decode('utf-8').rstrip('\0').split('\0'): # pylint: disable=W1401
# Backslash is not anomalous
if not is_python3():
d = d.decode('utf-8')
for line in d.rstrip('\0').split('\0'):
if '\n' in line:
key, val = line.split('\n', 1)
else:
@ -492,7 +503,7 @@ def close_ssh():
d = ssh_sock(create=False)
if d:
try:
os.rmdir(os.path.dirname(d))
platform_utils.rmdir(os.path.dirname(d))
except OSError:
pass
@ -524,7 +535,7 @@ def GetUrlCookieFile(url, quiet):
for line in p.stdout:
line = line.strip()
if line.startswith(cookieprefix):
cookiefile = line[len(cookieprefix):]
cookiefile = os.path.expanduser(line[len(cookieprefix):])
if line.startswith(proxyprefix):
proxy = line[len(proxyprefix):]
# Leave subprocess open, as cookie file may be transient.
@ -543,7 +554,10 @@ def GetUrlCookieFile(url, quiet):
if e.errno == errno.ENOENT:
pass # No persistent proxy.
raise
yield GitConfig.ForUser().GetString('http.cookiefile'), None
cookiefile = GitConfig.ForUser().GetString('http.cookiefile')
if cookiefile:
cookiefile = os.path.expanduser(cookiefile)
yield cookiefile, None
def _preconnect(url):
m = URI_ALL.match(url)
@ -604,7 +618,7 @@ class Remote(object):
connectionUrl = self._InsteadOf()
return _preconnect(connectionUrl)
def ReviewUrl(self, userEmail):
def ReviewUrl(self, userEmail, validate_certs):
if self._review_url is None:
if self.review is None:
return None
@ -612,7 +626,7 @@ class Remote(object):
u = self.review
if u.startswith('persistent-'):
u = u[len('persistent-'):]
if u.split(':')[0] not in ('http', 'https', 'sso'):
if u.split(':')[0] not in ('http', 'https', 'sso', 'ssh'):
u = 'http://%s' % u
if u.endswith('/Gerrit'):
u = u[:len(u) - len('/Gerrit')]
@ -628,13 +642,20 @@ class Remote(object):
host, port = os.environ['REPO_HOST_PORT_INFO'].split()
self._review_url = self._SshReviewUrl(userEmail, host, port)
REVIEW_CACHE[u] = self._review_url
elif u.startswith('sso:'):
elif u.startswith('sso:') or u.startswith('ssh:'):
self._review_url = u # Assume it's right
REVIEW_CACHE[u] = self._review_url
elif 'REPO_IGNORE_SSH_INFO' in os.environ:
self._review_url = http_url
REVIEW_CACHE[u] = self._review_url
else:
try:
info_url = u + 'ssh_info'
info = urllib.request.urlopen(info_url).read()
if not validate_certs:
context = ssl._create_unverified_context()
info = urllib.request.urlopen(info_url, context=context).read()
else:
info = urllib.request.urlopen(info_url).read()
if info == 'NOT_AVAILABLE' or '<' in info:
# If `info` contains '<', we assume the server gave us some sort
# of HTML response back, like maybe a login page.
@ -643,10 +664,7 @@ class Remote(object):
self._review_url = http_url
else:
host, port = info.split()
if _open_ssh(host, port):
self._review_url = self._SshReviewUrl(userEmail, host, port)
else:
self._review_url = http_url
self._review_url = self._SshReviewUrl(userEmail, host, port)
except urllib.error.HTTPError as e:
raise UploadError('%s: %s' % (self.review, str(e)))
except urllib.error.URLError as e:

View File

@ -15,12 +15,14 @@
import os
from trace import Trace
import platform_utils
HEAD = 'HEAD'
R_HEADS = 'refs/heads/'
R_TAGS = 'refs/tags/'
R_PUB = 'refs/published/'
R_M = 'refs/remotes/m/'
HEAD = 'HEAD'
R_CHANGES = 'refs/changes/'
R_HEADS = 'refs/heads/'
R_TAGS = 'refs/tags/'
R_PUB = 'refs/published/'
R_M = 'refs/remotes/m/'
class GitRefs(object):
@ -126,9 +128,9 @@ class GitRefs(object):
def _ReadLoose(self, prefix):
base = os.path.join(self._gitdir, prefix)
for name in os.listdir(base):
for name in platform_utils.listdir(base):
p = os.path.join(base, name)
if os.path.isdir(p):
if platform_utils.isdir(p):
self._mtime[prefix] = os.path.getmtime(base)
self._ReadLoose(prefix + name + '/')
elif name.endswith('.lock'):
@ -138,7 +140,7 @@ class GitRefs(object):
def _ReadLoose1(self, path, name):
try:
fd = open(path, 'rb')
fd = open(path)
except IOError:
return

View File

@ -1,5 +1,5 @@
#!/bin/sh
# From Gerrit Code Review 2.12.1
# From Gerrit Code Review 2.14.6
#
# Part of Gerrit Code Review (https://www.gerritcodereview.com/)
#
@ -20,7 +20,7 @@
unset GREP_OPTIONS
CHANGE_ID_AFTER="Bug|Issue|Test"
CHANGE_ID_AFTER="Bug|Depends-On|Issue|Test|Feature|Fixes|Fixed"
MSG="$1"
# Check for, and add if missing, a unique Change-Id

View File

@ -1,9 +1,9 @@
#!/bin/sh
#
# An example hook script to verify if you are on battery, in case you
# are running Linux or OS X. Called by git-gc --auto with no arguments.
# The hook should exit with non-zero status after issuing an appropriate
# message if it wants to stop the auto repacking.
# are running Windows, Linux or OS X. Called by git-gc --auto with no
# arguments. The hook should exit with non-zero status after issuing an
# appropriate message if it wants to stop the auto repacking.
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -19,7 +19,17 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
if test -x /sbin/on_ac_power && /sbin/on_ac_power
if uname -s | grep -q "_NT-"
then
if test -x $SYSTEMROOT/System32/Wbem/wmic
then
STATUS=$(wmic path win32_battery get batterystatus /format:list | tr -d '\r\n')
[ "$STATUS" = "BatteryStatus=2" ] && exit 0 || exit 1
fi
exit 0
fi
if test -x /sbin/on_ac_power && (/sbin/on_ac_power;test $? -ne 1)
then
exit 0
elif test "$(cat /sys/class/power_supply/AC/online 2>/dev/null)" = 1

26
main.py
View File

@ -37,6 +37,7 @@ except ImportError:
kerberos = None
from color import SetDefaultColoring
import event_log
from trace import SetTrace
from git_command import git, GitCommand
from git_config import init_ssh, close_ssh
@ -54,15 +55,13 @@ from error import NoSuchProjectError
from error import RepoChangedException
import gitc_utils
from manifest_xml import GitcManifest, XmlManifest
from pager import RunPager
from pager import RunPager, TerminatePager
from wrapper import WrapperPath, Wrapper
from subcmds import all_commands
if not is_python3():
# pylint:disable=W0622
input = raw_input
# pylint:enable=W0622
global_options = optparse.OptionParser(
usage="repo [-p|--paginate|--no-pager] COMMAND [ARGS]"
@ -85,6 +84,9 @@ global_options.add_option('--time',
global_options.add_option('--version',
dest='show_version', action='store_true',
help='display this version of repo')
global_options.add_option('--event-log',
dest='event_log', action='store',
help='filename of event log to append timeline to')
class _Repo(object):
def __init__(self, repodir):
@ -176,6 +178,8 @@ class _Repo(object):
RunPager(config)
start = time.time()
cmd_event = cmd.event_log.Add(name, event_log.TASK_COMMAND, start)
cmd.event_log.SetParent(cmd_event)
try:
result = cmd.Execute(copts, cargs)
except (DownloadError, ManifestInvalidRevisionError,
@ -198,8 +202,13 @@ class _Repo(object):
else:
print('error: project group must be enabled for the project in the current directory', file=sys.stderr)
result = 1
except SystemExit as e:
if e.code:
result = e.code
raise
finally:
elapsed = time.time() - start
finish = time.time()
elapsed = finish - start
hours, remainder = divmod(elapsed, 3600)
minutes, seconds = divmod(remainder, 60)
if gopts.time:
@ -209,6 +218,12 @@ class _Repo(object):
print('real\t%dh%dm%.3fs' % (hours, minutes, seconds),
file=sys.stderr)
cmd.event_log.FinishEvent(cmd_event, finish,
result is None or result == 0)
if gopts.event_log:
cmd.event_log.Write(os.path.abspath(
os.path.expanduser(gopts.event_log)))
return result
@ -379,7 +394,7 @@ class _KerberosAuthHandler(urllib.request.BaseHandler):
self.context = None
self.handler_order = urllib.request.BaseHandler.handler_order - 50
def http_error_401(self, req, fp, code, msg, headers): # pylint:disable=unused-argument
def http_error_401(self, req, fp, code, msg, headers):
host = req.get_host()
retry = self.http_error_auth_reqed('www-authenticate', host, req, headers)
return retry
@ -525,6 +540,7 @@ def _Main(argv):
print('fatal: %s' % e, file=sys.stderr)
result = 128
TerminatePager()
sys.exit(result)
if __name__ == '__main__':

View File

@ -32,6 +32,7 @@ else:
import gitc_utils
from git_config import GitConfig
from git_refs import R_HEADS, HEAD
import platform_utils
from project import RemoteSpec, Project, MetaProject
from error import ManifestParseError, ManifestInvalidRevisionError
@ -40,18 +41,30 @@ LOCAL_MANIFEST_NAME = 'local_manifest.xml'
LOCAL_MANIFESTS_DIR_NAME = 'local_manifests'
# urljoin gets confused if the scheme is not known.
urllib.parse.uses_relative.extend(['ssh', 'git', 'persistent-https', 'rpc'])
urllib.parse.uses_netloc.extend(['ssh', 'git', 'persistent-https', 'rpc'])
urllib.parse.uses_relative.extend([
'ssh',
'git',
'persistent-https',
'sso',
'rpc'])
urllib.parse.uses_netloc.extend([
'ssh',
'git',
'persistent-https',
'sso',
'rpc'])
class _Default(object):
"""Project defaults within the manifest."""
revisionExpr = None
destBranchExpr = None
upstreamExpr = None
remote = None
sync_j = 1
sync_c = False
sync_s = False
sync_tags = True
def __eq__(self, other):
return self.__dict__ == other.__dict__
@ -100,7 +113,8 @@ class _XmlRemote(object):
return url
def ToRemoteSpec(self, projectName):
url = self.resolvedFetchUrl.rstrip('/') + '/' + projectName
fetchUrl = self.resolvedFetchUrl.rstrip('/')
url = fetchUrl + '/' + projectName
remoteName = self.name
if self.remoteAlias:
remoteName = self.remoteAlias
@ -108,7 +122,8 @@ class _XmlRemote(object):
url=url,
pushUrl=self.pushUrl,
review=self.reviewUrl,
orig_name=self.name)
orig_name=self.name,
fetchUrl=self.fetchUrl)
class XmlManifest(object):
"""manages the repo configuration file"""
@ -153,8 +168,8 @@ class XmlManifest(object):
try:
if os.path.lexists(self.manifestFile):
os.remove(self.manifestFile)
os.symlink('manifests/%s' % name, self.manifestFile)
platform_utils.remove(self.manifestFile)
platform_utils.symlink(os.path.join('manifests', name), self.manifestFile)
except OSError as e:
raise ManifestParseError('cannot link manifest %s: %s' % (name, str(e)))
@ -216,6 +231,9 @@ class XmlManifest(object):
if d.destBranchExpr:
have_default = True
e.setAttribute('dest-branch', d.destBranchExpr)
if d.upstreamExpr:
have_default = True
e.setAttribute('upstream', d.upstreamExpr)
if d.sync_j > 1:
have_default = True
e.setAttribute('sync-j', '%d' % d.sync_j)
@ -225,6 +243,9 @@ class XmlManifest(object):
if d.sync_s:
have_default = True
e.setAttribute('sync-s', 'true')
if not d.sync_tags:
have_default = True
e.setAttribute('sync-tags', 'false')
if have_default:
root.appendChild(e)
root.appendChild(doc.createTextNode(''))
@ -278,7 +299,8 @@ class XmlManifest(object):
revision = self.remotes[p.remote.orig_name].revision or d.revisionExpr
if not revision or revision != p.revisionExpr:
e.setAttribute('revision', p.revisionExpr)
if p.upstream and p.upstream != p.revisionExpr:
if (p.upstream and (p.upstream != p.revisionExpr or
p.upstream != d.upstreamExpr)):
e.setAttribute('upstream', p.upstream)
if p.dest_branch and p.dest_branch != d.destBranchExpr:
@ -314,6 +336,9 @@ class XmlManifest(object):
if p.sync_s:
e.setAttribute('sync-s', 'true')
if not p.sync_tags:
e.setAttribute('sync-tags', 'false')
if p.clone_depth:
e.setAttribute('clone-depth', str(p.clone_depth))
@ -383,6 +408,10 @@ class XmlManifest(object):
def IsArchive(self):
return self.manifestProject.config.GetBoolean('repo.archive')
@property
def HasSubmodules(self):
return self.manifestProject.config.GetBoolean('repo.submodules')
def _Unload(self):
self._loaded = False
self._projects = {}
@ -417,7 +446,7 @@ class XmlManifest(object):
local_dir = os.path.abspath(os.path.join(self.repodir, LOCAL_MANIFESTS_DIR_NAME))
try:
for local_file in sorted(os.listdir(local_dir)):
for local_file in sorted(platform_utils.listdir(local_dir)):
if local_file.endswith('.xml'):
local = os.path.join(local_dir, local_file)
nodes.append(self._ParseManifestXml(local, self.repodir))
@ -454,8 +483,7 @@ class XmlManifest(object):
raise ManifestParseError("no <manifest> in %s" % (path,))
nodes = []
for node in manifest.childNodes: # pylint:disable=W0631
# We only get here if manifest is initialised
for node in manifest.childNodes:
if node.nodeName == 'include':
name = self._reqatt(node, 'name')
fp = os.path.join(include_root, name)
@ -547,12 +575,15 @@ class XmlManifest(object):
groups = node.getAttribute('groups')
if groups:
groups = self._ParseGroups(groups)
revision = node.getAttribute('revision')
for p in self._projects[name]:
if path and p.relpath != path:
continue
if groups:
p.groups.extend(groups)
if revision:
p.revisionExpr = revision
if node.nodeName == 'repo-hooks':
# Get the name of the project and the (space-separated) list of enabled.
repo_hooks_project = self._reqatt(node, 'in-project')
@ -667,6 +698,7 @@ class XmlManifest(object):
d.revisionExpr = None
d.destBranchExpr = node.getAttribute('dest-branch') or None
d.upstreamExpr = node.getAttribute('upstream') or None
sync_j = node.getAttribute('sync-j')
if sync_j == '' or sync_j is None:
@ -685,6 +717,12 @@ class XmlManifest(object):
d.sync_s = False
else:
d.sync_s = sync_s.lower() in ("yes", "true", "1")
sync_tags = node.getAttribute('sync-tags')
if not sync_tags:
d.sync_tags = True
else:
d.sync_tags = sync_tags.lower() in ("yes", "true", "1")
return d
def _ParseNotice(self, node):
@ -779,6 +817,12 @@ class XmlManifest(object):
else:
sync_s = sync_s.lower() in ("yes", "true", "1")
sync_tags = node.getAttribute('sync-tags')
if not sync_tags:
sync_tags = self._default.sync_tags
else:
sync_tags = sync_tags.lower() in ("yes", "true", "1")
clone_depth = node.getAttribute('clone-depth')
if clone_depth:
try:
@ -791,7 +835,7 @@ class XmlManifest(object):
dest_branch = node.getAttribute('dest-branch') or self._default.destBranchExpr
upstream = node.getAttribute('upstream')
upstream = node.getAttribute('upstream') or self._default.upstreamExpr
groups = ''
if node.hasAttribute('groups'):
@ -824,6 +868,7 @@ class XmlManifest(object):
groups = groups,
sync_c = sync_c,
sync_s = sync_s,
sync_tags = sync_tags,
clone_depth = clone_depth,
upstream = upstream,
parent = parent,

View File

@ -16,19 +16,53 @@
from __future__ import print_function
import os
import select
import subprocess
import sys
import platform_utils
active = False
pager_process = None
old_stdout = None
old_stderr = None
def RunPager(globalConfig):
global active
if not os.isatty(0) or not os.isatty(1):
return
pager = _SelectPager(globalConfig)
if pager == '' or pager == 'cat':
return
if platform_utils.isWindows():
_PipePager(pager);
else:
_ForkPager(pager)
def TerminatePager():
global pager_process, old_stdout, old_stderr
if pager_process:
sys.stdout.flush()
sys.stderr.flush()
pager_process.stdin.close()
pager_process.wait();
pager_process = None
# Restore initial stdout/err in case there is more output in this process
# after shutting down the pager process
sys.stdout = old_stdout
sys.stderr = old_stderr
def _PipePager(pager):
global pager_process, old_stdout, old_stderr
assert pager_process is None, "Only one active pager process at a time"
# Create pager process, piping stdout/err into its stdin
pager_process = subprocess.Popen([pager], stdin=subprocess.PIPE, stdout=sys.stdout, stderr=sys.stderr)
old_stdout = sys.stdout
old_stderr = sys.stderr
sys.stdout = pager_process.stdin
sys.stderr = pager_process.stdin
def _ForkPager(pager):
global active
# This process turns into the pager; a child it forks will
# do the real processing and output back to the pager. This
# is necessary to keep the pager in control of the tty.

414
platform_utils.py Normal file
View File

@ -0,0 +1,414 @@
#
# Copyright (C) 2016 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.
import errno
import os
import platform
import select
import shutil
import stat
from pyversion import is_python3
if is_python3():
from queue import Queue
else:
from Queue import Queue
from threading import Thread
def isWindows():
""" Returns True when running with the native port of Python for Windows,
False when running on any other platform (including the Cygwin port of
Python).
"""
# Note: The cygwin port of Python returns "CYGWIN_NT_xxx"
return platform.system() == "Windows"
class FileDescriptorStreams(object):
""" Platform agnostic abstraction enabling non-blocking I/O over a
collection of file descriptors. This abstraction is required because
fctnl(os.O_NONBLOCK) is not supported on Windows.
"""
@classmethod
def create(cls):
""" Factory method: instantiates the concrete class according to the
current platform.
"""
if isWindows():
return _FileDescriptorStreamsThreads()
else:
return _FileDescriptorStreamsNonBlocking()
def __init__(self):
self.streams = []
def add(self, fd, dest, std_name):
""" Wraps an existing file descriptor as a stream.
"""
self.streams.append(self._create_stream(fd, dest, std_name))
def remove(self, stream):
""" Removes a stream, when done with it.
"""
self.streams.remove(stream)
@property
def is_done(self):
""" Returns True when all streams have been processed.
"""
return len(self.streams) == 0
def select(self):
""" Returns the set of streams that have data available to read.
The returned streams each expose a read() and a close() method.
When done with a stream, call the remove(stream) method.
"""
raise NotImplementedError
def _create_stream(fd, dest, std_name):
""" Creates a new stream wrapping an existing file descriptor.
"""
raise NotImplementedError
class _FileDescriptorStreamsNonBlocking(FileDescriptorStreams):
""" Implementation of FileDescriptorStreams for platforms that support
non blocking I/O.
"""
class Stream(object):
""" Encapsulates a file descriptor """
def __init__(self, fd, dest, std_name):
self.fd = fd
self.dest = dest
self.std_name = std_name
self.set_non_blocking()
def set_non_blocking(self):
import fcntl
flags = fcntl.fcntl(self.fd, fcntl.F_GETFL)
fcntl.fcntl(self.fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
def fileno(self):
return self.fd.fileno()
def read(self):
return self.fd.read(4096)
def close(self):
self.fd.close()
def _create_stream(self, fd, dest, std_name):
return self.Stream(fd, dest, std_name)
def select(self):
ready_streams, _, _ = select.select(self.streams, [], [])
return ready_streams
class _FileDescriptorStreamsThreads(FileDescriptorStreams):
""" Implementation of FileDescriptorStreams for platforms that don't support
non blocking I/O. This implementation requires creating threads issuing
blocking read operations on file descriptors.
"""
def __init__(self):
super(_FileDescriptorStreamsThreads, self).__init__()
# The queue is shared accross all threads so we can simulate the
# behavior of the select() function
self.queue = Queue(10) # Limit incoming data from streams
def _create_stream(self, fd, dest, std_name):
return self.Stream(fd, dest, std_name, self.queue)
def select(self):
# Return only one stream at a time, as it is the most straighforward
# thing to do and it is compatible with the select() function.
item = self.queue.get()
stream = item.stream
stream.data = item.data
return [stream]
class QueueItem(object):
""" Item put in the shared queue """
def __init__(self, stream, data):
self.stream = stream
self.data = data
class Stream(object):
""" Encapsulates a file descriptor """
def __init__(self, fd, dest, std_name, queue):
self.fd = fd
self.dest = dest
self.std_name = std_name
self.queue = queue
self.data = None
self.thread = Thread(target=self.read_to_queue)
self.thread.daemon = True
self.thread.start()
def close(self):
self.fd.close()
def read(self):
data = self.data
self.data = None
return data
def read_to_queue(self):
""" The thread function: reads everything from the file descriptor into
the shared queue and terminates when reaching EOF.
"""
for line in iter(self.fd.readline, b''):
self.queue.put(_FileDescriptorStreamsThreads.QueueItem(self, line))
self.fd.close()
self.queue.put(_FileDescriptorStreamsThreads.QueueItem(self, None))
def symlink(source, link_name):
"""Creates a symbolic link pointing to source named link_name.
Note: On Windows, source must exist on disk, as the implementation needs
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)
if isdir(target):
platform_utils_win32.create_dirsymlink(_makelongpath(source), link_name)
else:
platform_utils_win32.create_filesymlink(_makelongpath(source), link_name)
else:
return os.symlink(source, link_name)
def _validate_winpath(path):
path = os.path.normpath(path)
if _winpath_is_valid(path):
return path
raise ValueError("Path \"%s\" must be a relative path or an absolute "
"path starting with a drive letter".format(path))
def _winpath_is_valid(path):
"""Windows only: returns True if path is relative (e.g. ".\\foo") or is
absolute including a drive letter (e.g. "c:\\foo"). Returns False if path
is ambiguous (e.g. "x:foo" or "\\foo").
"""
assert isWindows()
path = os.path.normpath(path)
drive, tail = os.path.splitdrive(path)
if tail:
if not drive:
return tail[0] != os.sep # "\\foo" is invalid
else:
return tail[0] == os.sep # "x:foo" is invalid
else:
return not drive # "x:" is invalid
def _makelongpath(path):
"""Return the input path normalized to support the Windows long path syntax
("\\\\?\\" prefix) if needed, i.e. if the input path is longer than the
MAX_PATH limit.
"""
if isWindows():
# Note: MAX_PATH is 260, but, for directories, the maximum value is actually 246.
if len(path) < 246:
return path
if path.startswith(u"\\\\?\\"):
return path
if not os.path.isabs(path):
return path
# Append prefix and ensure unicode so that the special longpath syntax
# is supported by underlying Win32 API calls
return u"\\\\?\\" + os.path.normpath(path)
else:
return path
def rmtree(path):
"""shutil.rmtree(path) wrapper with support for long paths on Windows.
Availability: Unix, Windows."""
if isWindows():
shutil.rmtree(_makelongpath(path), onerror=handle_rmtree_error)
else:
shutil.rmtree(path)
def handle_rmtree_error(function, path, excinfo):
# Allow deleting read-only files
os.chmod(path, stat.S_IWRITE)
function(path)
def rename(src, dst):
"""os.rename(src, dst) wrapper with support for long paths on Windows.
Availability: Unix, Windows."""
if isWindows():
# On Windows, rename fails if destination exists, see
# https://docs.python.org/2/library/os.html#os.rename
try:
os.rename(_makelongpath(src), _makelongpath(dst))
except OSError as e:
if e.errno == errno.EEXIST:
os.remove(_makelongpath(dst))
os.rename(_makelongpath(src), _makelongpath(dst))
else:
raise
else:
os.rename(src, dst)
def remove(path):
"""Remove (delete) the file path. This is a replacement for os.remove that
allows deleting read-only files on Windows, with support for long paths and
for deleting directory symbolic links.
Availability: Unix, Windows."""
if isWindows():
longpath = _makelongpath(path)
try:
os.remove(longpath)
except OSError as e:
if e.errno == errno.EACCES:
os.chmod(longpath, stat.S_IWRITE)
# Directory symbolic links must be deleted with 'rmdir'.
if islink(longpath) and isdir(longpath):
os.rmdir(longpath)
else:
os.remove(longpath)
else:
raise
else:
os.remove(path)
def walk(top, topdown=True, onerror=None, followlinks=False):
"""os.walk(path) wrapper with support for long paths on Windows.
Availability: Windows, Unix.
"""
if isWindows():
return _walk_windows_impl(top, topdown, onerror, followlinks)
else:
return os.walk(top, topdown, onerror, followlinks)
def _walk_windows_impl(top, topdown, onerror, followlinks):
try:
names = listdir(top)
except Exception as err:
if onerror is not None:
onerror(err)
return
dirs, nondirs = [], []
for name in names:
if isdir(os.path.join(top, name)):
dirs.append(name)
else:
nondirs.append(name)
if topdown:
yield top, dirs, nondirs
for name in dirs:
new_path = os.path.join(top, name)
if followlinks or not islink(new_path):
for x in _walk_windows_impl(new_path, topdown, onerror, followlinks):
yield x
if not topdown:
yield top, dirs, nondirs
def listdir(path):
"""os.listdir(path) wrapper with support for long paths on Windows.
Availability: Windows, Unix.
"""
return os.listdir(_makelongpath(path))
def rmdir(path):
"""os.rmdir(path) wrapper with support for long paths on Windows.
Availability: Windows, Unix.
"""
os.rmdir(_makelongpath(path))
def isdir(path):
"""os.path.isdir(path) wrapper with support for long paths on Windows.
Availability: Windows, Unix.
"""
return os.path.isdir(_makelongpath(path))
def islink(path):
"""os.path.islink(path) wrapper with support for long paths on Windows.
Availability: Windows, Unix.
"""
if isWindows():
import platform_utils_win32
return platform_utils_win32.islink(_makelongpath(path))
else:
return os.path.islink(path)
def readlink(path):
"""Return a string representing the path to which the symbolic link
points. The result may be either an absolute or relative pathname;
if it is relative, it may be converted to an absolute pathname using
os.path.join(os.path.dirname(path), result).
Availability: Windows, Unix.
"""
if isWindows():
import platform_utils_win32
return platform_utils_win32.readlink(_makelongpath(path))
else:
return os.readlink(path)
def realpath(path):
"""Return the canonical path of the specified filename, eliminating
any symbolic links encountered in the path.
Availability: Windows, Unix.
"""
if isWindows():
current_path = os.path.abspath(path)
path_tail = []
for c in range(0, 100): # Avoid cycles
if islink(current_path):
target = readlink(current_path)
current_path = os.path.join(os.path.dirname(current_path), target)
else:
basename = os.path.basename(current_path)
if basename == '':
path_tail.append(current_path)
break
path_tail.append(basename)
current_path = os.path.dirname(current_path)
path_tail.reverse()
result = os.path.normpath(os.path.join(*path_tail))
return result
else:
return os.path.realpath(path)

225
platform_utils_win32.py Normal file
View File

@ -0,0 +1,225 @@
#
# Copyright (C) 2016 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.
import errno
from ctypes import WinDLL, get_last_error, FormatError, WinError, addressof
from ctypes import c_buffer
from ctypes.wintypes import BOOL, LPCWSTR, DWORD, HANDLE, POINTER, c_ubyte
from ctypes.wintypes import WCHAR, USHORT, LPVOID, Structure, Union, ULONG
from ctypes.wintypes import byref
kernel32 = WinDLL('kernel32', use_last_error=True)
LPDWORD = POINTER(DWORD)
UCHAR = c_ubyte
# Win32 error codes
ERROR_SUCCESS = 0
ERROR_NOT_SUPPORTED = 50
ERROR_PRIVILEGE_NOT_HELD = 1314
# Win32 API entry points
CreateSymbolicLinkW = kernel32.CreateSymbolicLinkW
CreateSymbolicLinkW.restype = BOOL
CreateSymbolicLinkW.argtypes = (LPCWSTR, # lpSymlinkFileName In
LPCWSTR, # lpTargetFileName In
DWORD) # dwFlags In
# Symbolic link creation flags
SYMBOLIC_LINK_FLAG_FILE = 0x00
SYMBOLIC_LINK_FLAG_DIRECTORY = 0x01
# symlink support for CreateSymbolicLink() starting with Windows 10 (1703, v10.0.14972)
SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE = 0x02
GetFileAttributesW = kernel32.GetFileAttributesW
GetFileAttributesW.restype = DWORD
GetFileAttributesW.argtypes = (LPCWSTR,) # lpFileName In
INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF
FILE_ATTRIBUTE_REPARSE_POINT = 0x00400
CreateFileW = kernel32.CreateFileW
CreateFileW.restype = HANDLE
CreateFileW.argtypes = (LPCWSTR, # lpFileName In
DWORD, # dwDesiredAccess In
DWORD, # dwShareMode In
LPVOID, # lpSecurityAttributes In_opt
DWORD, # dwCreationDisposition In
DWORD, # dwFlagsAndAttributes In
HANDLE) # hTemplateFile In_opt
CloseHandle = kernel32.CloseHandle
CloseHandle.restype = BOOL
CloseHandle.argtypes = (HANDLE,) # hObject In
INVALID_HANDLE_VALUE = HANDLE(-1).value
OPEN_EXISTING = 3
FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
DeviceIoControl = kernel32.DeviceIoControl
DeviceIoControl.restype = BOOL
DeviceIoControl.argtypes = (HANDLE, # hDevice In
DWORD, # dwIoControlCode In
LPVOID, # lpInBuffer In_opt
DWORD, # nInBufferSize In
LPVOID, # lpOutBuffer Out_opt
DWORD, # nOutBufferSize In
LPDWORD, # lpBytesReturned Out_opt
LPVOID) # lpOverlapped Inout_opt
# Device I/O control flags and options
FSCTL_GET_REPARSE_POINT = 0x000900A8
IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003
IO_REPARSE_TAG_SYMLINK = 0xA000000C
MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 0x4000
class GENERIC_REPARSE_BUFFER(Structure):
_fields_ = (('DataBuffer', UCHAR * 1),)
class SYMBOLIC_LINK_REPARSE_BUFFER(Structure):
_fields_ = (('SubstituteNameOffset', USHORT),
('SubstituteNameLength', USHORT),
('PrintNameOffset', USHORT),
('PrintNameLength', USHORT),
('Flags', ULONG),
('PathBuffer', WCHAR * 1))
@property
def PrintName(self):
arrayt = WCHAR * (self.PrintNameLength // 2)
offset = type(self).PathBuffer.offset + self.PrintNameOffset
return arrayt.from_address(addressof(self) + offset).value
class MOUNT_POINT_REPARSE_BUFFER(Structure):
_fields_ = (('SubstituteNameOffset', USHORT),
('SubstituteNameLength', USHORT),
('PrintNameOffset', USHORT),
('PrintNameLength', USHORT),
('PathBuffer', WCHAR * 1))
@property
def PrintName(self):
arrayt = WCHAR * (self.PrintNameLength // 2)
offset = type(self).PathBuffer.offset + self.PrintNameOffset
return arrayt.from_address(addressof(self) + offset).value
class REPARSE_DATA_BUFFER(Structure):
class REPARSE_BUFFER(Union):
_fields_ = (('SymbolicLinkReparseBuffer', SYMBOLIC_LINK_REPARSE_BUFFER),
('MountPointReparseBuffer', MOUNT_POINT_REPARSE_BUFFER),
('GenericReparseBuffer', GENERIC_REPARSE_BUFFER))
_fields_ = (('ReparseTag', ULONG),
('ReparseDataLength', USHORT),
('Reserved', USHORT),
('ReparseBuffer', REPARSE_BUFFER))
_anonymous_ = ('ReparseBuffer',)
def create_filesymlink(source, link_name):
"""Creates a Windows file symbolic link source pointing to link_name."""
_create_symlink(source, link_name, SYMBOLIC_LINK_FLAG_FILE)
def create_dirsymlink(source, link_name):
"""Creates a Windows directory symbolic link source pointing to link_name.
"""
_create_symlink(source, link_name, SYMBOLIC_LINK_FLAG_DIRECTORY)
def _create_symlink(source, link_name, dwFlags):
# Note: Win32 documentation for CreateSymbolicLink is incorrect.
# On success, the function returns "1".
# On error, the function returns some random value (e.g. 1280).
# The best bet seems to use "GetLastError" and check for error/success.
CreateSymbolicLinkW(link_name, source, dwFlags | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE)
code = get_last_error()
if code != ERROR_SUCCESS:
# See https://github.com/golang/go/pull/24307/files#diff-b87bc12e4da2497308f9ef746086e4f0
# "the unprivileged create flag is unsupported below Windows 10 (1703, v10.0.14972).
# retry without it."
CreateSymbolicLinkW(link_name, source, dwFlags)
code = get_last_error()
if code != ERROR_SUCCESS:
error_desc = FormatError(code).strip()
if code == ERROR_PRIVILEGE_NOT_HELD:
raise OSError(errno.EPERM, error_desc, link_name)
_raise_winerror(
code,
'Error creating symbolic link \"%s\"'.format(link_name))
def islink(path):
result = GetFileAttributesW(path)
if result == INVALID_FILE_ATTRIBUTES:
return False
return bool(result & FILE_ATTRIBUTE_REPARSE_POINT)
def readlink(path):
reparse_point_handle = CreateFileW(path,
0,
0,
None,
OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT |
FILE_FLAG_BACKUP_SEMANTICS,
None)
if reparse_point_handle == INVALID_HANDLE_VALUE:
_raise_winerror(
get_last_error(),
'Error opening symblic link \"%s\"'.format(path))
target_buffer = c_buffer(MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
n_bytes_returned = DWORD()
io_result = DeviceIoControl(reparse_point_handle,
FSCTL_GET_REPARSE_POINT,
None,
0,
target_buffer,
len(target_buffer),
byref(n_bytes_returned),
None)
CloseHandle(reparse_point_handle)
if not io_result:
_raise_winerror(
get_last_error(),
'Error reading symblic link \"%s\"'.format(path))
rdb = REPARSE_DATA_BUFFER.from_buffer(target_buffer)
if rdb.ReparseTag == IO_REPARSE_TAG_SYMLINK:
return _preserve_encoding(path, rdb.SymbolicLinkReparseBuffer.PrintName)
elif rdb.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT:
return _preserve_encoding(path, rdb.MountPointReparseBuffer.PrintName)
# Unsupported reparse point type
_raise_winerror(
ERROR_NOT_SUPPORTED,
'Error reading symblic link \"%s\"'.format(path))
def _preserve_encoding(source, target):
"""Ensures target is the same string type (i.e. unicode or str) as source."""
if isinstance(source, unicode):
return unicode(target)
return str(target)
def _raise_winerror(code, error_desc):
win_error_desc = FormatError(code).strip()
error_desc = "%s: %s".format(error_desc, win_error_desc)
raise WinError(code, error_desc)

View File

@ -21,7 +21,8 @@ from trace import IsTrace
_NOT_TTY = not os.isatty(2)
class Progress(object):
def __init__(self, title, total=0, units=''):
def __init__(self, title, total=0, units='', print_newline=False,
always_print_percentage=False):
self._title = title
self._total = total
self._done = 0
@ -29,6 +30,8 @@ class Progress(object):
self._start = time()
self._show = False
self._units = units
self._print_newline = print_newline
self._always_print_percentage = always_print_percentage
def update(self, inc=1):
self._done += inc
@ -50,13 +53,14 @@ class Progress(object):
else:
p = (100 * self._done) / self._total
if self._lastp != p:
if self._lastp != p or self._always_print_percentage:
self._lastp = p
sys.stderr.write('\r%s: %3d%% (%d%s/%d%s) ' % (
sys.stderr.write('\r%s: %3d%% (%d%s/%d%s)%s' % (
self._title,
p,
self._done, self._units,
self._total, self._units))
self._total, self._units,
"\n" if self._print_newline else ""))
sys.stderr.flush()
def end(self):

353
project.py Normal file → Executable file
View File

@ -35,6 +35,7 @@ from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, \
from error import GitError, HookError, UploadError, DownloadError
from error import ManifestInvalidRevisionError
from error import NoManifestException
import platform_utils
from trace import IsTrace, Trace
from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M
@ -47,9 +48,7 @@ else:
import urlparse
urllib = imp.new_module('urllib')
urllib.parse = urlparse
# pylint:disable=W0622
input = raw_input
# pylint:enable=W0622
def _lwrite(path, content):
@ -62,9 +61,9 @@ def _lwrite(path, content):
fd.close()
try:
os.rename(lock, path)
platform_utils.rename(lock, path)
except OSError:
os.remove(lock)
platform_utils.remove(lock)
raise
@ -102,9 +101,9 @@ def _ProjectHooks():
"""
global _project_hook_list
if _project_hook_list is None:
d = os.path.realpath(os.path.abspath(os.path.dirname(__file__)))
d = platform_utils.realpath(os.path.abspath(os.path.dirname(__file__)))
d = os.path.join(d, 'hooks')
_project_hook_list = [os.path.join(d, x) for x in os.listdir(d)]
_project_hook_list = [os.path.join(d, x) for x in platform_utils.listdir(d)]
return _project_hook_list
@ -176,12 +175,22 @@ class ReviewableBranch(object):
def UploadForReview(self, people,
auto_topic=False,
draft=False,
dest_branch=None):
private=False,
notify=None,
wip=False,
dest_branch=None,
validate_certs=True,
push_options=None):
self.project.UploadForReview(self.name,
people,
auto_topic=auto_topic,
draft=draft,
dest_branch=dest_branch)
private=private,
notify=notify,
wip=wip,
dest_branch=dest_branch,
validate_certs=validate_certs,
push_options=push_options)
def GetPublishedRefs(self):
refs = {}
@ -243,10 +252,10 @@ class _CopyFile(object):
try:
# remove existing file first, since it might be read-only
if os.path.exists(dest):
os.remove(dest)
platform_utils.remove(dest)
else:
dest_dir = os.path.dirname(dest)
if not os.path.isdir(dest_dir):
if not platform_utils.isdir(dest_dir):
os.makedirs(dest_dir)
shutil.copy(src, dest)
# make the file read-only
@ -268,16 +277,16 @@ class _LinkFile(object):
def __linkIt(self, relSrc, absDest):
# link file if it does not exist or is out of date
if not os.path.islink(absDest) or (os.readlink(absDest) != relSrc):
if not platform_utils.islink(absDest) or (platform_utils.readlink(absDest) != relSrc):
try:
# remove existing file first, since it might be read-only
if os.path.lexists(absDest):
os.remove(absDest)
platform_utils.remove(absDest)
else:
dest_dir = os.path.dirname(absDest)
if not os.path.isdir(dest_dir):
if not platform_utils.isdir(dest_dir):
os.makedirs(dest_dir)
os.symlink(relSrc, absDest)
platform_utils.symlink(relSrc, absDest)
except IOError:
_error('Cannot link file %s to %s', relSrc, absDest)
@ -295,7 +304,7 @@ class _LinkFile(object):
else:
# Entity doesn't exist assume there is a wild card
absDestDir = self.abs_dest
if os.path.exists(absDestDir) and not os.path.isdir(absDestDir):
if os.path.exists(absDestDir) and not platform_utils.isdir(absDestDir):
_error('Link error: src with wildcard, %s must be a directory',
absDestDir)
else:
@ -323,13 +332,15 @@ class RemoteSpec(object):
pushUrl=None,
review=None,
revision=None,
orig_name=None):
orig_name=None,
fetchUrl=None):
self.name = name
self.url = url
self.pushUrl = pushUrl
self.review = review
self.revision = revision
self.orig_name = orig_name
self.fetchUrl = fetchUrl
class RepoHook(object):
@ -649,6 +660,7 @@ class Project(object):
groups=None,
sync_c=False,
sync_s=False,
sync_tags=True,
clone_depth=None,
upstream=None,
parent=None,
@ -672,6 +684,7 @@ class Project(object):
groups: The `groups` attribute of manifest.xml's project element.
sync_c: The `sync-c` attribute of manifest.xml's project element.
sync_s: The `sync-s` attribute of manifest.xml's project element.
sync_tags: The `sync-tags` attribute of manifest.xml's project element.
upstream: The `upstream` attribute of manifest.xml's project element.
parent: The parent Project object.
is_derived: False if the project was explicitly defined in the manifest;
@ -687,7 +700,7 @@ class Project(object):
self.gitdir = gitdir.replace('\\', '/')
self.objdir = objdir.replace('\\', '/')
if worktree:
self.worktree = os.path.normpath(worktree.replace('\\', '/'))
self.worktree = os.path.normpath(worktree).replace('\\', '/')
else:
self.worktree = None
self.relpath = relpath
@ -704,6 +717,7 @@ class Project(object):
self.groups = groups
self.sync_c = sync_c
self.sync_s = sync_s
self.sync_tags = sync_tags
self.clone_depth = clone_depth
self.upstream = upstream
self.parent = parent
@ -738,7 +752,7 @@ class Project(object):
@property
def Exists(self):
return os.path.isdir(self.gitdir) and os.path.isdir(self.objdir)
return platform_utils.isdir(self.gitdir) and platform_utils.isdir(self.objdir)
@property
def CurrentBranch(self):
@ -911,13 +925,15 @@ class Project(object):
else:
return False
def PrintWorkTreeStatus(self, output_redir=None):
def PrintWorkTreeStatus(self, output_redir=None, quiet=False):
"""Prints the status of the repository to stdout.
Args:
output: If specified, redirect the output to this object.
quiet: If True then only print the project name. Do not print
the modified files, branch name, etc.
"""
if not os.path.isdir(self.worktree):
if not platform_utils.isdir(self.worktree):
if output_redir is None:
output_redir = sys.stdout
print(file=output_redir)
@ -941,6 +957,10 @@ class Project(object):
out.redirect(output_redir)
out.project('project %-40s', self.relpath + '/ ')
if quiet:
out.nl()
return 'DIRTY'
branch = self.CurrentBranch
if branch is None:
out.nobranch('(*** NO BRANCH ***)')
@ -1099,7 +1119,12 @@ class Project(object):
people=([], []),
auto_topic=False,
draft=False,
dest_branch=None):
private=False,
notify=None,
wip=False,
dest_branch=None,
validate_certs=True,
push_options=None):
"""Uploads the named branch for code review.
"""
if branch is None:
@ -1124,18 +1149,17 @@ class Project(object):
branch.remote.projectname = self.name
branch.remote.Save()
url = branch.remote.ReviewUrl(self.UserEmail)
url = branch.remote.ReviewUrl(self.UserEmail, validate_certs)
if url is None:
raise UploadError('review not configured')
cmd = ['push']
if url.startswith('ssh://'):
rp = ['gerrit receive-pack']
for e in people[0]:
rp.append('--reviewer=%s' % sq(e))
for e in people[1]:
rp.append('--cc=%s' % sq(e))
cmd.append('--receive-pack=%s' % " ".join(rp))
cmd.append('--receive-pack=gerrit receive-pack')
for push_option in (push_options or []):
cmd.append('-o')
cmd.append(push_option)
cmd.append(url)
@ -1150,11 +1174,17 @@ class Project(object):
dest_branch)
if auto_topic:
ref_spec = ref_spec + '/' + branch.name
if not url.startswith('ssh://'):
rp = ['r=%s' % p for p in people[0]] + \
['cc=%s' % p for p in people[1]]
if rp:
ref_spec = ref_spec + '%' + ','.join(rp)
opts = ['r=%s' % p for p in people[0]]
opts += ['cc=%s' % p for p in people[1]]
if notify:
opts += ['notify=' + notify]
if private:
opts += ['private']
if wip:
opts += ['wip']
if opts:
ref_spec = ref_spec + '%' + ','.join(opts)
cmd.append(ref_spec)
if GitCommand(self, cmd, bare=True).Wait() != 0:
@ -1192,7 +1222,8 @@ class Project(object):
no_tags=False,
archive=False,
optimized_fetch=False,
prune=False):
prune=False,
submodules=False):
"""Perform only the network IO portion of the sync process.
Local working directory/branch state is not affected.
"""
@ -1218,7 +1249,7 @@ class Project(object):
if not self._ExtractArchive(tarpath, path=topdir):
return False
try:
os.remove(tarpath)
platform_utils.remove(tarpath)
except OSError as e:
_warn("Cannot remove archive %s: %s", tarpath, str(e))
self._CopyAndLinkFiles()
@ -1234,9 +1265,10 @@ class Project(object):
if is_new:
alt = os.path.join(self.gitdir, 'objects/info/alternates')
try:
fd = open(alt, 'rb')
fd = open(alt)
try:
alt_dir = fd.readline().rstrip()
# This works for both absolute and relative alternate directories.
alt_dir = os.path.join(self.objdir, 'objects', fd.readline().rstrip())
finally:
fd.close()
except IOError:
@ -1258,21 +1290,41 @@ class Project(object):
elif self.manifest.default.sync_c:
current_branch_only = True
if not no_tags:
if not self.sync_tags:
no_tags = True
if self.clone_depth:
depth = self.clone_depth
else:
depth = self.manifest.manifestProject.config.GetString('repo.depth')
need_to_fetch = not (optimized_fetch and
(ID_RE.match(self.revisionExpr) and
self._CheckForSha1()))
self._CheckForImmutableRevision()))
if (need_to_fetch and
not self._RemoteFetch(initial=is_new, quiet=quiet, alt_dir=alt_dir,
current_branch_only=current_branch_only,
no_tags=no_tags, prune=prune)):
no_tags=no_tags, prune=prune, depth=depth,
submodules=submodules)):
return False
mp = self.manifest.manifestProject
dissociate = mp.config.GetBoolean('repo.dissociate')
if dissociate:
alternates_file = os.path.join(self.gitdir, 'objects/info/alternates')
if os.path.exists(alternates_file):
cmd = ['repack', '-a', '-d']
if GitCommand(self, cmd, bare=True).Wait() != 0:
return False
platform_utils.remove(alternates_file)
if self.worktree:
self._InitMRef()
else:
self._InitMirrorHead()
try:
os.remove(os.path.join(self.gitdir, 'FETCH_HEAD'))
platform_utils.remove(os.path.join(self.gitdir, 'FETCH_HEAD'))
except OSError:
pass
return True
@ -1320,11 +1372,11 @@ class Project(object):
raise ManifestInvalidRevisionError('revision %s in %s not found' %
(self.revisionExpr, self.name))
def Sync_LocalHalf(self, syncbuf, force_sync=False):
def Sync_LocalHalf(self, syncbuf, force_sync=False, submodules=False):
"""Perform only the local IO portion of the sync process.
Network access is not required.
"""
self._InitWorkTree(force_sync=force_sync)
self._InitWorkTree(force_sync=force_sync, submodules=submodules)
all_refs = self.bare_ref.all
self.CleanPublishedCache(all_refs)
revid = self.GetRevisionId(all_refs)
@ -1333,6 +1385,9 @@ class Project(object):
self._FastForward(revid)
self._CopyAndLinkFiles()
def _dosubmodules():
self._SyncSubmodules(quiet=True)
head = self.work_git.GetHead()
if head.startswith(R_HEADS):
branch = head[len(R_HEADS):]
@ -1366,6 +1421,8 @@ class Project(object):
try:
self._Checkout(revid, quiet=True)
if submodules:
self._SyncSubmodules(quiet=True)
except GitError as e:
syncbuf.fail(self, e)
return
@ -1390,6 +1447,8 @@ class Project(object):
branch.name)
try:
self._Checkout(revid, quiet=True)
if submodules:
self._SyncSubmodules(quiet=True)
except GitError as e:
syncbuf.fail(self, e)
return
@ -1415,6 +1474,8 @@ class Project(object):
# strict subset. We can fast-forward safely.
#
syncbuf.later1(self, _doff)
if submodules:
syncbuf.later1(self, _dosubmodules)
return
# Examine the local commits not in the remote. Find the
@ -1466,19 +1527,28 @@ class Project(object):
branch.Save()
if cnt_mine > 0 and self.rebase:
def _docopyandlink():
self._CopyAndLinkFiles()
def _dorebase():
self._Rebase(upstream='%s^1' % last_mine, onto=revid)
self._CopyAndLinkFiles()
syncbuf.later2(self, _dorebase)
if submodules:
syncbuf.later2(self, _dosubmodules)
syncbuf.later2(self, _docopyandlink)
elif local_changes:
try:
self._ResetHard(revid)
if submodules:
self._SyncSubmodules(quiet=True)
self._CopyAndLinkFiles()
except GitError as e:
syncbuf.fail(self, e)
return
else:
syncbuf.later1(self, _doff)
if submodules:
syncbuf.later1(self, _dosubmodules)
def AddCopyFile(self, src, dest, absdest):
# dest should already be an absolute path, but src is project relative
@ -1764,7 +1834,7 @@ class Project(object):
except GitError:
return [], []
finally:
os.remove(temp_gitmodules_path)
platform_utils.remove(temp_gitmodules_path)
names = set()
paths = {}
@ -1825,6 +1895,8 @@ class Project(object):
result.extend(project.GetDerivedSubprojects())
continue
if url.startswith('..'):
url = urllib.parse.urljoin("%s/" % self.remote.url, url)
remote = RemoteSpec(self.remote.name,
url=url,
pushUrl=self.remote.pushUrl,
@ -1843,6 +1915,7 @@ class Project(object):
groups=self.groups,
sync_c=self.sync_c,
sync_s=self.sync_s,
sync_tags=self.sync_tags,
parent=self,
is_derived=True)
result.append(subproject)
@ -1851,7 +1924,7 @@ class Project(object):
# Direct Git Commands ##
def _CheckForSha1(self):
def _CheckForImmutableRevision(self):
try:
# if revision (sha or tag) is not present then following function
# throws an error.
@ -1880,23 +1953,18 @@ class Project(object):
quiet=False,
alt_dir=None,
no_tags=False,
prune=False):
prune=False,
depth=None,
submodules=False):
is_sha1 = False
tag_name = None
depth = None
# The depth should not be used when fetching to a mirror because
# it will result in a shallow repository that cannot be cloned or
# fetched from.
if not self.manifest.IsMirror:
if self.clone_depth:
depth = self.clone_depth
else:
depth = self.manifest.manifestProject.config.GetString('repo.depth')
# The repo project should never be synced with partial depth
if self.relpath == '.repo/repo':
depth = None
# The repo project should also never be synced with partial depth.
if self.manifest.IsMirror or self.relpath == '.repo/repo':
depth = None
if depth:
current_branch_only = True
@ -1910,7 +1978,9 @@ class Project(object):
tag_name = self.revisionExpr[len(R_TAGS):]
if is_sha1 or tag_name is not None:
if self._CheckForSha1():
if self._CheckForImmutableRevision():
print('Skipped fetching project %s (already have persistent ref)'
% self.name)
return True
if is_sha1 and not depth:
# When syncing a specific commit and --depth is not set:
@ -1958,15 +2028,17 @@ class Project(object):
ids.add(ref_id)
tmp.add(r)
tmp_packed = ''
old_packed = ''
tmp_packed_lines = []
old_packed_lines = []
for r in sorted(all_refs):
line = '%s %s\n' % (all_refs[r], r)
tmp_packed += line
tmp_packed_lines.append(line)
if r not in tmp:
old_packed += line
old_packed_lines.append(line)
tmp_packed = ''.join(tmp_packed_lines)
old_packed = ''.join(old_packed_lines)
_lwrite(packed_refs, tmp_packed)
else:
alt_dir = None
@ -1999,6 +2071,9 @@ class Project(object):
if prune:
cmd.append('--prune')
if submodules:
cmd.append('--recurse-submodules=on-demand')
spec = []
if not current_branch_only:
# Fetch whole repo
@ -2054,24 +2129,25 @@ class Project(object):
if old_packed != '':
_lwrite(packed_refs, old_packed)
else:
os.remove(packed_refs)
platform_utils.remove(packed_refs)
self.bare_git.pack_refs('--all', '--prune')
if is_sha1 and current_branch_only and self.upstream:
if is_sha1 and current_branch_only:
# We just synced the upstream given branch; verify we
# got what we wanted, else trigger a second run of all
# refs.
if not self._CheckForSha1():
if not depth:
# Avoid infinite recursion when depth is True (since depth implies
# current_branch_only)
return self._RemoteFetch(name=name, current_branch_only=False,
initial=False, quiet=quiet, alt_dir=alt_dir)
if self.clone_depth:
self.clone_depth = None
if not self._CheckForImmutableRevision():
if current_branch_only and depth:
# Sync the current branch only with depth set to None
return self._RemoteFetch(name=name,
current_branch_only=current_branch_only,
initial=False, quiet=quiet, alt_dir=alt_dir)
initial=False, quiet=quiet, alt_dir=alt_dir,
depth=None)
else:
# Avoid infinite recursion: sync all branches with depth set to None
return self._RemoteFetch(name=name, current_branch_only=False,
initial=False, quiet=quiet, alt_dir=alt_dir,
depth=None)
return ok
@ -2111,18 +2187,18 @@ class Project(object):
cmd.append(bundle_dst)
for f in remote.fetch:
cmd.append(str(f))
cmd.append('refs/tags/*:refs/tags/*')
cmd.append('+refs/tags/*:refs/tags/*')
ok = GitCommand(self, cmd, bare=True).Wait() == 0
if os.path.exists(bundle_dst):
os.remove(bundle_dst)
platform_utils.remove(bundle_dst)
if os.path.exists(bundle_tmp):
os.remove(bundle_tmp)
platform_utils.remove(bundle_tmp)
return ok
def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet):
if os.path.exists(dstPath):
os.remove(dstPath)
platform_utils.remove(dstPath)
cmd = ['curl', '--fail', '--output', tmpPath, '--netrc', '--location']
if quiet:
@ -2132,7 +2208,7 @@ class Project(object):
if size >= 1024:
cmd += ['--continue-at', '%d' % (size,)]
else:
os.remove(tmpPath)
platform_utils.remove(tmpPath)
if 'http_proxy' in os.environ and 'darwin' == sys.platform:
cmd += ['--proxy', os.environ['http_proxy']]
with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, _proxy):
@ -2163,10 +2239,10 @@ class Project(object):
if os.path.exists(tmpPath):
if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
os.rename(tmpPath, dstPath)
platform_utils.rename(tmpPath, dstPath)
return True
else:
os.remove(tmpPath)
platform_utils.remove(tmpPath)
return False
else:
return False
@ -2201,6 +2277,16 @@ class Project(object):
if self._allrefs:
raise GitError('%s cherry-pick %s ' % (self.name, rev))
def _LsRemote(self, refs):
cmd = ['ls-remote', self.remote.name, refs]
p = GitCommand(self, cmd, capture_stdout=True)
if p.Wait() == 0:
if hasattr(p.stdout, 'decode'):
return p.stdout.decode('utf-8')
else:
return p.stdout
return None
def _Revert(self, rev):
cmd = ['revert']
cmd.append('--no-edit')
@ -2218,6 +2304,13 @@ class Project(object):
if GitCommand(self, cmd).Wait() != 0:
raise GitError('%s reset --hard %s ' % (self.name, rev))
def _SyncSubmodules(self, quiet=True):
cmd = ['submodule', 'update', '--init', '--recursive']
if quiet:
cmd.append('-q')
if GitCommand(self, cmd).Wait() != 0:
raise GitError('%s submodule update --init --recursive %s ' % self.name)
def _Rebase(self, upstream, onto=None):
cmd = ['rebase']
if onto is not None:
@ -2257,10 +2350,10 @@ class Project(object):
print("Retrying clone after deleting %s" %
self.gitdir, file=sys.stderr)
try:
shutil.rmtree(os.path.realpath(self.gitdir))
if self.worktree and os.path.exists(os.path.realpath
platform_utils.rmtree(platform_utils.realpath(self.gitdir))
if self.worktree and os.path.exists(platform_utils.realpath
(self.worktree)):
shutil.rmtree(os.path.realpath(self.worktree))
platform_utils.rmtree(platform_utils.realpath(self.worktree))
return self._InitGitDir(mirror_git=mirror_git, force_sync=False)
except:
raise e
@ -2286,6 +2379,10 @@ class Project(object):
ref_dir = None
if ref_dir:
if not os.path.isabs(ref_dir):
# The alternate directory is relative to the object database.
ref_dir = os.path.relpath(ref_dir,
os.path.join(self.objdir, 'objects'))
_lwrite(os.path.join(self.gitdir, 'objects/info/alternates'),
os.path.join(ref_dir, 'objects') + '\n')
@ -2302,9 +2399,9 @@ class Project(object):
self.config.SetString('core.bare', None)
except Exception:
if init_obj_dir and os.path.exists(self.objdir):
shutil.rmtree(self.objdir)
platform_utils.rmtree(self.objdir)
if init_git_dir and os.path.exists(self.gitdir):
shutil.rmtree(self.gitdir)
platform_utils.rmtree(self.gitdir)
raise
def _UpdateHooks(self):
@ -2312,7 +2409,7 @@ class Project(object):
self._InitHooks()
def _InitHooks(self):
hooks = os.path.realpath(self._gitdir_path('hooks'))
hooks = platform_utils.realpath(self._gitdir_path('hooks'))
if not os.path.exists(hooks):
os.makedirs(hooks)
for stock_hook in _ProjectHooks():
@ -2328,20 +2425,21 @@ class Project(object):
continue
dst = os.path.join(hooks, name)
if os.path.islink(dst):
if platform_utils.islink(dst):
continue
if os.path.exists(dst):
if filecmp.cmp(stock_hook, dst, shallow=False):
os.remove(dst)
platform_utils.remove(dst)
else:
_warn("%s: Not replacing locally modified %s hook",
self.relpath, name)
continue
try:
os.symlink(os.path.relpath(stock_hook, os.path.dirname(dst)), dst)
platform_utils.symlink(
os.path.relpath(stock_hook, os.path.dirname(dst)), dst)
except OSError as e:
if e.errno == errno.EPERM:
raise GitError('filesystem must support symlinks')
raise GitError(self._get_symlink_error_message())
else:
raise
@ -2389,11 +2487,12 @@ class Project(object):
symlink_dirs += self.working_tree_dirs
to_symlink = symlink_files + symlink_dirs
for name in set(to_symlink):
dst = os.path.realpath(os.path.join(destdir, name))
dst = platform_utils.realpath(os.path.join(destdir, name))
if os.path.lexists(dst):
src = os.path.realpath(os.path.join(srcdir, name))
src = platform_utils.realpath(os.path.join(srcdir, name))
# Fail if the links are pointing to the wrong place
if src != dst:
_error('%s is different in %s vs %s', name, destdir, srcdir)
raise GitError('--force-sync not enabled; cannot overwrite a local '
'work tree. If you\'re comfortable with the '
'possibility of losing the work tree\'s git metadata,'
@ -2420,12 +2519,12 @@ class Project(object):
to_copy = []
if copy_all:
to_copy = os.listdir(gitdir)
to_copy = platform_utils.listdir(gitdir)
dotgit = os.path.realpath(dotgit)
dotgit = platform_utils.realpath(dotgit)
for name in set(to_copy).union(to_symlink):
try:
src = os.path.realpath(os.path.join(gitdir, name))
src = platform_utils.realpath(os.path.join(gitdir, name))
dst = os.path.join(dotgit, name)
if os.path.lexists(dst):
@ -2435,28 +2534,30 @@ class Project(object):
if name in symlink_dirs and not os.path.lexists(src):
os.makedirs(src)
if name in to_symlink:
platform_utils.symlink(
os.path.relpath(src, os.path.dirname(dst)), dst)
elif copy_all and not platform_utils.islink(dst):
if platform_utils.isdir(src):
shutil.copytree(src, dst)
elif os.path.isfile(src):
shutil.copy(src, dst)
# If the source file doesn't exist, ensure the destination
# file doesn't either.
if name in symlink_files and not os.path.lexists(src):
try:
os.remove(dst)
platform_utils.remove(dst)
except OSError:
pass
if name in to_symlink:
os.symlink(os.path.relpath(src, os.path.dirname(dst)), dst)
elif copy_all and not os.path.islink(dst):
if os.path.isdir(src):
shutil.copytree(src, dst)
elif os.path.isfile(src):
shutil.copy(src, dst)
except OSError as e:
if e.errno == errno.EPERM:
raise DownloadError('filesystem must support symlinks')
raise DownloadError(self._get_symlink_error_message())
else:
raise
def _InitWorkTree(self, force_sync=False):
def _InitWorkTree(self, force_sync=False, submodules=False):
dotgit = os.path.join(self.worktree, '.git')
init_dotgit = not os.path.exists(dotgit)
try:
@ -2470,8 +2571,8 @@ class Project(object):
except GitError as e:
if force_sync:
try:
shutil.rmtree(dotgit)
return self._InitWorkTree(force_sync=False)
platform_utils.rmtree(dotgit)
return self._InitWorkTree(force_sync=False, submodules=submodules)
except:
raise e
raise e
@ -2485,14 +2586,24 @@ class Project(object):
if GitCommand(self, cmd).Wait() != 0:
raise GitError("cannot initialize work tree")
if submodules:
self._SyncSubmodules(quiet=True)
self._CopyAndLinkFiles()
except Exception:
if init_dotgit:
shutil.rmtree(dotgit)
platform_utils.rmtree(dotgit)
raise
def _get_symlink_error_message(self):
if platform_utils.isWindows():
return ('Unable to create symbolic link. Please re-run the command as '
'Administrator, or see '
'https://github.com/git-for-windows/git/wiki/Symbolic-Links '
'for other options.')
return 'filesystem must support symlinks'
def _gitdir_path(self, path):
return os.path.realpath(os.path.join(self.gitdir, path))
return platform_utils.realpath(os.path.join(self.gitdir, path))
def _revlist(self, *args, **kw):
a = []
@ -2567,7 +2678,7 @@ class Project(object):
out = p.stdout
if out:
# Backslash is not anomalous
return out[:-1].split('\0') # pylint: disable=W1401
return out[:-1].split('\0')
return []
def DiffZ(self, name, *args):
@ -2584,7 +2695,7 @@ class Project(object):
out = p.process.stdout.read()
r = {}
if out:
out = iter(out[:-1].split('\0')) # pylint: disable=W1401
out = iter(out[:-1].split('\0'))
while out:
try:
info = next(out)
@ -2627,11 +2738,11 @@ class Project(object):
else:
path = os.path.join(self._project.worktree, '.git', HEAD)
try:
fd = open(path, 'rb')
fd = open(path)
except IOError as e:
raise NoManifestException(path, str(e))
try:
line = fd.read()
line = fd.readline()
finally:
fd.close()
try:
@ -2833,13 +2944,14 @@ class SyncBuffer(object):
self.detach_head = detach_head
self.clean = True
self.recent_clean = True
def info(self, project, fmt, *args):
self._messages.append(_InfoMessage(project, fmt % args))
def fail(self, project, err=None):
self._failures.append(_Failure(project, err))
self.clean = False
self._MarkUnclean()
def later1(self, project, what):
self._later_queue1.append(_Later(project, what))
@ -2853,6 +2965,15 @@ class SyncBuffer(object):
self._PrintMessages()
return self.clean
def Recently(self):
recent_clean = self.recent_clean
self.recent_clean = True
return recent_clean
def _MarkUnclean(self):
self.clean = False
self.recent_clean = False
def _RunLater(self):
for q in ['_later_queue1', '_later_queue2']:
if not self._RunQueue(q):
@ -2861,7 +2982,7 @@ class SyncBuffer(object):
def _RunQueue(self, queue):
for m in getattr(self, queue):
if not m.Run(self):
self.clean = False
self._MarkUnclean()
return False
setattr(self, queue, [])
return True
@ -2903,14 +3024,14 @@ class MetaProject(Project):
self.revisionExpr = base
self.revisionId = None
def MetaBranchSwitch(self):
def MetaBranchSwitch(self, submodules=False):
""" Prepare MetaProject for manifest branch switch
"""
# detach and delete manifest branch, allowing a new
# branch to take over
syncbuf = SyncBuffer(self.config, detach_head=True)
self.Sync_LocalHalf(syncbuf)
self.Sync_LocalHalf(syncbuf, submodules=submodules)
syncbuf.Finish()
return GitCommand(self,

51
repo
View File

@ -23,7 +23,7 @@ REPO_REV = 'stable'
# limitations under the License.
# increment this whenever we make important changes to this script
VERSION = (1, 23)
VERSION = (1, 25)
# increment this if the MAINTAINER_KEYS block is modified
KEYRING_VERSION = (1, 2)
@ -113,13 +113,14 @@ repodir = '.repo' # name of repo's private directory
S_repo = 'repo' # special repo repository
S_manifests = 'manifests' # special manifest repository
REPO_MAIN = S_repo + '/main.py' # main script
MIN_PYTHON_VERSION = (2, 6) # minimum supported python version
MIN_PYTHON_VERSION = (2, 7) # minimum supported python version
GITC_CONFIG_FILE = '/gitc/.config'
GITC_FS_ROOT_DIR = '/gitc/manifest-rw/'
import errno
import optparse
import platform
import re
import shutil
import stat
@ -143,13 +144,20 @@ def _print(*objects, **kwargs):
out = kwargs.get('file', sys.stdout)
out.write(sep.join(objects) + end)
# On Windows stderr is buffered, so flush to maintain the order of error messages.
if out == sys.stderr and platform.system() == "Windows":
out.flush()
# Python version check
ver = sys.version_info
if (ver[0], ver[1]) < MIN_PYTHON_VERSION:
_print('error: Python version %s unsupported.\n'
'Please use Python 2.6 - 2.7 instead.'
% sys.version.split(' ')[0], file=sys.stderr)
_print('error: Python version {} unsupported.\n'
'Please use Python {}.{} instead.'.format(
sys.version.split(' ')[0],
MIN_PYTHON_VERSION[0],
MIN_PYTHON_VERSION[1],
), file=sys.stderr)
sys.exit(1)
home_dot_repo = os.path.expanduser('~/.repoconfig')
@ -175,6 +183,9 @@ group.add_option('-b', '--manifest-branch',
group.add_option('-m', '--manifest-name',
dest='manifest_name',
help='initial manifest file', metavar='NAME.xml')
group.add_option('--current-branch',
dest='current_branch_only', action='store_true',
help='fetch only current manifest branch from server')
group.add_option('--mirror',
dest='mirror', action='store_true',
help='create a replica of the remote repositories '
@ -182,6 +193,9 @@ group.add_option('--mirror',
group.add_option('--reference',
dest='reference',
help='location of mirror directory', metavar='DIR')
group.add_option('--dissociate',
dest='dissociate', action='store_true',
help='dissociate from reference mirrors after clone')
group.add_option('--depth', type='int', default=None,
dest='depth',
help='create a shallow clone with given depth; see git clone')
@ -189,6 +203,9 @@ group.add_option('--archive',
dest='archive', action='store_true',
help='checkout an archive instead of a git repository for '
'each project. See git archive.')
group.add_option('--submodules',
dest='submodules', action='store_true',
help='sync any submodules associated with the manifest repo')
group.add_option('-g', '--groups',
dest='groups', default='default',
help='restrict manifest projects to ones with specified '
@ -202,6 +219,9 @@ group.add_option('-p', '--platform',
group.add_option('--no-clone-bundle',
dest='no_clone_bundle', action='store_true',
help='disable use of /clone.bundle on HTTP/HTTPS')
group.add_option('--no-tags',
dest='no_tags', action='store_true',
help="don't fetch tags in the manifest")
# Tool
@ -353,6 +373,11 @@ def _Init(args, gitc_init=False):
rev = 'refs/remotes/origin/%s^0' % branch
_Checkout(dst, branch, rev, opt.quiet)
if not os.path.isfile(os.path.join(dst, 'repo')):
_print("warning: '%s' does not look like a git-repo repository, is "
"REPO_URL set correctly?" % url, file=sys.stderr)
except CloneFailure:
if opt.quiet:
_print('fatal: repo init failed; run without --quiet to see why',
@ -488,7 +513,7 @@ def _InitHttp():
p = n.hosts[host]
mgr.add_password(p[1], 'http://%s/' % host, p[0], p[2])
mgr.add_password(p[1], 'https://%s/' % host, p[0], p[2])
except: # pylint: disable=bare-except
except:
pass
handlers.append(urllib.request.HTTPBasicAuthHandler(mgr))
handlers.append(urllib.request.HTTPDigestAuthHandler(mgr))
@ -514,7 +539,7 @@ def _Fetch(url, local, src, quiet):
err = None
cmd.append(src)
cmd.append('+refs/heads/*:refs/remotes/origin/*')
cmd.append('refs/tags/*:refs/tags/*')
cmd.append('+refs/tags/*:refs/tags/*')
proc = subprocess.Popen(cmd, cwd=local, stderr=err)
if err:
@ -853,7 +878,10 @@ def main(orig_args):
try:
_Init(args, gitc_init=(cmd == 'gitc-init'))
except CloneFailure:
shutil.rmtree(os.path.join(repodir, S_repo), ignore_errors=True)
path = os.path.join(repodir, S_repo)
_print("fatal: cloning the git-repo repository failed, will remove "
"'%s' " % path, file=sys.stderr)
shutil.rmtree(path, ignore_errors=True)
sys.exit(1)
repo_main, rel_repo_dir = _FindRepo()
else:
@ -871,7 +899,10 @@ def main(orig_args):
me.extend(orig_args)
me.extend(extra_args)
try:
os.execv(sys.executable, me)
if platform.system() == "Windows":
sys.exit(subprocess.call(me))
else:
os.execv(sys.executable, me)
except OSError as e:
_print("fatal: unable to start %s" % repo_main, file=sys.stderr)
_print("fatal: %s" % e, file=sys.stderr)
@ -881,6 +912,6 @@ def main(orig_args):
if __name__ == '__main__':
if ver[0] == 3:
_print('warning: Python 3 support is currently experimental. YMMV.\n'
'Please use Python 2.6 - 2.7 instead.',
'Please use Python 2.7 instead.',
file=sys.stderr)
main(sys.argv[1:])

View File

@ -16,6 +16,7 @@
from __future__ import print_function
import sys
from command import Command
from collections import defaultdict
from git_command import git
from progress import Progress
@ -23,49 +24,75 @@ class Abandon(Command):
common = True
helpSummary = "Permanently abandon a development branch"
helpUsage = """
%prog <branchname> [<project>...]
%prog [--all | <branchname>] [<project>...]
This subcommand permanently abandons a development branch by
deleting it (and all its history) from your local repository.
It is equivalent to "git branch -D <branchname>".
"""
def _Options(self, p):
p.add_option('--all',
dest='all', action='store_true',
help='delete all branches in all projects')
def Execute(self, opt, args):
if not args:
if not opt.all and not args:
self.Usage()
nb = args[0]
if not git.check_ref_format('heads/%s' % nb):
print("error: '%s' is not a valid name" % nb, file=sys.stderr)
sys.exit(1)
if not opt.all:
nb = args[0]
if not git.check_ref_format('heads/%s' % nb):
print("error: '%s' is not a valid name" % nb, file=sys.stderr)
sys.exit(1)
else:
args.insert(0,None)
nb = "'All local branches'"
nb = args[0]
err = []
success = []
err = defaultdict(list)
success = defaultdict(list)
all_projects = self.GetProjects(args[1:])
pm = Progress('Abandon %s' % nb, len(all_projects))
for project in all_projects:
pm.update()
status = project.AbandonBranch(nb)
if status is not None:
if status:
success.append(project)
else:
err.append(project)
if opt.all:
branches = project.GetBranches().keys()
else:
branches = [nb]
for name in branches:
status = project.AbandonBranch(name)
if status is not None:
if status:
success[name].append(project)
else:
err[name].append(project)
pm.end()
width = 25
for name in branches:
if width < len(name):
width = len(name)
if err:
for p in err:
print("error: %s/: cannot abandon %s" % (p.relpath, nb),
file=sys.stderr)
for br in err.keys():
err_msg = "error: cannot abandon %s" %br
print(err_msg, file=sys.stderr)
for proj in err[br]:
print(' '*len(err_msg) + " | %s" % proj.relpath, file=sys.stderr)
sys.exit(1)
elif not success:
print('error: no project has branch %s' % nb, file=sys.stderr)
print('error: no project has local branch(es) : %s' % nb,
file=sys.stderr)
sys.exit(1)
else:
print('Abandoned in %d project(s):\n %s'
% (len(success), '\n '.join(p.relpath for p in success)),
file=sys.stderr)
print('Abandoned branches:', file=sys.stderr)
for br in success.keys():
if len(all_projects) > 1 and len(all_projects) == len(success[br]):
result = "all project"
else:
result = "%s" % (
('\n'+' '*width + '| ').join(p.relpath for p in success[br]))
print("%s%s| %s\n" % (br,' '*(width-len(br)), result),file=sys.stderr)

View File

@ -67,8 +67,7 @@ class Branches(Command):
Summarizes the currently available topic branches.
Branch Display
--------------
# Branch Display
The branch display output by this command is organized into four
columns of information; for example:

14
subcmds/download.py Normal file → Executable file
View File

@ -26,11 +26,12 @@ class Download(Command):
common = True
helpSummary = "Download and checkout a change"
helpUsage = """
%prog {project change[/patchset]}...
%prog {[project] change[/patchset]}...
"""
helpDescription = """
The '%prog' command downloads a change from the review system and
makes it available in your project's local working directory.
If no project is specified try to use current directory as a project.
"""
def _Options(self, p):
@ -55,12 +56,21 @@ makes it available in your project's local working directory.
m = CHANGE_RE.match(a)
if m:
if not project:
self.Usage()
project = self.GetProjects(".")[0]
chg_id = int(m.group(1))
if m.group(2):
ps_id = int(m.group(2))
else:
ps_id = 1
refs = 'refs/changes/%2.2d/%d/' % (chg_id % 100, chg_id)
output = project._LsRemote(refs + '*')
if output:
regex = refs + r'(\d+)'
rcomp = re.compile(regex, re.I)
for line in output.splitlines():
match = rcomp.search(line)
if match:
ps_id = max(int(match.group(1)), ps_id)
to_get.append((project, chg_id, ps_id))
else:
project = self.GetProjects([a])[0]

View File

@ -15,17 +15,16 @@
from __future__ import print_function
import errno
import fcntl
import multiprocessing
import re
import os
import select
import signal
import sys
import subprocess
from color import Coloring
from command import Command, MirrorSafeCommand
import platform_utils
_CAN_COLOR = [
'branch',
@ -54,8 +53,7 @@ Executes the same shell command in each project.
The -r option allows running the command only on projects matching
regex or wildcard expression.
Output Formatting
-----------------
# Output Formatting
The -p option causes '%prog' to bind pipes to the command's stdin,
stdout and stderr streams, and pipe all output into a continuous
@ -72,8 +70,7 @@ command produces output only on stderr. Normally the -p option
causes command output to be suppressed until the command produces
at least one byte of output on stdout.
Environment
-----------
# Environment
pwd is the project's working directory. If the current client is
a mirror client, then pwd is the Git repository.
@ -105,6 +102,13 @@ annotating tree details.
shell positional arguments ($1, $2, .., $#) are set to any arguments
following <command>.
Example: to list projects:
%prog% forall -c 'echo $REPO_PROJECT'
Notice that $REPO_PROJECT is quoted to ensure it is expanded in
the context of running <command> instead of in the calling shell.
Unless -p is used, stdin, stdout, stderr are inherited from the
terminal and are not redirected.
@ -199,14 +203,12 @@ without iterating through the remaining projects.
break
else:
cn = None
# pylint: disable=W0631
if cn and cn in _CAN_COLOR:
class ColorCmd(Coloring):
def __init__(self, config, cmd):
Coloring.__init__(self, config, cmd)
if ColorCmd(self.manifest.manifestProject.config, cn).is_on:
cmd.insert(cmd.index(cn) + 1, '--color')
# pylint: enable=W0631
mirror = self.manifest.IsMirror
rc = 0
@ -344,35 +346,25 @@ def DoWork(project, mirror, opt, cmd, shell, cnt, config):
if opt.project_header:
out = ForallColoring(config)
out.redirect(sys.stdout)
class sfd(object):
def __init__(self, fd, dest):
self.fd = fd
self.dest = dest
def fileno(self):
return self.fd.fileno()
empty = True
errbuf = ''
p.stdin.close()
s_in = [sfd(p.stdout, sys.stdout),
sfd(p.stderr, sys.stderr)]
s_in = platform_utils.FileDescriptorStreams.create()
s_in.add(p.stdout, sys.stdout, 'stdout')
s_in.add(p.stderr, sys.stderr, 'stderr')
for s in s_in:
flags = fcntl.fcntl(s.fd, fcntl.F_GETFL)
fcntl.fcntl(s.fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
while s_in:
in_ready, _out_ready, _err_ready = select.select(s_in, [], [])
while not s_in.is_done:
in_ready = s_in.select()
for s in in_ready:
buf = s.fd.read(4096)
buf = s.read()
if not buf:
s.fd.close()
s.close()
s_in.remove(s)
continue
if not opt.verbose:
if s.fd != p.stdout:
if s.std_name == 'stderr':
errbuf += buf
continue

View File

@ -14,18 +14,14 @@
# limitations under the License.
from __future__ import print_function
import os
import shutil
import sys
from command import Command, GitcClientCommand
import gitc_utils
import platform_utils
from pyversion import is_python3
if not is_python3():
# pylint:disable=W0622
input = raw_input
# pylint:enable=W0622
class GitcDelete(Command, GitcClientCommand):
common = True
@ -52,4 +48,4 @@ and all locally downloaded sources.
if not response == 'yes':
print('Response was not "yes"\n Exiting...')
sys.exit(1)
shutil.rmtree(self.gitc_manifest.gitc_client_dir)
platform_utils.rmtree(self.gitc_manifest.gitc_client_dir)

View File

@ -33,8 +33,7 @@ class Grep(PagedCommand):
helpDescription = """
Search for the specified patterns in all project files.
Boolean Options
---------------
# Boolean Options
The following options can appear as often as necessary to express
the pattern to locate:
@ -47,8 +46,7 @@ in order to scan multiple trees. If the same file matches in more
than one tree, only the first result is reported, prefixed by the
revision name it was found under.
Examples
-------
# Examples
Look for a line that has '#define' and either 'MAX_PATH or 'PATH_MAX':

View File

@ -107,15 +107,13 @@ Displays detailed usage information about a command.
self.heading('%s', heading)
self.nl()
self.heading('%s', ''.ljust(len(heading), '-'))
self.nl()
me = 'repo %s' % cmd.NAME
body = body.strip()
body = body.replace('%prog', me)
asciidoc_hdr = re.compile(r'^\n?([^\n]{1,})\n([=~-]{2,})$')
asciidoc_hdr = re.compile(r'^\n?#+ (.+)$')
for para in body.split("\n\n"):
if para.startswith(' '):
self.write('%s', para)
@ -125,19 +123,8 @@ Displays detailed usage information about a command.
m = asciidoc_hdr.match(para)
if m:
title = m.group(1)
section_type = m.group(2)
if section_type[0] in ('=', '-'):
p = self.heading
else:
def _p(fmt, *args):
self.write(' ')
self.heading(fmt, *args)
p = _p
p('%s', title)
self.heading(m.group(1))
self.nl()
p('%s', ''.ljust(len(title), section_type[0]))
self.nl()
continue

View File

@ -17,7 +17,6 @@ from __future__ import print_function
import os
import platform
import re
import shutil
import sys
from pyversion import is_python3
@ -35,6 +34,7 @@ from error import ManifestParseError
from project import SyncBuffer
from git_config import GitConfig
from git_command import git_require, MIN_GIT_VERSION
import platform_utils
class Init(InteractiveCommand, MirrorSafeCommand):
common = True
@ -61,14 +61,18 @@ directory use as much data as possible from the local reference
directory when fetching from the server. This will make the sync
go a lot faster by reducing data traffic on the network.
The --dissociate option can be used to borrow the objects from
the directory specified with the --reference option only to reduce
network transfer, and stop borrowing from them after a first clone
is made by making necessary local copies of borrowed objects.
The --no-clone-bundle option disables any attempt to use
$URL/clone.bundle to bootstrap a new Git repository from a
resumeable bundle file on a content delivery network. This
may be necessary if there are problems with the local Python
HTTP client or proxy configuration, but the Git binary works.
Switching Manifest Branches
---------------------------
# Switching Manifest Branches
To switch to another manifest branch, `repo init -b otherbranch`
may be used in an existing client. However, as this only updates the
@ -91,6 +95,9 @@ to update the working directory files.
g.add_option('-b', '--manifest-branch',
dest='manifest_branch',
help='manifest branch or revision', metavar='REVISION')
g.add_option('--current-branch',
dest='current_branch_only', action='store_true',
help='fetch only current manifest branch from server')
g.add_option('-m', '--manifest-name',
dest='manifest_name', default='default.xml',
help='initial manifest file', metavar='NAME.xml')
@ -101,6 +108,9 @@ to update the working directory files.
g.add_option('--reference',
dest='reference',
help='location of mirror directory', metavar='DIR')
g.add_option('--dissociate',
dest='dissociate', action='store_true',
help='dissociate from reference mirrors after clone')
g.add_option('--depth', type='int', default=None,
dest='depth',
help='create a shallow clone with given depth; see git clone')
@ -108,6 +118,9 @@ to update the working directory files.
dest='archive', action='store_true',
help='checkout an archive instead of a git repository for '
'each project. See git archive.')
g.add_option('--submodules',
dest='submodules', action='store_true',
help='sync any submodules associated with the manifest repo')
g.add_option('-g', '--groups',
dest='groups', default='default',
help='restrict manifest projects to ones with specified '
@ -121,6 +134,9 @@ to update the working directory files.
g.add_option('--no-clone-bundle',
dest='no_clone_bundle', action='store_true',
help='disable use of /clone.bundle on HTTP/HTTPS')
g.add_option('--no-tags',
dest='no_tags', action='store_true',
help="don't fetch tags in the manifest")
# Tool
g = p.add_option_group('repo Version options')
@ -166,7 +182,8 @@ to update the working directory files.
if not mirrored_manifest_git.endswith(".git"):
mirrored_manifest_git += ".git"
if not os.path.exists(mirrored_manifest_git):
mirrored_manifest_git = os.path.join(opt.reference + '/.repo/manifests.git')
mirrored_manifest_git = os.path.join(opt.reference,
'.repo/manifests.git')
m._InitGitDir(mirror_git=mirrored_manifest_git)
@ -210,6 +227,9 @@ to update the working directory files.
if opt.reference:
m.config.SetString('repo.reference', opt.reference)
if opt.dissociate:
m.config.SetString('repo.dissociate', 'true')
if opt.archive:
if is_new:
m.config.SetString('repo.archive', 'true')
@ -230,22 +250,27 @@ to update the working directory files.
'in another location.', file=sys.stderr)
sys.exit(1)
if opt.submodules:
m.config.SetString('repo.submodules', 'true')
if not m.Sync_NetworkHalf(is_new=is_new, quiet=opt.quiet,
clone_bundle=not opt.no_clone_bundle):
clone_bundle=not opt.no_clone_bundle,
current_branch_only=opt.current_branch_only,
no_tags=opt.no_tags, submodules=opt.submodules):
r = m.GetRemote(m.remote.name)
print('fatal: cannot obtain manifest %s' % r.url, file=sys.stderr)
# Better delete the manifest git dir if we created it; otherwise next
# time (when user fixes problems) we won't go through the "is_new" logic.
if is_new:
shutil.rmtree(m.gitdir)
platform_utils.rmtree(m.gitdir)
sys.exit(1)
if opt.manifest_branch:
m.MetaBranchSwitch()
m.MetaBranchSwitch(submodules=opt.submodules)
syncbuf = SyncBuffer(m.config)
m.Sync_LocalHalf(syncbuf)
m.Sync_LocalHalf(syncbuf, submodules=opt.submodules)
syncbuf.Finish()
if is_new or m.CurrentBranch is None:

View File

@ -39,7 +39,7 @@ in a Git repository for use during future 'repo init' invocations.
helptext = self._helpDescription + '\n'
r = os.path.dirname(__file__)
r = os.path.dirname(r)
fd = open(os.path.join(r, 'docs', 'manifest-format.txt'))
fd = open(os.path.join(r, 'docs', 'manifest-format.md'))
for line in fd:
helptext += line
fd.close()

View File

@ -60,8 +60,8 @@ The '%prog' command stages files to prepare the next commit.
out.nl()
for i in range(len(all_projects)):
p = all_projects[i]
out.write('%3d: %s', i + 1, p.relpath + '/')
project = all_projects[i]
out.write('%3d: %s', i + 1, project.relpath + '/')
out.nl()
out.nl()

View File

@ -18,7 +18,7 @@ import os
import sys
from command import Command
from git_config import IsId
from git_config import IsImmutable
from git_command import git
import gitc_utils
from progress import Progress
@ -96,11 +96,11 @@ revision specified in the manifest.
project.Sync_LocalHalf(sync_buf)
project.revisionId = gitc_project.old_revision
# If the current revision is a specific SHA1 then we can't push back
# to it; so substitute with dest_branch if defined, or with manifest
# default revision instead.
# If the current revision is immutable, such as a SHA1, a tag or
# a change, then we can't push back to it. Substitute with
# dest_branch, if defined; or with manifest default revision instead.
branch_merge = ''
if IsId(project.revisionExpr):
if IsImmutable(project.revisionExpr):
if project.dest_branch:
branch_merge = project.dest_branch
else:

View File

@ -26,6 +26,7 @@ import itertools
import os
from color import Coloring
import platform_utils
class Status(PagedCommand):
common = True
@ -49,8 +50,7 @@ includes deeper items. For example, if dir/subdir/proj1 and
dir/subdir/proj2 are repo projects, dir/subdir/proj3 will be shown
if it is not known to repo.
Status Display
--------------
# Status Display
The status display is organized into three columns of information,
for example if the file 'subcmds/status.py' is modified in the
@ -89,8 +89,10 @@ the following meanings:
p.add_option('-o', '--orphans',
dest='orphans', action='store_true',
help="include objects in working directory outside of repo projects")
p.add_option('-q', '--quiet', action='store_true',
help="only print the name of modified projects")
def _StatusHelper(self, project, clean_counter, sem):
def _StatusHelper(self, project, clean_counter, sem, quiet):
"""Obtains the status for a specific project.
Obtains the status for a project, redirecting the output to
@ -104,7 +106,7 @@ the following meanings:
output: Where to output the status.
"""
try:
state = project.PrintWorkTreeStatus()
state = project.PrintWorkTreeStatus(quiet=quiet)
if state == 'CLEAN':
next(clean_counter)
finally:
@ -114,7 +116,7 @@ the following meanings:
"""find 'dirs' that are present in 'proj_dirs_parents' but not in 'proj_dirs'"""
status_header = ' --\t'
for item in dirs:
if not os.path.isdir(item):
if not platform_utils.isdir(item):
outstring.append(''.join([status_header, item]))
continue
if item in proj_dirs:
@ -132,7 +134,7 @@ the following meanings:
if opt.jobs == 1:
for project in all_projects:
state = project.PrintWorkTreeStatus()
state = project.PrintWorkTreeStatus(quiet=opt.quiet)
if state == 'CLEAN':
next(counter)
else:
@ -142,13 +144,13 @@ the following meanings:
sem.acquire()
t = _threading.Thread(target=self._StatusHelper,
args=(project, counter, sem))
args=(project, counter, sem, opt.quiet))
threads.append(t)
t.daemon = True
t.start()
for t in threads:
t.join()
if len(all_projects) == next(counter):
if not opt.quiet and len(all_projects) == next(counter):
print('nothing to commit (working directory clean)')
if opt.orphans:

View File

@ -19,7 +19,6 @@ import netrc
from optparse import SUPPRESS_HELP
import os
import re
import shutil
import socket
import subprocess
import sys
@ -64,6 +63,7 @@ try:
except ImportError:
multiprocessing = None
import event_log
from git_command import GIT, git_require
from git_config import GetUrlCookieFile
from git_refs import R_HEADS, HEAD
@ -72,6 +72,7 @@ from project import Project
from project import RemoteSpec
from command import Command, MirrorSafeCommand
from error import RepoChangedException, GitError, ManifestParseError
import platform_utils
from project import SyncBuffer
from progress import Progress
from wrapper import Wrapper
@ -154,8 +155,7 @@ exist locally.
The --prune option can be used to remove any refs that no longer
exist on the remote.
SSH Connections
---------------
# SSH Connections
If at least one project remote URL uses an SSH connection (ssh://,
git+ssh://, or user@host:path syntax) repo will automatically
@ -169,8 +169,7 @@ environment variable to 'ssh'. For example:
export GIT_SSH=ssh
%prog
Compatibility
~~~~~~~~~~~~~
# Compatibility
This feature is automatically disabled on Windows, due to the lack
of UNIX domain socket support.
@ -255,7 +254,7 @@ later is required to fix a server side protocol bug.
dest='repo_upgraded', action='store_true',
help=SUPPRESS_HELP)
def _FetchProjectList(self, opt, projects, *args, **kwargs):
def _FetchProjectList(self, opt, projects, sem, *args, **kwargs):
"""Main function of the fetch threads when jobs are > 1.
Delegates most of the work to _FetchHelper.
@ -263,15 +262,20 @@ later is required to fix a server side protocol bug.
Args:
opt: Program options returned from optparse. See _Options().
projects: Projects to fetch.
sem: We'll release() this semaphore when we exit so that another thread
can be started up.
*args, **kwargs: Remaining arguments to pass to _FetchHelper. See the
_FetchHelper docstring for details.
"""
for project in projects:
success = self._FetchHelper(opt, project, *args, **kwargs)
if not success and not opt.force_broken:
break
try:
for project in projects:
success = self._FetchHelper(opt, project, *args, **kwargs)
if not success and not opt.force_broken:
break
finally:
sem.release()
def _FetchHelper(self, opt, project, lock, fetched, pm, sem, err_event):
def _FetchHelper(self, opt, project, lock, fetched, pm, err_event):
"""Fetch git objects for a single project.
Args:
@ -283,8 +287,6 @@ later is required to fix a server side protocol bug.
(with our lock held).
pm: Instance of a Project object. We will call pm.update() (with our
lock held).
sem: We'll release() this semaphore when we exit so that another thread
can be started up.
err_event: We'll set this event in the case of an error (after printing
out info about the error).
@ -301,9 +303,10 @@ later is required to fix a server side protocol bug.
# - We always set err_event in the case of an exception.
# - We always make sure we call sem.release().
# - We always make sure we unlock the lock if we locked it.
start = time.time()
success = False
try:
try:
start = time.time()
success = project.Sync_NetworkHalf(
quiet=opt.quiet,
current_branch_only=opt.current_branch_only,
@ -321,7 +324,9 @@ later is required to fix a server side protocol bug.
if not success:
err_event.set()
print('error: Cannot fetch %s' % project.name, file=sys.stderr)
print('error: Cannot fetch %s from %s'
% (project.name, project.remote.url),
file=sys.stderr)
if opt.force_broken:
print('warn: --force-broken, continuing to sync',
file=sys.stderr)
@ -340,14 +345,18 @@ later is required to fix a server side protocol bug.
finally:
if did_lock:
lock.release()
sem.release()
finish = time.time()
self.event_log.AddSync(project, event_log.TASK_SYNC_NETWORK,
start, finish, success)
return success
def _Fetch(self, projects, opt):
fetched = set()
lock = _threading.Lock()
pm = Progress('Fetching projects', len(projects))
pm = Progress('Fetching projects', len(projects),
print_newline=not(opt.quiet),
always_print_percentage=opt.quiet)
objdir_project_map = dict()
for project in projects:
@ -365,10 +374,10 @@ later is required to fix a server side protocol bug.
sem.acquire()
kwargs = dict(opt=opt,
projects=project_list,
sem=sem,
lock=lock,
fetched=fetched,
pm=pm,
sem=sem,
err_event=err_event)
if self.jobs > 1:
t = _threading.Thread(target = self._FetchProjectList,
@ -384,7 +393,7 @@ later is required to fix a server side protocol bug.
t.join()
# If we saw an error, exit with code 1 so that other scripts can check.
if err_event.isSet():
if err_event.isSet() and not opt.force_broken:
print('\nerror: Exited sync due to fetch errors', file=sys.stderr)
sys.exit(1)
@ -464,9 +473,9 @@ later is required to fix a server side protocol bug.
# working git repository around. There shouldn't be any git projects here,
# so rmtree works.
try:
shutil.rmtree(os.path.join(path, '.git'))
except OSError:
print('Failed to remove %s' % os.path.join(path, '.git'), file=sys.stderr)
platform_utils.rmtree(os.path.join(path, '.git'))
except OSError as e:
print('Failed to remove %s (%s)' % (os.path.join(path, '.git'), str(e)), file=sys.stderr)
print('error: Failed to delete obsolete path %s' % path, file=sys.stderr)
print(' remove manually, then run sync again', file=sys.stderr)
return -1
@ -475,23 +484,29 @@ later is required to fix a server side protocol bug.
# another git project
dirs_to_remove = []
failed = False
for root, dirs, files in os.walk(path):
for root, dirs, files in platform_utils.walk(path):
for f in files:
try:
os.remove(os.path.join(root, f))
except OSError:
print('Failed to remove %s' % os.path.join(root, f), file=sys.stderr)
platform_utils.remove(os.path.join(root, f))
except OSError as e:
print('Failed to remove %s (%s)' % (os.path.join(root, f), str(e)), file=sys.stderr)
failed = True
dirs[:] = [d for d in dirs
if not os.path.lexists(os.path.join(root, d, '.git'))]
dirs_to_remove += [os.path.join(root, d) for d in dirs
if os.path.join(root, d) not in dirs_to_remove]
for d in reversed(dirs_to_remove):
if len(os.listdir(d)) == 0:
if platform_utils.islink(d):
try:
os.rmdir(d)
except OSError:
print('Failed to remove %s' % os.path.join(root, d), file=sys.stderr)
platform_utils.remove(d)
except OSError as e:
print('Failed to remove %s (%s)' % (os.path.join(root, d), str(e)), file=sys.stderr)
failed = True
elif len(platform_utils.listdir(d)) == 0:
try:
platform_utils.rmdir(d)
except OSError as e:
print('Failed to remove %s (%s)' % (os.path.join(root, d), str(e)), file=sys.stderr)
failed = True
continue
if failed:
@ -502,8 +517,8 @@ later is required to fix a server side protocol bug.
# Try deleting parent dirs if they are empty
project_dir = path
while project_dir != self.manifest.topdir:
if len(os.listdir(project_dir)) == 0:
os.rmdir(project_dir)
if len(platform_utils.listdir(project_dir)) == 0:
platform_utils.rmdir(project_dir)
else:
break
project_dir = os.path.dirname(project_dir)
@ -695,7 +710,7 @@ later is required to fix a server side protocol bug.
else: # Not smart sync or smart tag mode
if os.path.isfile(smart_sync_manifest_path):
try:
os.remove(smart_sync_manifest_path)
platform_utils.remove(smart_sync_manifest_path)
except OSError as e:
print('error: failed to remove existing smart sync override manifest: %s' %
e, file=sys.stderr)
@ -710,15 +725,24 @@ later is required to fix a server side protocol bug.
_PostRepoUpgrade(self.manifest, quiet=opt.quiet)
if not opt.local_only:
mp.Sync_NetworkHalf(quiet=opt.quiet,
current_branch_only=opt.current_branch_only,
no_tags=opt.no_tags,
optimized_fetch=opt.optimized_fetch)
start = time.time()
success = mp.Sync_NetworkHalf(quiet=opt.quiet,
current_branch_only=opt.current_branch_only,
no_tags=opt.no_tags,
optimized_fetch=opt.optimized_fetch,
submodules=self.manifest.HasSubmodules)
finish = time.time()
self.event_log.AddSync(mp, event_log.TASK_SYNC_NETWORK,
start, finish, success)
if mp.HasChanges:
syncbuf = SyncBuffer(mp.config)
mp.Sync_LocalHalf(syncbuf)
if not syncbuf.Finish():
start = time.time()
mp.Sync_LocalHalf(syncbuf, submodules=self.manifest.HasSubmodules)
clean = syncbuf.Finish()
self.event_log.AddSync(mp, event_log.TASK_SYNC_LOCAL,
start, time.time(), clean)
if not clean:
sys.exit(1)
self._ReloadManifest(manifest_name)
if opt.jobs is None:
@ -755,8 +779,8 @@ later is required to fix a server side protocol bug.
# generate a new args list to represent the opened projects.
# TODO: make this more reliable -- if there's a project name/path overlap,
# this may choose the wrong project.
args = [os.path.relpath(self.manifest.paths[p].worktree, os.getcwd())
for p in opened_projects]
args = [os.path.relpath(self.manifest.paths[path].worktree, os.getcwd())
for path in opened_projects]
if not args:
return
all_projects = self.GetProjects(args,
@ -812,7 +836,10 @@ later is required to fix a server side protocol bug.
for project in all_projects:
pm.update()
if project.worktree:
start = time.time()
project.Sync_LocalHalf(syncbuf, force_sync=opt.force_sync)
self.event_log.AddSync(project, event_log.TASK_SYNC_LOCAL,
start, time.time(), syncbuf.Recently())
pm.end()
print(file=sys.stderr)
if not syncbuf.Finish():
@ -896,6 +923,7 @@ def _VerifyTag(project):
return False
return True
class _FetchTimes(object):
_ALPHA = 0.5
@ -926,7 +954,7 @@ class _FetchTimes(object):
f.close()
except (IOError, ValueError):
try:
os.remove(self._path)
platform_utils.remove(self._path)
except OSError:
pass
self._times = {}
@ -950,7 +978,7 @@ class _FetchTimes(object):
f.close()
except (IOError, TypeError):
try:
os.remove(self._path)
platform_utils.remove(self._path)
except OSError:
pass

View File

@ -25,12 +25,10 @@ from git_command import GitCommand
from project import RepoHook
from pyversion import is_python3
# pylint:disable=W0622
if not is_python3():
input = raw_input
else:
unicode = str
# pylint:enable=W0622
UNUSUAL_COMMIT_THRESHOLD = 5
@ -80,8 +78,7 @@ added to the respective list of users, and emails are sent to any
new users. Users passed as --reviewers must already be registered
with the code review system, or the upload will fail.
Configuration
-------------
# Configuration
review.URL.autoupload:
@ -128,10 +125,9 @@ is set to "true" then repo will assume you always want the equivalent
of the -t option to the repo command. If unset or set to "false" then
repo will make use of only the command line option.
References
----------
# References
Gerrit Code Review: http://code.google.com/p/gerrit/
Gerrit Code Review: https://www.gerritcodereview.com/
"""
@ -154,6 +150,19 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
p.add_option('-d', '--draft',
action='store_true', dest='draft', default=False,
help='If specified, upload as a draft.')
p.add_option('--ne', '--no-emails',
action='store_false', dest='notify', default=True,
help='If specified, do not send emails on upload.')
p.add_option('-p', '--private',
action='store_true', dest='private', default=False,
help='If specified, upload as a private change.')
p.add_option('-w', '--wip',
action='store_true', dest='wip', default=False,
help='If specified, upload as a work-in-progress change.')
p.add_option('-o', '--push-option',
type='string', action='append', dest='push_options',
default=[],
help='Additional push options to transmit')
p.add_option('-D', '--destination', '--dest',
type='string', action='store', dest='dest_branch',
metavar='BRANCH',
@ -175,6 +184,9 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
# Never run upload hooks, but upload anyway (AKA bypass hooks).
# - no-verify=True, verify=True:
# Invalid
p.add_option('--no-cert-checks',
dest='validate_certs', action='store_false', default=True,
help='Disable verifying ssl certs (unsafe).')
p.add_option('--no-verify',
dest='bypass_hooks', action='store_true',
help='Do not run the upload hook.')
@ -198,7 +210,8 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
commit_list = branch.commits
destination = opt.dest_branch or project.dest_branch or project.revisionExpr
print('Upload project %s/ to remote branch %s:' % (project.relpath, destination))
print('Upload project %s/ to remote branch %s%s:' %
(project.relpath, destination, ' (draft)' if opt.draft else ''))
print(' branch %s (%2d commit%s, %s):' % (
name,
len(commit_list),
@ -377,7 +390,16 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
branch.uploaded = False
continue
branch.UploadForReview(people, auto_topic=opt.auto_topic, draft=opt.draft, dest_branch=destination)
branch.UploadForReview(people,
auto_topic=opt.auto_topic,
draft=opt.draft,
private=opt.private,
notify=None if opt.notify else 'NONE',
wip=opt.wip,
dest_branch=destination,
validate_certs=opt.validate_certs,
push_options=opt.push_options)
branch.uploaded = True
except UploadError as e:
branch.error = e
@ -463,8 +485,8 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
self.manifest.topdir,
self.manifest.manifestProject.GetRemote('origin').url,
abort_if_user_denies=True)
pending_proj_names = [project.name for (project, avail) in pending]
pending_worktrees = [project.worktree for (project, avail) in pending]
pending_proj_names = [project.name for (project, available) in pending]
pending_worktrees = [project.worktree for (project, available) in pending]
try:
hook.Run(opt.allow_all_hooks, project_list=pending_proj_names,
worktree_list=pending_worktrees)