Merge "Allow hacking 0.6.0 and fix versions"
diff --git a/.gitignore b/.gitignore
index 436c408..d1c5cdb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@
 ChangeLog
 dist/*
 *.egg*
+.idea
diff --git a/jeepyb/cmd/notify_impact.py b/jeepyb/cmd/notify_impact.py
index 55e44c8..93f2e10 100644
--- a/jeepyb/cmd/notify_impact.py
+++ b/jeepyb/cmd/notify_impact.py
@@ -80,14 +80,12 @@
     """Process DocImpact flag.
 
     If the 'DocImpact' flag is present, create a new documentation bug in
-    the openstack-manuals launchpad project based on the git_log, then
-    (and for non-documentation impacts) notify the mailing list of impact,
-    unless a bug was created.
+    the openstack-manuals launchpad project based on the git_log.
+    For non-documentation impacts notify the mailing list of impact.
     """
     if args.impact.lower() == 'docimpact':
-        buglink = create_bug(git_log, args, 'openstack-manuals')
-        if buglink is not None:
-            return
+        create_bug(git_log, args, 'openstack-manuals')
+        return
 
     email_content = EMAIL_TEMPLATE % (args.impact,
                                       args.change_url, git_log)
diff --git a/jeepyb/cmd/run_mirror.py b/jeepyb/cmd/run_mirror.py
index 70a89c9..5e54583 100644
--- a/jeepyb/cmd/run_mirror.py
+++ b/jeepyb/cmd/run_mirror.py
@@ -208,12 +208,15 @@
                     out = self.run_command("git reset --hard %s" % branch)
                     out = self.run_command("git clean -x -f -d -q")
                 reqlist = []
-                for requires_file in ("requirements.txt",
-                                      "test-requirements.txt",
-                                      "tools/pip-requires",
-                                      "tools/test-requires"):
-                    if os.path.exists(requires_file):
-                        reqlist.append(requires_file)
+                if os.path.exists('global-requirements.txt'):
+                    reqlist.append('global-requirements.txt')
+                else:
+                    for requires_file in ("requirements.txt",
+                                          "test-requirements.txt",
+                                          "tools/pip-requires",
+                                          "tools/test-requires"):
+                        if os.path.exists(requires_file):
+                            reqlist.append(requires_file)
                 if reqlist:
                     out = self.run_command(venv_format %
                                            (pip_cache_dir, venv))
@@ -223,6 +226,9 @@
                     out = self.run_command(upgrade_format %
                                            (pip, pip_cache_dir,
                                             build, "pip"))
+                    out = self.run_command(upgrade_format %
+                                           (pip, pip_cache_dir,
+                                            build, "virtualenv"))
                     if os.path.exists(build):
                         shutil.rmtree(build)
                     new_reqs = self.process_http_requirements(reqlist,
diff --git a/jeepyb/cmd/update_bug.py b/jeepyb/cmd/update_bug.py
index bc433ae..21cc030 100644
--- a/jeepyb/cmd/update_bug.py
+++ b/jeepyb/cmd/update_bug.py
@@ -179,6 +179,7 @@
         'stackforge/puppet-ceilometer': 'puppet-openstack',
         'stackforge/puppet-cinder': 'puppet-openstack',
         'stackforge/puppet-glance': 'puppet-openstack',
+        'stackforge/puppet-heat': 'puppet-openstack',
         'stackforge/puppet-horizon': 'puppet-openstack',
         'stackforge/puppet-keystone': 'puppet-openstack',
         'stackforge/puppet-nova': 'puppet-openstack',
@@ -230,29 +231,76 @@
     ]
 
 
-def process_bugtask(launchpad, bugtask, git_log, args):
-    """Apply changes to bugtask, based on hook / branch..."""
+class Task:
+    def __init__(self, lp_task, prefix):
+        '''Prefixes associated with bug references will allow for certain
+        changes to be made to the bug's launchpad (lp) page. The following
+        tokens represent the automation currently taking place.
+
+        ::
+        add_comment       -> Adds a comment to the bug's lp page.
+        set_in_progress   -> Sets the bug's lp status to 'In Progress'.
+        set_fix_released  -> Sets the bug's lp status to 'Fix Released'.
+        set_fix_committed -> Sets the bug's lp status to 'Fix Committed'.
+        ::
+
+        changes_needed, when populated, simply indicates the actions that are
+        available to be taken based on the value of 'prefix'.
+        '''
+        self.lp_task = lp_task
+        self.changes_needed = []
+
+        # If no prefix was matched, default to 'closes'.
+        prefix = prefix.split('-')[0].lower() if prefix else 'closes'
+
+        if prefix in ('closes', 'fixes', 'resolves'):
+            self.changes_needed.extend(('add_comment',
+                                        'set_in_progress',
+                                        'set_fix_committed',
+                                        'set_fix_released'))
+        elif prefix in ('partial',):
+            self.changes_needed.extend(('add_comment', 'set_in_progress'))
+        elif prefix in ('related', 'impacts', 'affects'):
+            self.changes_needed.extend(('add_comment',))
+        else:
+            # prefix is not recognized.
+            self.changes_needed.extend(('add_comment',))
+
+    def needs_change(self, change):
+        '''Return a boolean indicating if given 'change' needs to be made.'''
+        if change in self.changes_needed:
+            return True
+        else:
+            return False
+
+
+def process_bugtask(launchpad, task, git_log, args):
+    """Apply changes to lp bug tasks, based on hook / branch."""
+
+    bugtask = task.lp_task
 
     if args.hook == "change-merged":
         if args.branch == 'master':
-            if is_direct_release(args.project):
+            if (is_direct_release(args.project) and
+                    task.needs_change('set_fix_released')):
                 set_fix_released(bugtask)
             else:
-                if bugtask.status != u'Fix Released':
+                if (bugtask.status != u'Fix Released' and
+                        task.needs_change('set_fix_committed')):
                     set_fix_committed(bugtask)
         elif args.branch == 'milestone-proposed':
             release_fixcommitted(bugtask)
         elif args.branch.startswith('stable/'):
             series = args.branch[7:]
-            # Look for a related task matching the series
+            # Look for a related task matching the series.
             for reltask in bugtask.related_tasks:
                 if (reltask.bug_target_name.endswith("/" + series) and
-                        reltask.status != u'Fix Released'):
-                    # Use fixcommitted if there is any
+                        reltask.status != u'Fix Released' and
+                        task.needs_change('set_fix_committed')):
                     set_fix_committed(reltask)
                     break
             else:
-                # Use tagging if there isn't any
+                # Use tag_in_branchname if there isn't any.
                 tag_in_branchname(bugtask, args.branch)
 
         add_change_merged_message(bugtask, args.change_url, args.project,
@@ -261,13 +309,15 @@
 
     if args.hook == "patchset-created":
         if args.branch == 'master':
-            if bugtask.status not in [u'Fix Committed', u'Fix Released']:
-                set_in_progress(bugtask, launchpad, args.uploader,
-                                args.change_url)
+            if (bugtask.status not in [u'Fix Committed', u'Fix Released'] and
+                    task.needs_change('set_in_progress')):
+                set_in_progress(bugtask, launchpad,
+                                args.uploader, args.change_url)
         elif args.branch.startswith('stable/'):
             series = args.branch[7:]
             for reltask in bugtask.related_tasks:
                 if (reltask.bug_target_name.endswith("/" + series) and
+                        task.needs_change('set_in_progress') and
                         reltask.status not in [u'Fix Committed',
                                                u'Fix Released']):
                     set_in_progress(reltask, launchpad,
@@ -280,23 +330,44 @@
 
 
 def find_bugs(launchpad, git_log, args):
-    """Find bugs referenced in the git log and return related bugtasks."""
+    '''Find bugs referenced in the git log and return related tasks.
 
-    bug_regexp = r'([Bb]ug|[Ll][Pp])[\s#:]*(\d+)'
-    tokens = re.split(bug_regexp, git_log)
+    Our regular expression is composed of three major parts:
+    part1: Matches only at start-of-line (required). Optionally matches any
+           word or hyphen separated words.
+    part2: Matches the words 'bug' or 'lp' on a word boundry (required).
+    part3: Matches a whole number (required).
 
-    # Extract unique bug tasks
+    The following patterns will be matched properly:
+    bug # 555555
+    Closes-Bug: 555555
+    Fixes: bug # 555555
+    Resolves: bug 555555
+    Partial-Bug: lp bug # 555555
+
+    :returns: an iterable containing Task objects.
+    '''
+
+    part1 = r'^[\t ]*(?P<prefix>[-\w]+)?[\s:]*'
+    part2 = r'(?:\b(?:bug|lp)\b[\s#:]*)+'
+    part3 = r'(?P<bug_number>\d+)\s*?$'
+    regexp = part1 + part2 + part3
+    matches = re.finditer(regexp, git_log, flags=re.I | re.M)
+
+    # Extract unique bug tasks and associated prefixes.
     bugtasks = {}
-    for token in tokens:
-        if re.match('^\d+$', token) and (token not in bugtasks):
+    for match in matches:
+        prefix = match.group('prefix')
+        bug_num = match.group('bug_number')
+        if bug_num not in bugtasks:
             try:
-                lp_bug = launchpad.bugs[token]
+                lp_bug = launchpad.bugs[bug_num]
                 for lp_task in lp_bug.bug_tasks:
                     if lp_task.bug_target_name == git2lp(args.project):
-                        bugtasks[token] = lp_task
+                        bugtasks[bug_num] = Task(lp_task, prefix)
                         break
             except KeyError:
-                # Unknown bug
+                # Unknown bug.
                 pass
 
     return bugtasks.values()
@@ -313,31 +384,31 @@
 def main():
     parser = argparse.ArgumentParser()
     parser.add_argument('hook')
-    #common
+    # common
     parser.add_argument('--change', default=None)
     parser.add_argument('--change-url', default=None)
     parser.add_argument('--project', default=None)
     parser.add_argument('--branch', default=None)
     parser.add_argument('--commit', default=None)
-    #change-merged
+    # change-merged
     parser.add_argument('--submitter', default=None)
-    #patchset-created
+    # patchset-created
     parser.add_argument('--uploader', default=None)
     parser.add_argument('--patchset', default=None)
 
     args = parser.parse_args()
 
-    # Connect to Launchpad
+    # Connect to Launchpad.
     lpconn = launchpad.Launchpad.login_with(
         'Gerrit User Sync', uris.LPNET_SERVICE_ROOT, GERRIT_CACHE_DIR,
         credentials_file=GERRIT_CREDENTIALS, version='devel')
 
-    # Get git log
+    # Get git log.
     git_log = extract_git_log(args)
 
-    # Process bugtasks found in git log
-    for bugtask in find_bugs(lpconn, git_log, args):
-        process_bugtask(lpconn, bugtask, git_log, args)
+    # Process tasks found in git log.
+    for task in find_bugs(lpconn, git_log, args):
+        process_bugtask(lpconn, task, git_log, args)
 
 if __name__ == "__main__":
     main()