| [9633] | 1 | #!/usr/bin/env python |
|---|
| 2 | # |
|---|
| 3 | # This script is run after receive-pack has accepted a pack and the |
|---|
| 4 | # repository has been updated. It is passed arguments in through stdin |
|---|
| 5 | # in the form |
|---|
| 6 | # <oldrev> <newrev> <refname> |
|---|
| 7 | # For example: |
|---|
| 8 | # aa453216d1b3e49e7f6f98441fa56946ddcd6a20 68f7abf4e6f922807889f52bc043ecd31b79f814 refs/heads/master |
|---|
| 9 | |
|---|
| 10 | # GOAL: Prevent patches that have appeared in one branch from |
|---|
| 11 | # reposting to trac when they are moved to another branch |
|---|
| 12 | # (this was causing duplicate comments / time from topic branches |
|---|
| 13 | # being merged into main |
|---|
| 14 | |
|---|
| 15 | # This specific script will query the repository trying to isolate what |
|---|
| 16 | # in this receive is a new commit that the repository has not yet |
|---|
| 17 | # seen. It does this by a big call to git rev-parse, including revs |
|---|
| 18 | # that are now reachable, excluding everything else (tags, heads, |
|---|
| 19 | # oldrevs). |
|---|
| 20 | |
|---|
| 21 | # http://www.kernel.org/pub/software/scm/git/docs/git-rev-list.html |
|---|
| 22 | |
|---|
| 23 | # Once it has isolated what is new it posts those to trac. |
|---|
| 24 | |
|---|
| 25 | |
|---|
| 26 | import os, os.path, sys,logging, getpass, optparse, re |
|---|
| 27 | import subprocess, threading, time, errno |
|---|
| 28 | from optparse import OptionParser |
|---|
| 29 | from subprocess import PIPE |
|---|
| 30 | |
|---|
| 31 | TRAC_POST_COMMIT = "/home/ACCELERATION/russ/trac-dev/TandE/trac0.12/scripts/trac-post-commit.py" |
|---|
| 32 | |
|---|
| 33 | |
|---|
| 34 | |
|---|
| 35 | |
|---|
| 36 | logdir=os.getenv("LOGDIR") or "/var/log/commit-hooks" |
|---|
| 37 | log = logging.getLogger('gpr') |
|---|
| 38 | |
|---|
| 39 | ## Fn to easy working with remote processes |
|---|
| 40 | def capturedCall(cmd, **kwargs) : |
|---|
| 41 | """Do the equivelent of the subprocess.call except |
|---|
| 42 | log the stderr and stdout where appropriate.""" |
|---|
| 43 | p= capturedPopen(cmd,**kwargs) |
|---|
| 44 | rc = p.wait() |
|---|
| 45 | #this is a cheap attempt to make sure the monitors |
|---|
| 46 | #are scheduled and hopefully finished. |
|---|
| 47 | time.sleep(0.01) |
|---|
| 48 | time.sleep(0.01) |
|---|
| 49 | return rc |
|---|
| 50 | |
|---|
| 51 | #be warned, if you see your pipelines hanging: |
|---|
| 52 | #http://old.nabble.com/subprocess.Popen-pipeline-bug--td16026600.html |
|---|
| 53 | #close_fds=True |
|---|
| 54 | |
|---|
| 55 | ## Fn to easy working with remote processes |
|---|
| 56 | def capturedPopen(cmd, stdin=None, stdout=None, stderr=None, |
|---|
| 57 | logger=log,cd=None, |
|---|
| 58 | stdout_level=logging.INFO, |
|---|
| 59 | stderr_level=logging.WARNING, **kwargs) : |
|---|
| 60 | """Equivalent to subprocess.Popen except log stdout and stderr |
|---|
| 61 | where appropriate. Also log the command being called.""" |
|---|
| 62 | #we use None as sigil values for stdin,stdout,stderr above so we |
|---|
| 63 | # can distinguish from the caller passing in Pipe. |
|---|
| 64 | if(logger): |
|---|
| 65 | #if we are logging, record the command we're running, |
|---|
| 66 | #trying to strip out passwords. |
|---|
| 67 | logger.debug("Running cmd: %s", |
|---|
| 68 | isinstance(cmd,str) and cmd |
|---|
| 69 | or subprocess.list2cmdline([i for i in cmd |
|---|
| 70 | if not i.startswith('-p')])) |
|---|
| 71 | |
|---|
| 72 | if cd : |
|---|
| 73 | #subprocess does this already with the cwd arg, |
|---|
| 74 | #convert cd over so as not to break anyone's. |
|---|
| 75 | kwargs['cwd']=cd |
|---|
| 76 | p = subprocess.Popen(cmd, stdin=stdin, |
|---|
| 77 | stdout=(stdout or (logger and PIPE)), |
|---|
| 78 | stderr=(stderr or (logger and PIPE)), |
|---|
| 79 | **kwargs) |
|---|
| 80 | if logger : |
|---|
| 81 | def monitor(level, src, name) : |
|---|
| 82 | lname = "%s.%s" % (cmd[0], name) |
|---|
| 83 | if(hasattr(logger, 'name')) : |
|---|
| 84 | lname = "%s.%s" % (logger.name, lname) |
|---|
| 85 | sublog = logging.getLogger(lname) |
|---|
| 86 | |
|---|
| 87 | def tfn() : |
|---|
| 88 | l = src.readline() |
|---|
| 89 | while l != "": |
|---|
| 90 | sublog.log(level,l.strip()) |
|---|
| 91 | l = src.readline() |
|---|
| 92 | |
|---|
| 93 | th = threading.Thread(target=tfn,name=lname) |
|---|
| 94 | p.__setattr__("std%s_thread" % name, th) |
|---|
| 95 | th.start() |
|---|
| 96 | |
|---|
| 97 | if stdout == None : monitor(stdout_level, p.stdout,"out") |
|---|
| 98 | if stderr == None : monitor(stderr_level, p.stderr,"err") |
|---|
| 99 | return p |
|---|
| 100 | |
|---|
| 101 | |
|---|
| 102 | |
|---|
| 103 | def gitPopen(gitdir, cmd, **kwargs) : |
|---|
| 104 | """Popen git with the given command and the git-dir given. kwargs |
|---|
| 105 | are passed onwards to popen.""" |
|---|
| 106 | cmd = ["git","--git-dir="+gitdir] + cmd |
|---|
| 107 | return capturedPopen(cmd, logger=log, **kwargs) |
|---|
| 108 | |
|---|
| 109 | def find_all_refs(gitdir) : |
|---|
| 110 | "Get a list of all ref names in the git database, i.e. any head or tag name" |
|---|
| 111 | git = gitPopen(gitdir, ["show-ref"], stdout=PIPE) |
|---|
| 112 | return set(line.split()[1] for line in git.stdout) |
|---|
| 113 | |
|---|
| 114 | |
|---|
| 115 | def new_commits(gitdir, ref_updates) : |
|---|
| 116 | """For the given gitdir and list of ref_updates (an array that |
|---|
| 117 | holds [oldrev,newrev,refname] arrays) find any commit that is new |
|---|
| 118 | to this repo. |
|---|
| 119 | |
|---|
| 120 | This works primarily by issuing a: |
|---|
| 121 | git rev-list new1 ^old1 new2 ^old2 ^refs/tags/foo ^refs/heads/bar |
|---|
| 122 | |
|---|
| 123 | This function yields commits that are new in the format: |
|---|
| 124 | [hash, author, date, message] |
|---|
| 125 | """ |
|---|
| 126 | #the set of previously reachable roots starts as a list of all |
|---|
| 127 | #refs currently known, which is post-receive so we will need to |
|---|
| 128 | #remove some from here. Everything left will become ^refs. |
|---|
| 129 | prev_roots = find_all_refs(gitdir) |
|---|
| 130 | log.debug("Found %s named refs", len(prev_roots)) |
|---|
| 131 | |
|---|
| 132 | #open the rev-list process and make a writer function to it. |
|---|
| 133 | grl = gitPopen(gitdir, ["rev-list","--reverse", "--stdin", |
|---|
| [10840] | 134 | "--pretty=tformat:%an <%ae>%n%ci%n%s%n%+b"], |
|---|
| 135 | stdin=PIPE, stdout=PIPE) |
|---|
| [9633] | 136 | def w(ref) : grl.stdin.write(ref + "\n") |
|---|
| 137 | |
|---|
| 138 | for (old,new,ref) in ref_updates : |
|---|
| [11259] | 139 | #branch deletion: newval is 00000, skip the ref, leave it in |
|---|
| 140 | #the list of prev_roots |
|---|
| 141 | if re.match("^0+$",new) : continue |
|---|
| [9633] | 142 | |
|---|
| [11259] | 143 | #Include the newrev as now reachable. |
|---|
| 144 | w(new) |
|---|
| [9633] | 145 | |
|---|
| [11259] | 146 | #a ref that is being updated should be removed from the |
|---|
| 147 | #previous list and ... |
|---|
| 148 | prev_roots.discard(ref) |
|---|
| 149 | #instead write out the negative line directly. However, if it |
|---|
| 150 | #is a new branch (denoted by all 0s) there is no negative to |
|---|
| 151 | #include for this ref. |
|---|
| 152 | if re.search("[1-9]",old) : |
|---|
| 153 | w("^" + old) |
|---|
| 154 | else : |
|---|
| 155 | log.info("New ref %r", ref) |
|---|
| [9633] | 156 | |
|---|
| 157 | |
|---|
| 158 | log.debug("After discarding updates, writing %s prev_roots", |
|---|
| [10840] | 159 | len(prev_roots)) |
|---|
| [9633] | 160 | #write lines for (not reachable from anything else') |
|---|
| 161 | for ref in prev_roots : w("^" + ref) |
|---|
| 162 | grl.stdin.close() |
|---|
| 163 | |
|---|
| 164 | ### this is a little parser for the format |
|---|
| 165 | #commit <hash> |
|---|
| 166 | #<Author> |
|---|
| 167 | #<Date> |
|---|
| 168 | #<msg> |
|---|
| 169 | #<blank line> |
|---|
| 170 | commit = None |
|---|
| 171 | msg = "" |
|---|
| 172 | def finish() : |
|---|
| [11259] | 173 | commit.append(msg[:-1]) #-1 to strip one \n from the pair. |
|---|
| 174 | log.info("New commit: %r", commit) |
|---|
| 175 | return commit |
|---|
| [9633] | 176 | |
|---|
| 177 | while True : |
|---|
| [11259] | 178 | line = grl.stdout.readline() |
|---|
| 179 | #blank line and exit code set, we're done here. |
|---|
| 180 | if line == '' and grl.poll() != None : |
|---|
| 181 | if commit: yield finish() |
|---|
| 182 | log.debug("Exiting loop: %s", grl.poll()) |
|---|
| 183 | break |
|---|
| [9633] | 184 | |
|---|
| [11259] | 185 | m = re.match("commit ([0-9a-f]+)$", line) |
|---|
| 186 | if m : #start of a new commit |
|---|
| 187 | if commit: yield finish() |
|---|
| 188 | log.debug("Starting new commit: %s", m.group(1)) |
|---|
| 189 | hash = m.group(1) |
|---|
| 190 | author = grl.stdout.readline().strip() |
|---|
| 191 | date = grl.stdout.readline().strip() |
|---|
| 192 | commit = [hash,author,date] |
|---|
| 193 | msg = grl.stdout.readline() |
|---|
| 194 | else : |
|---|
| 195 | msg += line |
|---|
| [9633] | 196 | |
|---|
| 197 | def post(commits, gitdir, cname, trac_env) : |
|---|
| 198 | for [rev,author,date,msg] in commits : |
|---|
| [11259] | 199 | #this subprocess uses python logging with the same formatter, |
|---|
| 200 | #so tell it not to log, and pass through our streams and its |
|---|
| 201 | #logging should just fall in line. |
|---|
| 202 | log.debug("Posting %s to trac", rev) |
|---|
| 203 | capturedCall(["python", TRAC_POST_COMMIT, |
|---|
| 204 | "-p", trac_env or "", |
|---|
| 205 | "-r", rev, |
|---|
| 206 | "-u", author, |
|---|
| 207 | "-m", msg, |
|---|
| 208 | cname], |
|---|
| 209 | logger=None, |
|---|
| 210 | stdout=sys.stdout, |
|---|
| 211 | stderr=sys.stderr) |
|---|
| [9633] | 212 | |
|---|
| 213 | def process(gitdir, cname, trac_env, ref_updates) : |
|---|
| 214 | log.info("Push by %r; CNAME: %r, TRAC_ENV: %r, updating %s refs", |
|---|
| [10840] | 215 | getpass.getuser(), cname, trac_env, len(updates)) |
|---|
| [9633] | 216 | |
|---|
| 217 | post(new_commits(gitdir,ref_updates), gitdir, cname, trac_env) |
|---|
| 218 | log.info("Finished commit hook loop, git-post-receive") |
|---|
| 219 | |
|---|
| 220 | |
|---|
| 221 | |
|---|
| 222 | ################################################################# |
|---|
| 223 | #### Runtime control |
|---|
| 224 | |
|---|
| 225 | parser = OptionParser(""" """) |
|---|
| 226 | parser.add_option("-v", "--verbose", action="store_true", dest="verbose", |
|---|
| [10840] | 227 | help="Show more verbose log messages.") |
|---|
| [9633] | 228 | |
|---|
| 229 | |
|---|
| 230 | if __name__ == "__main__": |
|---|
| 231 | (options, args) = parser.parse_args() |
|---|
| 232 | |
|---|
| 233 | #when run as a hook the directory is the git repo. |
|---|
| 234 | #either /var/git/ServerManagement.git |
|---|
| 235 | #or /var/git/ServerManagement/.git |
|---|
| 236 | gitdir = os.getcwd() |
|---|
| 237 | |
|---|
| 238 | cname = os.getenv("CNAME") |
|---|
| 239 | if cname == None : |
|---|
| [11259] | 240 | if len(args) >= 1 : |
|---|
| 241 | cname = args.pop(0) |
|---|
| 242 | else : |
|---|
| 243 | #strip off .git if it is bare or /.git if it is a checkout. |
|---|
| 244 | cname = re.sub("/?\.git$","", gitdir) |
|---|
| 245 | cname = os.path.basename(cname) |
|---|
| [9633] | 246 | TRAC_ENV = os.getenv("TRAC_ENV") or os.path.join("/var/trac/",cname) |
|---|
| 247 | |
|---|
| 248 | #### Logging configuration |
|---|
| 249 | log.setLevel(logging.DEBUG) |
|---|
| 250 | ## log verbosely to a file |
|---|
| 251 | logfile=os.path.join(logdir, "%s.git-post-receive.log" % cname) |
|---|
| 252 | fh = logging.FileHandler(logfile,mode='a') |
|---|
| 253 | fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)-8s %(message)s', |
|---|
| [10840] | 254 | datefmt='%Y%m%d %H:%M:%S')) |
|---|
| [9633] | 255 | |
|---|
| 256 | ## and to standard error keep the level higher |
|---|
| 257 | sh = logging.StreamHandler() |
|---|
| 258 | sh.setLevel(options.verbose and logging.DEBUG or logging.INFO) |
|---|
| 259 | sh.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)-8s %(message)s", |
|---|
| [10840] | 260 | datefmt='%H:%M:%S')) |
|---|
| [9633] | 261 | |
|---|
| 262 | log.addHandler(sh) |
|---|
| 263 | log.addHandler(fh) |
|---|
| 264 | log.info("----- git-post-receive.py -----") |
|---|
| 265 | |
|---|
| 266 | #Where will we be posting to? |
|---|
| 267 | if not os.path.exists(TRAC_ENV) : |
|---|
| 268 | logging.warn("None existant trac_env: %s", TRAC_ENV) |
|---|
| 269 | TRAC_ENV = None |
|---|
| 270 | #actually read the ref updates from stdin |
|---|
| 271 | updates = [line.split() for line in sys.stdin] |
|---|
| 272 | process(gitdir, cname, TRAC_ENV, updates) |
|---|
| [9762] | 273 | |
|---|
| 274 | # # The MIT License |
|---|
| 275 | |
|---|
| 276 | # # Copyright (c) 2010 Acceleration.net |
|---|
| 277 | |
|---|
| 278 | # # Permission is hereby granted, free of charge, to any person obtaining a copy |
|---|
| 279 | # # of this software and associated documentation files (the "Software"), to deal |
|---|
| 280 | # # in the Software without restriction, including without limitation the rights |
|---|
| 281 | # # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|---|
| 282 | # # copies of the Software, and to permit persons to whom the Software is |
|---|
| 283 | # # furnished to do so, subject to the following conditions: |
|---|
| 284 | |
|---|
| 285 | # # The above copyright notice and this permission notice shall be included in |
|---|
| 286 | # # all copies or substantial portions of the Software. |
|---|
| 287 | |
|---|
| 288 | # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|---|
| 289 | # # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|---|
| 290 | # # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|---|
| 291 | # # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|---|
| 292 | # # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|---|
| 293 | # # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|---|
| 294 | # # THE SOFTWARE. |
|---|