Merge "Be more careful about when to close pull requests"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 01ee443..3902173 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,12 +1,16 @@
 If you would like to contribute to the development of OpenStack,
-you must follow the steps in the "If you're a developer, start here"
-section of this page: [http://wiki.openstack.org/HowToContribute](http://wiki.openstack.org/HowToContribute#If_you.27re_a_developer.2C_start_here:)
+you must follow the steps in this page:
 
-Once those steps have been completed, changes to OpenStack
-should be submitted for review via the Gerrit tool, following
-the workflow documented at [http://wiki.openstack.org/GerritWorkflow](http://wiki.openstack.org/GerritWorkflow).
+   [http://docs.openstack.org/infra/manual/developers.html](http://docs.openstack.org/infra/manual/developers.html)
+
+If you already have a good understanding of how the system works and your
+OpenStack accounts are set up, you can skip to the development workflow section
+of this documentation to learn how changes to OpenStack should be submitted for
+review via the Gerrit tool:
+
+   [http://docs.openstack.org/infra/manual/developers.html#development-workflow](http://docs.openstack.org/infra/manual/developers.html#development-workflow)
 
 Pull requests submitted through GitHub will be ignored.
 
-Bugs should be filed [on Launchpad](https://bugs.launchpad.net/openstack-ci),
+Bugs should be filed [on StoryBoard](https://storyboard.openstack.org/#!/project/722),
 not in GitHub's issue tracker.
diff --git a/README.rst b/README.rst
index ef52cf5..3890b1e 100644
--- a/README.rst
+++ b/README.rst
@@ -4,4 +4,4 @@
 
 jeepyb is a collection of tools which make managing a gerrit easier.
 Specifically, management of gerrit projects and their associated upstream
-integration with things like github and launchpad.
+integration with things like github, launchpad, and storyboard.
diff --git a/jeepyb/cmd/close_pull_requests.py b/jeepyb/cmd/close_pull_requests.py
index 086d761..c1b6a1f 100644
--- a/jeepyb/cmd/close_pull_requests.py
+++ b/jeepyb/cmd/close_pull_requests.py
@@ -50,8 +50,9 @@
 
 %(project)s uses Gerrit for code review.
 
-Please visit http://wiki.openstack.org/GerritWorkflow and follow the
-instructions there to upload your change to Gerrit.
+Please visit
+http://docs.openstack.org/infra/manual/developers.html#development-workflow
+and follow the instructions there to upload your change to Gerrit.
 """
 
 log = logging.getLogger("close_pull_requests")
diff --git a/jeepyb/cmd/expire_old_reviews.py b/jeepyb/cmd/expire_old_reviews.py
index fe5956f..ef67e3a 100644
--- a/jeepyb/cmd/expire_old_reviews.py
+++ b/jeepyb/cmd/expire_old_reviews.py
@@ -15,7 +15,7 @@
 
 # This script is designed to expire old code reviews that have not been touched
 # using the following rules:
-# 1. if negative comment and no activity in 1 week, expire
+# 1. if negative comment and no recent activity, expire
 
 import argparse
 import json
@@ -27,7 +27,7 @@
 
 
 def expire_patch_set(ssh, patch_id, patch_subject):
-    message = ('Code review expired after 1 week of no activity'
+    message = ('Code review expired due to no recent activity'
                ' after a negative review. It can be restored using'
                ' the \`Restore Change\` button under the Patch Set'
                ' on the web interface.')
@@ -47,10 +47,13 @@
     parser = argparse.ArgumentParser()
     parser.add_argument('user', help='The gerrit admin user')
     parser.add_argument('ssh_key', help='The gerrit admin SSH key file')
+    parser.add_argument('--age', dest='age', default='1w',
+                        help='The minimum age of a review to expire')
     options = parser.parse_args()
 
     GERRIT_USER = options.user
     GERRIT_SSH_KEY = options.ssh_key
+    EXPIRY_AGE = options.age
 
     logging.basicConfig(format='%(asctime)-6s: %(name)s - %(levelname)s'
                                ' - %(message)s',
@@ -68,7 +71,7 @@
     logger.info('Searching no activity on negative review for 1 week')
     stdin, stdout, stderr = ssh.exec_command(
         'gerrit query --current-patch-set --all-approvals'
-        ' --format JSON status:reviewed age:1w')
+        ' --format JSON status:reviewed age:' + EXPIRY_AGE)
 
     for line in stdout:
         row = json.loads(line)
diff --git a/jeepyb/cmd/manage_projects.py b/jeepyb/cmd/manage_projects.py
index 1dc6578..6ad970e 100644
--- a/jeepyb/cmd/manage_projects.py
+++ b/jeepyb/cmd/manage_projects.py
@@ -395,6 +395,10 @@
 
 
 def update_local_copy(repo_path, track_upstream, git_opts, ssh_env):
+    # first do a clean of the branch to prevent possible
+    # problems due to previous runs
+    git_command(repo_path, "clean -fdx")
+
     has_upstream_remote = (
         'upstream' in git_command_output(repo_path, 'remote')[1])
     if track_upstream:
@@ -568,7 +572,7 @@
         'github-config',
         '/etc/github/github-projects.secure.config')
 
-    gerrit = gerritlib.gerrit.Gerrit('localhost',
+    gerrit = gerritlib.gerrit.Gerrit(GERRIT_HOST,
                                      GERRIT_USER,
                                      GERRIT_PORT,
                                      GERRIT_KEY)
@@ -596,7 +600,10 @@
                     continue
 
                 project_git = "%s.git" % project
-                remote_url = "ssh://localhost:%s/%s" % (GERRIT_PORT, project)
+                remote_url = "ssh://%s:%s/%s" % (
+                    GERRIT_HOST,
+                    GERRIT_PORT,
+                    project)
                 git_opts = dict(upstream=upstream,
                                 repo_path=repo_path,
                                 remote_url=remote_url)
diff --git a/jeepyb/cmd/openstackwatch.py b/jeepyb/cmd/openstackwatch.py
index c984818..726912a 100644
--- a/jeepyb/cmd/openstackwatch.py
+++ b/jeepyb/cmd/openstackwatch.py
@@ -32,9 +32,9 @@
 import time
 
 import PyRSS2Gen
-import six.moves.urllib.parse as urlparse
+import six.moves.urllib.request as urlrequest
 
-PROJECTS = ['openstack/nova', 'openstack/keystone', 'opensack/swift']
+PROJECTS = ['openstack/nova', 'openstack/keystone', 'openstack/swift']
 JSON_URL = 'https://review.openstack.org/query'
 DEBUG = False
 OUTPUT_MODE = 'multiple'
@@ -94,7 +94,7 @@
     url = CONFIG['json_url']
     if project:
         url += "+project:" + project
-    fp = urlparse.urlretrieve(url)
+    fp = urlrequest.urlretrieve(url)
     ret = open(fp[0]).read()
     return ret
 
diff --git a/jeepyb/cmd/update_blueprint.py b/jeepyb/cmd/update_blueprint.py
index 2ce52f3..0231375 100644
--- a/jeepyb/cmd/update_blueprint.py
+++ b/jeepyb/cmd/update_blueprint.py
@@ -68,11 +68,18 @@
 
 
 def update_spec(launchpad, project, name, subject, link, topic=None):
+    spec = None
+
     if p.is_no_launchpad_blueprints(project):
         return
 
-    project = p.project_to_group(project)
-    spec = launchpad.projects[project].getSpecification(name=name)
+    projects = p.project_to_groups(project)
+
+    for project in projects:
+        spec = launchpad.projects[project].getSpecification(name=name)
+        if spec:
+            break
+
     if not spec:
         return
 
diff --git a/jeepyb/cmd/update_bug.py b/jeepyb/cmd/update_bug.py
index 8644106..f886ac2 100644
--- a/jeepyb/cmd/update_bug.py
+++ b/jeepyb/cmd/update_bug.py
@@ -221,7 +221,7 @@
                 if (bugtask.status != u'Fix Released' and
                         task.needs_change('set_fix_committed')):
                     set_fix_committed(bugtask)
-        elif args.branch == 'milestone-proposed':
+        elif args.branch.startswith('proposed/'):
             release_fixcommitted(bugtask)
         elif args.branch.startswith('stable/'):
             series = args.branch[7:]
@@ -290,7 +290,7 @@
     if p.is_no_launchpad_bugs(project):
         return []
 
-    project = p.project_to_group(project)
+    projects = p.project_to_groups(project)
 
     part1 = r'^[\t ]*(?P<prefix>[-\w]+)?[\s:]*'
     part2 = r'(?:\b(?:bug|lp)\b[\s#:]*)+'
@@ -307,7 +307,7 @@
             try:
                 lp_bug = launchpad.bugs[bug_num]
                 for lp_task in lp_bug.bug_tasks:
-                    if lp_task.bug_target_name == project:
+                    if lp_task.bug_target_name in projects:
                         bugtasks[bug_num] = Task(lp_task, prefix)
                         break
             except KeyError:
diff --git a/jeepyb/cmd/welcome_message.py b/jeepyb/cmd/welcome_message.py
index 456c4ea..305ac7c 100644
--- a/jeepyb/cmd/welcome_message.py
+++ b/jeepyb/cmd/welcome_message.py
@@ -89,7 +89,7 @@
     Thanks again for supporting OpenStack, we look forward to working with you.
 
     IRC: https://wiki.openstack.org/wiki/IRC
-    Workflow: https://wiki.openstack.org/wiki/Gerrit_Workflow
+    Workflow: http://docs.openstack.org/infra/manual/developers.html
     Commit Messages: https://wiki.openstack.org/wiki/GitCommitMessages
     """
 
diff --git a/jeepyb/gerritdb.py b/jeepyb/gerritdb.py
index 8343b8e..767991f 100644
--- a/jeepyb/gerritdb.py
+++ b/jeepyb/gerritdb.py
@@ -60,4 +60,11 @@
             import psycopg2
             db_connection = psycopg2.connect(
                 host=DB_HOST, user=DB_USER, password=DB_PASS, database=DB_DB)
+    else:
+        try:
+            # Make sure the database is responding and reconnect if not
+            db_connection.ping(True)
+        except AttributeError:
+            # This database driver lacks a ping implementation
+            pass
     return db_connection
diff --git a/jeepyb/projects.py b/jeepyb/projects.py
index e2015c1..81db5ba 100644
--- a/jeepyb/projects.py
+++ b/jeepyb/projects.py
@@ -18,6 +18,8 @@
 - project: some/project
   launchpad: awesomeproject
   description: Best project ever.
+  groups:
+    - awesome-group
   options:
     - direct-release
     - no-launchpad-bugs
@@ -32,10 +34,12 @@
 registry = u.ProjectsRegistry()
 
 
-def project_to_group(project_full_name):
-    return registry[project_full_name].get(
-        'group', registry[project_full_name].get(
-            'launchpad', u.short_project_name(project_full_name)))
+def project_to_groups(project_full_name):
+    return registry[project_full_name] \
+        .get('groups',
+             [registry[project_full_name].get('group',
+                                              u.short_project_name(
+                                                  project_full_name))])
 
 
 def _is_no_launchpad(project_full_name, obj_type):