Hi,
I am interested in committing translations updates with pootle to a subversion
repository.
First I assumed, that there would be not svn/cvs-integration at all, as it was
not mentioned in README (it felt like a planned-but-not-yet-implemented
feature).
Then I happily discovered versioncontrol.py.
But the current integration lacks one feature, that would be quite important
for me:
As the languages files of my currently-to-be-translated project are
spread around different directories in the repository, I symlinked them to the
appropriate places below /var/lib/pootle/. This feels like a very reasonable
solution for me.
As the current implementation of versioncontrol.py does not follow symlinks as
a fallback, I can not use it.
So I added a check for the symlinked parent directory.
As the current state of versioncontrol.py does not seem to be easy to extend
for other version control systems, I turned it into a class based module. Now it
should be very easy to add other version control systems. (I left some parts of
the old interface there, to keep the changes in other files small - this needs
to be cleaned up)
Additionally I replaced the commandline calls of "svn" with the python module
"svn" (http://pysvn.tigris.org).
The attached patch should be functionally equal to the original code (taken from
the current subversion repository yesterday). I tested updating and committing
for subversion successfully.
But I had to turn off the checks for "commit" permissions in the source code,
as I could not discover a way to give my user "commit" permissions
via /etc/pootle/user.prefs or with the admin webinterface.
I could not find any example regarding this. Could you give me a hint?
I hope, that the attached patch appears to be useful for you, as I would like
to contribute more to integration of version control support.
thanks for pootle,
Lars
diff -ruN Pootle.orig/indexpage.py Pootle/indexpage.py
--- Pootle.orig/indexpage.py 2007-01-10 05:26:20.000000000 +0100
+++ Pootle/indexpage.py 2007-01-12 02:25:13.829865799 +0100
@@ -704,12 +704,12 @@
molink = {"href": moname, "text": self.localize('MO file')}
actionlinks.append(molink)
if "update" in linksrequired and "admin" in self.rights:
- if versioncontrol.hasversioning(os.path.join(self.project.podir, self.dirname)):
+ if versioncontrol.hasversioning(os.path.join(self.project.podir, self.dirname, basename)):
# l10n: Update from version control (like CVS or Subversion)
updatelink = {"href": "index.html?editing=1&doupdate=1&updatefile=%s" % (basename), "text": self.localize('Update')}
actionlinks.append(updatelink)
if "commit" in linksrequired and "commit" in self.rights:
- if versioncontrol.hasversioning(os.path.join(self.project.podir, self.dirname)):
+ if versioncontrol.hasversioning(os.path.join(self.project.podir, self.dirname, basename)):
# l10n: Commit to version control (like CVS or Subversion)
commitlink = {"href": "index.html?editing=1&docommit=1&commitfile=%s" % (basename), "text": self.localize('Commit')}
actionlinks.append(commitlink)
diff -ruN Pootle.orig/versioncontrol.py Pootle/versioncontrol.py
--- Pootle.orig/versioncontrol.py 2007-01-10 05:26:20.000000000 +0100
+++ Pootle/versioncontrol.py 2007-01-12 04:05:40.661694921 +0100
@@ -25,6 +25,9 @@
import os
import popen2
+
+## first: define some useful functions
+
def pipe(command):
"""runs a command and returns the output and the error as a tuple"""
# p = subprocess.Popen(command, shell=True, close_fds=True,
@@ -43,175 +46,279 @@
"""Shell-escape any non-alphanumeric characters"""
return re.sub(r'(\W)', r'\\\1', path)
-def cvsreadfile(cvsroot, path, revision=None):
- """
- Read a single file from the CVS repository without checking out a full working directory
-
- cvsroot: the CVSROOT for the repository
- path: path to the file relative to cvs root
- revision: revision or tag to get (retrieves from HEAD if None)
- """
- path = shellescape(path)
- if revision:
- command = "cvs -d %s -Q co -p -r%s %s" % (cvsroot, revision, path)
- else:
- command = "cvs -d %s -Q co -p %s" % (cvsroot, path)
-
- output, error = pipe(command)
-
- if error.startswith('cvs checkout'):
- raise IOError("Could not read %s from %s: %s" % (path, cvsroot, output))
- elif error.startswith('cvs [checkout aborted]'):
- raise IOError("Could not read %s from %s: %s" % (path, cvsroot, output))
- return output
-
-def cvsupdatefile(path, revision=None):
- """Does a clean update of the given path"""
- dirname = shellescape(os.path.dirname(path))
- filename = shellescape(os.path.basename(path))
- basecommand = ""
- if dirname:
- basecommand = "cd %s ; " % dirname
- command = basecommand + "mv %s %s.bak ; " % (filename, filename)
- if revision:
- command += "cvs -Q update -C -r%s %s" % (revision, filename)
- else:
- command += "cvs -Q update -C %s" % (filename)
- output, error = pipe(command)
- if error:
- pipe(basecommand + "mv %s.bak %s" % (filename, filename))
- raise IOError("Error running CVS command '%s': %s" % (command, error))
- pipe(basecommand + "rm %s.bak" % filename)
- return output
-
-def cvscommitfile(path, message=None):
- """Commits the file and supplies the given commit message if present"""
- dirname = shellescape(os.path.dirname(path))
- filename = shellescape(os.path.basename(path))
- basecommand = ""
- if dirname:
- basecommand = "cd %s ; " % dirname
- if message:
- message = ' -m "%s" ' % message
- elif message is None:
- message = ""
- command = basecommand + "cvs -Q commit %s %s" % (message, filename)
- output, error = pipe(command)
- if error:
- raise IOError("Error running CVS command '%s': %s" % (command, error))
-
-def getcvsrevision(cvsentries, filename):
- """returns the revision number the file was checked out with by looking in the lines of cvsentries"""
- for cvsentry in cvsentries:
- cvsentryparts = cvsentry.split("/")
- if len(cvsentryparts) < 6:
- continue
- if os.path.normcase(cvsentryparts[1]) == os.path.normcase(filename):
- return cvsentryparts[2].strip()
- return None
-
-def getcvstag(cvsentries, filename):
- """returns the sticky tag the file was checked out with by looking in the lines of cvsentries"""
- for cvsentry in cvsentries:
- cvsentryparts = cvsentry.split("/")
- if len(cvsentryparts) < 6:
- continue
- if os.path.normcase(cvsentryparts[1]) == os.path.normcase(filename):
- if cvsentryparts[5].startswith("T"):
- return cvsentryparts[5][1:].strip()
- return None
-
-def svnreadfile(path, revision=None):
- """Get a clean version of a file from the SVN repository"""
- path = shellescape(path)
- if revision:
- command = "svn cat -r %s %s" % (revision, path)
- else:
- command = "svn cat %s" % path
- output, error = pipe(command)
- if error:
- raise IOError("Subversion error running '%s': %s" % (command, error))
- return output
-
-def svnupdatefile(path, revision=None):
- """Does a clean update of the given path"""
- path = shellescape(path)
- command = "svn revert %s ; " % path
- if revision:
- command += "svn update -r%s %s" % (revision, path)
- else:
- command += "svn update %s" % (path)
- output, error = pipe(command)
- if error:
- raise IOError("Subversion error running '%s': %s" % (command, error))
- return output
-
-def svncommitfile(path, message=None):
- """Commits the file and supplies the given commit message if present"""
- dirname = shellescape(os.path.dirname(path))
- filename = shellescape(os.path.basename(path))
- basecommand = ""
- if dirname:
- basecommand = "cd %s ; " % dirname
- if message:
- message = ' -m "%s" ' % message
- elif message is None:
- message = ""
- command = basecommand + "svn -q --non-interactive commit %s %s" % (message, filename)
- output, error = pipe(command)
- if error:
- raise IOError("Error running SVN command '%s': %s" % (command, error))
-
-def hascvs(parentdir):
- cvsdir = os.path.join(parentdir, "CVS")
- return os.path.isdir(cvsdir)
-
-def hassvn(parentdir):
- svndir = os.path.join(parentdir, ".svn")
- return os.path.isdir(svndir)
-
-def hasversioning(parentdir):
- return hascvs(parentdir) or hassvn(parentdir)
-
-def getcleanfile(filename, revision=None):
- parentdir = os.path.dirname(filename)
- if hascvs(parentdir):
- cvsdir = os.path.join(parentdir, "CVS")
- cvsroot = open(os.path.join(cvsdir, "Root"), "r").read().strip()
- cvspath = open(os.path.join(cvsdir, "Repository"), "r").read().strip()
- basename = os.path.basename(filename)
- cvsfilename = os.path.join(cvspath, basename)
- if revision is None:
- cvsentries = open(os.path.join(cvsdir, "Entries"), "r").readlines()
- revision = getcvstag(cvsentries, basename)
- if revision == "BASE":
- cvsentries = open(os.path.join(cvsdir, "Entries"), "r").readlines()
- revision = getcvsrevision(cvsentries, basename)
- return cvsreadfile(cvsroot, cvsfilename, revision)
- if hassvn(parentdir):
- return svnreadfile(filename, revision)
- raise IOError("Could not find version control information")
-
-def updatefile(filename, revision=None):
- parentdir = os.path.dirname(filename)
- if hascvs(parentdir):
- return cvsupdatefile(filename, revision)
- if hassvn(parentdir):
- return svnupdatefile(filename, revision)
- raise IOError("Could not find version control information")
-
-def commitfile(filename, message=None):
- parentdir = os.path.dirname(filename)
- if hascvs(parentdir):
- return cvscommitfile(filename, message)
- if hassvn(parentdir):
- return svncommitfile(filename, message)
- raise IOError("Could not find version control information")
-
+
+
+class GenericVersionControlSystem:
+ """should be the super class for all version control classes"""
+
+ ## as long as noone overrides this, the test below always succeeds
+ MARKER_DIR = "."
+
+
+ def __init__(self, location):
+ """default version control checker: just test if self.MARKER_DIR exists"""
+ parent_dir = os.path.dirname(os.path.abspath(location))
+ if not os.path.isdir(os.path.join(parent_dir, self.MARKER_DIR)):
+ raise IOError("Could not find version control information: %s" % location)
+
+
+
+class CVS(GenericVersionControlSystem):
+ """manage items versioned by CVS"""
+
+ MARKER_DIR = "CVS"
+
+ def __init__(self, location):
+ GenericVersionControlSystem.__init__(self, location)
+ self.cvsdir = os.path.join(os.path.dirname(os.path.abspath(location)), "CVS")
+ self.location = os.path.abspath(location)
+
+
+ def _readfile(self, cvsroot, path, revision=None):
+ """
+ Read a single file from the CVS repository without checking out a full working directory
+
+ cvsroot: the CVSROOT for the repository
+ path: path to the file relative to cvs root
+ revision: revision or tag to get (retrieves from HEAD if None)
+ """
+ path = shellescape(path)
+ if revision:
+ command = "cvs -d %s -Q co -p -r%s %s" % (cvsroot, revision, path)
+ else:
+ command = "cvs -d %s -Q co -p %s" % (cvsroot, path)
+
+ output, error = pipe(command)
+
+ if error.startswith('cvs checkout'):
+ raise IOError("Could not read %s from %s: %s" % (path, cvsroot, output))
+ elif error.startswith('cvs [checkout aborted]'):
+ raise IOError("Could not read %s from %s: %s" % (path, cvsroot, output))
+ return output
+
+
+ def getcleanfile(self, revision=None):
+ """Get the content of the file for the given revision"""
+ parentdir = os.path.dirname(self.location)
+ cvsdir = os.path.join(parentdir, "CVS")
+ cvsroot = open(os.path.join(cvsdir, "Root"), "r").read().strip()
+ cvspath = open(os.path.join(cvsdir, "Repository"), "r").read().strip()
+ cvsfilename = os.path.join(cvspath, os.path.basename(self.location))
+ if revision is None:
+ cvsentries = open(os.path.join(cvsdir, "Entries"), "r").readlines()
+ revision = self._getcvstag(cvsentries)
+ if revision == "BASE":
+ cvsentries = open(os.path.join(cvsdir, "Entries"), "r").readlines()
+ revision = self._getcvsrevision(cvsentries)
+ return self._readfile(cvsroot, cvsfilename, revision)
+
+
+ def update(self, revision=None):
+ """Does a clean update of the given path"""
+ dirname = shellescape(os.path.dirname(self.location))
+ filename = shellescape(os.path.basename(self.location))
+ basecommand = ""
+ if dirname:
+ basecommand = "cd %s ; " % dirname
+ command = basecommand + "mv %s %s.bak ; " % (filename, filename)
+ if revision:
+ command += "cvs -Q update -C -r%s %s" % (revision, filename)
+ else:
+ command += "cvs -Q update -C %s" % (filename)
+ output, error = pipe(command)
+ if error:
+ pipe(basecommand + "mv %s.bak %s" % (filename, filename))
+ raise IOError("Error running CVS command '%s': %s" % (command, error))
+ pipe(basecommand + "rm %s.bak" % filename)
+ return output
+
+
+ def commit(self, message=None):
+ """Commits the file and supplies the given commit message if present"""
+ dirname = shellescape(os.path.dirname(self.location))
+ filename = shellescape(os.path.basename(self.location))
+ basecommand = ""
+ if dirname:
+ basecommand = "cd %s ; " % dirname
+ if message:
+ message = ' -m "%s" ' % message
+ elif message is None:
+ message = ""
+ command = basecommand + "cvs -Q commit %s %s" % (message, filename)
+ output, error = pipe(command)
+ if error:
+ raise IOError("Error running CVS command '%s': %s" % (command, error))
+
+
+ def _getcvsrevision(self, cvsentries):
+ """returns the revision number the file was checked out with by looking in the lines of cvsentries"""
+ filename = os.path.basename(self.location)
+ for cvsentry in cvsentries:
+ cvsentryparts = cvsentry.split("/")
+ if len(cvsentryparts) < 6:
+ continue
+ if os.path.normcase(cvsentryparts[1]) == os.path.normcase(filename):
+ return cvsentryparts[2].strip()
+ return None
+
+
+ def _getcvstag(self, cvsentries):
+ """returns the sticky tag the file was checked out with by looking in the lines of cvsentries"""
+ filename = os.path.basename(self.location)
+ for cvsentry in cvsentries:
+ cvsentryparts = cvsentry.split("/")
+ if len(cvsentryparts) < 6:
+ continue
+ if os.path.normcase(cvsentryparts[1]) == os.path.normcase(filename):
+ if cvsentryparts[5].startswith("T"):
+ return cvsentryparts[5][1:].strip()
+ return None
+
+
+
+class SVN(GenericVersionControlSystem):
+ """manage items that are under revision control of Subversion"""
+
+ MARKER_DIR = ".svn"
+
+ def __init__(self, location):
+ GenericVersionControlSystem.__init__(self, location)
+ try:
+ import pysvn
+ except ImportError:
+ ## no svn support - throw an IOException
+ raise IOError, "Python module pysvn not found!"
+ self.svn = pysvn.Client()
+ self.location = os.path.abspath(location)
+
+
+ def _convert_revision(self, revision=None):
+ """convert the given revision into a suitable pysvn.Revision object
+
+ raise IOError if the type of 'revision' is not one of:
+ [ 'None', 'int', 'float', 'str' ]
+ """
+ import pysvn
+ if revision is None:
+ return pysvn.Revision(pysvn.opt_revision_kind.head)
+ elif type(revision) is int:
+ ## revision number is given
+ return pysvn.Revision(pysvn.opt_revision_kind.number, revision)
+ elif type(revision) is float:
+ ## revision date is given (e.g. as 'time.time()')
+ return pysvn.Revision(pysvn.opt_revision_kind.date, revision)
+ elif type(revision) is str:
+ ## special revisions
+ if revision == "BASE":
+ return pysvn.Revision(pysvn.opt_revision_kind.base)
+ elif revision == "HEAD":
+ return pysvn.Revision(pysvn.opt_revision_kind.head)
+ raise IOError("Invalid revision: %s (%s)" % (revision, type(revision)))
+
+
+ def update(self, revision=None):
+ """update the working copy - remove local modifications if necessary"""
+ ## turn the given revision value into a pysvn.Revision object
+ ## revert the local copy (remove local changes)
+ self.svn.revert(self.location)
+ ## update the working copy to the given revision
+ ## possible IOErrors raised by '_convert_revision' may pass
+ up_rev = self.svn.update(self.location, revision=self._convert_revision(revision))
+ ## check the result (pysvn does never raise an exception here)
+ if up_rev[0].number >= 0:
+ return
+ else:
+ raise IOError("Failed to update path: %s" % str(self.location))
+
+
+ def commit(self, message=None):
+ """commit the file and attach a log message"""
+ import pysvn
+ try:
+ self.svn.checkin(self.location, str(message))
+ except pysvn.ClientError, err_msg:
+ raise IOError("Failed to checkin changes of '%s': %s" % \
+ (self.location, err_msg))
+
+
+ def getcleanfile(self, revision=None):
+ """return the content of the 'head' revision of the file"""
+ import pysvn
+ head_rev = pysvn.Revision(pysvn.opt_revision_kind.head)
+ try:
+ ## return the content of the file for the revision
+ ## we have to check the current version of pysvn before
+ (pysvn_major, pysvn_minor) = (pysvn.version[0], pysvn.version[1])
+ if (pysvn_major >= 2) or ((pysvn_major == 2) and (pysvn_minor >= 4)):
+ ## always use 'head' instead of 'working'
+ return self.svn.cat(self.location, self._convert_revision(revision), head_rev)
+ else:
+ ## pysvn < v1.4 support only two parameters
+ return self.svn.cat(self.location, self._convert_revision(revision))
+ except pysvn.ClientError, err_msg:
+ raise IOError("Failed to read content of '%s': %s" % \
+ (self.location, err_msg))
+
+
+
+## which versioning systems are supported by default?
+DEFAULT_VERSIONING_SYSTEMS = [ CVS, SVN ]
+
+
+def get_versioned_object(
+ location,
+ versioning_systems=DEFAULT_VERSIONING_SYSTEMS,
+ follow_symlinks=True):
+ """return a versioned object for the given file"""
+ ## go through all versioning systems and return a versioned object if possible
+ for vers_sys in versioning_systems:
+ try:
+ return vers_sys(location)
+ except IOError:
+ continue
+ ## if 'location' is a symlink, then we should try the original file
+ if follow_symlinks and os.path.islink(location):
+ return get_versioned_object(os.path.realpath(location),
+ versioning_systems = versioning_systems,
+ follow_symlinks = False)
+ ## if everything fails:
+ raise IOError("Could not find version control information: %s" % location)
+
+
+## stay compatible to the previous version
+updatefile = lambda filename: get_versioned_object(filename).update()
+getcleanfile = lambda filename, revision: get_versioned_object(filename).getcleanfile(revision)
+commitfile = lambda filename, message: get_versioned_object(filename).commit(message)
+
+
+def hascvs(item):
+ try:
+ get_versioned_object(item, versioning_systems=CVS)
+ return True
+ except IOError:
+ return False
+
+
+def hassvn(item):
+ try:
+ get_versioned_object(item, versioning_systems=SVN)
+ return True
+ except IOError:
+ return False
+
+
+def hasversioning(item):
+ try:
+ get_versioned_object(item)
+ return True
+ except IOError, err_msg:
+ return False
+
+
+
if __name__ == "__main__":
import sys
filenames = sys.argv[1:]
-------------------------------------------------------------------------
Take Surveys. Earn Cash. Influence the Future of IT
Join SourceForge.net's Techsay panel and you'll get the chance to share your
opinions on IT & business topics through brief surveys - and earn cash
http://www.techsay.com/default.php?page=join.php&p=sourceforge&CID=DEVDEV
_______________________________________________
Translate-pootle mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/translate-pootle