source: timingandestimationplugin/branches/trac0.12-Permissions/scripts/git_post_receive.py

Last change on this file was 11259, checked in by Russ Tyndall, 12 years ago

fixing indentation in git_post_receieve

Thanks for the patch, sorry for the bad changeset

fix #9774

  • Property svn:executable set to *
File size: 10.9 KB
Line 
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
26import os, os.path, sys,logging, getpass, optparse, re
27import subprocess, threading, time, errno
28from optparse import OptionParser
29from subprocess import PIPE
30
31TRAC_POST_COMMIT = "/home/ACCELERATION/russ/trac-dev/TandE/trac0.12/scripts/trac-post-commit.py"
32
33
34
35
36logdir=os.getenv("LOGDIR") or "/var/log/commit-hooks"
37log = logging.getLogger('gpr')
38
39## Fn to easy working with remote processes
40def 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
56def 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
103def 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
109def 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
115def 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",
134                "--pretty=tformat:%an <%ae>%n%ci%n%s%n%+b"],
135           stdin=PIPE, stdout=PIPE)
136    def w(ref) : grl.stdin.write(ref + "\n")
137
138    for (old,new,ref) in ref_updates :
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
142
143        #Include the newrev as now reachable.
144        w(new)
145
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)
156
157
158    log.debug("After discarding updates, writing %s prev_roots",
159          len(prev_roots))
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() :
173        commit.append(msg[:-1]) #-1 to strip one \n from the pair.
174        log.info("New commit: %r", commit)
175        return commit
176
177    while True :
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
184
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
196
197def post(commits, gitdir, cname, trac_env) :
198    for [rev,author,date,msg] in commits :
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)
212
213def process(gitdir, cname, trac_env, ref_updates) :
214    log.info("Push by %r; CNAME: %r, TRAC_ENV: %r, updating %s refs",
215         getpass.getuser(), cname, trac_env, len(updates))
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
225parser = OptionParser(""" """)
226parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
227          help="Show more verbose log messages.")
228
229
230if __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 :
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)
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',
254                      datefmt='%Y%m%d %H:%M:%S'))
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",
260                      datefmt='%H:%M:%S'))
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)
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.
Note: See TracBrowser for help on using the repository browser.