X-Git-Url: https://git.ralfj.de/git-mirror.git/blobdiff_plain/6843793205d96449af070b5c7b776eaf205f5ad7..d1fcac9c35289f6fac7ad302d41dba33467ceb9d:/git_mirror.py diff --git a/git_mirror.py b/git_mirror.py index af963df..16a8896 100644 --- a/git_mirror.py +++ b/git_mirror.py @@ -22,7 +22,8 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #============================================================================== import sys, os, os.path, subprocess -import configparser, itertools, json, re +import configparser, itertools, re +import hmac, hashlib import email.mime.text, email.utils, smtplib mail_sender = "null@localhost" @@ -34,7 +35,7 @@ class GitCommand: If is true, throw an exception of the process fails with non-zero exit code. Otherwise, do not. In any case, return a pair of the captured output and the exit code.''' cmd = ["git", name.replace('_', '-')] + list(args) - with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT if capture_stderr else None) as p: + with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT if capture_stderr else sys.stderr) as p: (stdout, stderr) = p.communicate() assert stderr is None code = p.returncode @@ -76,21 +77,13 @@ def send_mail(subject, text, recipients, sender, replyTo = None): s.sendmail(sender, recipients, msg.as_string()) s.quit() -def get_github_payload(): - '''Reeturn the github-style JSON encoded payload (as if we were called as a github webhook)''' - try: - data = sys.stdin.buffer.read() - data = json.loads(data.decode('utf-8')) - return data - except: - return {} # nothing read - class Repo: def __init__(self, name, conf): '''Creates a repository from a section of the git-mirror configuration file''' 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.deploy_key = conf['deploy-key'] # the SSH ky used for authenticating against remote hosts self.mirrors = {} # maps mirrors to their URLs mirror_prefix = 'mirror-' @@ -101,6 +94,11 @@ 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) + + def compute_hmac(self, data): + h = hmac.new(self.hmac_secret, digestmod = hashlib.sha1) + h.update(data) + return h.hexdigest() def find_mirror_by_url(self, match_urls): for mirror, url in self.mirrors.items(): @@ -111,28 +109,30 @@ class Repo: def setup_env(self): '''Setup the environment to work with this repository''' os.chdir(self.local) - ssh_set_ident = os.path.join(os.path.dirname(__file__), 'ssh-set-ident.conf') - os.setenv('GIT_SSH', ssh_set_ident) + ssh_set_ident = os.path.join(os.path.dirname(__file__), 'ssh-set-ident.sh') + os.putenv('GIT_SSH', ssh_set_ident) ssh_ident = os.path.join(os.path.expanduser('~/.ssh'), self.deploy_key) - os.setenv('SSH_IDENT', ssh_ident) + os.putenv('GIT_MIRROR_SSH_IDENT', ssh_ident) - def update_mirrors(self, ref, oldsha, newsha, except_mirrors = [], suppress_stderr = False): + def update_mirrors(self, ref, oldsha, newsha): '''Update the from to on all mirrors. The update must already have happened locally.''' assert len(oldsha) == 40 and len(newsha) == 40, "These are not valid SHAs." + source_mirror = os.getenv("GIT_MIRROR_SOURCE") # in case of a self-call via the hooks, we can skip one of the mirrors self.setup_env() # check for a forced update is_forced = newsha != git_nullsha and oldsha != git_nullsha and git_is_forced_update(oldsha, newsha) # tell all the mirrors for mirror in self.mirrors: - if mirror in except_mirrors: + if mirror == source_mirror: continue + sys.stdout.write("Updating mirror {0}\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) - git.push('--force', self.mirrors[mirror], newsha+":"+ref, capture_stderr = suppress_stderr) + git.push('--force', self.mirrors[mirror], newsha+":"+ref) else: # nicely update ref remotely (this avoids data loss due to race conditions) - git.push(self.mirrors[mirror], newsha+":"+ref, capture_stderr = suppress_stderr) + git.push(self.mirrors[mirror], newsha+":"+ref) def update_ref_from_mirror(self, ref, oldsha, newsha, mirror, suppress_stderr = False): '''Update the local version of this to what's currently on the given . and are checked. Then update all the other mirrors.''' @@ -156,7 +156,7 @@ class Repo: assert local_sha in (oldsha, newsha), "Someone lied about the old SHA." # 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 + return "Local repository is already up-to-date." # update local state from local_sha to newsha. if newsha != git_nullsha: # We *could* now fetch the remote ref and immediately update the local one. However, then we would have to @@ -171,8 +171,15 @@ class Repo: # ref does not exist anymore. delete it. assert local_sha != git_nullsha, "Why didn't we bail out earlier if there is nothing to do...?" git.update_ref("-d", ref, local_sha) # this checks that the old value is still local_sha - # update all the mirrors - self.update_mirrors(ref, oldsha, newsha, [mirror], suppress_stderr) + # 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(['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')) + 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)) + return stdout def find_repo_by_directory(repos, dir): for (name, repo) in repos.items(): @@ -184,7 +191,7 @@ def load_repos(): global mail_sender conffile = os.path.join(os.path.dirname(__file__), 'git-mirror.conf') conf = read_config(conffile) - mail_sender = conf['mail-sender'] + mail_sender = conf['DEFAULT']['mail-sender'] repos = {} for name, section in conf.items():