mirror of
https://gerrit.googlesource.com/git-repo
synced 2025-06-30 20:17:08 +00:00
Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
d572a13021 | |||
3ba5f95b46 | |||
2630dd9787 | |||
dafb1d68d3 | |||
4655e81a75 | |||
723c5dc3d6 | |||
e6a0eeb80d | |||
0960b5b53d | |||
fc06ced9f9 | |||
fce89f218a | |||
37282b4b9c | |||
835cd6888f | |||
8ced8641c8 | |||
2536f80625 |
@ -25,7 +25,8 @@ following DTD:
|
|||||||
default?,
|
default?,
|
||||||
manifest-server?,
|
manifest-server?,
|
||||||
remove-project*,
|
remove-project*,
|
||||||
project*)>
|
project*,
|
||||||
|
repo-hooks?)>
|
||||||
|
|
||||||
<!ELEMENT notice (#PCDATA)>
|
<!ELEMENT notice (#PCDATA)>
|
||||||
|
|
||||||
@ -49,6 +50,10 @@ following DTD:
|
|||||||
|
|
||||||
<!ELEMENT remove-project (EMPTY)>
|
<!ELEMENT remove-project (EMPTY)>
|
||||||
<!ATTLIST remove-project name CDATA #REQUIRED>
|
<!ATTLIST remove-project name CDATA #REQUIRED>
|
||||||
|
|
||||||
|
<!ELEMENT repo-hooks (EMPTY)>
|
||||||
|
<!ATTLIST repo-hooks in-project CDATA #REQUIRED>
|
||||||
|
<!ATTLIST repo-hooks enabled-list CDATA #REQUIRED>
|
||||||
]>
|
]>
|
||||||
|
|
||||||
A description of the elements and their attributes follows.
|
A description of the elements and their attributes follows.
|
||||||
|
7
error.py
7
error.py
@ -75,3 +75,10 @@ class RepoChangedException(Exception):
|
|||||||
"""
|
"""
|
||||||
def __init__(self, extra_args=[]):
|
def __init__(self, extra_args=[]):
|
||||||
self.extra_args = extra_args
|
self.extra_args = extra_args
|
||||||
|
|
||||||
|
class HookError(Exception):
|
||||||
|
"""Thrown if a 'repo-hook' could not be run.
|
||||||
|
|
||||||
|
The common case is that the file wasn't present when we tried to run it.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
@ -171,6 +171,14 @@ class XmlManifest(object):
|
|||||||
ce.setAttribute('dest', c.dest)
|
ce.setAttribute('dest', c.dest)
|
||||||
e.appendChild(ce)
|
e.appendChild(ce)
|
||||||
|
|
||||||
|
if self._repo_hooks_project:
|
||||||
|
root.appendChild(doc.createTextNode(''))
|
||||||
|
e = doc.createElement('repo-hooks')
|
||||||
|
e.setAttribute('in-project', self._repo_hooks_project.name)
|
||||||
|
e.setAttribute('enabled-list',
|
||||||
|
' '.join(self._repo_hooks_project.enabled_repo_hooks))
|
||||||
|
root.appendChild(e)
|
||||||
|
|
||||||
doc.writexml(fd, '', ' ', '\n', 'UTF-8')
|
doc.writexml(fd, '', ' ', '\n', 'UTF-8')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -188,6 +196,11 @@ class XmlManifest(object):
|
|||||||
self._Load()
|
self._Load()
|
||||||
return self._default
|
return self._default
|
||||||
|
|
||||||
|
@property
|
||||||
|
def repo_hooks_project(self):
|
||||||
|
self._Load()
|
||||||
|
return self._repo_hooks_project
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def notice(self):
|
def notice(self):
|
||||||
self._Load()
|
self._Load()
|
||||||
@ -207,6 +220,7 @@ class XmlManifest(object):
|
|||||||
self._projects = {}
|
self._projects = {}
|
||||||
self._remotes = {}
|
self._remotes = {}
|
||||||
self._default = None
|
self._default = None
|
||||||
|
self._repo_hooks_project = None
|
||||||
self._notice = None
|
self._notice = None
|
||||||
self.branch = None
|
self.branch = None
|
||||||
self._manifest_server = None
|
self._manifest_server = None
|
||||||
@ -239,15 +253,15 @@ class XmlManifest(object):
|
|||||||
def _ParseManifest(self, is_root_file):
|
def _ParseManifest(self, is_root_file):
|
||||||
root = xml.dom.minidom.parse(self.manifestFile)
|
root = xml.dom.minidom.parse(self.manifestFile)
|
||||||
if not root or not root.childNodes:
|
if not root or not root.childNodes:
|
||||||
raise ManifestParseError, \
|
raise ManifestParseError(
|
||||||
"no root node in %s" % \
|
"no root node in %s" %
|
||||||
self.manifestFile
|
self.manifestFile)
|
||||||
|
|
||||||
config = root.childNodes[0]
|
config = root.childNodes[0]
|
||||||
if config.nodeName != 'manifest':
|
if config.nodeName != 'manifest':
|
||||||
raise ManifestParseError, \
|
raise ManifestParseError(
|
||||||
"no <manifest> in %s" % \
|
"no <manifest> in %s" %
|
||||||
self.manifestFile
|
self.manifestFile)
|
||||||
|
|
||||||
for node in config.childNodes:
|
for node in config.childNodes:
|
||||||
if node.nodeName == 'remove-project':
|
if node.nodeName == 'remove-project':
|
||||||
@ -255,25 +269,30 @@ class XmlManifest(object):
|
|||||||
try:
|
try:
|
||||||
del self._projects[name]
|
del self._projects[name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ManifestParseError, \
|
raise ManifestParseError(
|
||||||
'project %s not found' % \
|
'project %s not found' %
|
||||||
(name)
|
(name))
|
||||||
|
|
||||||
|
# If the manifest removes the hooks project, treat it as if it deleted
|
||||||
|
# the repo-hooks element too.
|
||||||
|
if self._repo_hooks_project and (self._repo_hooks_project.name == name):
|
||||||
|
self._repo_hooks_project = None
|
||||||
|
|
||||||
for node in config.childNodes:
|
for node in config.childNodes:
|
||||||
if node.nodeName == 'remote':
|
if node.nodeName == 'remote':
|
||||||
remote = self._ParseRemote(node)
|
remote = self._ParseRemote(node)
|
||||||
if self._remotes.get(remote.name):
|
if self._remotes.get(remote.name):
|
||||||
raise ManifestParseError, \
|
raise ManifestParseError(
|
||||||
'duplicate remote %s in %s' % \
|
'duplicate remote %s in %s' %
|
||||||
(remote.name, self.manifestFile)
|
(remote.name, self.manifestFile))
|
||||||
self._remotes[remote.name] = remote
|
self._remotes[remote.name] = remote
|
||||||
|
|
||||||
for node in config.childNodes:
|
for node in config.childNodes:
|
||||||
if node.nodeName == 'default':
|
if node.nodeName == 'default':
|
||||||
if self._default is not None:
|
if self._default is not None:
|
||||||
raise ManifestParseError, \
|
raise ManifestParseError(
|
||||||
'duplicate default in %s' % \
|
'duplicate default in %s' %
|
||||||
(self.manifestFile)
|
(self.manifestFile))
|
||||||
self._default = self._ParseDefault(node)
|
self._default = self._ParseDefault(node)
|
||||||
if self._default is None:
|
if self._default is None:
|
||||||
self._default = _Default()
|
self._default = _Default()
|
||||||
@ -281,29 +300,52 @@ class XmlManifest(object):
|
|||||||
for node in config.childNodes:
|
for node in config.childNodes:
|
||||||
if node.nodeName == 'notice':
|
if node.nodeName == 'notice':
|
||||||
if self._notice is not None:
|
if self._notice is not None:
|
||||||
raise ManifestParseError, \
|
raise ManifestParseError(
|
||||||
'duplicate notice in %s' % \
|
'duplicate notice in %s' %
|
||||||
(self.manifestFile)
|
(self.manifestFile))
|
||||||
self._notice = self._ParseNotice(node)
|
self._notice = self._ParseNotice(node)
|
||||||
|
|
||||||
for node in config.childNodes:
|
for node in config.childNodes:
|
||||||
if node.nodeName == 'manifest-server':
|
if node.nodeName == 'manifest-server':
|
||||||
url = self._reqatt(node, 'url')
|
url = self._reqatt(node, 'url')
|
||||||
if self._manifest_server is not None:
|
if self._manifest_server is not None:
|
||||||
raise ManifestParseError, \
|
raise ManifestParseError(
|
||||||
'duplicate manifest-server in %s' % \
|
'duplicate manifest-server in %s' %
|
||||||
(self.manifestFile)
|
(self.manifestFile))
|
||||||
self._manifest_server = url
|
self._manifest_server = url
|
||||||
|
|
||||||
for node in config.childNodes:
|
for node in config.childNodes:
|
||||||
if node.nodeName == 'project':
|
if node.nodeName == 'project':
|
||||||
project = self._ParseProject(node)
|
project = self._ParseProject(node)
|
||||||
if self._projects.get(project.name):
|
if self._projects.get(project.name):
|
||||||
raise ManifestParseError, \
|
raise ManifestParseError(
|
||||||
'duplicate project %s in %s' % \
|
'duplicate project %s in %s' %
|
||||||
(project.name, self.manifestFile)
|
(project.name, self.manifestFile))
|
||||||
self._projects[project.name] = project
|
self._projects[project.name] = project
|
||||||
|
|
||||||
|
for node in config.childNodes:
|
||||||
|
if node.nodeName == 'repo-hooks':
|
||||||
|
# Get the name of the project and the (space-separated) list of enabled.
|
||||||
|
repo_hooks_project = self._reqatt(node, 'in-project')
|
||||||
|
enabled_repo_hooks = self._reqatt(node, 'enabled-list').split()
|
||||||
|
|
||||||
|
# Only one project can be the hooks project
|
||||||
|
if self._repo_hooks_project is not None:
|
||||||
|
raise ManifestParseError(
|
||||||
|
'duplicate repo-hooks in %s' %
|
||||||
|
(self.manifestFile))
|
||||||
|
|
||||||
|
# Store a reference to the Project.
|
||||||
|
try:
|
||||||
|
self._repo_hooks_project = self._projects[repo_hooks_project]
|
||||||
|
except KeyError:
|
||||||
|
raise ManifestParseError(
|
||||||
|
'project %s not found for repo-hooks' %
|
||||||
|
(repo_hooks_project))
|
||||||
|
|
||||||
|
# Store the enabled hooks in the Project object.
|
||||||
|
self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks
|
||||||
|
|
||||||
def _AddMetaProjectMirror(self, m):
|
def _AddMetaProjectMirror(self, m):
|
||||||
name = None
|
name = None
|
||||||
m_url = m.GetRemote(m.remote.name).url
|
m_url = m.GetRemote(m.remote.name).url
|
||||||
|
339
project.py
339
project.py
@ -12,6 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import traceback
|
||||||
import errno
|
import errno
|
||||||
import filecmp
|
import filecmp
|
||||||
import os
|
import os
|
||||||
@ -24,7 +25,7 @@ import urllib2
|
|||||||
from color import Coloring
|
from color import Coloring
|
||||||
from git_command import GitCommand
|
from git_command import GitCommand
|
||||||
from git_config import GitConfig, IsId
|
from git_config import GitConfig, IsId
|
||||||
from error import GitError, ImportError, UploadError
|
from error import GitError, HookError, ImportError, UploadError
|
||||||
from error import ManifestInvalidRevisionError
|
from error import ManifestInvalidRevisionError
|
||||||
|
|
||||||
from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M
|
from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M
|
||||||
@ -54,14 +55,25 @@ def not_rev(r):
|
|||||||
def sq(r):
|
def sq(r):
|
||||||
return "'" + r.replace("'", "'\''") + "'"
|
return "'" + r.replace("'", "'\''") + "'"
|
||||||
|
|
||||||
hook_list = None
|
_project_hook_list = None
|
||||||
def repo_hooks():
|
def _ProjectHooks():
|
||||||
global hook_list
|
"""List the hooks present in the 'hooks' directory.
|
||||||
if hook_list is None:
|
|
||||||
|
These hooks are project hooks and are copied to the '.git/hooks' directory
|
||||||
|
of all subprojects.
|
||||||
|
|
||||||
|
This function caches the list of hooks (based on the contents of the
|
||||||
|
'repo/hooks' directory) on the first call.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of absolute paths to all of the files in the hooks directory.
|
||||||
|
"""
|
||||||
|
global _project_hook_list
|
||||||
|
if _project_hook_list is None:
|
||||||
d = os.path.abspath(os.path.dirname(__file__))
|
d = os.path.abspath(os.path.dirname(__file__))
|
||||||
d = os.path.join(d , 'hooks')
|
d = os.path.join(d , 'hooks')
|
||||||
hook_list = map(lambda x: os.path.join(d, x), os.listdir(d))
|
_project_hook_list = map(lambda x: os.path.join(d, x), os.listdir(d))
|
||||||
return hook_list
|
return _project_hook_list
|
||||||
|
|
||||||
def relpath(dst, src):
|
def relpath(dst, src):
|
||||||
src = os.path.dirname(src)
|
src = os.path.dirname(src)
|
||||||
@ -223,6 +235,249 @@ class RemoteSpec(object):
|
|||||||
self.url = url
|
self.url = url
|
||||||
self.review = review
|
self.review = review
|
||||||
|
|
||||||
|
class RepoHook(object):
|
||||||
|
"""A RepoHook contains information about a script to run as a hook.
|
||||||
|
|
||||||
|
Hooks are used to run a python script before running an upload (for instance,
|
||||||
|
to run presubmit checks). Eventually, we may have hooks for other actions.
|
||||||
|
|
||||||
|
This shouldn't be confused with files in the 'repo/hooks' directory. Those
|
||||||
|
files are copied into each '.git/hooks' folder for each project. Repo-level
|
||||||
|
hooks are associated instead with repo actions.
|
||||||
|
|
||||||
|
Hooks are always python. When a hook is run, we will load the hook into the
|
||||||
|
interpreter and execute its main() function.
|
||||||
|
"""
|
||||||
|
def __init__(self,
|
||||||
|
hook_type,
|
||||||
|
hooks_project,
|
||||||
|
topdir,
|
||||||
|
abort_if_user_denies=False):
|
||||||
|
"""RepoHook constructor.
|
||||||
|
|
||||||
|
Params:
|
||||||
|
hook_type: A string representing the type of hook. This is also used
|
||||||
|
to figure out the name of the file containing the hook. For
|
||||||
|
example: 'pre-upload'.
|
||||||
|
hooks_project: The project containing the repo hooks. If you have a
|
||||||
|
manifest, this is manifest.repo_hooks_project. OK if this is None,
|
||||||
|
which will make the hook a no-op.
|
||||||
|
topdir: Repo's top directory (the one containing the .repo directory).
|
||||||
|
Scripts will run with CWD as this directory. If you have a manifest,
|
||||||
|
this is manifest.topdir
|
||||||
|
abort_if_user_denies: If True, we'll throw a HookError() if the user
|
||||||
|
doesn't allow us to run the hook.
|
||||||
|
"""
|
||||||
|
self._hook_type = hook_type
|
||||||
|
self._hooks_project = hooks_project
|
||||||
|
self._topdir = topdir
|
||||||
|
self._abort_if_user_denies = abort_if_user_denies
|
||||||
|
|
||||||
|
# Store the full path to the script for convenience.
|
||||||
|
if self._hooks_project:
|
||||||
|
self._script_fullpath = os.path.join(self._hooks_project.worktree,
|
||||||
|
self._hook_type + '.py')
|
||||||
|
else:
|
||||||
|
self._script_fullpath = None
|
||||||
|
|
||||||
|
def _GetHash(self):
|
||||||
|
"""Return a hash of the contents of the hooks directory.
|
||||||
|
|
||||||
|
We'll just use git to do this. This hash has the property that if anything
|
||||||
|
changes in the directory we will return a different has.
|
||||||
|
|
||||||
|
SECURITY CONSIDERATION:
|
||||||
|
This hash only represents the contents of files in the hook directory, not
|
||||||
|
any other files imported or called by hooks. Changes to imported files
|
||||||
|
can change the script behavior without affecting the hash.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A string representing the hash. This will always be ASCII so that it can
|
||||||
|
be printed to the user easily.
|
||||||
|
"""
|
||||||
|
assert self._hooks_project, "Must have hooks to calculate their hash."
|
||||||
|
|
||||||
|
# We will use the work_git object rather than just calling GetRevisionId().
|
||||||
|
# That gives us a hash of the latest checked in version of the files that
|
||||||
|
# the user will actually be executing. Specifically, GetRevisionId()
|
||||||
|
# doesn't appear to change even if a user checks out a different version
|
||||||
|
# of the hooks repo (via git checkout) nor if a user commits their own revs.
|
||||||
|
#
|
||||||
|
# NOTE: Local (non-committed) changes will not be factored into this hash.
|
||||||
|
# I think this is OK, since we're really only worried about warning the user
|
||||||
|
# about upstream changes.
|
||||||
|
return self._hooks_project.work_git.rev_parse('HEAD')
|
||||||
|
|
||||||
|
def _GetMustVerb(self):
|
||||||
|
"""Return 'must' if the hook is required; 'should' if not."""
|
||||||
|
if self._abort_if_user_denies:
|
||||||
|
return 'must'
|
||||||
|
else:
|
||||||
|
return 'should'
|
||||||
|
|
||||||
|
def _CheckForHookApproval(self):
|
||||||
|
"""Check to see whether this hook has been approved.
|
||||||
|
|
||||||
|
We'll look at the hash of all of the hooks. If this matches the hash that
|
||||||
|
the user last approved, we're done. If it doesn't, we'll ask the user
|
||||||
|
about approval.
|
||||||
|
|
||||||
|
Note that we ask permission for each individual hook even though we use
|
||||||
|
the hash of all hooks when detecting changes. We'd like the user to be
|
||||||
|
able to approve / deny each hook individually. We only use the hash of all
|
||||||
|
hooks because there is no other easy way to detect changes to local imports.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if this hook is approved to run; False otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HookError: Raised if the user doesn't approve and abort_if_user_denies
|
||||||
|
was passed to the consturctor.
|
||||||
|
"""
|
||||||
|
hooks_dir = self._hooks_project.worktree
|
||||||
|
hooks_config = self._hooks_project.config
|
||||||
|
git_approval_key = 'repo.hooks.%s.approvedhash' % self._hook_type
|
||||||
|
|
||||||
|
# Get the last hash that the user approved for this hook; may be None.
|
||||||
|
old_hash = hooks_config.GetString(git_approval_key)
|
||||||
|
|
||||||
|
# Get the current hash so we can tell if scripts changed since approval.
|
||||||
|
new_hash = self._GetHash()
|
||||||
|
|
||||||
|
if old_hash is not None:
|
||||||
|
# User previously approved hook and asked not to be prompted again.
|
||||||
|
if new_hash == old_hash:
|
||||||
|
# Approval matched. We're done.
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# Give the user a reason why we're prompting, since they last told
|
||||||
|
# us to "never ask again".
|
||||||
|
prompt = 'WARNING: Scripts have changed since %s was allowed.\n\n' % (
|
||||||
|
self._hook_type)
|
||||||
|
else:
|
||||||
|
prompt = ''
|
||||||
|
|
||||||
|
# Prompt the user if we're not on a tty; on a tty we'll assume "no".
|
||||||
|
if sys.stdout.isatty():
|
||||||
|
prompt += ('Repo %s run the script:\n'
|
||||||
|
' %s\n'
|
||||||
|
'\n'
|
||||||
|
'Do you want to allow this script to run '
|
||||||
|
'(yes/yes-never-ask-again/NO)? ') % (
|
||||||
|
self._GetMustVerb(), self._script_fullpath)
|
||||||
|
response = raw_input(prompt).lower()
|
||||||
|
print
|
||||||
|
|
||||||
|
# User is doing a one-time approval.
|
||||||
|
if response in ('y', 'yes'):
|
||||||
|
return True
|
||||||
|
elif response == 'yes-never-ask-again':
|
||||||
|
hooks_config.SetString(git_approval_key, new_hash)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# For anything else, we'll assume no approval.
|
||||||
|
if self._abort_if_user_denies:
|
||||||
|
raise HookError('You must allow the %s hook or use --no-verify.' %
|
||||||
|
self._hook_type)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _ExecuteHook(self, **kwargs):
|
||||||
|
"""Actually execute the given hook.
|
||||||
|
|
||||||
|
This will run the hook's 'main' function in our python interpreter.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
kwargs: Keyword arguments to pass to the hook. These are often specific
|
||||||
|
to the hook type. For instance, pre-upload hooks will contain
|
||||||
|
a project_list.
|
||||||
|
"""
|
||||||
|
# Keep sys.path and CWD stashed away so that we can always restore them
|
||||||
|
# upon function exit.
|
||||||
|
orig_path = os.getcwd()
|
||||||
|
orig_syspath = sys.path
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Always run hooks with CWD as topdir.
|
||||||
|
os.chdir(self._topdir)
|
||||||
|
|
||||||
|
# Put the hook dir as the first item of sys.path so hooks can do
|
||||||
|
# relative imports. We want to replace the repo dir as [0] so
|
||||||
|
# hooks can't import repo files.
|
||||||
|
sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:]
|
||||||
|
|
||||||
|
# Exec, storing global context in the context dict. We catch exceptions
|
||||||
|
# and convert to a HookError w/ just the failing traceback.
|
||||||
|
context = {}
|
||||||
|
try:
|
||||||
|
execfile(self._script_fullpath, context)
|
||||||
|
except Exception:
|
||||||
|
raise HookError('%s\nFailed to import %s hook; see traceback above.' % (
|
||||||
|
traceback.format_exc(), self._hook_type))
|
||||||
|
|
||||||
|
# Running the script should have defined a main() function.
|
||||||
|
if 'main' not in context:
|
||||||
|
raise HookError('Missing main() in: "%s"' % self._script_fullpath)
|
||||||
|
|
||||||
|
|
||||||
|
# Add 'hook_should_take_kwargs' to the arguments to be passed to main.
|
||||||
|
# We don't actually want hooks to define their main with this argument--
|
||||||
|
# it's there to remind them that their hook should always take **kwargs.
|
||||||
|
# For instance, a pre-upload hook should be defined like:
|
||||||
|
# def main(project_list, **kwargs):
|
||||||
|
#
|
||||||
|
# This allows us to later expand the API without breaking old hooks.
|
||||||
|
kwargs = kwargs.copy()
|
||||||
|
kwargs['hook_should_take_kwargs'] = True
|
||||||
|
|
||||||
|
# Call the main function in the hook. If the hook should cause the
|
||||||
|
# build to fail, it will raise an Exception. We'll catch that convert
|
||||||
|
# to a HookError w/ just the failing traceback.
|
||||||
|
try:
|
||||||
|
context['main'](**kwargs)
|
||||||
|
except Exception:
|
||||||
|
raise HookError('%s\nFailed to run main() for %s hook; see traceback '
|
||||||
|
'above.' % (
|
||||||
|
traceback.format_exc(), self._hook_type))
|
||||||
|
finally:
|
||||||
|
# Restore sys.path and CWD.
|
||||||
|
sys.path = orig_syspath
|
||||||
|
os.chdir(orig_path)
|
||||||
|
|
||||||
|
def Run(self, user_allows_all_hooks, **kwargs):
|
||||||
|
"""Run the hook.
|
||||||
|
|
||||||
|
If the hook doesn't exist (because there is no hooks project or because
|
||||||
|
this particular hook is not enabled), this is a no-op.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_allows_all_hooks: If True, we will never prompt about running the
|
||||||
|
hook--we'll just assume it's OK to run it.
|
||||||
|
kwargs: Keyword arguments to pass to the hook. These are often specific
|
||||||
|
to the hook type. For instance, pre-upload hooks will contain
|
||||||
|
a project_list.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HookError: If there was a problem finding the hook or the user declined
|
||||||
|
to run a required hook (from _CheckForHookApproval).
|
||||||
|
"""
|
||||||
|
# No-op if there is no hooks project or if hook is disabled.
|
||||||
|
if ((not self._hooks_project) or
|
||||||
|
(self._hook_type not in self._hooks_project.enabled_repo_hooks)):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Bail with a nice error if we can't find the hook.
|
||||||
|
if not os.path.isfile(self._script_fullpath):
|
||||||
|
raise HookError('Couldn\'t find repo hook: "%s"' % self._script_fullpath)
|
||||||
|
|
||||||
|
# Make sure the user is OK with running the hook.
|
||||||
|
if (not user_allows_all_hooks) and (not self._CheckForHookApproval()):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Run the hook with the same version of python we're using.
|
||||||
|
self._ExecuteHook(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Project(object):
|
class Project(object):
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
manifest,
|
manifest,
|
||||||
@ -264,6 +519,10 @@ class Project(object):
|
|||||||
self.bare_git = self._GitGetByExec(self, bare=True)
|
self.bare_git = self._GitGetByExec(self, bare=True)
|
||||||
self.bare_ref = GitRefs(gitdir)
|
self.bare_ref = GitRefs(gitdir)
|
||||||
|
|
||||||
|
# This will be filled in if a project is later identified to be the
|
||||||
|
# project containing repo hooks.
|
||||||
|
self.enabled_repo_hooks = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def Exists(self):
|
def Exists(self):
|
||||||
return os.path.isdir(self.gitdir)
|
return os.path.isdir(self.gitdir)
|
||||||
@ -391,13 +650,18 @@ class Project(object):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def PrintWorkTreeStatus(self):
|
def PrintWorkTreeStatus(self, output_redir=None):
|
||||||
"""Prints the status of the repository to stdout.
|
"""Prints the status of the repository to stdout.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
output: If specified, redirect the output to this object.
|
||||||
"""
|
"""
|
||||||
if not os.path.isdir(self.worktree):
|
if not os.path.isdir(self.worktree):
|
||||||
print ''
|
if output_redir == None:
|
||||||
print 'project %s/' % self.relpath
|
output_redir = sys.stdout
|
||||||
print ' missing (run "repo sync")'
|
print >>output_redir, ''
|
||||||
|
print >>output_redir, 'project %s/' % self.relpath
|
||||||
|
print >>output_redir, ' missing (run "repo sync")'
|
||||||
return
|
return
|
||||||
|
|
||||||
self.work_git.update_index('-q',
|
self.work_git.update_index('-q',
|
||||||
@ -412,6 +676,8 @@ class Project(object):
|
|||||||
return 'CLEAN'
|
return 'CLEAN'
|
||||||
|
|
||||||
out = StatusColoring(self.config)
|
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
|
branch = self.CurrentBranch
|
||||||
@ -461,6 +727,7 @@ class Project(object):
|
|||||||
else:
|
else:
|
||||||
out.write('%s', line)
|
out.write('%s', line)
|
||||||
out.nl()
|
out.nl()
|
||||||
|
|
||||||
return 'DIRTY'
|
return 'DIRTY'
|
||||||
|
|
||||||
def PrintWorkTreeDiff(self):
|
def PrintWorkTreeDiff(self):
|
||||||
@ -680,11 +947,11 @@ class Project(object):
|
|||||||
"""Perform only the local IO portion of the sync process.
|
"""Perform only the local IO portion of the sync process.
|
||||||
Network access is not required.
|
Network access is not required.
|
||||||
"""
|
"""
|
||||||
self._InitWorkTree()
|
|
||||||
all = self.bare_ref.all
|
all = self.bare_ref.all
|
||||||
self.CleanPublishedCache(all)
|
self.CleanPublishedCache(all)
|
||||||
|
|
||||||
revid = self.GetRevisionId(all)
|
revid = self.GetRevisionId(all)
|
||||||
|
|
||||||
|
self._InitWorkTree()
|
||||||
head = self.work_git.GetHead()
|
head = self.work_git.GetHead()
|
||||||
if head.startswith(R_HEADS):
|
if head.startswith(R_HEADS):
|
||||||
branch = head[len(R_HEADS):]
|
branch = head[len(R_HEADS):]
|
||||||
@ -900,6 +1167,13 @@ class Project(object):
|
|||||||
|
|
||||||
def CheckoutBranch(self, name):
|
def CheckoutBranch(self, name):
|
||||||
"""Checkout a local topic branch.
|
"""Checkout a local topic branch.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The name of the branch to checkout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the checkout succeeded; False if it didn't; None if the branch
|
||||||
|
didn't exist.
|
||||||
"""
|
"""
|
||||||
rev = R_HEADS + name
|
rev = R_HEADS + name
|
||||||
head = self.work_git.GetHead()
|
head = self.work_git.GetHead()
|
||||||
@ -914,7 +1188,7 @@ class Project(object):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
# Branch does not exist in this project
|
# Branch does not exist in this project
|
||||||
#
|
#
|
||||||
return False
|
return None
|
||||||
|
|
||||||
if head.startswith(R_HEADS):
|
if head.startswith(R_HEADS):
|
||||||
try:
|
try:
|
||||||
@ -937,13 +1211,19 @@ class Project(object):
|
|||||||
|
|
||||||
def AbandonBranch(self, name):
|
def AbandonBranch(self, name):
|
||||||
"""Destroy a local topic branch.
|
"""Destroy a local topic branch.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The name of the branch to abandon.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the abandon succeeded; False if it didn't; None if the branch
|
||||||
|
didn't exist.
|
||||||
"""
|
"""
|
||||||
rev = R_HEADS + name
|
rev = R_HEADS + name
|
||||||
all = self.bare_ref.all
|
all = self.bare_ref.all
|
||||||
if rev not in all:
|
if rev not in all:
|
||||||
# Doesn't exist; assume already abandoned.
|
# Doesn't exist
|
||||||
#
|
return None
|
||||||
return True
|
|
||||||
|
|
||||||
head = self.work_git.GetHead()
|
head = self.work_git.GetHead()
|
||||||
if head == rev:
|
if head == rev:
|
||||||
@ -1192,10 +1472,10 @@ class Project(object):
|
|||||||
hooks = self._gitdir_path('hooks')
|
hooks = self._gitdir_path('hooks')
|
||||||
if not os.path.exists(hooks):
|
if not os.path.exists(hooks):
|
||||||
os.makedirs(hooks)
|
os.makedirs(hooks)
|
||||||
for stock_hook in repo_hooks():
|
for stock_hook in _ProjectHooks():
|
||||||
name = os.path.basename(stock_hook)
|
name = os.path.basename(stock_hook)
|
||||||
|
|
||||||
if name in ('commit-msg') and not self.remote.review:
|
if name in ('commit-msg',) and not self.remote.review:
|
||||||
# Don't install a Gerrit Code Review hook if this
|
# Don't install a Gerrit Code Review hook if this
|
||||||
# project does not appear to use it for reviews.
|
# project does not appear to use it for reviews.
|
||||||
#
|
#
|
||||||
@ -1288,6 +1568,11 @@ class Project(object):
|
|||||||
cmd.append(HEAD)
|
cmd.append(HEAD)
|
||||||
if GitCommand(self, cmd).Wait() != 0:
|
if GitCommand(self, cmd).Wait() != 0:
|
||||||
raise GitError("cannot initialize work tree")
|
raise GitError("cannot initialize work tree")
|
||||||
|
|
||||||
|
rr_cache = os.path.join(self.gitdir, 'rr-cache')
|
||||||
|
if not os.path.exists(rr_cache):
|
||||||
|
os.makedirs(rr_cache)
|
||||||
|
|
||||||
self._CopyFiles()
|
self._CopyFiles()
|
||||||
|
|
||||||
def _gitdir_path(self, path):
|
def _gitdir_path(self, path):
|
||||||
@ -1446,6 +1731,22 @@ class Project(object):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
|
"""Allow arbitrary git commands using pythonic syntax.
|
||||||
|
|
||||||
|
This allows you to do things like:
|
||||||
|
git_obj.rev_parse('HEAD')
|
||||||
|
|
||||||
|
Since we don't have a 'rev_parse' method defined, the __getattr__ will
|
||||||
|
run. We'll replace the '_' with a '-' and try to run a git command.
|
||||||
|
Any other arguments will be passed to the git command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The name of the git command to call. Any '_' characters will
|
||||||
|
be replaced with '-'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A callable object that will try to call git with the named command.
|
||||||
|
"""
|
||||||
name = name.replace('_', '-')
|
name = name.replace('_', '-')
|
||||||
def runner(*args):
|
def runner(*args):
|
||||||
cmdv = [name]
|
cmdv = [name]
|
||||||
|
@ -41,21 +41,30 @@ It is equivalent to "git branch -D <branchname>".
|
|||||||
|
|
||||||
nb = args[0]
|
nb = args[0]
|
||||||
err = []
|
err = []
|
||||||
|
success = []
|
||||||
all = self.GetProjects(args[1:])
|
all = self.GetProjects(args[1:])
|
||||||
|
|
||||||
pm = Progress('Abandon %s' % nb, len(all))
|
pm = Progress('Abandon %s' % nb, len(all))
|
||||||
for project in all:
|
for project in all:
|
||||||
pm.update()
|
pm.update()
|
||||||
if not project.AbandonBranch(nb):
|
|
||||||
err.append(project)
|
status = project.AbandonBranch(nb)
|
||||||
|
if status is not None:
|
||||||
|
if status:
|
||||||
|
success.append(project)
|
||||||
|
else:
|
||||||
|
err.append(project)
|
||||||
pm.end()
|
pm.end()
|
||||||
|
|
||||||
if err:
|
if err:
|
||||||
if len(err) == len(all):
|
for p in err:
|
||||||
print >>sys.stderr, 'error: no project has branch %s' % nb
|
print >>sys.stderr,\
|
||||||
else:
|
"error: %s/: cannot abandon %s" \
|
||||||
for p in err:
|
% (p.relpath, nb)
|
||||||
print >>sys.stderr,\
|
|
||||||
"error: %s/: cannot abandon %s" \
|
|
||||||
% (p.relpath, nb)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
elif not success:
|
||||||
|
print >>sys.stderr, 'error: no project has branch %s' % nb
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print >>sys.stderr, 'Abandoned in %d project(s):\n %s' % (
|
||||||
|
len(success), '\n '.join(p.relpath for p in success))
|
||||||
|
@ -38,21 +38,27 @@ The command is equivalent to:
|
|||||||
|
|
||||||
nb = args[0]
|
nb = args[0]
|
||||||
err = []
|
err = []
|
||||||
|
success = []
|
||||||
all = self.GetProjects(args[1:])
|
all = self.GetProjects(args[1:])
|
||||||
|
|
||||||
pm = Progress('Checkout %s' % nb, len(all))
|
pm = Progress('Checkout %s' % nb, len(all))
|
||||||
for project in all:
|
for project in all:
|
||||||
pm.update()
|
pm.update()
|
||||||
if not project.CheckoutBranch(nb):
|
|
||||||
err.append(project)
|
status = project.CheckoutBranch(nb)
|
||||||
|
if status is not None:
|
||||||
|
if status:
|
||||||
|
success.append(project)
|
||||||
|
else:
|
||||||
|
err.append(project)
|
||||||
pm.end()
|
pm.end()
|
||||||
|
|
||||||
if err:
|
if err:
|
||||||
if len(err) == len(all):
|
for p in err:
|
||||||
print >>sys.stderr, 'error: no project has branch %s' % nb
|
print >>sys.stderr,\
|
||||||
else:
|
"error: %s/: cannot checkout %s" \
|
||||||
for p in err:
|
% (p.relpath, nb)
|
||||||
print >>sys.stderr,\
|
sys.exit(1)
|
||||||
"error: %s/: cannot checkout %s" \
|
elif not success:
|
||||||
% (p.relpath, nb)
|
print >>sys.stderr, 'error: no project has branch %s' % nb
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
114
subcmds/cherry_pick.py
Normal file
114
subcmds/cherry_pick.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
#
|
||||||
|
# Copyright (C) 2010 The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import sys, re, string, random, os
|
||||||
|
from command import Command
|
||||||
|
from git_command import GitCommand
|
||||||
|
|
||||||
|
CHANGE_ID_RE = re.compile(r'^\s*Change-Id: I([0-9a-f]{40})\s*$')
|
||||||
|
|
||||||
|
class CherryPick(Command):
|
||||||
|
common = True
|
||||||
|
helpSummary = "Cherry-pick a change."
|
||||||
|
helpUsage = """
|
||||||
|
%prog <sha1>
|
||||||
|
"""
|
||||||
|
helpDescription = """
|
||||||
|
'%prog' cherry-picks a change from one branch to another.
|
||||||
|
The change id will be updated, and a reference to the old
|
||||||
|
change id will be added.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _Options(self, p):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def Execute(self, opt, args):
|
||||||
|
if len(args) != 1:
|
||||||
|
self.Usage()
|
||||||
|
|
||||||
|
reference = args[0]
|
||||||
|
|
||||||
|
p = GitCommand(None,
|
||||||
|
['rev-parse', '--verify', reference],
|
||||||
|
capture_stdout = True,
|
||||||
|
capture_stderr = True)
|
||||||
|
if p.Wait() != 0:
|
||||||
|
print >>sys.stderr, p.stderr
|
||||||
|
sys.exit(1)
|
||||||
|
sha1 = p.stdout.strip()
|
||||||
|
|
||||||
|
p = GitCommand(None, ['cat-file', 'commit', sha1], capture_stdout=True)
|
||||||
|
if p.Wait() != 0:
|
||||||
|
print >>sys.stderr, "error: Failed to retrieve old commit message"
|
||||||
|
sys.exit(1)
|
||||||
|
old_msg = self._StripHeader(p.stdout)
|
||||||
|
|
||||||
|
p = GitCommand(None,
|
||||||
|
['cherry-pick', sha1],
|
||||||
|
capture_stdout = True,
|
||||||
|
capture_stderr = True)
|
||||||
|
status = p.Wait()
|
||||||
|
|
||||||
|
print >>sys.stdout, p.stdout
|
||||||
|
print >>sys.stderr, p.stderr
|
||||||
|
|
||||||
|
if status == 0:
|
||||||
|
# The cherry-pick was applied correctly. We just need to edit the
|
||||||
|
# commit message.
|
||||||
|
new_msg = self._Reformat(old_msg, sha1)
|
||||||
|
|
||||||
|
p = GitCommand(None, ['commit', '--amend', '-F', '-'],
|
||||||
|
provide_stdin = True,
|
||||||
|
capture_stdout = True,
|
||||||
|
capture_stderr = True)
|
||||||
|
p.stdin.write(new_msg)
|
||||||
|
if p.Wait() != 0:
|
||||||
|
print >>sys.stderr, "error: Failed to update commit message"
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print >>sys.stderr, """\
|
||||||
|
NOTE: When committing (please see above) and editing the commit message,
|
||||||
|
please remove the old Change-Id-line and add:
|
||||||
|
"""
|
||||||
|
print >>sys.stderr, self._GetReference(sha1)
|
||||||
|
print >>sys.stderr
|
||||||
|
|
||||||
|
def _IsChangeId(self, line):
|
||||||
|
return CHANGE_ID_RE.match(line)
|
||||||
|
|
||||||
|
def _GetReference(self, sha1):
|
||||||
|
return "(cherry picked from commit %s)" % sha1
|
||||||
|
|
||||||
|
def _StripHeader(self, commit_msg):
|
||||||
|
lines = commit_msg.splitlines()
|
||||||
|
return "\n".join(lines[lines.index("")+1:])
|
||||||
|
|
||||||
|
def _Reformat(self, old_msg, sha1):
|
||||||
|
new_msg = []
|
||||||
|
|
||||||
|
for line in old_msg.splitlines():
|
||||||
|
if not self._IsChangeId(line):
|
||||||
|
new_msg.append(line)
|
||||||
|
|
||||||
|
# Add a blank line between the message and the change id/reference
|
||||||
|
try:
|
||||||
|
if new_msg[-1].strip() != "":
|
||||||
|
new_msg.append("")
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
new_msg.append(self._GetReference(sha1))
|
||||||
|
return "\n".join(new_msg)
|
@ -14,6 +14,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from color import Coloring
|
from color import Coloring
|
||||||
@ -137,6 +138,11 @@ to update the working directory files.
|
|||||||
if not m.Sync_NetworkHalf():
|
if not m.Sync_NetworkHalf():
|
||||||
r = m.GetRemote(m.remote.name)
|
r = m.GetRemote(m.remote.name)
|
||||||
print >>sys.stderr, 'fatal: cannot obtain manifest %s' % r.url
|
print >>sys.stderr, 'fatal: cannot obtain manifest %s' % r.url
|
||||||
|
|
||||||
|
# Better delete the manifest git dir if we created it; otherwise next
|
||||||
|
# time (when user fixes problems) we won't go through the "is_new" logic.
|
||||||
|
if is_new:
|
||||||
|
shutil.rmtree(m.gitdir)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
syncbuf = SyncBuffer(m.config)
|
syncbuf = SyncBuffer(m.config)
|
||||||
|
48
subcmds/list.py
Normal file
48
subcmds/list.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
#
|
||||||
|
# Copyright (C) 2011 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 command import Command, MirrorSafeCommand
|
||||||
|
|
||||||
|
class List(Command, MirrorSafeCommand):
|
||||||
|
common = True
|
||||||
|
helpSummary = "List projects and their associated directories"
|
||||||
|
helpUsage = """
|
||||||
|
%prog [<project>...]
|
||||||
|
"""
|
||||||
|
helpDescription = """
|
||||||
|
List all projects; pass '.' to list the project for the cwd.
|
||||||
|
|
||||||
|
This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def Execute(self, opt, args):
|
||||||
|
"""List all projects and the associated directories.
|
||||||
|
|
||||||
|
This may be possible to do with 'repo forall', but repo newbies have
|
||||||
|
trouble figuring that out. The idea here is that it should be more
|
||||||
|
discoverable.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
opt: The options. We don't take any.
|
||||||
|
args: Positional args. Can be a list of projects to list, or empty.
|
||||||
|
"""
|
||||||
|
projects = self.GetProjects(args)
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for project in projects:
|
||||||
|
lines.append("%s : %s" % (project.relpath, project.name))
|
||||||
|
|
||||||
|
lines.sort()
|
||||||
|
print '\n'.join(lines)
|
@ -15,6 +15,15 @@
|
|||||||
|
|
||||||
from command import PagedCommand
|
from command import PagedCommand
|
||||||
|
|
||||||
|
try:
|
||||||
|
import threading as _threading
|
||||||
|
except ImportError:
|
||||||
|
import dummy_threading as _threading
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
import sys
|
||||||
|
import StringIO
|
||||||
|
|
||||||
class Status(PagedCommand):
|
class Status(PagedCommand):
|
||||||
common = True
|
common = True
|
||||||
helpSummary = "Show the working tree status"
|
helpSummary = "Show the working tree status"
|
||||||
@ -27,6 +36,9 @@ and the most recent commit on this branch (HEAD), in each project
|
|||||||
specified. A summary is displayed, one line per file where there
|
specified. A summary is displayed, one line per file where there
|
||||||
is a difference between these three states.
|
is a difference between these three states.
|
||||||
|
|
||||||
|
The -j/--jobs option can be used to run multiple status queries
|
||||||
|
in parallel.
|
||||||
|
|
||||||
Status Display
|
Status Display
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
@ -60,9 +72,34 @@ the following meanings:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def _Options(self, p):
|
||||||
|
p.add_option('-j', '--jobs',
|
||||||
|
dest='jobs', action='store', type='int', default=2,
|
||||||
|
help="number of projects to check simultaneously")
|
||||||
|
|
||||||
|
def _StatusHelper(self, project, clean_counter, sem, output):
|
||||||
|
"""Obtains the status for a specific project.
|
||||||
|
|
||||||
|
Obtains the status for a project, redirecting the output to
|
||||||
|
the specified object. It will release the semaphore
|
||||||
|
when done.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project: Project to get status of.
|
||||||
|
clean_counter: Counter for clean projects.
|
||||||
|
sem: Semaphore, will call release() when complete.
|
||||||
|
output: Where to output the status.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
state = project.PrintWorkTreeStatus(output)
|
||||||
|
if state == 'CLEAN':
|
||||||
|
clean_counter.next()
|
||||||
|
finally:
|
||||||
|
sem.release()
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
all = self.GetProjects(args)
|
all = self.GetProjects(args)
|
||||||
clean = 0
|
counter = itertools.count()
|
||||||
|
|
||||||
on = {}
|
on = {}
|
||||||
for project in all:
|
for project in all:
|
||||||
@ -77,9 +114,24 @@ the following meanings:
|
|||||||
for cb in branch_names:
|
for cb in branch_names:
|
||||||
print '# on branch %s' % cb
|
print '# on branch %s' % cb
|
||||||
|
|
||||||
for project in all:
|
if opt.jobs == 1:
|
||||||
state = project.PrintWorkTreeStatus()
|
for project in all:
|
||||||
if state == 'CLEAN':
|
state = project.PrintWorkTreeStatus()
|
||||||
clean += 1
|
if state == 'CLEAN':
|
||||||
if len(all) == clean:
|
counter.next()
|
||||||
|
else:
|
||||||
|
sem = _threading.Semaphore(opt.jobs)
|
||||||
|
threads_and_output = []
|
||||||
|
for project in all:
|
||||||
|
sem.acquire()
|
||||||
|
output = StringIO.StringIO()
|
||||||
|
t = _threading.Thread(target=self._StatusHelper,
|
||||||
|
args=(project, counter, sem, output))
|
||||||
|
threads_and_output.append((t, output))
|
||||||
|
t.start()
|
||||||
|
for (t, output) in threads_and_output:
|
||||||
|
t.join()
|
||||||
|
sys.stdout.write(output.getvalue())
|
||||||
|
output.close()
|
||||||
|
if len(all) == counter.next():
|
||||||
print 'nothing to commit (working directory clean)'
|
print 'nothing to commit (working directory clean)'
|
||||||
|
@ -39,6 +39,10 @@ from project import R_HEADS
|
|||||||
from project import SyncBuffer
|
from project import SyncBuffer
|
||||||
from progress import Progress
|
from progress import Progress
|
||||||
|
|
||||||
|
class _FetchError(Exception):
|
||||||
|
"""Internal error thrown in _FetchHelper() when we don't want stack trace."""
|
||||||
|
pass
|
||||||
|
|
||||||
class Sync(Command, MirrorSafeCommand):
|
class Sync(Command, MirrorSafeCommand):
|
||||||
jobs = 1
|
jobs = 1
|
||||||
common = True
|
common = True
|
||||||
@ -135,20 +139,61 @@ later is required to fix a server side protocol bug.
|
|||||||
dest='repo_upgraded', action='store_true',
|
dest='repo_upgraded', action='store_true',
|
||||||
help=SUPPRESS_HELP)
|
help=SUPPRESS_HELP)
|
||||||
|
|
||||||
def _FetchHelper(self, opt, project, lock, fetched, pm, sem):
|
def _FetchHelper(self, opt, project, lock, fetched, pm, sem, err_event):
|
||||||
if not project.Sync_NetworkHalf(quiet=opt.quiet):
|
"""Main function of the fetch threads when jobs are > 1.
|
||||||
print >>sys.stderr, 'error: Cannot fetch %s' % project.name
|
|
||||||
if opt.force_broken:
|
|
||||||
print >>sys.stderr, 'warn: --force-broken, continuing to sync'
|
|
||||||
else:
|
|
||||||
sem.release()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
lock.acquire()
|
Args:
|
||||||
fetched.add(project.gitdir)
|
opt: Program options returned from optparse. See _Options().
|
||||||
pm.update()
|
project: Project object for the project to fetch.
|
||||||
lock.release()
|
lock: Lock for accessing objects that are shared amongst multiple
|
||||||
sem.release()
|
_FetchHelper() threads.
|
||||||
|
fetched: set object that we will add project.gitdir to when we're done
|
||||||
|
(with our lock held).
|
||||||
|
pm: Instance of a Project object. We will call pm.update() (with our
|
||||||
|
lock held).
|
||||||
|
sem: We'll release() this semaphore when we exit so that another thread
|
||||||
|
can be started up.
|
||||||
|
err_event: We'll set this event in the case of an error (after printing
|
||||||
|
out info about the error).
|
||||||
|
"""
|
||||||
|
# We'll set to true once we've locked the lock.
|
||||||
|
did_lock = False
|
||||||
|
|
||||||
|
# Encapsulate everything in a try/except/finally so that:
|
||||||
|
# - We always set err_event in the case of an exception.
|
||||||
|
# - We always make sure we call sem.release().
|
||||||
|
# - We always make sure we unlock the lock if we locked it.
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
success = project.Sync_NetworkHalf(quiet=opt.quiet)
|
||||||
|
|
||||||
|
# Lock around all the rest of the code, since printing, updating a set
|
||||||
|
# and Progress.update() are not thread safe.
|
||||||
|
lock.acquire()
|
||||||
|
did_lock = True
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
print >>sys.stderr, 'error: Cannot fetch %s' % project.name
|
||||||
|
if opt.force_broken:
|
||||||
|
print >>sys.stderr, 'warn: --force-broken, continuing to sync'
|
||||||
|
else:
|
||||||
|
raise _FetchError()
|
||||||
|
|
||||||
|
fetched.add(project.gitdir)
|
||||||
|
pm.update()
|
||||||
|
except BaseException, e:
|
||||||
|
# Notify the _Fetch() function about all errors.
|
||||||
|
err_event.set()
|
||||||
|
|
||||||
|
# If we got our own _FetchError, we don't want a stack trace.
|
||||||
|
# However, if we got something else (something in Sync_NetworkHalf?),
|
||||||
|
# we'd like one (so re-raise after we've set err_event).
|
||||||
|
if not isinstance(e, _FetchError):
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
if did_lock:
|
||||||
|
lock.release()
|
||||||
|
sem.release()
|
||||||
|
|
||||||
def _Fetch(self, projects, opt):
|
def _Fetch(self, projects, opt):
|
||||||
fetched = set()
|
fetched = set()
|
||||||
@ -169,7 +214,13 @@ later is required to fix a server side protocol bug.
|
|||||||
threads = set()
|
threads = set()
|
||||||
lock = _threading.Lock()
|
lock = _threading.Lock()
|
||||||
sem = _threading.Semaphore(self.jobs)
|
sem = _threading.Semaphore(self.jobs)
|
||||||
|
err_event = _threading.Event()
|
||||||
for project in projects:
|
for project in projects:
|
||||||
|
# Check for any errors before starting any new threads.
|
||||||
|
# ...we'll let existing threads finish, though.
|
||||||
|
if err_event.isSet():
|
||||||
|
break
|
||||||
|
|
||||||
sem.acquire()
|
sem.acquire()
|
||||||
t = _threading.Thread(target = self._FetchHelper,
|
t = _threading.Thread(target = self._FetchHelper,
|
||||||
args = (opt,
|
args = (opt,
|
||||||
@ -177,13 +228,19 @@ later is required to fix a server side protocol bug.
|
|||||||
lock,
|
lock,
|
||||||
fetched,
|
fetched,
|
||||||
pm,
|
pm,
|
||||||
sem))
|
sem,
|
||||||
|
err_event))
|
||||||
threads.add(t)
|
threads.add(t)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
for t in threads:
|
for t in threads:
|
||||||
t.join()
|
t.join()
|
||||||
|
|
||||||
|
# If we saw an error, exit with code 1 so that other scripts can check.
|
||||||
|
if err_event.isSet():
|
||||||
|
print >>sys.stderr, '\nerror: Exited sync due to fetch errors'
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
pm.end()
|
pm.end()
|
||||||
for project in projects:
|
for project in projects:
|
||||||
project.bare_git.gc('--auto')
|
project.bare_git.gc('--auto')
|
||||||
|
@ -19,7 +19,8 @@ import sys
|
|||||||
|
|
||||||
from command import InteractiveCommand
|
from command import InteractiveCommand
|
||||||
from editor import Editor
|
from editor import Editor
|
||||||
from error import UploadError
|
from error import HookError, UploadError
|
||||||
|
from project import RepoHook
|
||||||
|
|
||||||
UNUSUAL_COMMIT_THRESHOLD = 5
|
UNUSUAL_COMMIT_THRESHOLD = 5
|
||||||
|
|
||||||
@ -120,6 +121,29 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
|
|||||||
type='string', action='append', dest='cc',
|
type='string', action='append', dest='cc',
|
||||||
help='Also send email to these email addresses.')
|
help='Also send email to these email addresses.')
|
||||||
|
|
||||||
|
# Options relating to upload hook. Note that verify and no-verify are NOT
|
||||||
|
# opposites of each other, which is why they store to different locations.
|
||||||
|
# We are using them to match 'git commit' syntax.
|
||||||
|
#
|
||||||
|
# Combinations:
|
||||||
|
# - no-verify=False, verify=False (DEFAULT):
|
||||||
|
# If stdout is a tty, can prompt about running upload hooks if needed.
|
||||||
|
# If user denies running hooks, the upload is cancelled. If stdout is
|
||||||
|
# not a tty and we would need to prompt about upload hooks, upload is
|
||||||
|
# cancelled.
|
||||||
|
# - no-verify=False, verify=True:
|
||||||
|
# Always run upload hooks with no prompt.
|
||||||
|
# - no-verify=True, verify=False:
|
||||||
|
# Never run upload hooks, but upload anyway (AKA bypass hooks).
|
||||||
|
# - no-verify=True, verify=True:
|
||||||
|
# Invalid
|
||||||
|
p.add_option('--no-verify',
|
||||||
|
dest='bypass_hooks', action='store_true',
|
||||||
|
help='Do not run the upload hook.')
|
||||||
|
p.add_option('--verify',
|
||||||
|
dest='allow_all_hooks', action='store_true',
|
||||||
|
help='Run the upload hook without prompting.')
|
||||||
|
|
||||||
def _SingleBranch(self, opt, branch, people):
|
def _SingleBranch(self, opt, branch, people):
|
||||||
project = branch.project
|
project = branch.project
|
||||||
name = branch.name
|
name = branch.name
|
||||||
@ -313,17 +337,27 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
|
|||||||
reviewers = []
|
reviewers = []
|
||||||
cc = []
|
cc = []
|
||||||
|
|
||||||
|
for project in project_list:
|
||||||
|
avail = project.GetUploadableBranches()
|
||||||
|
if avail:
|
||||||
|
pending.append((project, avail))
|
||||||
|
|
||||||
|
if pending and (not opt.bypass_hooks):
|
||||||
|
hook = RepoHook('pre-upload', self.manifest.repo_hooks_project,
|
||||||
|
self.manifest.topdir, abort_if_user_denies=True)
|
||||||
|
pending_proj_names = [project.name for (project, avail) in pending]
|
||||||
|
try:
|
||||||
|
hook.Run(opt.allow_all_hooks, project_list=pending_proj_names)
|
||||||
|
except HookError, e:
|
||||||
|
print >>sys.stderr, "ERROR: %s" % str(e)
|
||||||
|
return
|
||||||
|
|
||||||
if opt.reviewers:
|
if opt.reviewers:
|
||||||
reviewers = _SplitEmails(opt.reviewers)
|
reviewers = _SplitEmails(opt.reviewers)
|
||||||
if opt.cc:
|
if opt.cc:
|
||||||
cc = _SplitEmails(opt.cc)
|
cc = _SplitEmails(opt.cc)
|
||||||
people = (reviewers,cc)
|
people = (reviewers,cc)
|
||||||
|
|
||||||
for project in project_list:
|
|
||||||
avail = project.GetUploadableBranches()
|
|
||||||
if avail:
|
|
||||||
pending.append((project, avail))
|
|
||||||
|
|
||||||
if not pending:
|
if not pending:
|
||||||
print >>sys.stdout, "no branches ready for upload"
|
print >>sys.stdout, "no branches ready for upload"
|
||||||
elif len(pending) == 1 and len(pending[0][1]) == 1:
|
elif len(pending) == 1 and len(pending[0][1]) == 1:
|
||||||
|
Reference in New Issue
Block a user