#!/usr/bin/env python
#
# lock-commit.py: Determines if someone is attempting to commit
#                 changes on a resource locked by someone else.
#
# Author: Stein Roger Skaflotten
#
# $HeadURL$
# $LastChangedDate$
# $LastChangedBy$
# $LastChangedRevision$
#
# USAGE: lock-commit.py REPOS-DIR TXNID
#
#

import os
import sys
import string
import time
import re
import svn.fs
import svn.repos
import svn.core
import base64

def main(pool, repos_dir, txn):
  repos = Repository(repos_dir, txn, pool)

  pc = CommitHandler(pool, repos)
  pc.checkLocks()

class CommitHandler:

  def __init__(self, pool, repos):
    self.pool = pool
    self.repos = repos

    # get all the changes and sort by path
    editor = svn.repos.RevisionChangeCollector(repos.fs_ptr, repos.rev, 
self.pool)
    e_ptr, e_baton = svn.delta.make_editor(editor, self.pool)
    svn.repos.svn_repos_replay(repos.root_this, e_ptr, e_baton, self.pool)

    self.changelist = editor.changes.items()
    self.changelist.sort()

  def checkLocks(self):

    subpool = svn.core.svn_pool_create(self.pool)

    # check for locks not owned by the committer
    canCommit = self.checkChanges(self.repos, self.changelist, subpool)

    # clean up
    svn.core.svn_pool_clear(subpool)
    svn.core.svn_pool_destroy(subpool)

    # pre-commit will fail and the stderr will be propagated to the client
    # with a note that a given resource is locked
    if canCommit == True:
      sys.exit(0)
    else:
      sys.exit(1)

  def checkChanges(self, repos, changelist, pool):

    # these are sorted by path already
    for path, change in changelist:

      # The revprop names must be "human readable", but path names
      # can contain all sorts of stuff. So assume that the client
      # has base64 encoded it and removed [+,\n,=]
      lockPropName = 
"lock-"+(base64.encodestring(path)).replace('=','-').replace('\n','-').replace('+','-')

      # get the current revprop val
      propval = svn.fs.revision_prop(repos.fs_ptr, 0, lockPropName, 
repos.pool);

      # no revprop value  =&gt; not locked
      if type(propval) == type(None):
        pass
      elif propval != repos.author:
        # fail on first locked resource (accumulate to let client know about 
all locked resource?)
        sys.stderr.write('\n'+propval+' has exclusively locked '+path)
        return False

      # Remove lock on deleted resource, otherwise the lock pops up
      # again if a file with the same name is created later on
      #
      # Should perhaps do this in a post-commit hook, though, since
      # doing it here means the lock is lost if the commit fails and
      # the user decides to revert the deletion/rename. Not a crisis...
      if change.path is None:
        svn.fs.change_rev_prop(repos.fs_ptr, 0, lockPropName, None, 
repos.pool);

    return True

class Repository:

  def __init__(self, repos_dir, txn_id, pool):
    self.repos_dir = repos_dir
    self.txn_id = txn_id
    self.pool = pool

    db_path = svn.repos.svn_repos_find_root_path(repos_dir, pool)
    if not db_path:
      db_path = repos_dir

    self.fs_ptr = svn.repos.svn_repos_fs(svn.repos.svn_repos_open(db_path, 
pool))

    self.txn = svn.fs.open_txn(self.fs_ptr, txn_id, self.pool)
    self.root_this = svn.fs.txn_root(self.txn, self.pool)
    self.author = self.get_rev_prop(svn.core.SVN_PROP_REVISION_AUTHOR)
    self.reposuuid = svn.fs.get_uuid(self.fs_ptr, self.pool)

    # RevisionChangeCollector (an editor) subtracts 1 to get the rev to
    # compare against. Since we're not a new rev yet (beeing called
    # before the commit), add 1 to  make the editor pick the base rev.
    self.rev = svn.fs.txn_base_revision(self.txn) + 1

  def get_rev_prop(self, propname):
    return svn.fs.txn_prop(self.txn, propname, self.pool)


# enable True/False in older vsns of Python
try:
  _unused = True
except NameError:
  True = 1
  False = 0

if __name__ == '__main__':
  def usage():
    sys.stderr.write('USAGE: %s REPOS-DIR TXNID \n\n' % (sys.argv[0]))
    sys.exit(1)

  if len(sys.argv) != 3:
    usage()

  repos_dir = sys.argv[1]
  txn_id = sys.argv[2]

  # run main as svn app
  svn.core.run_app(main, repos_dir, txn_id)


