Allow running the pre-upload hook outside of the context of repo.
This can be useful for two things:
1. Running hooks itself without running 'repo upload'. This might
be useful since it should be a bit quicker.
2. Running hooks even if you don't have a full Chromium OS checkout.
In this case, you could just fetch down the hooks and a project,
then run the hooks directly. In this mode, the '--project' option
is required.
BUG=None
TEST=Ran pre-upload hooks in the context of repo (ran repo upload).
TEST=Ran doctests:
1. Ran .../hooks/pre-upload.py --test -v
TEST=Ran pre-upload hooks in a bare checkout of u-boot:
1. mkdir /tmp/pureuboot
2. cd /tmp/pureuboot/
3. git init
4. git remote add cros http://git.chromium.org/git/chromiumos/third_party/u-boot
5. git fetch cros
6. git checkout -b testing --track cros/master
7. echo ' foobar ' >> Makefile
8. git commit -am "testing"
9. .../hooks/pre-upload.py --project=chromiumos/third_party/u-boot
10. Saw hook report an error (including signed-off-by, showing that we got
u-boot hooks).
TEST=Ran pre-upload hooks manually in chromiumos checkout:
1. cd ~/chromiumos/src/third_party/u-boot/files
2. repo start testing .
3. echo ' foobar ' >> Makefile
4. git commit -am "testing"
5. ~/chromiumos/src/repohooks/pre-upload.py
6. Saw hook report an error (including signed-off-by, showing that we got
u-boot hooks).
Change-Id: Ia9ee51d21bba5a3fdc94f140e2d2308ed97f0273
Reviewed-on: https://gerrit.chromium.org/gerrit/11056
Tested-by: Doug Anderson <dianders@chromium.org>
Reviewed-by: Mandeep Singh Baines <msb@chromium.org>
diff --git a/pre-upload.py b/pre-upload.py
old mode 100644
new mode 100755
index 13408e5..4a6c6f2
--- a/pre-upload.py
+++ b/pre-upload.py
@@ -1,9 +1,11 @@
+#!/usr/bin/env python
# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import ConfigParser
import json
+import optparse
import os
import re
import sys
@@ -48,18 +50,43 @@
_CONFIG_FILE = 'PRESUBMIT.cfg'
+# Exceptions
+
+
+class BadInvocation(Exception):
+ """An Exception indicating a bad invocation of the program."""
+ pass
+
+
# General Helpers
-def _run_command(cmd):
- """Executes the passed in command and returns raw stdout output."""
- return subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
+def _run_command(cmd, cwd=None, stderr=None):
+ """Executes the passed in command and returns raw stdout output.
+
+ Args:
+ cmd: The command to run; should be a list of strings.
+ cwd: The directory to switch to for running the command.
+ stderr: Can be one of None (print stderr to console), subprocess.STDOUT
+ (combine stderr with stdout), or subprocess.PIPE (ignore stderr).
+
+ Returns:
+ The standard out from the process.
+ """
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
+ return p.communicate()[0]
def _get_hooks_dir():
"""Returns the absolute path to the repohooks directory."""
- cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
- return _run_command(cmd).strip()
+ if __name__ == '__main__':
+ # Works when file is run on its own (__file__ is defined)...
+ return os.path.abspath(os.path.dirname(__file__))
+ else:
+ # We need to do this when we're run through repo. Since repo executes
+ # us with execfile(), we don't get __file__ defined.
+ cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
+ return _run_command(cmd).strip()
def _match_regex_list(subject, expressions):
@@ -404,16 +431,20 @@
return hooks
-def _run_project_hooks(project):
+def _run_project_hooks(project, proj_dir=None):
"""For each project run its project specific hook from the hooks dictionary.
Args:
- project: name of project to run hooks for.
+ project: The name of project to run hooks for.
+ proj_dir: If non-None, this is the directory the project is in. If None,
+ we'll ask repo.
Returns:
Boolean value of whether any errors were ecountered while running the hooks.
"""
- proj_dir = _run_command(['repo', 'forall', project, '-c', 'pwd']).strip()
+ if proj_dir is None:
+ proj_dir = _run_command(['repo', 'forall', project, '-c', 'pwd']).strip()
+
pwd = os.getcwd()
# hooks assume they are run from the root of the project
os.chdir(proj_dir)
@@ -441,7 +472,6 @@
os.chdir(pwd)
return error_found
-
# Main
@@ -460,5 +490,142 @@
sys.exit(1)
+def _identify_project(path):
+ """Identify the repo project associated with the given path.
+
+ Returns:
+ A string indicating what project is associated with the path passed in or
+ a blank string upon failure.
+ """
+ return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
+ stderr=subprocess.PIPE, cwd=path).strip()
+
+
+def direct_main(args, verbose=False):
+ """Run hooks directly (outside of the context of repo).
+
+ # Setup for doctests below.
+ # ...note that some tests assume that running pre-upload on this CWD is fine.
+ # TODO: Use mock and actually mock out _run_project_hooks() for tests.
+ >>> mydir = os.path.dirname(os.path.abspath(__file__))
+ >>> olddir = os.getcwd()
+
+ # OK to run w/ no arugments; will run with CWD.
+ >>> os.chdir(mydir)
+ >>> direct_main(['prog_name'], verbose=True)
+ Running hooks on chromiumos/repohooks
+ 0
+ >>> os.chdir(olddir)
+
+ # Run specifying a dir
+ >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
+ Running hooks on chromiumos/repohooks
+ 0
+
+ # Not a problem to use a bogus project; we'll just get default settings.
+ >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
+ Running hooks on X
+ 0
+
+ # Run with project but no dir
+ >>> os.chdir(mydir)
+ >>> direct_main(['prog_name', '--project=X'], verbose=True)
+ Running hooks on X
+ 0
+ >>> os.chdir(olddir)
+
+ # Try with a non-git CWD
+ >>> os.chdir('/tmp')
+ >>> direct_main(['prog_name'])
+ Traceback (most recent call last):
+ ...
+ BadInvocation: The current directory is not part of a git project.
+
+ # Check various bad arguments...
+ >>> direct_main(['prog_name', 'bogus'])
+ Traceback (most recent call last):
+ ...
+ BadInvocation: Unexpected arguments: bogus
+ >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
+ Traceback (most recent call last):
+ ...
+ BadInvocation: Invalid dir: bogusdir
+ >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
+ Traceback (most recent call last):
+ ...
+ BadInvocation: Not a git directory: /tmp
+
+ Args:
+ args: The value of sys.argv
+
+ Returns:
+ 0 if no pre-upload failures, 1 if failures.
+
+ Raises:
+ BadInvocation: On some types of invocation errors.
+ """
+ desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
+ parser = optparse.OptionParser(description=desc)
+
+ parser.add_option('--dir', default=None,
+ help='The directory that the project lives in. If not '
+ 'specified, use the git project root based on the cwd.')
+ parser.add_option('--project', default=None,
+ help='The project repo path; this can affect how the hooks '
+ 'get run, since some hooks are project-specific. For '
+ 'chromite this is chromiumos/chromite. If not specified, '
+ 'the repo tool will be used to figure this out based on '
+ 'the dir.')
+
+ opts, args = parser.parse_args(args[1:])
+
+ if args:
+ raise BadInvocation('Unexpected arguments: %s' % ' '.join(args))
+
+ # Check/normlaize git dir; if unspecified, we'll use the root of the git
+ # project from CWD
+ if opts.dir is None:
+ git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
+ stderr=subprocess.PIPE).strip()
+ if not git_dir:
+ raise BadInvocation('The current directory is not part of a git project.')
+ opts.dir = os.path.dirname(os.path.abspath(git_dir))
+ elif not os.path.isdir(opts.dir):
+ raise BadInvocation('Invalid dir: %s' % opts.dir)
+ elif not os.path.isdir(os.path.join(opts.dir, '.git')):
+ raise BadInvocation('Not a git directory: %s' % opts.dir)
+
+ # Identify the project if it wasn't specified; this _requires_ the repo
+ # tool to be installed and for the project to be part of a repo checkout.
+ if not opts.project:
+ opts.project = _identify_project(opts.dir)
+ if not opts.project:
+ raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
+
+ if verbose:
+ print "Running hooks on %s" % (opts.project)
+
+ found_error = _run_project_hooks(opts.project, proj_dir=opts.dir)
+ if found_error:
+ return 1
+ return 0
+
+
+def _test():
+ """Run any built-in tests."""
+ import doctest
+ doctest.testmod()
+
+
if __name__ == '__main__':
- main()
+ if sys.argv[1:2] == ["--test"]:
+ _test()
+ exit_code = 0
+ else:
+ prog_name = os.path.basename(sys.argv[0])
+ try:
+ exit_code = direct_main(sys.argv)
+ except BadInvocation, e:
+ print >>sys.stderr, "%s: %s" % (prog_name, str(e))
+ exit_code = 1
+ sys.exit(exit_code)