Merge branch 'master' into feature/gearman

Change-Id: I9494196710a96414573b11788ed6007817c9f774
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 2e05cc2..6943bd4 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -216,7 +216,7 @@
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
 man_pages = [
-    ('index', 'zuul', u'Zuul Documentation',
+    ('index', 'zuul-server', u'Zuul Documentation',
      [u'OpenStack'], 1)
 ]
 
diff --git a/doc/source/zuul.rst b/doc/source/zuul.rst
index 4b61507..4877b30 100644
--- a/doc/source/zuul.rst
+++ b/doc/source/zuul.rst
@@ -91,6 +91,14 @@
   Directory that Zuul should clone local git repositories to.
   ``git_dir=/var/lib/zuul/git``
 
+**git_user_email**
+  Optional: Value to pass to `git config user.email`.
+  ``git_user_email=zuul@example.com``
+
+**git_user_name**
+  Optional: Value to pass to `git config user.name`.
+  ``git_user_name=zuul``
+
 **push_change_refs**
   Boolean value (``true`` or ``false``) that determines if Zuul should
   push change refs to the git origin server for the git repositories in
diff --git a/etc/status/public_html/app.js b/etc/status/public_html/app.js
index f7de2cc..8ac6108 100644
--- a/etc/status/public_html/app.js
+++ b/etc/status/public_html/app.js
@@ -22,7 +22,7 @@
         demo = location.search.match(/[?&]demo=([^?&]*)/),
         source = demo ?
             './status-' + (demo[1] || 'basic') + '.json-sample' :
-            '/zuul/status.json';
+            'status.json';
 
     zuul = {
         enabled: true,
diff --git a/etc/zuul.conf-sample b/etc/zuul.conf-sample
index 75d1ca5..47e1d7e 100644
--- a/etc/zuul.conf-sample
+++ b/etc/zuul.conf-sample
@@ -15,4 +15,6 @@
 pidfile=/var/run/zuul/zuul.pid
 state_dir=/var/lib/zuul
 git_dir=/var/lib/zuul/git
+;git_user_email=zuul@example.com
+;git_user_name=zuul
 status_url=https://jenkins.example.com/zuul/status
diff --git a/tests/fixtures/layout.yaml b/tests/fixtures/layout.yaml
index 8d6c852..2695719 100644
--- a/tests/fixtures/layout.yaml
+++ b/tests/fixtures/layout.yaml
@@ -40,6 +40,25 @@
         approval: 
           - approved: 1
 
+  - name: dup1
+    manager: IndependentPipelineManager
+    trigger:
+      - event: change-restored
+    success:
+      verified: 1
+    failure:
+      verified: -1
+
+  - name: dup2
+    manager: IndependentPipelineManager
+    trigger:
+      - event: change-restored
+    success:
+      verified: 1
+    failure:
+      verified: -1
+
+
 jobs:
   - name: ^.*-merge$
     failure-message: Unable to merge change
@@ -73,6 +92,10 @@
         - project-testfile
     post:
       - project-post
+    dup1:
+      - project-test1
+    dup2:
+      - project-test1
 
   - name: org/project1
     check:
diff --git a/tests/fixtures/zuul.conf b/tests/fixtures/zuul.conf
index 8948ae8..7c61de1 100644
--- a/tests/fixtures/zuul.conf
+++ b/tests/fixtures/zuul.conf
@@ -9,5 +9,7 @@
 [zuul]
 layout_config=layout.yaml
 git_dir=/tmp/zuul-test/git
+git_user_email=zuul@example.com
+git_user_name=zuul
 push_change_refs=true
 url_pattern=http://logs.example.com/{change.number}/{change.patchset}/{pipeline.name}/{job.name}/{build.number}
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index 4d3351e..b633ef6 100644
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -78,6 +78,10 @@
     path = os.path.join(UPSTREAM_ROOT, project)
     repo = git.Repo.init(path)
 
+    repo.config_writer().set_value('user', 'email', 'user@example.com')
+    repo.config_writer().set_value('user', 'name', 'User Name')
+    repo.config_writer().write()
+
     fn = os.path.join(path, 'README')
     f = open(fn, 'w')
     f.write("test\n")
@@ -180,6 +184,7 @@
         self.depends_on_change = None
         self.needed_by_changes = []
         self.fail_merge = False
+        self.messages = []
         self.data = {
             'branch': branch,
             'comments': [],
@@ -245,6 +250,19 @@
                  "uploader": {"name": "User Name"}}
         return event
 
+    def getChangeRestoredEvent(self):
+        event = {"type": "change-restored",
+                 "change": {"project": self.project,
+                            "branch": self.branch,
+                            "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
+                            "number": str(self.number),
+                            "subject": self.subject,
+                            "owner": {"name": "User Name"},
+                            "url": "https://hostname/3"},
+                 "restorer": {"name": "User Name"},
+                 "reason": ""}
+        return event
+
     def addApproval(self, category, value):
         approval = {'description': self.categories[category][0],
                     'type': category,
@@ -376,6 +394,7 @@
     def review(self, project, changeid, message, action):
         number, ps = changeid.split(',')
         change = self.changes[int(number)]
+        change.messages.append(message)
         if 'submit' in action:
             change.setMerged()
         if message:
@@ -956,6 +975,36 @@
         self.assertReportedStat(
             'zuul.pipeline.gate.org.project.total_changes', '1|c')
 
+    def test_duplicate_pipelines(self):
+        "Test that a change matching multiple pipelines works"
+        builds = self.worker.running_builds
+        history = self.worker.build_history
+
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        self.fake_gerrit.addEvent(A.getChangeRestoredEvent())
+        self.waitUntilSettled()
+
+        print builds
+        print A.messages
+
+        self.assertEmptyQueues()
+
+        assert len(history) == 2
+        history[0].name == 'project-test1'
+        history[1].name == 'project-test1'
+
+        assert len(A.messages) == 2
+        if 'dup1/project-test1' in A.messages[0]:
+            assert 'dup1/project-test1' in A.messages[0]
+            assert 'dup2/project-test1' not in A.messages[0]
+            assert 'dup1/project-test1' not in A.messages[1]
+            assert 'dup2/project-test1' in A.messages[1]
+        else:
+            assert 'dup1/project-test1' in A.messages[1]
+            assert 'dup2/project-test1' not in A.messages[1]
+            assert 'dup1/project-test1' not in A.messages[0]
+            assert 'dup2/project-test1' in A.messages[0]
+
     def test_parallel_changes(self):
         "Test that changes are tested in parallel and merged in series"
         builds = self.worker.running_builds
diff --git a/zuul/merger.py b/zuul/merger.py
index 7ad7eed..aaffa43 100644
--- a/zuul/merger.py
+++ b/zuul/merger.py
@@ -26,11 +26,16 @@
 class Repo(object):
     log = logging.getLogger("zuul.Repo")
 
-    def __init__(self, remote, local):
+    def __init__(self, remote, local, email, username):
         self.remote_url = remote
         self.local_path = local
         self._ensure_cloned()
         self.repo = git.Repo(self.local_path)
+        if email:
+            self.repo.config_writer().set_value('user', 'email', email)
+        if username:
+            self.repo.config_writer().set_value('user', 'name', username)
+        self.repo.config_writer().write()
 
     def _ensure_cloned(self):
         if not os.path.exists(self.local_path):
@@ -117,7 +122,8 @@
 class Merger(object):
     log = logging.getLogger("zuul.Merger")
 
-    def __init__(self, trigger, working_root, push_refs, sshkey):
+    def __init__(self, trigger, working_root, push_refs, sshkey, email,
+            username):
         self.trigger = trigger
         self.repos = {}
         self.working_root = working_root
@@ -126,6 +132,8 @@
         self.push_refs = push_refs
         if sshkey:
             self._makeSSHWrapper(sshkey)
+        self.email = email
+        self.username = username
 
     def _makeSSHWrapper(self, key):
         name = os.path.join(self.working_root, '.ssh_wrapper')
@@ -139,7 +147,8 @@
     def addProject(self, project, url):
         try:
             path = os.path.join(self.working_root, project.name)
-            repo = Repo(url, path)
+            repo = Repo(url, path, self.email, self.username)
+
             self.repos[project] = repo
         except:
             self.log.exception("Unable to initialize repo for %s" % project)
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index 8ff5a27..cf862e3 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -246,6 +246,16 @@
         else:
             merge_root = '/var/lib/zuul/git'
 
+        if self.config.has_option('zuul', 'git_user_email'):
+            merge_email = self.config.get('zuul', 'git_user_email')
+        else:
+            merge_email = None
+
+        if self.config.has_option('zuul', 'git_user_name'):
+            merge_name = self.config.get('zuul', 'git_user_name')
+        else:
+            merge_name = None
+
         if self.config.has_option('zuul', 'push_change_refs'):
             push_refs = self.config.getboolean('zuul', 'push_change_refs')
         else:
@@ -257,7 +267,7 @@
             sshkey = None
 
         self.merger = merger.Merger(self.trigger, merge_root, push_refs,
-                                    sshkey)
+                                    sshkey, merge_email, merge_name)
         for project in self.projects.values():
             url = self.trigger.getGitUrl(project)
             self.merger.addProject(project, url)