Compare commits

...

95 Commits

Author SHA1 Message Date
ba72d8301e GITC: Fix repo sync.
Fixing http://b/23785024 by calling os.getcwd() because variable
cwd no longer exists.

Change-Id: I21ff7d059e072f9f60726db76b67587a92c878ad
2015-09-03 10:47:44 -07:00
9ff2ece6ab gitc: Improve help visibility
This improves the visiblity of gitc-init if we can get the gitc config,
and hides it otherwise.

Change-Id: I82830b0b07c311e8c74397ba79eb4c361f8b6fb5
2015-09-01 12:23:56 -07:00
2487cb7b2c Fix gitc check if gitc isn't installed
This was doing cwd.startswith(''), which is always true.

Change-Id: Icc059c09492b31e2d7651e4a595bda783c5abc47
2015-08-31 15:59:54 -07:00
8ce5041596 GITC: Pull GITC Manifest Dir from the config.
Updates the repo launcher and gitc_utils to pull the manifest
directory location out of the gitc config file.

Change-Id: Id08381b8a7d61962093d5cddcb3ff6afbb13004b
2015-08-31 21:39:17 +00:00
f7a51898d3 GITC: Expand relative remote URLs.
The GITC filesystem does not understand relative URLs for remotes,
so now if a remote uses a relative URL, it will be be expanded to
be relative to the manifest URL.

Change-Id: Ie1210758560aeb1934da3f71496aaf19c2728214
2015-08-28 18:09:05 +00:00
b9a1b73425 GITC: Add repo start support.
Add repo start support for GITC checkouts. If the user is in
the GITC FS view, they can now run repo start to check out
the sources and create a new working branch.

When "repo start" is called on a GITC project, the revision
tag is set to an empty string and saved in a new tag:
old-revision. This tells the GITC filesystem to display the
local copy of the sources when being viewed. The local copy
is created by pulling the project sources and the new branch
is created based off the original project revision.

Updated main.py to setup each command's gitc_manifest when
appropriate.

Updated repo sync's logic to sync opened projects and
updating the GITC manifest file for the rest.

Change-Id: I7e4809d1c4fc43c69b26f2f1bebe45aab0cae628
2015-08-28 10:53:05 -07:00
dc2545cad6 project.py: Improve message shown when hook is not replaced
If a hook file has been modified locally, it will not be replaced.

Improve the message to make this clearer.

Also change it from an error to a warning.

Change-Id: I62c635390f24d2868db17717c247861b0381c99f
2015-08-25 05:40:46 +00:00
f33929d014 project.py: Consistently use the _error method to print error messages
Use the _error method instead of directly calling `print`.

Also add a new _warn convenience method.

Change-Id: Ia332c14ef8d9d1fe2df128dbf36b5521802ccdf1
2015-08-25 14:39:06 +09:00
3010e5ba64 Smartsync: Don't fail if there isn't a cookiefile
Change-Id: I434a259f43ca9808e88051ac8ba865c519a24702
2015-08-20 10:29:37 -07:00
ba7bc738c1 Sync: Refactor netrc parsing
Don't emit a message when the netrc file doesn't exist or couldn't
be opened.

Instead of trying to unpack the result of info.authenticators() and
catching the resulting TypeError when it's None, first store it to
a local and only unpack it if it has a value.

Also remove an unused import.

Change-Id: I5c404d91e48c261c1ab850c3e5f040c4f4c235cb
2015-08-20 17:05:17 +09:00
f4599a2a3d gitc_init: Remove unused import
Also add missing newline at the end of the file.

Change-Id: I206e6c4b033d223eb0ff5824ecbf6fd98c39c918
2015-08-20 16:45:39 +09:00
022a1d4e6e gitc_utils: Fix incorrect string format argument
Change-Id: Ibbac6e111833c8f5d93cb6cb4a10f8f2c4fd8e11
2015-08-20 16:41:04 +09:00
41d1baac31 gitc_utils: Remove unused variable
Change-Id: I569819675a99ff6c01fb57b23fed033c39d14d1f
2015-08-20 16:40:44 +09:00
46496d8761 gitc_utils: Remove unused import; add missing import
shutils is not used. Remove it.

sys is used, but not imported.  Import it.

Change-Id: I4ee582f33bbd451601097ef2f20bee23b54764cd
2015-08-20 16:37:09 +09:00
7c9263bce0 Merge "Support smart-sync through persistent-http[s]" 2015-08-19 23:36:35 +00:00
dab9e99f0f Merge "Fix formatting of message when retrying clone" 2015-08-19 17:54:50 +00:00
c5f15bf7c0 Merge "Include project path in --force-sync error message" 2015-08-19 17:53:28 +00:00
6d35d676db Merge "Copy clone-depth in repo manifest" 2015-08-19 17:50:15 +00:00
0745bb2657 Support smart-sync through persistent-http[s]
Use the same cookies and proxy that git traffic goes through for
persistent-http[s] to support authentication for smart-sync.

Change-Id: I20f4a281c259053a5a4fdbc48b1bca48e781c692
2015-08-19 10:22:11 -07:00
25857b8988 Fix formatting of message when retrying clone
Passing the force_sync variable into the string formatting results in
the message:

  "Retrying clone after deleting None"

or

  "Retrying clone after deleting True".

Pass the name of the git directory instead.

Also, move the print inside the if-block so it's only displayed
when the retry is actually going to be attempted.

Change-Id: I76d9ecc176cecee4ad512d13e9d1f6bd36aacbbb
2015-08-19 13:03:13 +00:00
bdb5271de3 GITC: Add repo sync support.
Add repo sync support for GITC checkouts. If the user is in the
GITC client directory they can still pull the sources as normal
if they pass in the --force-gitc argument. Otherwise the user
should call repo sync in the GITC view to update the user's
remote view. (This works because .repo in the GITC view will
link to .repo in the client config directory.)

Part of the support for this change is the refactoring of GITC
related code into gitc_utils.py.

Change-Id: I2636aaa50b450b6f091309db8dd0e8f4dbdad579
2015-08-18 11:59:10 -07:00
884092225d Copy clone-depth in repo manifest
This argument wasn't being copied, which caused syncs from generated
manifests to pull down too much of the git history.

Change-Id: I269bab788d4557267c081628b3f8c6aec7744e81
2015-08-17 15:30:27 -07:00
5d0c3a614e Merge "GITC: Add gitc-init subcommand to repo." 2015-08-12 23:25:20 +00:00
1efc2b4a01 GITC: Add gitc-init subcommand to repo.
Adds the new gitc-init command to set up a GITC client. Gitc-init
sets up the client directory and calls repo init within it. Once
the repo is initialized, then generates a GITC manifest file
by using git ls-remote on each project and retrieving the HEAD SHA
to use as the revision attribute.

Gitc-init inherits from and has all the options as repo init.

Change-Id: Icd7e47e90eab752a77de7c80ebc98cfe16bf6de3
2015-08-12 16:22:14 -07:00
2635c0e3b6 Merge "Fix shallow clone behavior" 2015-08-05 01:02:41 +00:00
43322283dc Merge "Support filtering by group on forall and list subcmd" 2015-08-05 01:01:02 +00:00
f9b7683a3b Include project path in --force-sync error message
For projects that have been cloned outside of the repo command (or
cloned a long time ago), commit abaa7f312f
introduced an error message to invite the user to use --force-sync.
However, due to the risk of data loss, it's useful to know which
project's git directory is being replaced before deciding whether or not
to provide --force-sync.

This change updates the exception's associated value to include the
project's relative path and explain to the user how they can resolve the
issue. A previous version of this commit used the project name. However,
for projects that have multiple work trees, the name can be ambiguous,
while the path clearly identifies which git directory will be replaced.

Change-Id: If717e66fda4d19accc0a8e889a91f4cd4ff14dff
2015-08-04 18:41:20 -04:00
eeab6860f1 Fix shallow clone behavior
The existing code here makes sure that switching clone-depth from on to
off actually causes the history to be fully restored. Unfortunately, it
does this by fetching the full history every time the fetch spec
changes. Switching between two clone-depth="1" branches will fetch far
more than the top commit.

Instead, when not using clone-depth, pass --depth=2147483647 to git
fetch so that it ensures that we have the entire history. That is
slightly less efficient, so limit it to only when there are shallow
objects in the project by checking for the existance of the 'shallow'
file.

Change-Id: Iee0cfc9c6992c208344b1d9123769992412db67b
2015-08-03 16:54:16 -07:00
7e59de2bcc Include dest-branch attribute in the 'manifest' subcommand's output
Change-Id: If4227d02005fddea82d9e698a373222100d8f710
2015-07-31 17:36:28 -04:00
163fdbf2fd Merge "Fix _ReferenceGitDir symlinking" 2015-07-31 18:01:32 +00:00
555be54790 Merge "Emit project info in case of sync exception." 2015-07-31 17:06:23 +00:00
c5cd433daf Emit project info in case of sync exception.
Previously repo would only print the failing project path if
Sync_NetworkHalf returned false/empty, but if it threw an
exception the print() was never called.

Change-Id: I58c41de43930df5e34b21561c205e062a72e290f
2015-07-31 14:03:50 +00:00
2a3e15217a Fix _ReferenceGitDir symlinking
This fixes these errors:

  ...
  File ".repo/repo/project.py", line 2371, in _ReferenceGitDir
    os.symlink(os.path.relpath(src, os.path.dirname(dst)), dst)
  OSError: [Errno 17] File exists

Which was happening for checkouts that were created before v1.12.8, when
project-objects was created. Nothing had yet been forcing these
checkouts to use project-objects, until the recent verification changes.

In this OSError case, we already created the symlink, so src == dst, and
the directory did not exist. This caused us to run os.makedirs the
os.symlink on the same file.

dst really should be the file in gitdir, not the target of that symlink
if it exists. So just use realpath for the dotgit portion of the path.

Change-Id: Iff5396a2093de91029c42cf38aa57131fd22981c
2015-07-30 21:29:53 -07:00
0369a069ad Support filtering by group on forall and list subcmd
Enable operating against groups of repositories. As it stands, it isn't
compatible with `-r/--regex`.

`repo forall -g groupname -c pwd` will  run `pwd` for all projects in
groupname.

`repo forall -g thisgroup,-butnotthisone -c pwd` will  run `pwd` for all
projects in `thisgroup` but not `butnotthisone`.

`repo list -g groupname -n` will list all the names of repos in
`groupname`.

Change-Id: Ia75c50ce52541d1c8cea2874b20a4db2e0e54960
2015-07-30 12:59:35 -05:00
abaa7f312f Add option to correct gitdir when syncing
In some cases, a user may wish to continue with a sync even though
it would require overwriting an existing git directory. This behavior
is not safe as a default because it could result in the loss of some
user data, but as an optional flag it allows the user more flexibility.

To support this, add a --force-sync flag to the sync command that will
attempt to overwrite the existing git dir if it is specified and the
existing git dir points to the wrong obj dir.

Change-Id: Ieddda8ad54e264a1eb4a9d54881dd6ebc8a03833
2015-07-29 14:44:46 -06:00
7cccfb2cf0 Merge "InitGitDir: Clean up created directories" 2015-07-29 18:49:12 +00:00
57f43f4944 Merge "Prevent repo info from crashing when default element doesn't exist." 2015-07-29 02:11:55 +00:00
17af578d72 Prevent repo info from crashing when default element doesn't exist.
repo info will crash when using a manifest with no default element despite
default being an optional element. Output nothing for "Manifest Branch" if no
default element exists (or if no default revision exists).

Change-Id: I7ebffa2408863837ba980f0ab6e593134400aea9
2015-07-27 16:56:31 -07:00
b1a07b8276 InitGitDir: Clean up created directories
If _InitGitDir fails, it leaves any progress it had made on the file
system. This can cause subsequent calls to repo sync to behave
differently. This is especially evident when _CheckDirReference() fails,
since it will not be invoked when sync is retried because both the
source and destination directories already exist.

To address this, have _InitGitDir() clean up any directories it has created
if it catches an exception. Also behave the same way for _InitWorkTree().

Change-Id: Ic16bb3feea649e115b59bd44be294e89e3692aeb
2015-07-27 13:33:43 -06:00
4e16c24981 Revert "Add --prune option to fetch when syncing a mirror repo"
For some users it is not desirable to remove refs that don't exist
on the remote server when syncing a mirror repo.

This reverts commit b4d43b9f66.

Change-Id: Ie849b66682138ef88da6cd1a5fbb27e993197dd7
2015-07-20 22:31:04 +09:00
b3d6e67196 Merge "Fail if gitdir does not point to objdir during sync" 2015-07-15 19:30:41 +00:00
503d66d8af Merge "project.RemoteFetch: Handle depth cases more robustly" 2015-07-15 19:29:14 +00:00
679bac4bf3 project.RemoteFetch: Handle depth cases more robustly
The fetch logic for the case where depth is set and revision is a
SHA1 has several failure modes that are not handled well by the
current logic.

1) 'git fetch <SHA1>' requires git version >= 1.8.3
2) 'git fetch <SHA1>' can be prevented by a configuration option on the server.
3) 'git fetch --depth=<N> <refspec>' can fail to contain a SHA1 specified by
   the manifest.

Each of these cases cause infinite recursion when _RemoteFetch() tries to call
itself with current_branch_only=False because current_branch_only is set to
True when depth != None.

To try to prevent the infinite recursion, we set self.clone_depth to None
before the first retry of _RemoteFetch(). This will allow the Fetch to
eventually succeed in the case where clone-depth is specified in the manifest.
A user specified depth from the init command will still recurse infinitely.

In addition, never try to fetch a SHA1 directly if the git version being used
is not at least 1.8.3.

Change-Id: I802fc17878c0929cfd63fff611633c1d3b54ecd3
2015-07-15 15:53:14 +00:00
97836cf09f Merge "Always output upstream if specified" 2015-07-13 16:36:28 +00:00
80e3a37ab5 Merge changes Iaefcbe14,I697a0f64,I19bfe9fe,I06e942c4
* changes:
  forall: use smart sync override manifest if it exists
  sync: Remove smart sync override manifest when not in smart sync mode
  forall: Don't try to get lrev of projects in mirror workspace
  sync: Improve error message when writing smart sync manifest fails
2015-07-11 14:01:16 +00:00
bb4a1b5274 Merge "Improve error message when syncing a project with invalid groups." 2015-07-10 22:00:47 +00:00
551dfecea9 Always output upstream if specified
Previously, in running the `manifest` command, we wouldn't output the
upstream if the default upstream would include the pinned sha1.
However, now that fetching refs/heads/* doesn't guarantee that we will
have the sha1, we need to always output the specified upstream branch.

Change-Id: Ib8b409a8ecd439397b38ee9649c530407797f841
2015-07-10 14:59:10 -07:00
6944cdb8d1 forall: use smart sync override manifest if it exists
If a workspace is synced with the -s or -t option, the included projects
may be different to those in the original manifest. However, when using
the forall command, the list of the projects from the original manifest
is used.

If the smart sync manifest file exists, use it to override the original
manifest.

Change-Id: Iaefcbe148d2158ac046f158d98bbd8b5a5378ce7
2015-07-06 16:18:06 +09:00
59b417493e sync: Remove smart sync override manifest when not in smart sync mode
When syncing with the -s or -t option, a smart_sync_override.xml file
is created. This file is left in the file system when syncing again
without the -s or -t option.

Remove the smart sync override manifest, if it exists, when not using
the -s or -t option.

Change-Id: I697a0f6405205ba5f84a4d470becf7cd23c07b4b
2015-07-06 16:18:06 +09:00
30d13eea86 forall: Don't try to get lrev of projects in mirror workspace
git rev-parse fails for projects that don't have an explicit revision
specified, and don't have a branch of the same name as the default
revision. This can be the case in a workspace synced with the smart
sync (-s) or smart tag (-t) option.

Change-Id: I19bfe9fe7396170379415d85f10f6440dc6ea08f
2015-07-06 16:18:06 +09:00
727cc3e324 sync: Improve error message when writing smart sync manifest fails
The error message only states that writing the manifest failed.

Include the exception message, so it's easier to track down the reason
that the write failed.

Change-Id: I06e942c48a19521ba45292199519dd0a8bdb1de7
2015-07-06 16:18:06 +09:00
c5ceeb1625 Merge "Fix 'repo cherry-pick' to avoid hanging on commit-msg update." 2015-06-25 14:53:46 +00:00
db75704bfc Fix 'repo cherry-pick' to avoid hanging on commit-msg update.
After performing the actual cherry-pick operation, the code
in cherry_pick.py opens a pipe to 'git commit -F' to rewrite the commit
message, emits the fixed-up commit msg to the pipe, then waits
for 'git commit' to complete. The child 'git' process winds up
hanging while reading from the pipe, however, since the parent
process still has it open. To fix the hang, change the parent process
to close its end of the pipe after it has emitted the message.

Change-Id: I5929371e69a5b076f09009d00d40a2c72ac8ac33
2015-06-22 08:00:20 -04:00
87ea5913f2 Improve error message when syncing a project with invalid groups.
Change-Id: Iaf5c2a0f00667dc09bcf455cfe2f39bfbaa2bfc0
2015-06-19 15:55:15 -07:00
185307d1dd Merge "Teach _LinkFile._Link to handle globs." 2015-06-09 00:14:13 +00:00
c116f94261 forall: setenv, only encode val if encode exists
Change-Id: I655e3043d0118c4e929897d3a51e5e013e5758dc
2015-06-04 00:34:19 +00:00
7993f3cdda init: don't call urllib.parse
it's actually urllib.parse.urlparse

Change-Id: Ie3532e54625e887c8682d92b932ea21a629e8d60
2015-06-04 00:33:33 +00:00
b1d1fd778d git_config: fix _SaveJson typo
Change-Id: I35ca2b3733e6d1508669f9a6690c6645c582912e
2015-06-04 00:22:23 +00:00
be4456cf24 error: fix typos
Change-Id: I09c47024ef54c360ea3c15c5d4f169e13444e412
2015-06-04 00:21:16 +00:00
cf738ed4a1 git_command: only decode when needed
strings no longer need decoding, since unicode is str

Change-Id: I9516d298fee7ddc058452394b7759327fe3aa7a8
2015-06-03 16:50:39 +01:00
6cfc68e1e6 decode the buffer before appending
output from a process is in bytes in python3. we need
to decode it.

in Python3, bytes don't have an encode attribute. use this
to identify it.

Change-Id: I152f2ec34614131027db680ead98b53f9b321ed5
2015-06-03 16:39:32 +01:00
4c426ef1d4 Teach _LinkFile._Link to handle globs.
This allows a project to use globs in the linkfile src attribute. When
a glob is used in the src the dest field must be a directory. Then
_LinkFile._Link(self) calls will create symbolic links in the dest
directory to all of the entries in the src as defined by the glob
specification.

Below all of the entries in master-configs/ will have symbolic links
in <root dir>/configs directory:

  <project name="helloworld.git" path="apps/helloworld">
      <linkfile src="master-configs/*" dest="configs"/>
  </project>

Change-Id: Idfed8fa47c83d2ca6e2b8e867731b8e2f9e2eb47
2015-06-03 08:05:17 -07:00
472ce9f5fa Merge changes I32da12c2,Ie4a65b3e
* changes:
  Skip sleep and retry if git remote update exits with a signal
  Catch exceptions in project list generator
2015-06-02 00:14:43 +00:00
0184dcc510 Make linkfile symlinks relative
The source (target) of the symlink is specified relative to a project
within a tree, and the destination is specified relative to the top
of the tree, so it should always be possible to create a relative symlink
to the target file.  Relative symlinks will allow moving an entire tree
without breaking the symlink, and copying a tree (with -p) without leaving
a symlink to the old tree.

Change-Id: I16492a8b59a137d2abe43ca78e3b212e2c835599
2015-06-01 01:24:38 +00:00
c4b301f988 Skip sleep and retry if git remote update exits with a signal
Pressing ctrl-c during repo sync often hangs for 30 to 45 seconds
due to the time.sleep and retry in _RemoteFetch.  If git exits with
a signal, for example -2 for SIGINT triggered by ctrl-c, skip the
sleep and retry.

Change-Id: I32da12c2dcc96d9cc0b12a066e824b12ebfb52a0
2015-05-13 18:11:34 +00:00
31a7be561e Catch exceptions in project list generator
If the generator that produces per-project worker arguments raises an
exception it triggers python bug http://bugs.python.org/issue8296.
Rewrite the generator expression as a generator function, and catch
Exceptions and KeyboardInterrupts to end the iteration.

Also add a pool worker initializer to disable SIGINT to prevent
KeyboardInterrupts inside multiprocessing.Pool in the worker threads
causing the same problem.

Fixes easy-to-reproduce hangs when hitting ctrl-c during
repo forall -c echo

Change-Id: Ie4a65b3e1e07a64ed6bb6ff20f3912c4326718ca
2015-05-13 11:09:38 -07:00
384b3c5948 Fail if gitdir does not point to objdir during sync
There are a set of cases that can cause the git directory in
.repo/projects to point to a directory in .repo/project-objects that
is not the one specified in the manifest. This results in a tree that
is not sane, and so should cause a failure.

In order to reproduce the failure case:
1) Sync to any manifest
2) Change the 'name' of a project to a different repository. Leave the
   'path' the same.
3) Resync the modified project. The project-objects directory will not
   be created, and the projects directory will remain pointed at the old
   project-objects.

Change-Id: Ie6711b1c773508850c5c9f748a27ff72d65e2bf2
2015-05-12 09:15:53 -06:00
35de228f33 Merge "Don't attempt to create "fully qualified names" for SHA1s" 2015-05-11 09:20:54 +00:00
ace097c36e Merge "Add option on sync to avoid fetching from remotes for existing sha1" 2015-05-01 07:51:52 +00:00
b155354034 Add option on sync to avoid fetching from remotes for existing sha1
In 2fb6466f79 an optimisation was
added to avoid fetching from remotes if the project is fixed to
a revision and the revision is already available locally.

This causes problems for users who expect all objects to be
fetched by default.

Change the logic so that the optimized behaviour is only enabled if
an option is explicitly given to repo sync.

Change-Id: I3b2794ddd8e0071b1787e166463cd8347ca9e24f
2015-04-30 14:29:02 +00:00
382582728e Don't attempt to create "fully qualified names" for SHA1s
Doing so breaks "repo init -b <SHA1>".

Change-Id: Ic071a1b099a9125db22ea446d7e92e7854d69b37
2015-04-30 14:54:47 +02:00
b4d43b9f66 Add --prune option to fetch when syncing a mirror repo
When syncing a mirror repo, add the --prune option to the fetch
command to force removal of stale refs from the mirror.

Change-Id: I4b43b2a5c86b9915627887c16f6569066f3ab978
2015-04-30 10:32:37 +09:00
4ccad7554b Fix substitution err for schemeless manifest urls
Previously, we used a regex that would only remove a phony string from
a url if it existed, but we recently replaced that with a slice.  This
change goes back to the previous behavior.

Change-Id: I8baf527be01c4b49d45b903b31a1cd6315563d5b
2015-04-29 10:45:37 -07:00
403b64edf4 Don't append branch to fetch spec when syncing to a mirror
Appending the branch to the fetch spec causes sync of a mirror to
fail for projects that don't have an explicit revision specified,
and don't have a branch of the same name as the default revision.

For example, a manifest defining a default revision:

 <default revision="master">

having a project without an explicit revision:

 <project name="path/to/project">

and not having a branch named "master", will cause repo sync to
fail for that project with the error:

 Couldn't find remote ref refs/heads/master

Modify the logic to not append the branch onto the fetch spec when
syncing to a mirror.

Change-Id: I5c4457bd125519abf27abe682dea62ad708978c9
2015-04-27 10:56:27 +09:00
a38769cda8 Merge "forall: use a generator to map the Pool" 2015-04-08 17:59:58 +00:00
44859d0267 Merge "status: lose dependence on StringIO" 2015-04-08 17:58:35 +00:00
6ad6dbefe7 forall: use a generator to map the Pool
Before, a list was generated, which is why there was a massive delay.

Using a generator will allow processes to start straight away

Change-Id: Ia325b0b340cc328c08c9bcc92a6709bbdaf6a664
2015-04-08 13:22:34 +01:00
33fe4e99f9 Remove deprecated include-ids setting from pylint config
Change-Id: Ie5ab21e434d24ff862bb5e0c263761370d71f56f
2015-04-07 11:10:17 +09:00
4214585073 Merge "Pylint and PEP8 fixes for color.py" 2015-04-07 02:06:49 +00:00
b51f07cd06 status: lose dependence on StringIO
buflist was being used, which isn't available in Python 3.

`Execute` was using StringIO to capture the output of `PrintWorkTreeStatus`,
only to redirect it straight to stdout.
Instead, just let `PrintWorkTreeStatus` do it's own thing directly to stdout.

for handling `_FindOrphans`, we swap StringIO for a list. Nothing was done
that needed a a file like object.

Change-Id: Ibdaae137904de66a5ffb590d84203ef0fe782d8b
2015-04-04 21:21:49 +01:00
04f2f0e186 Maintain fully qualified tracking branches
When running repo branch, the git merge line (in many circumstances)
is set to the revision of the project specified in the manifest.  If
this is a branch name that is not fully-qualified, we will end up with
something like "merge = master" instead of "merge = refs/heads/master".
This change examines the revision if we are going to use that and
changes branch short names to fully qualified branch names.

Change-Id: Ie1be94fb8d45df8eeac44a47f729a3819a05fa81
2015-04-01 17:43:36 +00:00
cb07ba7e3d Resolve fetch urls more efficiently
Instead of using regex, append the netloc and relative
scheme lists with the custom scheme.
The schemes will only be appended when needed, instead
of passing X amount of regex replaces.

see http://bugs.python.org/issue18828 for more details.

Change-Id: I10d26d5ddc32e7ed04c5a412bdd6e13ec59eb70f
2015-03-31 20:12:44 +00:00
23ff7df6a7 use the max depth instead of unshallow
This allows the use of older versions of git

Change-Id: I88ea685066603af19896a791829355ddbfa91ffe
2015-03-30 21:54:26 +00:00
cc1b1a703d Revert "Change the min git version from 1.7.2 to 1.8.2"
This reverts commit 52b99aa91d.

Change-Id: I01d93704c92f7af1ca2b36dbc9509ee1290e2d3c
2015-03-30 21:53:25 +00:00
bdf7ed2301 Pylint and PEP8 fixes for color.py
Change-Id: I1a676e25957a7b5dd800d2585a2ec7fe75295668
2015-03-28 21:12:27 +00:00
9c76f67f13 Always capture output for GitCommand
Switch the GitCommand program to always capture the output for stdout
and stderr.  And by default print the output while running.

The options capture_stdout and capture_stderr have effectively become
options to supress the printing of stdout and stderr.

Update the 'git fetch' to use '--progress' so that the progress messages
will be displayed.  git checks if the output location isatty() and if it
is not a TTY it will by default not print the progress messages.

Change-Id: Ifdae138e008f80a59195f9f43c911a1a5210ec60
2015-03-26 11:43:55 -07:00
52b99aa91d Change the min git version from 1.7.2 to 1.8.2
This is needed for the --unshallow option of git fetch.

Change-Id: Ifdc5cec6130315c643924328fea425f1b94cb04a
2015-03-18 21:43:39 +00:00
9371979628 Revert "Implementation of manifest defined githooks"
This reverts commit 38e4387f8e.

A "repo init" followed by "repo sync" is meant to be as safe as
"git clone".  In particular it should not run arbitrary code provided
by the manifest owner.

It would still be nice to have support for manifest-defined git hooks
--- they'd just need a prompt like the upload RepoHook has.  Hopefully
a later change can bring them back.

Change-Id: I5ecd90fb5c2ed64f103d856d1ffcba38a47b062d
Signed-off-by: Jonathan Nieder <jrn@google.com>
2015-03-17 11:29:58 -07:00
2086004261 Merge "Don't exit with error on HTTP 401 when downloading clone bundle" 2015-03-11 17:25:45 +00:00
2338788050 Don't exit with error on HTTP 401 when downloading clone bundle
If the server returns HTTP 401 (unauthorized) when attempting to
download clone bundle files, ignore it and continue, rather than
exiting with a fatal error.

Change-Id: I2c7ee03e149c354c7e4ad6ea1ebf266534778fe1
2015-03-11 07:43:40 +00:00
0402cd882a Add space between project path and branch in repo status.
Currently, paths longer than 39 chars have no space after them so it looks
like this:

project path/branch master

Change-Id: I4c1bb13648ac099ade8a8d4ebafa04131571f842
2015-03-11 07:42:17 +00:00
936183a492 git_config: add support for remote '.'
As a fix for issue #149, this patch add support for the remote '.'
(local).

As an alias for the local repository, remote '.' is lacking a fetch =
config in .git/config.

Without such refspec, repo info --overview is not able to process a
local tracking branch.

v2: Check for name == '.' before checking if merge starts with refs/,
    since the case where it's not is invalid.

Signed-off-by: Yann Droneaud <ydroneaud@opteya.com>
Signed-off-by: Filipe Brandenburger <filbranden@google.com>

Change-Id: I8c8fd8602cd68baecb530301ae41d37d751ec85d
2015-03-06 13:23:27 -08:00
85e8267031 Merge "Implementation of manifest defined githooks" 2015-03-05 20:52:30 +00:00
e30f46b957 Print stderr output from git command for RemoteFetch
The stderr output generated by git during a RemoteFetch was not being
printed.  This information is useful so print it.

Change-Id: I6e6ce12c4a57e5ca2359f76ce14f2fcbbc37a5ef
2015-02-25 14:29:28 -08:00
38e4387f8e Implementation of manifest defined githooks
When working within a team or corporation it is often
useful/required to use predefined git templates. This
change teaches repo to use a per-remote git hook template
structure.

The implementation is done as a continuation of the
existing projecthook functionality. The terminology is
therefore defined as projecthooks.

The downloaded projecthooks are stored in the .repo
directory as a metaproject separating them from the users
project forest.

The projecthooks are downloaded and set up when doing a
repo init and updated for each new repo init.

When downloading a mirror the projecthooks gits are
not added to the bare forest since the intention is to
ensure that the latest are used (allows for company policy
enforcement).

The projecthooks are defined in the manifest file in the
remote element as a subnode, the name refers to the
project name on the server referred to in the remote.
<remote name="myremote ..>
   <projecthook name="myprojecthookgit" revision="myrevision"/>
</remote>

The hooks found in the projecthook revision supersede
the stock hooks found in repo. This removes the need for
updating the projecthook gits for repo stock hook changes.

Change-Id: I6796b7b0342c1f83c35f4b3e46782581b069a561
Signed-off-by: Patrik Ryd <patrik.ryd@stericsson.com>
Signed-off-by: Ian Kumlien <ian.kumlien@gmail.com>
2015-02-03 16:01:15 +09:00
21 changed files with 1019 additions and 305 deletions

View File

@ -61,9 +61,6 @@ disable=R0903,R0912,R0913,R0914,R0915,W0141,C0111,C0103,W0603,W0703,R0911,C0301,
# (visual studio) and html
output-format=text
# Include message's id in output
include-ids=yes
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]".

View File

@ -18,41 +18,43 @@ import sys
import pager
COLORS = {None :-1,
'normal' :-1,
'black' : 0,
'red' : 1,
'green' : 2,
'yellow' : 3,
'blue' : 4,
COLORS = {None: -1,
'normal': -1,
'black': 0,
'red': 1,
'green': 2,
'yellow': 3,
'blue': 4,
'magenta': 5,
'cyan' : 6,
'white' : 7}
'cyan': 6,
'white': 7}
ATTRS = {None :-1,
'bold' : 1,
'dim' : 2,
'ul' : 4,
'blink' : 5,
ATTRS = {None: -1,
'bold': 1,
'dim': 2,
'ul': 4,
'blink': 5,
'reverse': 7}
RESET = "\033[m" # pylint: disable=W1401
# backslash is not anomalous
RESET = "\033[m"
def is_color(s):
return s in COLORS
def is_attr(s):
return s in ATTRS
def _Color(fg = None, bg = None, attr = None):
def _Color(fg=None, bg=None, attr=None):
fg = COLORS[fg]
bg = COLORS[bg]
attr = ATTRS[attr]
if attr >= 0 or fg >= 0 or bg >= 0:
need_sep = False
code = "\033[" #pylint: disable=W1401
code = "\033["
if attr >= 0:
code += chr(ord('0') + attr)
@ -71,7 +73,6 @@ def _Color(fg = None, bg = None, attr = None):
if bg >= 0:
if need_sep:
code += ';'
need_sep = True
if bg < 8:
code += '4%c' % (ord('0') + bg)
@ -82,9 +83,9 @@ def _Color(fg = None, bg = None, attr = None):
code = ''
return code
DEFAULT = None
def SetDefaultColoring(state):
"""Set coloring behavior to |state|.
@ -145,6 +146,7 @@ class Coloring(object):
def printer(self, opt=None, fg=None, bg=None, attr=None):
s = self
c = self.colorer(opt, fg, bg, attr)
def f(fmt, *args):
s._out.write(c(fmt, *args))
return f
@ -152,6 +154,7 @@ class Coloring(object):
def nofmt_printer(self, opt=None, fg=None, bg=None, attr=None):
s = self
c = self.nofmt_colorer(opt, fg, bg, attr)
def f(fmt):
s._out.write(c(fmt))
return f
@ -159,11 +162,13 @@ class Coloring(object):
def colorer(self, opt=None, fg=None, bg=None, attr=None):
if self._on:
c = self._parse(opt, fg, bg, attr)
def f(fmt, *args):
output = fmt % args
return ''.join([c, output, RESET])
return f
else:
def f(fmt, *args):
return fmt % args
return f
@ -171,6 +176,7 @@ class Coloring(object):
def nofmt_colorer(self, opt=None, fg=None, bg=None, attr=None):
if self._on:
c = self._parse(opt, fg, bg, attr)
def f(fmt):
return ''.join([c, fmt, RESET])
return f

View File

@ -106,13 +106,13 @@ class Command(object):
def _UpdatePathToProjectMap(self, project):
self._by_path[project.worktree] = project
def _GetProjectByPath(self, path):
def _GetProjectByPath(self, manifest, path):
project = None
if os.path.exists(path):
oldpath = None
while path \
and path != oldpath \
and path != self.manifest.topdir:
and path != manifest.topdir:
try:
project = self._by_path[path]
break
@ -126,15 +126,19 @@ class Command(object):
pass
return project
def GetProjects(self, args, missing_ok=False, submodules_ok=False):
def GetProjects(self, args, manifest=None, groups='', missing_ok=False,
submodules_ok=False):
"""A list of projects that match the arguments.
"""
all_projects_list = self.manifest.projects
if not manifest:
manifest = self.manifest
all_projects_list = manifest.projects
result = []
mp = self.manifest.manifestProject
mp = manifest.manifestProject
groups = mp.config.GetString('manifest.groups')
if not groups:
groups = mp.config.GetString('manifest.groups')
if not groups:
groups = 'default,platform-' + platform.system().lower()
groups = [x for x in re.split(r'[,\s]+', groups) if x]
@ -154,11 +158,11 @@ class Command(object):
self._ResetPathToProjectMap(all_projects_list)
for arg in args:
projects = self.manifest.GetProjectsWithName(arg)
projects = manifest.GetProjectsWithName(arg)
if not projects:
path = os.path.abspath(arg).replace('\\', '/')
project = self._GetProjectByPath(path)
project = self._GetProjectByPath(manifest, path)
# If it's not a derived project, update path->project mapping and
# search again, as arg might actually point to a derived subproject.
@ -169,7 +173,7 @@ class Command(object):
self._UpdatePathToProjectMap(subproject)
search_again = True
if search_again:
project = self._GetProjectByPath(path) or project
project = self._GetProjectByPath(manifest, path) or project
if project:
projects = [project]
@ -226,3 +230,8 @@ class MirrorSafeCommand(object):
"""Command permits itself to run within a mirror,
and does not require a working directory.
"""
class RequiresGitcCommand(object):
"""Command that requires GITC to be available, but does
not require the local client to be a GITC client.
"""

View File

@ -80,7 +80,7 @@ class NoSuchProjectError(Exception):
self.name = name
def __str__(self):
if self.Name is None:
if self.name is None:
return 'in current directory'
return self.name
@ -93,7 +93,7 @@ class InvalidProjectGroupsError(Exception):
self.name = name
def __str__(self):
if self.Name is None:
if self.name is None:
return 'in current directory'
return self.name

View File

@ -14,7 +14,9 @@
# limitations under the License.
from __future__ import print_function
import fcntl
import os
import select
import sys
import subprocess
import tempfile
@ -76,11 +78,24 @@ 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)
if p.Wait() == 0:
return p.stdout.decode('utf-8')
if hasattr(p.stdout, 'decode'):
return p.stdout.decode('utf-8')
else:
return p.stdout
return None
def version_tuple(self):
@ -139,6 +154,9 @@ class GitCommand(object):
if key in env:
del env[key]
# If we are not capturing std* then need to print it.
self.tee = {'stdout': not capture_stdout, 'stderr': not capture_stderr}
if disable_editor:
_setenv(env, 'GIT_EDITOR', ':')
if ssh_proxy:
@ -162,22 +180,21 @@ class GitCommand(object):
if gitdir:
_setenv(env, GIT_DIR, gitdir)
cwd = None
command.extend(cmdv)
command.append(cmdv[0])
# Need to use the --progress flag for fetch/clone so output will be
# displayed as by default git only does progress output if stderr is a TTY.
if sys.stderr.isatty() and cmdv[0] in ('fetch', 'clone'):
if '--progress' not in cmdv and '--quiet' not in cmdv:
command.append('--progress')
command.extend(cmdv[1:])
if provide_stdin:
stdin = subprocess.PIPE
else:
stdin = None
if capture_stdout:
stdout = subprocess.PIPE
else:
stdout = None
if capture_stderr:
stderr = subprocess.PIPE
else:
stderr = None
stdout = subprocess.PIPE
stderr = subprocess.PIPE
if IsTrace():
global LAST_CWD
@ -226,8 +243,36 @@ class GitCommand(object):
def Wait(self):
try:
p = self.process
(self.stdout, self.stderr) = p.communicate()
rc = p.returncode
rc = self._CaptureOutput()
finally:
_remove_ssh_client(p)
return rc
def _CaptureOutput(self):
p = self.process
s_in = [_sfd(p.stdout, sys.stdout, 'stdout'),
_sfd(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, [], [])
for s in in_ready:
buf = s.fd.read(4096)
if not buf:
s_in.remove(s)
continue
if not hasattr(buf, 'encode'):
buf = buf.decode()
if s.std_name == 'stdout':
self.stdout += buf
else:
self.stderr += buf
if self.tee[s.std_name]:
s.dest.write(buf)
s.dest.flush()
return p.wait()

View File

@ -15,6 +15,8 @@
from __future__ import print_function
import contextlib
import errno
import json
import os
import re
@ -280,7 +282,7 @@ class GitConfig(object):
finally:
fd.close()
except (IOError, TypeError):
if os.path.exists(self.json):
if os.path.exists(self._json):
os.remove(self._json)
def _ReadGit(self):
@ -502,6 +504,43 @@ def GetSchemeFromUrl(url):
return m.group(1)
return None
@contextlib.contextmanager
def GetUrlCookieFile(url, quiet):
if url.startswith('persistent-'):
try:
p = subprocess.Popen(
['git-remote-persistent-https', '-print_config', url],
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
try:
cookieprefix = 'http.cookiefile='
proxyprefix = 'http.proxy='
cookiefile = None
proxy = None
for line in p.stdout:
line = line.strip()
if line.startswith(cookieprefix):
cookiefile = line[len(cookieprefix):]
if line.startswith(proxyprefix):
proxy = line[len(proxyprefix):]
# Leave subprocess open, as cookie file may be transient.
if cookiefile or proxy:
yield cookiefile, proxy
return
finally:
p.stdin.close()
if p.wait():
err_msg = p.stderr.read()
if ' -print_config' in err_msg:
pass # Persistent proxy doesn't support -print_config.
elif not quiet:
print(err_msg, file=sys.stderr)
except OSError as e:
if e.errno == errno.ENOENT:
pass # No persistent proxy.
raise
yield GitConfig.ForUser().GetString('http.cookiefile'), None
def _preconnect(url):
m = URI_ALL.match(url)
if m:
@ -619,7 +658,7 @@ class Remote(object):
def ToLocal(self, rev):
"""Convert a remote revision string to something we have locally.
"""
if IsId(rev):
if self.name == '.' or IsId(rev):
return rev
if not rev.startswith('refs/'):

101
gitc_utils.py Normal file
View File

@ -0,0 +1,101 @@
#
# Copyright (C) 2015 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 os
import sys
import time
import git_command
import git_config
import wrapper
GITC_FS_ROOT_DIR = '/gitc/manifest-rw/'
NUM_BATCH_RETRIEVE_REVISIONID = 300
def get_gitc_manifest_dir():
return wrapper.Wrapper().get_gitc_manifest_dir()
def parse_clientdir(gitc_fs_path):
"""Parse a path in the GITC FS and return its client name.
@param gitc_fs_path: A subdirectory path within the GITC_FS_ROOT_DIR.
@returns: The GITC client name
"""
if (gitc_fs_path == GITC_FS_ROOT_DIR or
not gitc_fs_path.startswith(GITC_FS_ROOT_DIR)):
return None
return gitc_fs_path.split(GITC_FS_ROOT_DIR)[1].split('/')[0]
def _set_project_revisions(projects):
"""Sets the revisionExpr for a list of projects.
Because of the limit of open file descriptors allowed, length of projects
should not be overly large. Recommend calling this function multiple times
with each call not exceeding NUM_BATCH_RETRIEVE_REVISIONID projects.
@param projects: List of project objects to set the revionExpr for.
"""
# Retrieve the commit id for each project based off of it's current
# revisionExpr and it is not already a commit id.
project_gitcmds = [(
project, git_command.GitCommand(None,
['ls-remote',
project.remote.url,
project.revisionExpr],
capture_stdout=True, cwd='/tmp'))
for project in projects if not git_config.IsId(project.revisionExpr)]
for proj, gitcmd in project_gitcmds:
if gitcmd.Wait():
print('FATAL: Failed to retrieve revisionExpr for %s' % proj)
sys.exit(1)
proj.revisionExpr = gitcmd.stdout.split('\t')[0]
def generate_gitc_manifest(client_dir, manifest, projects=None):
"""Generate a manifest for shafsd to use for this GITC client.
@param client_dir: GITC client directory to install the .manifest file in.
@param manifest: XmlManifest object representing the repo manifest.
@param projects: List of projects we want to update, this must be a sublist
of manifest.projects to work properly. If not provided,
manifest.projects is used.
"""
print('Generating GITC Manifest by fetching revision SHAs for each '
'project.')
if projects is None:
projects = manifest.projects
index = 0
while index < len(projects):
_set_project_revisions(
projects[index:(index+NUM_BATCH_RETRIEVE_REVISIONID)])
index += NUM_BATCH_RETRIEVE_REVISIONID
# Save the manifest.
save_manifest(manifest, client_dir=client_dir)
def save_manifest(manifest, client_dir=None):
"""Save the manifest file in the client_dir.
@param client_dir: Client directory to save the manifest in.
@param manifest: Manifest object to save.
"""
if not client_dir:
client_dir = manifest.gitc_client_dir
with open(os.path.join(client_dir, '.manifest'), 'w') as f:
manifest.Save(f)
# TODO(sbasi/jorg): Come up with a solution to remove the sleep below.
# Give the GITC filesystem time to register the manifest changes.
time.sleep(3)

22
main.py
View File

@ -42,15 +42,18 @@ from git_command import git, GitCommand
from git_config import init_ssh, close_ssh
from command import InteractiveCommand
from command import MirrorSafeCommand
from command import RequiresGitcCommand
from subcmds.version import Version
from editor import Editor
from error import DownloadError
from error import InvalidProjectGroupsError
from error import ManifestInvalidRevisionError
from error import ManifestParseError
from error import NoManifestException
from error import NoSuchProjectError
from error import RepoChangedException
from manifest_xml import XmlManifest
import gitc_utils
from manifest_xml import GitcManifest, XmlManifest
from pager import RunPager
from wrapper import WrapperPath, Wrapper
@ -128,6 +131,12 @@ class _Repo(object):
cmd.repodir = self.repodir
cmd.manifest = XmlManifest(cmd.repodir)
cmd.gitc_manifest = None
gitc_client_name = gitc_utils.parse_clientdir(os.getcwd())
if gitc_client_name:
cmd.gitc_manifest = GitcManifest(cmd.repodir, gitc_client_name)
cmd.manifest.isGitcClient = True
Editor.globalConfig = cmd.manifest.globalConfig
if not isinstance(cmd, MirrorSafeCommand) and cmd.manifest.IsMirror:
@ -135,6 +144,11 @@ class _Repo(object):
file=sys.stderr)
return 1
if isinstance(cmd, RequiresGitcCommand) and not gitc_utils.get_gitc_manifest_dir():
print("fatal: '%s' requires GITC to be available" % name,
file=sys.stderr)
return 1
try:
copts, cargs = cmd.OptionParser.parse_args(argv)
copts = cmd.ReadEnvironmentOptions(copts)
@ -173,6 +187,12 @@ class _Repo(object):
else:
print('error: no project in current directory', file=sys.stderr)
result = 1
except InvalidProjectGroupsError as e:
if e.name:
print('error: project group must be enabled for project %s' % e.name, file=sys.stderr)
else:
print('error: project group must be enabled for the project in the current directory', file=sys.stderr)
result = 1
finally:
elapsed = time.time() - start
hours, remainder = divmod(elapsed, 3600)

View File

@ -29,6 +29,7 @@ else:
urllib = imp.new_module('urllib')
urllib.parse = urlparse
import gitc_utils
from git_config import GitConfig
from git_refs import R_HEADS, HEAD
from project import RemoteSpec, Project, MetaProject
@ -38,8 +39,9 @@ MANIFEST_FILE_NAME = 'manifest.xml'
LOCAL_MANIFEST_NAME = 'local_manifest.xml'
LOCAL_MANIFESTS_DIR_NAME = 'local_manifests'
urllib.parse.uses_relative.extend(['ssh', 'git'])
urllib.parse.uses_netloc.extend(['ssh', 'git'])
# 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'])
class _Default(object):
"""Project defaults within the manifest."""
@ -85,21 +87,14 @@ class _XmlRemote(object):
# urljoin will gets confused over quite a few things. The ones we care
# about here are:
# * no scheme in the base url, like <hostname:port>
# * persistent-https://
# * rpc://
# We handle this by replacing these with obscure protocols
# and then replacing them with the original when we are done.
# gopher -> <none>
# wais -> persistent-https
# nntp -> rpc
# We handle no scheme by replacing it with an obscure protocol, gopher
# and then replacing it with the original when we are done.
if manifestUrl.find(':') != manifestUrl.find('/') - 1:
manifestUrl = 'gopher://' + manifestUrl
manifestUrl = re.sub(r'^persistent-https://', 'wais://', manifestUrl)
manifestUrl = re.sub(r'^rpc://', 'nntp://', manifestUrl)
url = urllib.parse.urljoin(manifestUrl, url)
url = re.sub(r'^gopher://', '', url)
url = re.sub(r'^wais://', 'persistent-https://', url)
url = re.sub(r'^nntp://', 'rpc://', url)
url = urllib.parse.urljoin('gopher://' + manifestUrl, url)
url = re.sub(r'^gopher://', '', url)
else:
url = urllib.parse.urljoin(manifestUrl, url)
return url
def ToRemoteSpec(self, projectName):
@ -118,6 +113,7 @@ class XmlManifest(object):
self.manifestFile = os.path.join(self.repodir, MANIFEST_FILE_NAME)
self.globalConfig = GitConfig.ForUser()
self.localManifestWarning = False
self.isGitcClient = False
self.repoProject = MetaProject(self, 'repo',
gitdir = os.path.join(repodir, 'repo/.git'),
@ -208,6 +204,9 @@ class XmlManifest(object):
if d.revisionExpr:
have_default = True
e.setAttribute('revision', d.revisionExpr)
if d.destBranchExpr:
have_default = True
e.setAttribute('dest-branch', d.destBranchExpr)
if d.sync_j > 1:
have_default = True
e.setAttribute('sync-j', '%d' % d.sync_j)
@ -259,11 +258,13 @@ class XmlManifest(object):
else:
value = p.work_git.rev_parse(HEAD + '^0')
e.setAttribute('revision', value)
if peg_rev_upstream and value != p.revisionExpr:
# Only save the origin if the origin is not a sha1, and the default
# isn't our value, and the if the default doesn't already have that
# covered.
e.setAttribute('upstream', p.revisionExpr)
if peg_rev_upstream:
if p.upstream:
e.setAttribute('upstream', p.upstream)
elif value != p.revisionExpr:
# Only save the origin if the origin is not a sha1, and the default
# isn't our value
e.setAttribute('upstream', p.revisionExpr)
else:
revision = self.remotes[remoteName].revision or d.revisionExpr
if not revision or revision != p.revisionExpr:
@ -271,6 +272,9 @@ class XmlManifest(object):
if p.upstream and p.upstream != p.revisionExpr:
e.setAttribute('upstream', p.upstream)
if p.dest_branch and p.dest_branch != d.destBranchExpr:
e.setAttribute('dest-branch', p.dest_branch)
for c in p.copyfiles:
ce = doc.createElement('copyfile')
ce.setAttribute('src', c.src)
@ -301,6 +305,11 @@ class XmlManifest(object):
if p.sync_s:
e.setAttribute('sync-s', 'true')
if p.clone_depth:
e.setAttribute('clone-depth', str(p.clone_depth))
self._output_manifest_project_extras(p, e)
if p.subprojects:
subprojects = set(subp.name for subp in p.subprojects)
output_projects(p, e, list(sorted(subprojects)))
@ -318,6 +327,10 @@ class XmlManifest(object):
doc.writexml(fd, '', ' ', '\n', 'UTF-8')
def _output_manifest_project_extras(self, p, e):
"""Manifests can modify e if they support extra project attributes."""
pass
@property
def paths(self):
self._Load()
@ -707,7 +720,7 @@ class XmlManifest(object):
def _UnjoinName(self, parent_name, name):
return os.path.relpath(name, parent_name)
def _ParseProject(self, node, parent = None):
def _ParseProject(self, node, parent = None, **extra_proj_attrs):
"""
reads a <project> element from the manifest file
"""
@ -802,7 +815,8 @@ class XmlManifest(object):
clone_depth = clone_depth,
upstream = upstream,
parent = parent,
dest_branch = dest_branch)
dest_branch = dest_branch,
**extra_proj_attrs)
for n in node.childNodes:
if n.nodeName == 'copyfile':
@ -933,3 +947,26 @@ class XmlManifest(object):
diff['added'].append(toProjects[proj])
return diff
class GitcManifest(XmlManifest):
def __init__(self, repodir, gitc_client_name):
"""Initialize the GitcManifest object."""
super(GitcManifest, self).__init__(repodir)
self.isGitcClient = True
self.gitc_client_name = gitc_client_name
self.gitc_client_dir = os.path.join(gitc_utils.get_gitc_manifest_dir(),
gitc_client_name)
self.manifestFile = os.path.join(self.gitc_client_dir, '.manifest')
def _ParseProject(self, node, parent = None):
"""Override _ParseProject and add support for GITC specific attributes."""
return super(GitcManifest, self)._ParseProject(
node, parent=parent, old_revision=node.getAttribute('old-revision'))
def _output_manifest_project_extras(self, p, e):
"""Output GITC Specific Project attributes"""
if p.old_revision:
e.setAttribute('old-revision', str(p.old_revision))

View File

@ -13,9 +13,9 @@
# limitations under the License.
from __future__ import print_function
import contextlib
import errno
import filecmp
import glob
import os
import random
import re
@ -30,8 +30,8 @@ import traceback
from color import Coloring
from git_command import GitCommand, git_require
from git_config import GitConfig, IsId, GetSchemeFromUrl, ID_RE
from error import GitError, HookError, UploadError
from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, ID_RE
from error import GitError, HookError, UploadError, DownloadError
from error import ManifestInvalidRevisionError
from error import NoManifestException
from trace import IsTrace, Trace
@ -63,6 +63,10 @@ def _error(fmt, *args):
msg = fmt % args
print('error: %s' % msg, file=sys.stderr)
def _warn(fmt, *args):
msg = fmt % args
print('warn: %s' % msg, file=sys.stderr)
def not_rev(r):
return '^' + r
@ -233,28 +237,60 @@ class _CopyFile(object):
_error('Cannot copy file %s to %s', src, dest)
class _LinkFile(object):
def __init__(self, src, dest, abssrc, absdest):
def __init__(self, git_worktree, src, dest, relsrc, absdest):
self.git_worktree = git_worktree
self.src = src
self.dest = dest
self.abs_src = abssrc
self.src_rel_to_dest = relsrc
self.abs_dest = absdest
def _Link(self):
src = self.abs_src
dest = self.abs_dest
def __linkIt(self, relSrc, absDest):
# link file if it does not exist or is out of date
if not os.path.islink(dest) or os.readlink(dest) != src:
if not os.path.islink(absDest) or (os.readlink(absDest) != relSrc):
try:
# remove existing file first, since it might be read-only
if os.path.exists(dest):
os.remove(dest)
if os.path.exists(absDest):
os.remove(absDest)
else:
dest_dir = os.path.dirname(dest)
dest_dir = os.path.dirname(absDest)
if not os.path.isdir(dest_dir):
os.makedirs(dest_dir)
os.symlink(src, dest)
os.symlink(relSrc, absDest)
except IOError:
_error('Cannot link file %s to %s', src, dest)
_error('Cannot link file %s to %s', relSrc, absDest)
def _Link(self):
"""Link the self.rel_src_to_dest and self.abs_dest. Handles wild cards
on the src linking all of the files in the source in to the destination
directory.
"""
# We use the absSrc to handle the situation where the current directory
# is not the root of the repo
absSrc = os.path.join(self.git_worktree, self.src)
if os.path.exists(absSrc):
# Entity exists so just a simple one to one link operation
self.__linkIt(self.src_rel_to_dest, self.abs_dest)
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):
_error('Link error: src with wildcard, %s must be a directory',
absDestDir)
else:
absSrcFiles = glob.glob(absSrc)
for absSrcFile in absSrcFiles:
# Create a releative path from source dir to destination dir
absSrcDir = os.path.dirname(absSrcFile)
relSrcDir = os.path.relpath(absSrcDir, absDestDir)
# Get the source file name
srcFile = os.path.basename(absSrcFile)
# Now form the final full paths to srcFile. They will be
# absolute for the desintaiton and relative for the srouce.
absDest = os.path.join(absDestDir, srcFile)
relSrc = os.path.join(relSrcDir, srcFile)
self.__linkIt(relSrc, absDest)
class RemoteSpec(object):
def __init__(self,
@ -511,6 +547,12 @@ class RepoHook(object):
class Project(object):
# These objects can be shared between several working trees.
shareable_files = ['description', 'info']
shareable_dirs = ['hooks', 'objects', 'rr-cache', 'svn']
# These objects can only be used by a single working tree.
working_tree_files = ['config', 'packed-refs', 'shallow']
working_tree_dirs = ['logs', 'refs']
def __init__(self,
manifest,
name,
@ -529,7 +571,9 @@ class Project(object):
upstream=None,
parent=None,
is_derived=False,
dest_branch=None):
dest_branch=None,
optimized_fetch=False,
old_revision=None):
"""Init a Project object.
Args:
@ -551,6 +595,9 @@ class Project(object):
is_derived: False if the project was explicitly defined in the manifest;
True if the project is a discovered submodule.
dest_branch: The branch to which to push changes for review by default.
optimized_fetch: If True, when a project is set to a sha1 revision, only
fetch from the remote if the sha1 is not present locally.
old_revision: saved git commit id for open GITC projects.
"""
self.manifest = manifest
self.name = name
@ -579,6 +626,7 @@ class Project(object):
self.upstream = upstream
self.parent = parent
self.is_derived = is_derived
self.optimized_fetch = optimized_fetch
self.subprojects = []
self.snapshots = {}
@ -597,6 +645,7 @@ class Project(object):
self.bare_ref = GitRefs(gitdir)
self.bare_objdir = self._GitGetByExec(self, bare=True, gitdir=objdir)
self.dest_branch = dest_branch
self.old_revision = old_revision
# This will be filled in if a project is later identified to be the
# project containing repo hooks.
@ -608,7 +657,7 @@ class Project(object):
@property
def Exists(self):
return os.path.isdir(self.gitdir)
return os.path.isdir(self.gitdir) and os.path.isdir(self.objdir)
@property
def CurrentBranch(self):
@ -809,7 +858,7 @@ class Project(object):
out = StatusColoring(self.config)
if not output_redir == None:
out.redirect(output_redir)
out.project('project %-40s', self.relpath + '/')
out.project('project %-40s', self.relpath + '/ ')
branch = self.CurrentBranch
if branch is None:
@ -1050,24 +1099,24 @@ class Project(object):
tar.extractall(path=path)
return True
except (IOError, tarfile.TarError) as e:
print("error: Cannot extract archive %s: "
"%s" % (tarpath, str(e)), file=sys.stderr)
_error("Cannot extract archive %s: %s", tarpath, str(e))
return False
def Sync_NetworkHalf(self,
quiet=False,
is_new=None,
current_branch_only=False,
force_sync=False,
clone_bundle=True,
no_tags=False,
archive=False):
archive=False,
optimized_fetch=False):
"""Perform only the network IO portion of the sync process.
Local working directory/branch state is not affected.
"""
if archive and not isinstance(self, MetaProject):
if self.remote.url.startswith(('http://', 'https://')):
print("error: %s: Cannot fetch archives from http/https "
"remotes." % self.name, file=sys.stderr)
_error("%s: Cannot fetch archives from http/https remotes.", self.name)
return False
name = self.relpath.replace('\\', '/')
@ -1078,7 +1127,7 @@ class Project(object):
try:
self._FetchArchive(tarpath, cwd=topdir)
except GitError as e:
print('error: %s' % str(e), file=sys.stderr)
_error('%s', e)
return False
# From now on, we only need absolute tarpath
@ -1089,15 +1138,13 @@ class Project(object):
try:
os.remove(tarpath)
except OSError as e:
print("warn: Cannot remove archive %s: "
"%s" % (tarpath, str(e)), file=sys.stderr)
_warn("Cannot remove archive %s: %s", tarpath, str(e))
self._CopyAndLinkFiles()
return True
if is_new is None:
is_new = not self.Exists
if is_new:
self._InitGitDir()
self._InitGitDir(force_sync=force_sync)
else:
self._UpdateHooks()
self._InitRemote()
@ -1129,8 +1176,9 @@ class Project(object):
elif self.manifest.default.sync_c:
current_branch_only = True
has_sha1 = ID_RE.match(self.revisionExpr) and self._CheckForSha1()
if (not has_sha1 #Need to fetch since we don't already have this revision
need_to_fetch = not (optimized_fetch and \
(ID_RE.match(self.revisionExpr) and self._CheckForSha1()))
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)):
@ -1150,6 +1198,8 @@ class Project(object):
self._InitHooks()
def _CopyAndLinkFiles(self):
if self.manifest.isGitcClient:
return
for copyfile in self.copyfiles:
copyfile._Copy()
for linkfile in self.linkfiles:
@ -1189,11 +1239,11 @@ class Project(object):
'revision %s in %s not found' % (self.revisionExpr,
self.name))
def Sync_LocalHalf(self, syncbuf):
def Sync_LocalHalf(self, syncbuf, force_sync=False):
"""Perform only the local IO portion of the sync process.
Network access is not required.
"""
self._InitWorkTree()
self._InitWorkTree(force_sync=force_sync)
all_refs = self.bare_ref.all
self.CleanPublishedCache(all_refs)
revid = self.GetRevisionId(all_refs)
@ -1326,6 +1376,8 @@ class Project(object):
if not ID_RE.match(self.revisionExpr):
# in case of manifest sync the revisionExpr might be a SHA1
branch.merge = self.revisionExpr
if not branch.merge.startswith('refs/'):
branch.merge = R_HEADS + branch.merge
branch.Save()
if cnt_mine > 0 and self.rebase:
@ -1351,9 +1403,10 @@ class Project(object):
def AddLinkFile(self, src, dest, absdest):
# dest should already be an absolute path, but src is project relative
# make src an absolute path
abssrc = os.path.join(self.worktree, src)
self.linkfiles.append(_LinkFile(src, dest, abssrc, absdest))
# make src relative path to dest
absdestdir = os.path.dirname(absdest)
relsrc = os.path.relpath(os.path.join(self.worktree, src), absdestdir)
self.linkfiles.append(_LinkFile(self.worktree, src, dest, relsrc, absdest))
def AddAnnotation(self, name, value, keep):
self.annotations.append(_Annotation(name, value, keep))
@ -1377,9 +1430,11 @@ class Project(object):
## Branch Management ##
def StartBranch(self, name):
def StartBranch(self, name, branch_merge=''):
"""Create a new branch off the manifest's revision.
"""
if not branch_merge:
branch_merge = self.revisionExpr
head = self.work_git.GetHead()
if head == (R_HEADS + name):
return True
@ -1393,7 +1448,9 @@ class Project(object):
branch = self.GetBranch(name)
branch.remote = self.GetRemote(self.remote.name)
branch.merge = self.revisionExpr
branch.merge = branch_merge
if not branch.merge.startswith('refs/') and not ID_RE.match(branch_merge):
branch.merge = R_HEADS + branch_merge
revid = self.GetRevisionId(all_refs)
if head.startswith(R_HEADS):
@ -1401,7 +1458,6 @@ class Project(object):
head = all_refs[head]
except KeyError:
head = None
if revid and head and revid == head:
ref = os.path.join(self.gitdir, R_HEADS + name)
try:
@ -1827,6 +1883,13 @@ class Project(object):
if depth:
cmd.append('--depth=%s' % depth)
else:
# If this repo has shallow objects, then we don't know which refs have
# shallow objects or not. Tell git to unshallow all fetched refs. Don't
# do this with projects that don't have shallow objects, since it is less
# efficient.
if os.path.exists(os.path.join(self.gitdir, 'shallow')):
cmd.append('--depth=2147483647')
if quiet:
cmd.append('--quiet')
@ -1849,33 +1912,24 @@ class Project(object):
spec.append('tag')
spec.append(tag_name)
branch = self.revisionExpr
if is_sha1 and depth:
# Shallow checkout of a specific commit, fetch from that commit and not
# the heads only as the commit might be deeper in the history.
spec.append(branch)
else:
if is_sha1:
branch = self.upstream
if branch is not None and branch.strip():
if not branch.startswith('refs/'):
branch = R_HEADS + branch
spec.append(str((u'+%s:' % branch) + remote.ToLocal(branch)))
if not self.manifest.IsMirror:
branch = self.revisionExpr
if is_sha1 and depth and git_require((1, 8, 3)):
# Shallow checkout of a specific commit, fetch from that commit and not
# the heads only as the commit might be deeper in the history.
spec.append(branch)
else:
if is_sha1:
branch = self.upstream
if branch is not None and branch.strip():
if not branch.startswith('refs/'):
branch = R_HEADS + branch
spec.append(str((u'+%s:' % branch) + remote.ToLocal(branch)))
cmd.extend(spec)
shallowfetch = self.config.GetString('repo.shallowfetch')
if shallowfetch and shallowfetch != ' '.join(spec):
GitCommand(self, ['fetch', '--unshallow', name] + shallowfetch.split(),
bare=True, ssh_proxy=ssh_proxy).Wait()
if depth:
self.config.SetString('repo.shallowfetch', ' '.join(spec))
else:
self.config.SetString('repo.shallowfetch', None)
ok = False
for _i in range(2):
gitcmd = GitCommand(self, cmd, bare=True, capture_stderr=True,
ssh_proxy=ssh_proxy)
gitcmd = GitCommand(self, cmd, bare=True, ssh_proxy=ssh_proxy)
ret = gitcmd.Wait()
if ret == 0:
ok = True
@ -1885,9 +1939,9 @@ class Project(object):
"error:" in gitcmd.stderr and
"git remote prune" in gitcmd.stderr):
prunecmd = GitCommand(self, ['remote', 'prune', name], bare=True,
capture_stderr=True, ssh_proxy=ssh_proxy)
if prunecmd.Wait():
print(prunecmd.stderr, file=sys.stderr)
ssh_proxy=ssh_proxy)
ret = prunecmd.Wait()
if ret:
break
continue
elif current_branch_only and is_sha1 and ret == 128:
@ -1895,6 +1949,9 @@ class Project(object):
# mode, we just tried sync'ing from the upstream field; it doesn't exist, thus
# abort the optimization attempt and do a full sync.
break
elif ret < 0:
# Git died with a signal, exit immediately
break
time.sleep(random.randint(30, 45))
if initial:
@ -1910,8 +1967,15 @@ class Project(object):
# got what we wanted, else trigger a second run of all
# refs.
if not self._CheckForSha1():
return self._RemoteFetch(name=name, current_branch_only=False,
initial=False, quiet=quiet, alt_dir=alt_dir)
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
return self._RemoteFetch(name=name, current_branch_only=current_branch_only,
initial=False, quiet=quiet, alt_dir=alt_dir)
return ok
@ -1972,7 +2036,7 @@ class Project(object):
os.remove(tmpPath)
if 'http_proxy' in os.environ and 'darwin' == sys.platform:
cmd += ['--proxy', os.environ['http_proxy']]
with self._GetBundleCookieFile(srcUrl, quiet) as cookiefile:
with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
if cookiefile:
cmd += ['--cookie', cookiefile, '--cookie-jar', cookiefile]
if srcUrl.startswith('persistent-'):
@ -2020,40 +2084,6 @@ class Project(object):
except OSError:
return False
@contextlib.contextmanager
def _GetBundleCookieFile(self, url, quiet):
if url.startswith('persistent-'):
try:
p = subprocess.Popen(
['git-remote-persistent-https', '-print_config', url],
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
try:
prefix = 'http.cookiefile='
cookiefile = None
for line in p.stdout:
line = line.strip()
if line.startswith(prefix):
cookiefile = line[len(prefix):]
break
# Leave subprocess open, as cookie file may be transient.
if cookiefile:
yield cookiefile
return
finally:
p.stdin.close()
if p.wait():
err_msg = p.stderr.read()
if ' -print_config' in err_msg:
pass # Persistent proxy doesn't support -print_config.
elif not quiet:
print(err_msg, file=sys.stderr)
except OSError as e:
if e.errno == errno.ENOENT:
pass # No persistent proxy.
raise
yield GitConfig.ForUser().GetString('http.cookiefile')
def _Checkout(self, rev, quiet=False):
cmd = ['checkout']
if quiet:
@ -2104,52 +2134,77 @@ class Project(object):
if GitCommand(self, cmd).Wait() != 0:
raise GitError('%s merge %s ' % (self.name, head))
def _InitGitDir(self, mirror_git=None):
if not os.path.exists(self.gitdir):
def _InitGitDir(self, mirror_git=None, force_sync=False):
init_git_dir = not os.path.exists(self.gitdir)
init_obj_dir = not os.path.exists(self.objdir)
try:
# Initialize the bare repository, which contains all of the objects.
if not os.path.exists(self.objdir):
if init_obj_dir:
os.makedirs(self.objdir)
self.bare_objdir.init()
# If we have a separate directory to hold refs, initialize it as well.
if self.objdir != self.gitdir:
os.makedirs(self.gitdir)
self._ReferenceGitDir(self.objdir, self.gitdir, share_refs=False,
copy_all=True)
if init_git_dir:
os.makedirs(self.gitdir)
mp = self.manifest.manifestProject
ref_dir = mp.config.GetString('repo.reference') or ''
if init_obj_dir or init_git_dir:
self._ReferenceGitDir(self.objdir, self.gitdir, share_refs=False,
copy_all=True)
try:
self._CheckDirReference(self.objdir, self.gitdir, share_refs=False)
except GitError as e:
if force_sync:
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(self.worktree)):
shutil.rmtree(os.path.realpath(self.worktree))
return self._InitGitDir(mirror_git=mirror_git, force_sync=False)
except:
raise e
raise e
if ref_dir or mirror_git:
if not mirror_git:
mirror_git = os.path.join(ref_dir, self.name + '.git')
repo_git = os.path.join(ref_dir, '.repo', 'projects',
self.relpath + '.git')
if init_git_dir:
mp = self.manifest.manifestProject
ref_dir = mp.config.GetString('repo.reference') or ''
if os.path.exists(mirror_git):
ref_dir = mirror_git
if ref_dir or mirror_git:
if not mirror_git:
mirror_git = os.path.join(ref_dir, self.name + '.git')
repo_git = os.path.join(ref_dir, '.repo', 'projects',
self.relpath + '.git')
elif os.path.exists(repo_git):
ref_dir = repo_git
if os.path.exists(mirror_git):
ref_dir = mirror_git
elif os.path.exists(repo_git):
ref_dir = repo_git
else:
ref_dir = None
if ref_dir:
_lwrite(os.path.join(self.gitdir, 'objects/info/alternates'),
os.path.join(ref_dir, 'objects') + '\n')
self._UpdateHooks()
m = self.manifest.manifestProject.config
for key in ['user.name', 'user.email']:
if m.Has(key, include_defaults=False):
self.config.SetString(key, m.GetString(key))
if self.manifest.IsMirror:
self.config.SetString('core.bare', 'true')
else:
ref_dir = None
if ref_dir:
_lwrite(os.path.join(self.gitdir, 'objects/info/alternates'),
os.path.join(ref_dir, 'objects') + '\n')
self._UpdateHooks()
m = self.manifest.manifestProject.config
for key in ['user.name', 'user.email']:
if m.Has(key, include_defaults=False):
self.config.SetString(key, m.GetString(key))
if self.manifest.IsMirror:
self.config.SetString('core.bare', 'true')
else:
self.config.SetString('core.bare', None)
self.config.SetString('core.bare', None)
except Exception:
if init_obj_dir and os.path.exists(self.objdir):
shutil.rmtree(self.objdir)
if init_git_dir and os.path.exists(self.gitdir):
shutil.rmtree(self.gitdir)
raise
def _UpdateHooks(self):
if os.path.exists(self.gitdir):
@ -2178,7 +2233,7 @@ class Project(object):
if filecmp.cmp(stock_hook, dst, shallow=False):
os.remove(dst)
else:
_error("%s: Not replacing %s hook", self.relpath, name)
_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)
@ -2223,6 +2278,25 @@ class Project(object):
msg = 'manifest set to %s' % self.revisionExpr
self.bare_git.symbolic_ref('-m', msg, ref, dst)
def _CheckDirReference(self, srcdir, destdir, share_refs):
symlink_files = self.shareable_files
symlink_dirs = self.shareable_dirs
if share_refs:
symlink_files += self.working_tree_files
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))
if os.path.lexists(dst):
src = os.path.realpath(os.path.join(srcdir, name))
# Fail if the links are pointing to the wrong place
if src != dst:
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,'
' use `repo sync --force-sync {0}` to '
'proceed.'.format(self.relpath))
def _ReferenceGitDir(self, gitdir, dotgit, share_refs, copy_all):
"""Update |dotgit| to reference |gitdir|, using symlinks where possible.
@ -2234,26 +2308,25 @@ class Project(object):
copy_all: If true, copy all remaining files from |gitdir| -> |dotgit|.
This saves you the effort of initializing |dotgit| yourself.
"""
# These objects can be shared between several working trees.
symlink_files = ['description', 'info']
symlink_dirs = ['hooks', 'objects', 'rr-cache', 'svn']
symlink_files = self.shareable_files
symlink_dirs = self.shareable_dirs
if share_refs:
# These objects can only be used by a single working tree.
symlink_files += ['config', 'packed-refs', 'shallow']
symlink_dirs += ['logs', 'refs']
symlink_files += self.working_tree_files
symlink_dirs += self.working_tree_dirs
to_symlink = symlink_files + symlink_dirs
to_copy = []
if copy_all:
to_copy = os.listdir(gitdir)
dotgit = os.path.realpath(dotgit)
for name in set(to_copy).union(to_symlink):
try:
src = os.path.realpath(os.path.join(gitdir, name))
dst = os.path.realpath(os.path.join(dotgit, name))
dst = os.path.join(dotgit, name)
if os.path.lexists(dst) and not os.path.islink(dst):
raise GitError('cannot overwrite a local work tree')
if os.path.lexists(dst):
continue
# If the source dir doesn't exist, create an empty dir.
if name in symlink_dirs and not os.path.lexists(src):
@ -2276,26 +2349,44 @@ class Project(object):
shutil.copy(src, dst)
except OSError as e:
if e.errno == errno.EPERM:
raise GitError('filesystem must support symlinks')
raise DownloadError('filesystem must support symlinks')
else:
raise
def _InitWorkTree(self):
def _InitWorkTree(self, force_sync=False):
dotgit = os.path.join(self.worktree, '.git')
if not os.path.exists(dotgit):
os.makedirs(dotgit)
self._ReferenceGitDir(self.gitdir, dotgit, share_refs=True,
copy_all=False)
init_dotgit = not os.path.exists(dotgit)
try:
if init_dotgit:
os.makedirs(dotgit)
self._ReferenceGitDir(self.gitdir, dotgit, share_refs=True,
copy_all=False)
_lwrite(os.path.join(dotgit, HEAD), '%s\n' % self.GetRevisionId())
try:
self._CheckDirReference(self.gitdir, dotgit, share_refs=True)
except GitError as e:
if force_sync:
try:
shutil.rmtree(dotgit)
return self._InitWorkTree(force_sync=False)
except:
raise e
raise e
cmd = ['read-tree', '--reset', '-u']
cmd.append('-v')
cmd.append(HEAD)
if GitCommand(self, cmd).Wait() != 0:
raise GitError("cannot initialize work tree")
if init_dotgit:
_lwrite(os.path.join(dotgit, HEAD), '%s\n' % self.GetRevisionId())
self._CopyAndLinkFiles()
cmd = ['read-tree', '--reset', '-u']
cmd.append('-v')
cmd.append(HEAD)
if GitCommand(self, cmd).Wait() != 0:
raise GitError("cannot initialize work tree")
self._CopyAndLinkFiles()
except Exception:
if init_dotgit:
shutil.rmtree(dotgit)
raise
def _gitdir_path(self, path):
return os.path.realpath(os.path.join(self.gitdir, path))

70
repo
View File

@ -108,6 +108,7 @@ 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
GITC_CONFIG_FILE = '/gitc/.config'
import errno
@ -212,14 +213,41 @@ group.add_option('--config-name',
dest='config_name', action="store_true", default=False,
help='Always prompt for name/e-mail')
def _GitcInitOptions(init_optparse):
init_optparse.set_usage("repo gitc-init -u url -c client [options]")
g = init_optparse.add_option_group('GITC options')
g.add_option('-f', '--manifest-file',
dest='manifest_file',
help='Optional manifest file to use for this GITC client.')
g.add_option('-c', '--gitc-client',
dest='gitc_client',
help='The name for the new gitc_client instance.')
_gitc_manifest_dir = None
def get_gitc_manifest_dir():
global _gitc_manifest_dir
if _gitc_manifest_dir is None:
_gitc_manifest_dir = ''
try:
with open(GITC_CONFIG_FILE, 'r') as gitc_config:
for line in gitc_config:
match = re.match('gitc_dir=(?P<gitc_manifest_dir>.*)', line)
if match:
_gitc_manifest_dir = match.group('gitc_manifest_dir')
except IOError:
pass
return _gitc_manifest_dir
class CloneFailure(Exception):
"""Indicate the remote clone of repo itself failed.
"""
def _Init(args):
def _Init(args, gitc_init=False):
"""Installs repo by cloning it over the network.
"""
if gitc_init:
_GitcInitOptions(init_optparse)
opt, args = init_optparse.parse_args(args)
if args:
init_optparse.print_usage()
@ -242,6 +270,23 @@ def _Init(args):
raise CloneFailure()
try:
if gitc_init:
gitc_manifest_dir = get_gitc_manifest_dir()
if not gitc_manifest_dir:
_print('fatal: GITC filesystem is not available. Exiting...',
file=sys.stderr)
sys.exit(1)
if not opt.gitc_client:
_print('fatal: GITC client (-c) is required.', file=sys.stderr)
sys.exit(1)
client_dir = os.path.join(gitc_manifest_dir, opt.gitc_client)
if not os.path.exists(client_dir):
os.makedirs(client_dir)
os.chdir(client_dir)
if os.path.exists(repodir):
# This GITC Client has already initialized repo so continue.
return
os.mkdir(repodir)
except OSError as e:
if e.errno != errno.EEXIST:
@ -462,7 +507,7 @@ def _DownloadBundle(url, local, quiet):
try:
r = urllib.request.urlopen(url)
except urllib.error.HTTPError as e:
if e.code in [403, 404]:
if e.code in [401, 403, 404]:
return False
_print('fatal: Cannot get %s' % url, file=sys.stderr)
_print('fatal: HTTP error %s' % e.code, file=sys.stderr)
@ -640,6 +685,10 @@ def _ParseArguments(args):
def _Usage():
gitc_usage = ""
if get_gitc_manifest_dir():
gitc_usage = " gitc-init Initialize a GITC Client.\n"
_print(
"""usage: repo COMMAND [ARGS]
@ -648,7 +697,8 @@ repo is not yet installed. Use "repo init" to install it here.
The most commonly used repo commands are:
init Install repo in the current working directory
help Display detailed help on a command
""" + gitc_usage +
""" help Display detailed help on a command
For access to the full online help, install repo ("repo init").
""", file=sys.stderr)
@ -660,6 +710,10 @@ def _Help(args):
if args[0] == 'init':
init_optparse.print_help()
sys.exit(0)
elif args[0] == 'gitc-init':
_GitcInitOptions(init_optparse)
init_optparse.print_help()
sys.exit(0)
else:
_print("error: '%s' is not a bootstrap command.\n"
' For access to online help, install repo ("repo init").'
@ -725,6 +779,12 @@ def main(orig_args):
wrapper_path = os.path.abspath(__file__)
my_main, my_git = _RunSelf(wrapper_path)
cwd = os.getcwd()
if get_gitc_manifest_dir() and cwd.startswith(get_gitc_manifest_dir()):
_print('error: repo cannot be used in the GITC local manifest directory.'
'\nIf you want to work on this GITC client please rerun this '
'command from the corresponding client under /gitc/', file=sys.stderr)
sys.exit(1)
if not repo_main:
if opt.help:
_Usage()
@ -732,11 +792,11 @@ def main(orig_args):
_Help(args)
if not cmd:
_NotInstalled()
if cmd == 'init':
if cmd == 'init' or cmd == 'gitc-init':
if my_git:
_SetDefaultsTo(my_git)
try:
_Init(args)
_Init(args, gitc_init=(cmd == 'gitc-init'))
except CloneFailure:
shutil.rmtree(os.path.join(repodir, S_repo), ignore_errors=True)
sys.exit(1)

View File

@ -76,6 +76,7 @@ change id will be added.
capture_stdout = True,
capture_stderr = True)
p.stdin.write(new_msg)
p.stdin.close()
if p.Wait() != 0:
print("error: Failed to update commit message", file=sys.stderr)
sys.exit(1)

View File

@ -20,6 +20,7 @@ import multiprocessing
import re
import os
import select
import signal
import sys
import subprocess
@ -119,6 +120,9 @@ without iterating through the remaining projects.
p.add_option('-r', '--regex',
dest='regex', action='store_true',
help="Execute the command only on projects matching regex or wildcard expression")
p.add_option('-g', '--groups',
dest='groups',
help="Execute the command only on projects matching the specified groups")
p.add_option('-c', '--command',
help='Command (and arguments) to execute',
dest='command',
@ -150,11 +154,15 @@ without iterating through the remaining projects.
attributes that we need.
"""
if not self.manifest.IsMirror:
lrev = project.GetRevisionId()
else:
lrev = None
return {
'name': project.name,
'relpath': project.relpath,
'remote_name': project.remote.name,
'lrev': project.GetRevisionId(),
'lrev': lrev,
'rrev': project.revisionExpr,
'annotations': dict((a.name, a.value) for a in project.annotations),
'gitdir': project.gitdir,
@ -200,21 +208,26 @@ without iterating through the remaining projects.
mirror = self.manifest.IsMirror
rc = 0
smart_sync_manifest_name = "smart_sync_override.xml"
smart_sync_manifest_path = os.path.join(
self.manifest.manifestProject.worktree, smart_sync_manifest_name)
if os.path.isfile(smart_sync_manifest_path):
self.manifest.Override(smart_sync_manifest_path)
if not opt.regex:
projects = self.GetProjects(args)
projects = self.GetProjects(args, groups=opt.groups)
else:
projects = self.FindProjects(args)
os.environ['REPO_COUNT'] = str(len(projects))
pool = multiprocessing.Pool(opt.jobs)
pool = multiprocessing.Pool(opt.jobs, InitWorker)
try:
config = self.manifest.manifestProject.config
results_it = pool.imap(
DoWorkWrapper,
[[mirror, opt, cmd, shell, cnt, config, self._SerializeProject(p)]
for cnt, p in enumerate(projects)]
)
self.ProjectArgs(projects, mirror, opt, cmd, shell, config))
pool.close()
for r in results_it:
rc = rc or r
@ -236,12 +249,28 @@ without iterating through the remaining projects.
if rc != 0:
sys.exit(rc)
def ProjectArgs(self, projects, mirror, opt, cmd, shell, config):
for cnt, p in enumerate(projects):
try:
project = self._SerializeProject(p)
except Exception as e:
print('Project list error: %r' % e,
file=sys.stderr)
return
except KeyboardInterrupt:
print('Project list interrupted',
file=sys.stderr)
return
yield [mirror, opt, cmd, shell, cnt, config, project]
class WorkerKeyboardInterrupt(Exception):
""" Keyboard interrupt exception for worker processes. """
pass
def InitWorker():
signal.signal(signal.SIGINT, signal.SIG_IGN)
def DoWorkWrapper(args):
""" A wrapper around the DoWork() method.
@ -263,7 +292,9 @@ def DoWork(project, mirror, opt, cmd, shell, cnt, config):
def setenv(name, val):
if val is None:
val = ''
env[name] = val.encode()
if hasattr(val, 'encode'):
val = val.encode()
env[name] = val
setenv('REPO_PROJECT', project['name'])
setenv('REPO_PATH', project['relpath'])

82
subcmds/gitc_init.py Normal file
View File

@ -0,0 +1,82 @@
#
# Copyright (C) 2015 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 os
import sys
import gitc_utils
from command import RequiresGitcCommand
from subcmds import init
class GitcInit(init.Init, RequiresGitcCommand):
common = True
helpSummary = "Initialize a GITC Client."
helpUsage = """
%prog [options] [client name]
"""
helpDescription = """
The '%prog' command is ran to initialize a new GITC client for use
with the GITC file system.
This command will setup the client directory, initialize repo, just
like repo init does, and then downloads the manifest collection
and installs in in the .repo/directory of the GITC client.
Once this is done, a GITC manifest is generated by pulling the HEAD
SHA for each project and generates the properly formatted XML file
and installs it as .manifest in the GITC client directory.
The -c argument is required to specify the GITC client name.
The optional -f argument can be used to specify the manifest file to
use for this GITC client.
"""
def _Options(self, p):
super(GitcInit, self)._Options(p)
g = p.add_option_group('GITC options')
g.add_option('-f', '--manifest-file',
dest='manifest_file',
help='Optional manifest file to use for this GITC client.')
g.add_option('-c', '--gitc-client',
dest='gitc_client',
help='The name for the new gitc_client instance.')
def Execute(self, opt, args):
if not opt.gitc_client:
print('fatal: gitc client (-c) is required', file=sys.stderr)
sys.exit(1)
self.client_dir = os.path.join(gitc_utils.get_gitc_manifest_dir(),
opt.gitc_client)
if not os.path.exists(gitc_utils.get_gitc_manifest_dir()):
os.makedirs(gitc_utils.get_gitc_manifest_dir())
if not os.path.exists(self.client_dir):
os.mkdir(self.client_dir)
super(GitcInit, self).Execute(opt, args)
for name, remote in self.manifest.remotes.iteritems():
remote.fetchUrl = remote.resolvedFetchUrl
if opt.manifest_file:
if not os.path.exists(opt.manifest_file):
print('fatal: Specified manifest file %s does not exist.' %
opt.manifest_file)
sys.exit(1)
self.manifest.Override(opt.manifest_file)
gitc_utils.generate_gitc_manifest(self.client_dir, self.manifest)
print('Please run `cd %s` to view your GITC client.' %
os.path.join(gitc_utils.GITC_FS_ROOT_DIR, opt.gitc_client))

View File

@ -19,7 +19,8 @@ import sys
from formatter import AbstractFormatter, DumbWriter
from color import Coloring
from command import PagedCommand, MirrorSafeCommand
from command import PagedCommand, MirrorSafeCommand, RequiresGitcCommand
import gitc_utils
class Help(PagedCommand, MirrorSafeCommand):
common = False
@ -54,9 +55,17 @@ Displays detailed usage information about a command.
def _PrintCommonCommands(self):
print('usage: repo COMMAND [ARGS]')
print('The most commonly used repo commands are:')
def gitc_supported(cmd):
if not isinstance(cmd, RequiresGitcCommand):
return True
if gitc_utils.get_gitc_manifest_dir():
return True
return False
commandNames = list(sorted([name
for name, command in self.commands.items()
if command.common]))
if command.common and gitc_supported(command)]))
maxlen = 0
for name in commandNames:

View File

@ -59,7 +59,8 @@ class Info(PagedCommand):
or 'all,-notdefault')
self.heading("Manifest branch: ")
self.headtext(self.manifest.default.revisionExpr)
if self.manifest.default.revisionExpr:
self.headtext(self.manifest.default.revisionExpr)
self.out.nl()
self.heading("Manifest merge branch: ")
self.headtext(mergeBranch)

View File

@ -27,7 +27,7 @@ else:
import imp
import urlparse
urllib = imp.new_module('urllib')
urllib.parse = urlparse.urlparse
urllib.parse = urlparse
from color import Coloring
from command import InteractiveCommand, MirrorSafeCommand
@ -153,7 +153,7 @@ to update the working directory files.
# server where this git is located, so let's save that here.
mirrored_manifest_git = None
if opt.reference:
manifest_git_path = urllib.parse(opt.manifest_url).path[1:]
manifest_git_path = urllib.parse.urlparse(opt.manifest_url).path[1:]
mirrored_manifest_git = os.path.join(opt.reference, manifest_git_path)
if not mirrored_manifest_git.endswith(".git"):
mirrored_manifest_git += ".git"

View File

@ -35,6 +35,9 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
p.add_option('-r', '--regex',
dest='regex', action='store_true',
help="Filter the project list based on regex or wildcard matching of strings")
p.add_option('-g', '--groups',
dest='groups',
help="Filter the project list based on the groups the project is in")
p.add_option('-f', '--fullpath',
dest='fullpath', action='store_true',
help="Display the full work tree path instead of the relative path")
@ -62,7 +65,7 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
sys.exit(1)
if not opt.regex:
projects = self.GetProjects(args)
projects = self.GetProjects(args, groups=opt.groups)
else:
projects = self.FindProjects(args)

View File

@ -14,11 +14,15 @@
# limitations under the License.
from __future__ import print_function
import os
import sys
from command import Command
from git_config import IsId
from git_command import git
import gitc_utils
from progress import Progress
from project import SyncBuffer
class Start(Command):
common = True
@ -53,20 +57,50 @@ revision specified in the manifest.
print("error: at least one project must be specified", file=sys.stderr)
sys.exit(1)
all_projects = self.GetProjects(projects)
proj_name_to_gitc_proj_dict = {}
if self.gitc_manifest:
all_projects = self.GetProjects(projects, manifest=self.gitc_manifest,
missing_ok=True)
for project in all_projects:
if project.old_revision:
project.already_synced = True
else:
project.already_synced = False
project.old_revision = project.revisionExpr
proj_name_to_gitc_proj_dict[project.name] = project
project.revisionExpr = None
# Save the GITC manifest.
gitc_utils.save_manifest(self.gitc_manifest)
all_projects = self.GetProjects(projects,
missing_ok=bool(self.gitc_manifest))
pm = Progress('Starting %s' % nb, len(all_projects))
for project in all_projects:
pm.update()
if self.gitc_manifest:
gitc_project = proj_name_to_gitc_proj_dict[project.name]
# Sync projects that have already been opened.
if not gitc_project.already_synced:
proj_localdir = os.path.join(self.gitc_manifest.gitc_client_dir,
project.relpath)
project.worktree = proj_localdir
if not os.path.exists(proj_localdir):
os.makedirs(proj_localdir)
project.Sync_NetworkHalf()
sync_buf = SyncBuffer(self.manifest.manifestProject.config)
project.Sync_LocalHalf(sync_buf)
project.revisionExpr = 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.
branch_merge = ''
if IsId(project.revisionExpr):
if project.dest_branch:
project.revisionExpr = project.dest_branch
branch_merge = project.dest_branch
else:
project.revisionExpr = self.manifest.default.revisionExpr
if not project.StartBranch(nb):
branch_merge = self.manifest.default.revisionExpr
if not project.StartBranch(nb, branch_merge=branch_merge):
err.append(project)
pm.end()

View File

@ -22,15 +22,8 @@ except ImportError:
import glob
from pyversion import is_python3
if is_python3():
import io
else:
import StringIO as io
import itertools
import os
import sys
from color import Coloring
@ -97,7 +90,7 @@ the following meanings:
dest='orphans', action='store_true',
help="include objects in working directory outside of repo projects")
def _StatusHelper(self, project, clean_counter, sem, output):
def _StatusHelper(self, project, clean_counter, sem):
"""Obtains the status for a specific project.
Obtains the status for a project, redirecting the output to
@ -111,7 +104,7 @@ the following meanings:
output: Where to output the status.
"""
try:
state = project.PrintWorkTreeStatus(output)
state = project.PrintWorkTreeStatus()
if state == 'CLEAN':
next(clean_counter)
finally:
@ -122,16 +115,16 @@ the following meanings:
status_header = ' --\t'
for item in dirs:
if not os.path.isdir(item):
outstring.write(''.join([status_header, item]))
outstring.append(''.join([status_header, item]))
continue
if item in proj_dirs:
continue
if item in proj_dirs_parents:
self._FindOrphans(glob.glob('%s/.*' % item) + \
glob.glob('%s/*' % item), \
self._FindOrphans(glob.glob('%s/.*' % item) +
glob.glob('%s/*' % item),
proj_dirs, proj_dirs_parents, outstring)
continue
outstring.write(''.join([status_header, item, '/']))
outstring.append(''.join([status_header, item, '/']))
def Execute(self, opt, args):
all_projects = self.GetProjects(args)
@ -144,26 +137,17 @@ the following meanings:
next(counter)
else:
sem = _threading.Semaphore(opt.jobs)
threads_and_output = []
threads = []
for project in all_projects:
sem.acquire()
class BufList(io.StringIO):
def dump(self, ostream):
for entry in self.buflist:
ostream.write(entry)
output = BufList()
t = _threading.Thread(target=self._StatusHelper,
args=(project, counter, sem, output))
threads_and_output.append((t, output))
args=(project, counter, sem))
threads.append(t)
t.daemon = True
t.start()
for (t, output) in threads_and_output:
for t in threads:
t.join()
output.dump(sys.stdout)
output.close()
if len(all_projects) == next(counter):
print('nothing to commit (working directory clean)')
@ -188,23 +172,21 @@ the following meanings:
try:
os.chdir(self.manifest.topdir)
outstring = io.StringIO()
self._FindOrphans(glob.glob('.*') + \
glob.glob('*'), \
outstring = []
self._FindOrphans(glob.glob('.*') +
glob.glob('*'),
proj_dirs, proj_dirs_parents, outstring)
if outstring.buflist:
if outstring:
output = StatusColoring(self.manifest.globalConfig)
output.project('Objects not within a project (orphans)')
output.nl()
for entry in outstring.buflist:
for entry in outstring:
output.untracked(entry)
output.nl()
else:
print('No orphan files or directories')
outstring.close()
finally:
# Restore CWD.
os.chdir(orig_path)

View File

@ -23,18 +23,26 @@ import shutil
import socket
import subprocess
import sys
import tempfile
import time
from pyversion import is_python3
if is_python3():
import http.cookiejar as cookielib
import urllib.error
import urllib.parse
import urllib.request
import xmlrpc.client
else:
import cookielib
import imp
import urllib2
import urlparse
import xmlrpclib
urllib = imp.new_module('urllib')
urllib.error = urllib2
urllib.parse = urlparse
urllib.request = urllib2
xmlrpc = imp.new_module('xmlrpc')
xmlrpc.client = xmlrpclib
@ -57,7 +65,9 @@ except ImportError:
multiprocessing = None
from git_command import GIT, git_require
from git_config import GetUrlCookieFile
from git_refs import R_HEADS, HEAD
import gitc_utils
from project import Project
from project import RemoteSpec
from command import Command, MirrorSafeCommand
@ -119,6 +129,11 @@ credentials.
The -f/--force-broken option can be used to proceed with syncing
other projects if a project sync fails.
The --force-sync option can be used to overwrite existing git
directories if they have previously been linked to a different
object direcotry. WARNING: This may cause data to be lost since
refs may be removed when overwriting.
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
@ -131,6 +146,10 @@ of a project from server.
The -c/--current-branch option can be used to only fetch objects that
are on the branch specified by a project's revision.
The --optimized-fetch option can be used to only fetch projects that
are fixed to a sha1 revision if the sha1 revision does not already
exist locally.
SSH Connections
---------------
@ -170,6 +189,11 @@ later is required to fix a server side protocol bug.
p.add_option('-f', '--force-broken',
dest='force_broken', action='store_true',
help="continue sync even if a project fails to sync")
p.add_option('--force-sync',
dest='force_sync', action='store_true',
help="overwrite an existing git directory if it needs to "
"point to a different object directory. WARNING: this "
"may cause loss of data")
p.add_option('-l', '--local-only',
dest='local_only', action='store_true',
help="only update working tree, don't fetch")
@ -206,6 +230,9 @@ later is required to fix a server side protocol bug.
p.add_option('--no-tags',
dest='no_tags', action='store_true',
help="don't fetch tags")
p.add_option('--optimized-fetch',
dest='optimized_fetch', action='store_true',
help='only fetch projects fixed to sha1 if revision does not exist locally')
if show_smart:
p.add_option('-s', '--smart-sync',
dest='smart_sync', action='store_true',
@ -274,8 +301,10 @@ later is required to fix a server side protocol bug.
success = project.Sync_NetworkHalf(
quiet=opt.quiet,
current_branch_only=opt.current_branch_only,
force_sync=opt.force_sync,
clone_bundle=not opt.no_clone_bundle,
no_tags=opt.no_tags, archive=self.manifest.IsArchive)
no_tags=opt.no_tags, archive=self.manifest.IsArchive,
optimized_fetch=opt.optimized_fetch)
self._fetch_times.Set(project, time.time() - start)
# Lock around all the rest of the code, since printing, updating a set
@ -295,7 +324,9 @@ later is required to fix a server side protocol bug.
pm.update()
except _FetchError:
err_event.set()
except:
except Exception as e:
print('error: Cannot fetch %s (%s: %s)' \
% (project.name, type(e).__name__, str(e)), file=sys.stderr)
err_event.set()
raise
finally:
@ -509,6 +540,9 @@ later is required to fix a server side protocol bug.
self.manifest.Override(opt.manifest_name)
manifest_name = opt.manifest_name
smart_sync_manifest_name = "smart_sync_override.xml"
smart_sync_manifest_path = os.path.join(
self.manifest.manifestProject.worktree, smart_sync_manifest_name)
if opt.smart_sync or opt.smart_tag:
if not self.manifest.manifest_server:
@ -530,19 +564,18 @@ later is required to fix a server side protocol bug.
try:
info = netrc.netrc()
except IOError:
print('.netrc file does not exist or could not be opened',
file=sys.stderr)
# .netrc file does not exist or could not be opened
pass
else:
try:
parse_result = urllib.parse.urlparse(manifest_server)
if parse_result.hostname:
username, _account, password = \
info.authenticators(parse_result.hostname)
except TypeError:
# TypeError is raised when the given hostname is not present
# in the .netrc file.
print('No credentials found for %s in .netrc'
% parse_result.hostname, file=sys.stderr)
auth = info.authenticators(parse_result.hostname)
if auth:
username, _account, password = auth
else:
print('No credentials found for %s in .netrc'
% parse_result.hostname, file=sys.stderr)
except netrc.NetrcParseError as e:
print('Error parsing .netrc file: %s' % e, file=sys.stderr)
@ -551,8 +584,12 @@ later is required to fix a server side protocol bug.
(username, password),
1)
transport = PersistentTransport(manifest_server)
if manifest_server.startswith('persistent-'):
manifest_server = manifest_server[len('persistent-'):]
try:
server = xmlrpc.client.Server(manifest_server)
server = xmlrpc.client.Server(manifest_server, transport=transport)
if opt.smart_sync:
p = self.manifest.manifestProject
b = p.GetBranch(p.CurrentBranch)
@ -575,17 +612,16 @@ later is required to fix a server side protocol bug.
[success, manifest_str] = server.GetManifest(opt.smart_tag)
if success:
manifest_name = "smart_sync_override.xml"
manifest_path = os.path.join(self.manifest.manifestProject.worktree,
manifest_name)
manifest_name = smart_sync_manifest_name
try:
f = open(manifest_path, 'w')
f = open(smart_sync_manifest_path, 'w')
try:
f.write(manifest_str)
finally:
f.close()
except IOError:
print('error: cannot write manifest to %s' % manifest_path,
except IOError as e:
print('error: cannot write manifest to %s:\n%s'
% (smart_sync_manifest_path, e),
file=sys.stderr)
sys.exit(1)
self._ReloadManifest(manifest_name)
@ -602,6 +638,13 @@ later is required to fix a server side protocol bug.
% (self.manifest.manifest_server, e.errcode, e.errmsg),
file=sys.stderr)
sys.exit(1)
else: # Not smart sync or smart tag mode
if os.path.isfile(smart_sync_manifest_path):
try:
os.remove(smart_sync_manifest_path)
except OSError as e:
print('error: failed to remove existing smart sync override manifest: %s' %
e, file=sys.stderr)
rp = self.manifest.repoProject
rp.PreSync()
@ -615,7 +658,8 @@ later is required to fix a server side protocol bug.
if not opt.local_only:
mp.Sync_NetworkHalf(quiet=opt.quiet,
current_branch_only=opt.current_branch_only,
no_tags=opt.no_tags)
no_tags=opt.no_tags,
optimized_fetch=opt.optimized_fetch)
if mp.HasChanges:
syncbuf = SyncBuffer(mp.config)
@ -625,6 +669,35 @@ later is required to fix a server side protocol bug.
self._ReloadManifest(manifest_name)
if opt.jobs is None:
self.jobs = self.manifest.default.sync_j
# TODO (sbasi) - Add support for manifest changes, aka projects
# have been added or deleted from the manifest.
if self.gitc_manifest:
gitc_manifest_projects = self.GetProjects(args,
manifest=self.gitc_manifest,
missing_ok=True)
gitc_projects = []
opened_projects = []
for project in gitc_manifest_projects:
if not project.old_revision:
gitc_projects.append(project)
else:
opened_projects.append(project)
if gitc_projects and not opt.local_only:
print('Updating GITC client: %s' % self.gitc_manifest.gitc_client_name)
gitc_utils.generate_gitc_manifest(self.gitc_manifest.gitc_client_dir,
self.gitc_manifest,
gitc_projects)
print('GITC client successfully synced.')
# The opened projects need to be synced as normal, therefore we
# generate a new args list to represent the opened projects.
args = []
for proj in opened_projects:
args.append(os.path.relpath(proj.worktree, os.getcwd()))
if not args:
return
all_projects = self.GetProjects(args,
missing_ok=True,
submodules_ok=opt.fetch_submodules)
@ -678,7 +751,7 @@ later is required to fix a server side protocol bug.
for project in all_projects:
pm.update()
if project.worktree:
project.Sync_LocalHalf(syncbuf)
project.Sync_LocalHalf(syncbuf, force_sync=opt.force_sync)
pm.end()
print(file=sys.stderr)
if not syncbuf.Finish():
@ -819,3 +892,96 @@ class _FetchTimes(object):
os.remove(self._path)
except OSError:
pass
# This is a replacement for xmlrpc.client.Transport using urllib2
# and supporting persistent-http[s]. It cannot change hosts from
# request to request like the normal transport, the real url
# is passed during initialization.
class PersistentTransport(xmlrpc.client.Transport):
def __init__(self, orig_host):
self.orig_host = orig_host
def request(self, host, handler, request_body, verbose=False):
with GetUrlCookieFile(self.orig_host, not verbose) as (cookiefile, proxy):
# Python doesn't understand cookies with the #HttpOnly_ prefix
# Since we're only using them for HTTP, copy the file temporarily,
# stripping those prefixes away.
if cookiefile:
tmpcookiefile = tempfile.NamedTemporaryFile()
try:
with open(cookiefile) as f:
for line in f:
if line.startswith("#HttpOnly_"):
line = line[len("#HttpOnly_"):]
tmpcookiefile.write(line)
tmpcookiefile.flush()
cookiejar = cookielib.MozillaCookieJar(tmpcookiefile.name)
cookiejar.load()
finally:
tmpcookiefile.close()
else:
cookiejar = cookielib.CookieJar()
proxyhandler = urllib.request.ProxyHandler
if proxy:
proxyhandler = urllib.request.ProxyHandler({
"http": proxy,
"https": proxy })
opener = urllib.request.build_opener(
urllib.request.HTTPCookieProcessor(cookiejar),
proxyhandler)
url = urllib.parse.urljoin(self.orig_host, handler)
parse_results = urllib.parse.urlparse(url)
scheme = parse_results.scheme
if scheme == 'persistent-http':
scheme = 'http'
if scheme == 'persistent-https':
# If we're proxying through persistent-https, use http. The
# proxy itself will do the https.
if proxy:
scheme = 'http'
else:
scheme = 'https'
# Parse out any authentication information using the base class
host, extra_headers, _ = self.get_host_info(parse_results.netloc)
url = urllib.parse.urlunparse((
scheme,
host,
parse_results.path,
parse_results.params,
parse_results.query,
parse_results.fragment))
request = urllib.request.Request(url, request_body)
if extra_headers is not None:
for (name, header) in extra_headers:
request.add_header(name, header)
request.add_header('Content-Type', 'text/xml')
try:
response = opener.open(request)
except urllib.error.HTTPError as e:
if e.code == 501:
# We may have been redirected through a login process
# but our POST turned into a GET. Retry.
response = opener.open(request)
else:
raise
p, u = xmlrpc.client.getparser()
while 1:
data = response.read(1024)
if not data:
break
p.feed(data)
p.close()
return u.close()
def close(self):
pass