00001
00002 """
00003
00004 Usage examples
00005 ~~~~~~~~~~~~~~
00006
00007 ::
00008
00009 ./dbsvn.py --help
00010 ## full list of options and this help text
00011
00012 ./dbsvn.py ~/catdir -M
00013 ## check catalog and skip commit message test
00014
00015 ./dbsvn.py ~/catdir -m "test commit message dybsvn:source:dybgaudi/trunk/CalibWritingPkg/DBUPDATE.txt@12000 "
00016 ## check catalog and commit message
00017
00018 This script performs basic validations of SVN commits intended to lead to DB updates,
00019 it is used in two situations:
00020
00021 #. On the SVN server as part of the pre-commit hook that allows/denies the commit
00022 #. On the client, to allow testing of an intended commit before actually attempting the commit as shown above
00023
00024 NB this script DOES NOT perform commits, it only verifies them
00025
00026
00027 How this script fits into the workflow
00028 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
00029
00030 ::
00031
00032 cd ; svn co http://dayabay.ihep.ac.cn/svn/dybaux/catalog/tmp_offline_db
00033 ## TEMORARILY DYBAUX NEEDS SOME CLEAN UP ... SO USE THE BELOW URL
00034
00035 cd ; svn co http://dayabay.phys.ntu.edu.tw/repos/newtest/catalog/tmp_offline_db/
00036 ## check out catalog as left by the last updater
00037
00038 ./db.py offline_db rdumpcat ~/tmp_offline_db
00039 ## rdumpcat current offline_db on top of the SVN checkout and look for diffs
00040
00041 svn diff ~/tmp_offline_db
00042 ## COMPLAIN LOUDLY IF YOU SEE DIFFS HERE BEFORE YOU MAKE ANY UPDATES
00043
00044 ./db.py tmp_joe_offline_db rdumpcat ~/tmp_offline_db ## NB name switch
00045 ## write DBI catalog on top of working copy ~/tmp_offline_db
00046
00047 svn diff ~/tmp_offline_db
00048 ## see if changed files are as you expect
00049
00050 ./dbsvn.py ~/tmp_offline_db
00051 ## use this script to check the "svn diff" to see if looks like a valid DBI update
00052
00053 ./dbsvn.py ~/tmp_offline_db -m "Updating dybsvn:source:dybgaudi/trunk/CalibWritingPkg/DBUPDATE.txt@12000 "
00054 ## fails as annotation link refers to dummy path, no such package and no change to that file at that revision
00055
00056 ./dbsvn.py ~/tmp_offline_db -m "Annotation link dybsvn:source:dybgaudi/trunk/Database/DybDbiTest/tests/README "
00057 ## check the "svn diff" and intended commit message, fails as no revision
00058
00059 ./dbsvn.py ~/tmp_offline_db -m "Annotation link dybsvn:source:dybgaudi/trunk/Database/DybDbiTest/tests/README@10000 "
00060 ## fails as no change to that file at that revision
00061
00062 ./dbsvn.py ~/tmp_offline_db -m "Annotation link dybsvn:source:dybgaudi/trunk/Database/DybDbiTest/tests/README@9716 "
00063 ## succeeds
00064
00065 svn ci ~/tmp/offline_db -m "Updating dybsvn:source:dybgaudi/trunk/CalibWritingPkg/DBUPDATE.txt@12000 "
00066 ## attempt the actual commit
00067
00068
00069 What is validated by :file:`dbsvn.py`
00070 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
00071
00072 #. The commit message, eg "Updating dybsvn:source:dybgaudi/trunk/CalibWritingPkg/DBUPDATE.txt@12000 "
00073
00074 #. must provide valid dybsvn reference which includes dybgaudi/trunk package path and revision number
00075
00076 #. Which files (which represent tables) are changed
00077
00078 #. author must have permission for these files/tables
00079 #. change must effect DBI file/tablepairs (payload, validity)
00080
00081 #. What changes are made:
00082
00083 #. must be additions/subtractions only (allowing subtractions is for revertions)
00084 #. note that LOCALSEQNO (a DBI bookkeeping table) is a special case
00085
00086
00087 Rationale behind these validations
00088 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
00089
00090 #. valid DBI updates
00091 #. establish provenance and purpose
00092
00093 #. what purpose for the update
00094 #. where it comes from (which revision of which code was used)
00095 #. precise link to producing code and documentation
00096
00097
00098 Commit denial
00099 ~~~~~~~~~~~~~~
00100
00101 This script is invoked on the SVN server by the pre-commit hook (shown below)
00102 if any directories changed by the commit start with "catalog/".
00103 If this script exits normally with zero return code, the commit is
00104 allowed to proceed.
00105
00106 On the other hand, if this script returns a non-zero exit code,
00107 for example if an assert is tickled, then the commit is denied and stderr
00108 is returned to the failed committer.
00109
00110
00111 OVERRIDE commits
00112 ~~~~~~~~~~~~~~~~~
00113
00114 Administrators (configured using -X option on the server) can
00115 use the string "OVERRIDE" in commit messages to short circuit validation.
00116 This is needed for non-standard operations, currently:
00117
00118 #. adding/removing tables
00119
00120 A commit like the below from inside catalog will fail, assuming that the **dayabay** svn identity
00121 is not on the admin list::
00122
00123 svn --username dayabay ci -m "can dayabay use newtest OVERRIDE "
00124
00125
00126
00127 Deployment of pre-commit hook on SVN server
00128 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
00129
00130 Only SVN repository administrators need to understand this section.
00131
00132 The below commands are an example of creating a bash pre-commit wrapper.
00133 After changing the TARGET and apache user identity, the commands can be
00134 used to prepare the hook.
00135 Note that the pre-commit script is invoked by the server in a bare environment,
00136 so any customizations must be propagated in, deploy the code::
00137
00138 cd ; svn co http://dayabay.ihep.ac.cn/svn/dybsvn/dybgaudi/trunk/DybPython/python/DybPython
00139 export TARGET=/var/scm/repos/newtest/hooks/pre-commit
00140 sudo bash -c "cp $HOME/DybPython/{dbsvn,svndiff}.py $(dirname $TARGET)/ && chown nobody.nobody $(dirname $TARGET)/{dbsvn,svndiff}.py "
00141
00142 DBSVN_XREF=/var/scm/svn/dybsvn python dbsvn.py HOOK ## check the hook is customized as desired
00143 DBSVN_XREF=/var/scm/svn/dybsvn python dbsvn.py HOOK | sudo bash -c "cat - > $TARGET && chmod ugo+x $TARGET && chown nobody.nobody $TARGET "
00144
00145
00146 A pre-commit hook testing harness is available in bash functions :env:`trunk/svn/svnprecommit.bash`
00147
00148
00149 Typical Problems with the Hook
00150 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
00151
00152 **Mainly for admins**
00153
00154 If the precommit hook is mis-configured the likely result is that attempts to
00155 commit will hang. For example the :file:`dbsvn.py` invokation in the hook script needs to have:
00156
00157 #. a valid admin user (SVN identity)
00158 #. local filesystem repository path for the cross reference `-r` option
00159
00160 The default cross reference path is the dybsvn URL which might hang on the server
00161 as the user(root/nobody/...) that runs the SVN repository normally does not
00162 have user permissions to access sibling repository dybsvn. (have switched to non-interactive now)
00163
00164
00165
00166 """
00167
00168
00169 import sys, os, re
00170 from svndiff import Diff, SVN, SVNLook
00171
00172 log = lambda msg:sys.stderr.write(msg+"\n")
00173 cmd = lambda _:os.popen(_).read().rstrip()
00174
00175 class Hook(dict):
00176 tmpl = """#!/bin/bash
00177 export LD_LIBRARY_PATH=%(LD_LIBRARY_PATH)s
00178 export SVNLOOK=%(SVNLOOK)s
00179
00180 dbi=0
00181 dirs=$(%(SVNLOOK)s dirs-changed $1 --transaction $2 )
00182 for dir in $dirs ; do
00183 case $dir in
00184 catalog*) dbi=1 ;;
00185 esac
00186 echo "dir $dir dbi $dbi " >&2
00187 done
00188
00189 # skip DBI validation if none of the dirs-changed start with "catalog"
00190 [ "$dbi" == "0" ] && exit 0
00191
00192 %(PYTHON)s $(dirname $0)/dbsvn.py $* -X %(USER)s -r %(XREF)s
00193 exit $?
00194 """
00195 def __init__(self, *args, **kwargs):
00196 dict.__init__(self, *args, **kwargs)
00197 self.update( LD_LIBRARY_PATH=os.environ['LD_LIBRARY_PATH'], SVNLOOK=cmd("which svnlook"), PYTHON=cmd("which python"), USER=os.getlogin(), XREF=os.environ.get('DBSVN_XREF','http://dayabay.ihep.ac.cn/svn/dybsvn') )
00198 __str__ = lambda self:self.tmpl % self
00199
00200
00201
00202
00203
00204 class Msg(dict):
00205 """
00206 A valid DBI update commit message contains a link of the form::
00207
00208 dybsvn:source:dybgaudi/trunk/Database/DybDbiTest/tests/README@9716
00209
00210 The link provides crucial association between a DB update
00211 and the package and revision of the code used in the preparation
00212 and invokation of the update.
00213
00214 Dummy links will be denied, a real path that was modified at the specified
00215 revision is required.
00216
00217 """
00218 baseurl="http://dayabay.ihep.ac.cn/svn/dybsvn"
00219 tracurl="http://dayabay.ihep.ac.cn/tracs/dybsvn"
00220 _ptn = re.compile("(?P<link>dybsvn:source:(?P<path>dybgaudi/trunk/(?P<relpath>\S*))\@(?P<rev>\d*))\s*")
00221 logurl = property(lambda self:os.path.join(self.tracurl,"log", self['path'] ))
00222
00223 def validate_annotation_xref( self, refpath ):
00224 """
00225 Check that the annotation link points to a valid changed annotation file in xref repository
00226 """
00227 revision = self.get('rev', None)
00228 path = self.get('path', None)
00229 assert revision and path , "failed to find annotation link path and revision in commit message "
00230
00231 if refpath.startswith("http:"):
00232 xmd = SVN( path=refpath , revision=revision )
00233 elif os.path.exists(refpath):
00234 xmd = SVNLook( repo_path=refpath , revision=revision )
00235 else:
00236 assert 0, "refpath is invalid %s " % refpath
00237 changed = xmd.changed
00238
00239 errmsg = lambda:"\n".join([
00240 "INVALID COMMIT MESSAGE",
00241 "referenced repository path \"%s\" was not touched by revision \"%s\"" % ( path, revision) ,
00242 "the below paths were changed in this revision... ",] + changed +
00243 ["", "check trac url %s to find valid links/revisions " % self.logurl ,"" ] )
00244 assert path in changed, errmsg()
00245
00246 def __init__(self, msg ):
00247 dict.__init__(self)
00248 self.msg = msg
00249 m = self._ptn.search( msg )
00250 assert m, "INVALID COMMIT MESSAGE %s " % self.msg + self.__doc__
00251 self.update( m.groupdict() )
00252
00253 def __repr__(self):
00254 return "Msg %s %r " % ( self.msg, dict.__repr__(self) )
00255
00256
00257 class DBIValidate(list):
00258 """
00259 Basic validation of commit that represents an intended DB update
00260 """
00261 def __init__(self, diff, msg , author ):
00262 self.diff = diff
00263 self.msg = msg
00264 self.author = author
00265 self.tabledict = dict([(c.name,c.smry) for c in self.diff.children])
00266
00267 tables = property(lambda self:[c.name for c in self.diff.children])
00268 exts = property(lambda self:[c.ext for c in self.diff.children])
00269
00270 def validate_msg(self):
00271 pass
00272
00273 def validate_update( self):
00274 """
00275 Current checks do not verify tail addition
00276 """
00277 tdict = self.tabledict
00278 tabs = tdict.keys()
00279 print tabs
00280 print tdict
00281
00282 assert 'LOCALSEQNO' in tabs, "No LOCALSEQNO in %s " % tabs
00283 assert tdict['LOCALSEQNO'] == "-+", "Unexpected LOCALSEQNO change %r " % tdict
00284 tabs = filter(lambda t:t != 'LOCALSEQNO', tabs )
00285 assert len(tabs) % 2 == 0 , "An even number of changed tables is required %r " % tabs
00286 vlds = filter( lambda t:t[-3:] == 'Vld' , tabs )
00287 assert len(vlds) == len(tabs)/2 , "Need equal number of payload and validity table changes "
00288 for vld in vlds:
00289 pay = vld[:-3]
00290 assert pay in tabs, "Vld table %s is not paired " % vld
00291
00292 def __call__(self):
00293 self.validate_msg()
00294 self.validate_update()
00295
00296
00297 def main():
00298 from optparse import OptionParser
00299 op = OptionParser(usage=__doc__ )
00300 op.add_option("-v", "--verbose", action="store_true" )
00301 op.add_option("-m", "--message", help="Commit message to be validated, client side only. Default %default " )
00302 op.add_option("-r", "--refpath", help="URL or filesystem path of cross reference checking repository. Default %default " )
00303 op.add_option("-M", "--no-message-chk", action="store_true", help="Skip Commit message check, client side only. Default %default " )
00304 op.add_option("-a", "--author", help="Author identity, client side only. Default %default " )
00305 op.add_option("-X", "--admins", help="Comma separated list of SVN identity names of admins, server side. Default %default " )
00306 op.set_defaults( verbose=False, message="no-message", author="unknown", admins="", no_message_chk=False , refpath="http://dayabay.ihep.ac.cn/svn/dybsvn" )
00307 (opts_ , args) = op.parse_args()
00308 opts = vars(opts_)
00309 if len(args)==0:args = [os.getcwd()]
00310
00311 if len(args) == 1:
00312 if args[0] == "HOOK":
00313 print Hook()
00314 sys.exit(0)
00315 cmd = SVN(path=args[0], msg=opts['message'], author=opts['author'] )
00316 elif len(args) == 2:
00317 cmd = SVNLook(repo_path=args[0], txn_name=args[1], )
00318 else:
00319 print __doc__
00320 sys.exit(1)
00321
00322 admins = opts['admins'].split(",")
00323
00324
00325 author = cmd.author
00326 msg_ = cmd.msg
00327
00328
00329
00330
00331 if msg_.find("OVERRIDE") > -1:
00332 if author in admins:
00333 sys.exit(0)
00334 else:
00335 log("your identity %r is not in the admin users list %r so you cannot use the OVERRIDE control " % (author, admins) )
00336 sys.exit(1)
00337
00338
00339 lines = cmd.diff.split("\n")
00340
00341 if not opts['no_message_chk']:
00342 msg = Msg( msg_ )
00343 log("commit msg: %r author: %r admins: %r " % (msg,author,admins) )
00344 msg.validate_annotation_xref( opts['refpath'] )
00345 else:
00346 msg = None
00347
00348
00349
00350 maxl = 1000
00351 for i,line in enumerate(lines[0:maxl]):
00352 log("[%2d] %s " % (i+1, line))
00353 if len(lines) > maxl:
00354 log("TOTAL of %d lines truncated to max %d " %( len(lines), maxl) )
00355
00356
00357 diff = Diff(lines)
00358 diff.dump()
00359
00360 dbiv = DBIValidate( diff, msg, author )
00361 dbiv()
00362
00363 if __name__=='__main__':
00364 main()
00365
00366