Merge pull request #3 from damien-list/master
[git-mirror.git] / git_mirror.py
index 0fe699e40adb3ee40a067395550bd16744fcc6c7..6b7277592d7cc35ccbb8dd978ae66e8982bc8e94 100644 (file)
@@ -27,6 +27,18 @@ import hmac, hashlib
 import email.mime.text, email.utils, smtplib
 
 mail_sender = "null@localhost"
+config_file = os.path.join(os.path.dirname(__file__), 'git-mirror.conf')
+
+def Popen_quirky(cmd, **args):
+    '''
+    Runs cmd via subprocess.Popen; and if that fails, puts it into the shell (/bin/sh).
+    It seems that's what executing things in bash does, and even execve.  Also,
+    all so-far released versions of Gitolite get the shebang line wrong.
+    '''
+    try:
+        return subprocess.Popen(cmd, **args)
+    except OSError as e:
+        return subprocess.Popen(['/bin/sh'] + cmd, **args)
 
 class GitCommand:
     def __getattr__(self, name):
@@ -40,7 +52,7 @@ class GitCommand:
                 assert stderr is None
                 code = p.returncode
                 if check and code:
-                    raise Exception("Error running {0}: Non-zero exit code".format(cmd))
+                    raise Exception("Error running {}: Non-zero exit code".format(cmd))
             return (stdout.decode('utf-8').strip('\n'), code)
         return call
 
@@ -53,10 +65,10 @@ def git_is_forced_update(oldsha, newsha):
     assert code in (0, 1)
     return False if code == 0 else True # if oldsha is an ancestor of newsha, then this was a "good" (non-forced) update
 
-def read_config(fname, defSection = 'DEFAULT'):
+def read_config(defSection = 'DEFAULT'):
     '''Reads a config file that may have options outside of any section.'''
     config = configparser.ConfigParser()
-    with open(fname) as file:
+    with open(config_file) as file:
         stream = itertools.chain(("["+defSection+"]\n",), file)
         config.read_file(stream)
     return config
@@ -83,7 +95,7 @@ class Repo:
         self.name = name
         self.local = conf['local']
         self.owner = conf['owner'] # email address to notify in case of problems
-        self.hmac_secret = conf['hmac-secret'].encode('utf-8')
+        self.hmac_secret = conf['hmac-secret'].encode('utf-8') if 'hmac-secret' in conf else None
         self.deploy_key = conf['deploy-key'] # the SSH ky used for authenticating against remote hosts
         self.mirrors = {} # maps mirrors to their URLs
         mirror_prefix = 'mirror-'
@@ -93,9 +105,10 @@ class Repo:
     
     def mail_owner(self, msg):
         global mail_sender
-        send_mail("git-mirror {0}".format(self.name), msg, recipients = [self.owner], sender = mail_sender)
+        send_mail("git-mirror {}".format(self.name), msg, recipients = [self.owner], sender = mail_sender)
 
     def compute_hmac(self, data):
+        assert self.hmac_secret is not None
         h = hmac.new(self.hmac_secret, digestmod = hashlib.sha1)
         h.update(data)
         return h.hexdigest()
@@ -125,7 +138,7 @@ class Repo:
         for mirror in self.mirrors:
             if mirror == source_mirror:
                 continue
-            sys.stdout.write("Updating mirror {0}\n".format(mirror)); sys.stdout.flush()
+            sys.stdout.write("Updating mirror {}\n".format(mirror)); sys.stdout.flush()
             # update this mirror
             if is_forced:
                 # forcibly update ref remotely (someone already did a force push and hence accepted data loss)
@@ -144,16 +157,17 @@ class Repo:
             remote_sha = remote_state.split()[0]
         else:
             remote_sha = git_nullsha
-        assert newsha == remote_sha, "Someone lied about the new SHA, which should be {0}.".format(newsha)
+        assert newsha == remote_sha, "Someone lied about the new SHA, which should be {}.".format(newsha)
         # locally, we have to be at oldsha or newsha (the latter can happen if we already got this update, e.g. if it originated from us)
         local_state, code = git.show_ref(ref, check=False)
         if code == 0:
             local_sha = local_state.split()[0]
         else:
             if len(local_state):
-                raise Exception("Something went wrong getting the local state of {0}.".format(ref))
+                raise Exception("Something went wrong getting the local state of {}.".format(ref))
             local_sha = git_nullsha
-        assert local_sha in (oldsha, newsha), "Someone lied about the old SHA."
+        # some sanity checking, but deal gracefully with new branches appearing
+        assert local_sha in (git_nullsha, oldsha, newsha), "Someone lied about the old SHA: Local ({}) is neither old ({}) nor new ({})".format(local_sha, oldsha, newsha)
         # if we are already at newsha locally, we also ran the local hooks, so we do not have to do anything
         if local_sha == newsha:
             return "Local repository is already up-to-date."
@@ -174,11 +188,11 @@ class Repo:
         # Now run the post-receive hooks. This will *also* push the changes to all mirrors, as we
         # are one of these hooks!
         os.putenv("GIT_MIRROR_SOURCE", mirror) # tell ourselves which repo we do *not* have to update
-        with subprocess.Popen(['/bin/sh', 'hooks/post-receive'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as p:
-            (stdout, stderr) = p.communicate("{0} {1} {2}\n".format(oldsha, newsha, ref).encode('utf-8'))
+        with Popen_quirky(['hooks/post-receive'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as p:
+            (stdout, stderr) = p.communicate("{} {} {}\n".format(oldsha, newsha, ref).encode('utf-8'))
             stdout = stdout.decode('utf-8')
             if p.returncode:
-                raise Exception("post-receive git hook terminated with non-zero exit code {0}:\n{1}".format(p.returncode, stdout))
+                raise Exception("post-receive git hook terminated with non-zero exit code {}:\n{}".format(p.returncode, stdout))
         return stdout
 
 def find_repo_by_directory(repos, dir):
@@ -189,8 +203,7 @@ def find_repo_by_directory(repos, dir):
 
 def load_repos():
     global mail_sender
-    conffile = os.path.join(os.path.dirname(__file__), 'git-mirror.conf')
-    conf = read_config(conffile)
+    conf = read_config()
     mail_sender = conf['DEFAULT']['mail-sender']
     
     repos = {}