# # SVN.py - Subversion interface # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: SVN.py 1944 2007-08-24 23:38:42Z vss $ # $HeadURL: https://127.0.0.1/ditrack/src/tags/0.7/DITrack/SVN.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import os import re import sys import DITrack.Backend.Common import DITrack.Util.Misc at_rev_re = re.compile("^At revision (\\d+).$") update_file_re = re.compile("^([ADU])\\s+(.+)$") updated_rev_re = re.compile("^Updated to revision (\\d+).$") class Error(DITrack.Backend.Common.Error): def __init__(self, cmd, message, details): # XXX: we should really call the parent class constructor here self.cmd = cmd self.message = message self.details = details class UpdateStatus(DITrack.Backend.Common.UpdateStatus): """ Class representing a result of the update operation. """ def __init__(self, lines): """ LINES is the list of lines produced by 'svn up' command to be parsed. Raises ValueError is the data passed is somehow unparseable. """ if not lines: raise ValueError, "Empty input" self.modifications = [] if len(lines) == 1: m = at_rev_re.match(lines[0]) if not m: raise ValueError, "At unknown revision" else: m = updated_rev_re.match(lines[-1]) if not m: raise ValueError, "Updated to unknown revision" for str in lines[:-1]: str = str.rstrip() fm = update_file_re.match(str) if not fm: raise ValueError, "Can't parse: '%s'" % str status, fname = fm.group(1), fm.group(2) # XXX: will need to probably wrap into a class later self.modifications.append((status, fname)) # Should convert without problem since matched by the regexp. self.revision = int(m.group(1)) def __str__(self): """ Mimics Subversion 'update' command output. """ if self.modifications: return "%s\nUpdated to revision %d.\n" \ % ( "\n".join( map( lambda (x, y): "%s %s" % (x, y), self.modifications ) ), self.revision ) else: return "At revision %d.\n" % self.revision # XXX: this needs to be moved to the Client class def propget(propname, path, svn_path): p = os.popen("\"%s\" propget %s %s" % (svn_path, propname, path)) str = "" while 1: s = p.readline() if not s: break str = str + s ec = p.close() if not ec: return str.rstrip("\n") return None class Transaction: """ Single Subversion transaction which end with a commit. """ def __init__(self, svn): self.files = {} self.svn = svn def add(self, name): """ Add new file/directory into the transaction. """ self.svn.add(name) self.include(name) def commit(self, logmsg): """ Perform Subversion commit. """ fnames = self.files.keys() self.svn.commit(logmsg, fnames) def include(self, fname): """ Include file FNAME into the transaction. """ self.files[fname] = True def remove(self, name): """ Remove file/directory (nonrecursive) from the version control within the transaction. """ self.svn.remove(name) self.include(name) class Client: """ Subversion interface class. All Subversion-related operations come through this class instance. """ def __init__(self, svn_path): """ SVN_PATH is a path to Subversion command line client. """ self.svn_path = svn_path def add(self, fname): """ Schedule addition of a file. """ args = [ self.svn_path, "add", "-Nq", fname ] # XXX: raise exception on failure here return DITrack.Util.Misc.spawnvp(os.P_WAIT, self.svn_path, args) def commit(self, logmsg, fnames): assert(fnames) assert(logmsg) if os.name == "posix": args = [ self.svn_path, "commit", "-q", "-m", logmsg ] + fnames elif os.name == "nt": # XXX: spaces in file names should be handled by # DITrack.Util.Misc.spawnvp() args = [ self.svn_path, "commit", "-q", "-m", "\"%s\"" % logmsg ] + fnames # XXX: raise exception on failure here return DITrack.Util.Misc.spawnvp(os.P_WAIT, self.svn_path, args) def remove(self, fname): """ Schedule the removal of a file FNAME. """ args = [ self.svn_path, "rm", "-q", fname ] # XXX: raise exception on failure here return DITrack.Util.Misc.spawnvp(os.P_WAIT, self.svn_path, args) def start_txn(self): """ Start a new transaction. """ return Transaction(self) def update(self, wc_path): cmd = "%s update %s" % (self.svn_path, wc_path) p = os.popen(cmd) output = p.readlines() # The close method returns the exit status of the process. See # `pydoc os.popen`. status = p.close() if status: raise Error(cmd, "'svn update' didn't succeed", ["Exit status: %d" % status] ) try: us = UpdateStatus(output) except ValueError: raise Error(cmd, "Can't parse the output", map(lambda x: x.rstrip(), output) ) return us