[svn r52000] Merging the 'guido-auth-svn' branch back into the trunk. This means there's

a new class py.path.SvnAuth of which instances store user credentials and
auth config, and can be passed to py.path.svnurl and py.path.svnwc objects
to control SVN authentication behaviour.

--HG--
branch : trunk
This commit is contained in:
guido 2008-03-01 14:43:33 +01:00
parent f3f84fa36c
commit 17530e8ccd
9 changed files with 712 additions and 62 deletions

View File

@ -11,8 +11,8 @@ version = "1.0-pre-alpha"
initpkg(__name__,
description = "pylib and py.test: agile development and test support library",
revision = int('$LastChangedRevision: 51077 $'.split(':')[1][:-1]),
lastchangedate = '$LastChangedDate: 2008-01-27 12:55:27 +0100 (Sun, 27 Jan 2008) $',
revision = int('$LastChangedRevision: 52000 $'.split(':')[1][:-1]),
lastchangedate = '$LastChangedDate: 2008-03-01 14:43:33 +0100 (Sat, 01 Mar 2008) $',
version = version,
url = "http://codespeak.net/py",
download_url = "XXX", # "http://codespeak.net/download/py/py-%s.tar.gz" %(version,),
@ -67,6 +67,7 @@ initpkg(__name__,
'path.svnwc' : ('./path/svn/wccommand.py', 'SvnWCCommandPath'),
'path.svnurl' : ('./path/svn/urlcommand.py', 'SvnCommandPath'),
'path.local' : ('./path/local/local.py', 'LocalPath'),
'path.SvnAuth' : ('./path/svn/svncommon.py', 'SvnAuth'),
# some nice slightly magic APIs
'magic.__doc__' : ('./magic/__init__.py', '__doc__'),

View File

@ -33,6 +33,9 @@ option = py.test.config.addoptions("execnet options",
action='store', dest='docpath',
default="doc", type='string',
help="relative path to doc output location (relative from py/)"),
Option('', '--runslowtests',
action="store_true", dest="runslowtests", default=False,
help="run slow tests)"),
)
dist_rsync_roots = ['.']

View File

@ -4,6 +4,10 @@ import py, sys
from py.__.misc.killproc import killproc
def test_win_killsubprocess():
if sys.platform == 'win32' and not py.path.local.sysfind('taskkill'):
py.test.skip("you\'re using an older version of windows, which "
"doesn\'t support 'taskkill' - py.misc.killproc is not "
"available")
tmp = py.test.ensuretemp("test_win_killsubprocess")
t = tmp.join("t.py")
t.write("import time ; time.sleep(100)")

77
py/path/svn/auth.txt Normal file
View File

@ -0,0 +1,77 @@
SVN authentication support
==========================
This document describes authentication support for both py.path.svnwc and
py.path.svnurl (yet in its implemention phase). This feature allows using the
library in a completely automated fashion, without having to provide svn
credentials interactively.
Current implementation
----------------------
The credentials are passed to the constructor of the path objects, and are used
(transparently) for every action that accesses the server. Also, when provided,
they are passed recursively to all child objects created by methods such as
join(), ensure(), etc. (XXX currently only true for svnurl, not for svnwc)
To pass credentials to path objects, an SvnAuth class needs to be created to
hold them. This is then passed to the constructor or methods as the 'auth'
keyword argument. (XXX the latter currently only for svnwc, and preferrably
that needs to be removed in favour of an .auth attribute like in svnurl)
It is configurable whether the credentials are stored on disk. Storing them is
useful in certain situations (executive access to the repository will not
require the credentials to be passed) but might not be desired in others - for
instance if a webserver runs more than one application, one does not want to
pollute the webserver's home directory (if it even has one). This behaviour can
be controlled by passing a False value for the 'cache_auth' argument to
SvnAuth.
Also it is configurable what behaviour is displayed when the credentials do not
validate: if a keyword argument to the SvnAuth constructor called 'interactive'
has a True value (which is currently the default (XXX I think this should be
changed!)), an interactive prompt is displayed - this is useful for terminal
applications where you want to have an interactive fallback. When this has a
False value, an exception is raised (XXX define the exception properly).
Code examples
-------------
So, tying this together, code using this feature would look something like::
>>> auth = py.path.SvnAuth('user', 'pass', cache_auth=False,
... interactive=False)
>>> wcpath = py.path.svnwc(path, auth=auth)
>>> urlpath = py.path.svnurl(url, auth=auth)
Open issues
-----------
* How do we deal with externals properly?
It looks like the svn command-line client uses the credentials provided for
all externals, if possible, and either prompts for the password in
interactive mode, or barfs when --non-interactive is passed. I think it makes
sense to copy its behaviour here, pass the credentials to any child svn path
objects (as discussed above), and either let the command-line app ask for
creds or throw an exception when 'interactive' is set to False (see above).
Current idea: ignore this and let the client handle (so no passing auth
around to the children).
* Affected methods for svnwc:
- switch
- checkout
- update
- lock
- unlock
- diff (when using older revisions?)
- commit
- log
- status (for locking, etc.?)
* Affected methods for svnurl:
not appropriate - the auth is passed to the constructor rather or set to
path.auth rather than passed to all methods

View File

@ -65,6 +65,7 @@ class SvnPathBase(common.FSPathBase):
"""
obj = object.__new__(self.__class__)
obj.rev = kw.get('rev', self.rev)
obj.auth = kw.get('auth', self.auth)
dirname, basename, purebasename, ext = self._getbyspec(
"dirname,basename,purebasename,ext")
if 'basename' in kw:
@ -138,7 +139,7 @@ class SvnPathBase(common.FSPathBase):
args = tuple([arg.strip(self.sep) for arg in args])
parts = (self.strpath, ) + args
newpath = self.__class__(self.sep.join(parts), self.rev)
newpath = self.__class__(self.sep.join(parts), self.rev, self.auth)
return newpath
def propget(self, name):
@ -330,3 +331,27 @@ def url_from_path(path):
fspath = '%s@HEAD' % (fspath,)
return 'file://%s' % (fspath,)
class SvnAuth(object):
""" container for auth information for Subversion """
def __init__(self, username, password, cache_auth=True, interactive=True):
self.username = username
self.password = password
self.cache_auth = cache_auth
self.interactive = interactive
def makecmdoptions(self):
uname = self.username.replace('"', '\\"')
passwd = self.password.replace('"', '\\"')
ret = []
if uname:
ret.append('--username="%s"' % (uname,))
if passwd:
ret.append('--password="%s"' % (passwd,))
if not self.cache_auth:
ret.append('--no-auth-cache')
if not self.interactive:
ret.append('--non-interactive')
return ' '.join(ret)
def __str__(self):
return "<SvnAuth username=%s ...>" %(self.username,)

View File

@ -0,0 +1,479 @@
import py
from py.path import SvnAuth
import svntestbase
from threading import Thread
import time
from py.__.misc.killproc import killproc
from py.__.conftest import option
def make_repo_auth(repo, userdata):
""" write config to repo
user information in userdata is used for auth
userdata has user names as keys, and a tuple (password, readwrite) as
values, where 'readwrite' is either 'r' or 'rw'
"""
confdir = py.path.local(repo).join('conf')
confdir.join('svnserve.conf').write('''\
[general]
anon-access = none
password-db = passwd
authz-db = authz
realm = TestRepo
''')
authzdata = '[/]\n'
passwddata = '[users]\n'
for user in userdata:
authzdata += '%s = %s\n' % (user, userdata[user][1])
passwddata += '%s = %s\n' % (user, userdata[user][0])
confdir.join('authz').write(authzdata)
confdir.join('passwd').write(passwddata)
def serve_bg(repopath):
pidfile = py.path.local(repopath).join('pid')
port = 10000
e = None
while port < 10010:
cmd = 'svnserve -d -T --listen-port=%d --pid-file=%s -r %s' % (
port, pidfile, repopath)
try:
py.process.cmdexec(cmd)
except py.process.cmdexec.Error, e:
pass
else:
# XXX we assume here that the pid file gets written somewhere, I
# guess this should be relatively safe... (I hope, at least?)
while True:
pid = pidfile.read()
if pid:
break
# needs a bit more time to boot
time.sleep(0.1)
return port, int(pid)
port += 1
raise IOError('could not start svnserve: %s' % (e,))
class TestSvnAuth(object):
def test_basic(self):
auth = py.path.SvnAuth('foo', 'bar')
assert auth.username == 'foo'
assert auth.password == 'bar'
assert str(auth)
def test_makecmdoptions_uname_pw_makestr(self):
auth = py.path.SvnAuth('foo', 'bar')
assert auth.makecmdoptions() == '--username="foo" --password="bar"'
def test_makecmdoptions_quote_escape(self):
auth = py.path.SvnAuth('fo"o', '"ba\'r"')
assert auth.makecmdoptions() == '--username="fo\\"o" --password="\\"ba\'r\\""'
def test_makecmdoptions_no_cache_auth(self):
auth = py.path.SvnAuth('foo', 'bar', cache_auth=False)
assert auth.makecmdoptions() == ('--username="foo" --password="bar" '
'--no-auth-cache')
def test_makecmdoptions_no_interactive(self):
auth = py.path.SvnAuth('foo', 'bar', interactive=False)
assert auth.makecmdoptions() == ('--username="foo" --password="bar" '
'--non-interactive')
def test_makecmdoptions_no_interactive_no_cache_auth(self):
auth = py.path.SvnAuth('foo', 'bar', cache_auth=False,
interactive=False)
assert auth.makecmdoptions() == ('--username="foo" --password="bar" '
'--no-auth-cache --non-interactive')
class svnwc_no_svn(py.path.svnwc):
def __init__(self, *args, **kwargs):
self.commands = []
super(svnwc_no_svn, self).__init__(*args, **kwargs)
def _svn(self, *args):
self.commands.append(args)
class TestSvnWCAuth(object):
def setup_method(self, meth):
self.auth = SvnAuth('user', 'pass', cache_auth=False)
def test_checkout(self):
wc = svnwc_no_svn('foo', auth=self.auth)
wc.checkout('url')
assert wc.commands[0][-1] == ('--username="user" --password="pass" '
'--no-auth-cache')
def test_commit(self):
wc = svnwc_no_svn('foo', auth=self.auth)
wc.commit('msg')
assert wc.commands[0][-1] == ('--username="user" --password="pass" '
'--no-auth-cache')
def test_checkout_no_cache_auth(self):
wc = svnwc_no_svn('foo', auth=self.auth)
wc.checkout('url')
assert wc.commands[0][-1] == ('--username="user" --password="pass" '
'--no-auth-cache')
def test_checkout_auth_from_constructor(self):
wc = svnwc_no_svn('foo', auth=self.auth)
wc.checkout('url')
assert wc.commands[0][-1] == ('--username="user" --password="pass" '
'--no-auth-cache')
class svnurl_no_svn(py.path.svnurl):
cmdexec_output = 'test'
popen_output = 'test'
def _cmdexec(self, cmd):
self.commands.append(cmd)
return self.cmdexec_output
def _popen(self, cmd):
self.commands.append(cmd)
return self.popen_output
class TestSvnURLAuth(object):
def setup_method(self, meth):
svnurl_no_svn.commands = []
self.auth = SvnAuth('foo', 'bar')
def test_init(self):
u = svnurl_no_svn('http://foo.bar/svn')
assert u.auth is None
u = svnurl_no_svn('http://foo.bar/svn', auth=self.auth)
assert u.auth is self.auth
def test_new(self):
u = svnurl_no_svn('http://foo.bar/svn/foo', auth=self.auth)
new = u.new(basename='bar')
assert new.auth is self.auth
assert new.url == 'http://foo.bar/svn/bar'
def test_join(self):
u = svnurl_no_svn('http://foo.bar/svn', auth=self.auth)
new = u.join('foo')
assert new.auth is self.auth
assert new.url == 'http://foo.bar/svn/foo'
def test_listdir(self):
u = svnurl_no_svn('http://foo.bar/svn', auth=self.auth)
u.cmdexec_output = '''\
1717 johnny 1529 Nov 04 14:32 LICENSE.txt
1716 johnny 5352 Nov 04 14:28 README.txt
'''
paths = u.listdir()
assert paths[0].auth is self.auth
assert paths[1].auth is self.auth
assert paths[0].basename == 'LICENSE.txt'
def test_info(self):
u = svnurl_no_svn('http://foo.bar/svn/LICENSE.txt', auth=self.auth)
def dirpath(self):
return self
u.cmdexec_output = '''\
1717 johnny 1529 Nov 04 14:32 LICENSE.txt
1716 johnny 5352 Nov 04 14:28 README.txt
'''
org_dp = u.__class__.dirpath
u.__class__.dirpath = dirpath
try:
info = u.info()
finally:
u.dirpath = org_dp
assert info.size == 1529
def test_open(self):
u = svnurl_no_svn('http://foo.bar/svn', auth=self.auth)
foo = u.join('foo')
foo.check = lambda *args, **kwargs: True
ret = foo.open()
assert ret == 'test'
assert '--username="foo" --password="bar"' in foo.commands[0]
def test_dirpath(self):
u = svnurl_no_svn('http://foo.bar/svn/foo', auth=self.auth)
parent = u.dirpath()
assert parent.auth is self.auth
def test_mkdir(self):
u = svnurl_no_svn('http://foo.bar/svn', auth=self.auth)
u.mkdir('foo', msg='created dir foo')
assert '--username="foo" --password="bar"' in u.commands[0]
def test_copy(self):
u = svnurl_no_svn('http://foo.bar/svn', auth=self.auth)
u2 = svnurl_no_svn('http://foo.bar/svn2')
u.copy(u2, 'copied dir')
assert '--username="foo" --password="bar"' in u.commands[0]
def test_rename(self):
u = svnurl_no_svn('http://foo.bar/svn/foo', auth=self.auth)
u.rename('http://foo.bar/svn/bar', 'moved foo to bar')
assert '--username="foo" --password="bar"' in u.commands[0]
def test_remove(self):
u = svnurl_no_svn('http://foo.bar/svn/foo', auth=self.auth)
u.remove(msg='removing foo')
assert '--username="foo" --password="bar"' in u.commands[0]
def test_export(self):
u = svnurl_no_svn('http://foo.bar/svn', auth=self.auth)
target = py.path.local('/foo')
u.export(target)
assert '--username="foo" --password="bar"' in u.commands[0]
def test_log(self):
u = svnurl_no_svn('http://foo.bar/svn/foo', auth=self.auth)
u.popen_output = py.std.StringIO.StringIO('''\
<?xml version="1.0"?>
<log>
<logentry revision="51381">
<author>guido</author>
<date>2008-02-11T12:12:18.476481Z</date>
<msg>Creating branch to work on auth support for py.path.svn*.
</msg>
</logentry>
</log>
''')
u.check = lambda *args, **kwargs: True
ret = u.log(10, 20, verbose=True)
assert '--username="foo" --password="bar"' in u.commands[0]
assert len(ret) == 1
assert int(ret[0].rev) == 51381
assert ret[0].author == 'guido'
def test_propget(self):
u = svnurl_no_svn('http://foo.bar/svn', auth=self.auth)
u.propget('foo')
assert '--username="foo" --password="bar"' in u.commands[0]
class SvnAuthFunctionalTestBase(object):
def setup_class(cls):
if not option.runslowtests:
py.test.skip('skipping slow functional tests - use --runslowtests '
'to override')
def setup_method(self, meth):
func_name = meth.im_func.func_name
self.repo = svntestbase.make_test_repo('TestSvnAuthFunctional.%s' % (
func_name,))
repodir = str(self.repo)[7:]
if py.std.sys.platform == 'win32':
# remove trailing slash...
repodir = repodir[1:]
self.repopath = py.path.local(repodir)
self.temppath = py.test.ensuretemp('TestSvnAuthFunctional.%s' % (
func_name))
self.auth = py.path.SvnAuth('johnny', 'foo', cache_auth=False,
interactive=False)
def _start_svnserve(self):
make_repo_auth(self.repopath, {'johnny': ('foo', 'rw')})
try:
return serve_bg(self.repopath.dirpath())
except IOError, e:
py.test.skip(str(e))
class TestSvnWCAuthFunctional(SvnAuthFunctionalTestBase):
def test_checkout_constructor_arg(self):
port, pid = self._start_svnserve()
try:
wc = py.path.svnwc(self.temppath, auth=self.auth)
wc.checkout(
'svn://localhost:%s/%s' % (port, self.repopath.basename))
assert wc.join('.svn').check()
finally:
# XXX can we do this in a teardown_method too? not sure if that's
# guaranteed to get called...
killproc(pid)
def test_checkout_function_arg(self):
port, pid = self._start_svnserve()
try:
wc = py.path.svnwc(self.temppath, auth=self.auth)
wc.checkout(
'svn://localhost:%s/%s' % (port, self.repopath.basename))
assert wc.join('.svn').check()
finally:
killproc(pid)
def test_checkout_failing_non_interactive(self):
port, pid = self._start_svnserve()
try:
auth = py.path.SvnAuth('johnny', 'bar', cache_auth=False,
interactive=False)
wc = py.path.svnwc(self.temppath, auth)
py.test.raises(Exception,
("wc.checkout('svn://localhost:%s/%s' % "
"(port, self.repopath.basename))"))
finally:
killproc(pid)
def test_log(self):
port, pid = self._start_svnserve()
try:
wc = py.path.svnwc(self.temppath, self.auth)
wc.checkout(
'svn://localhost:%s/%s' % (port, self.repopath.basename))
foo = wc.ensure('foo.txt')
wc.commit('added foo.txt')
log = foo.log()
assert len(log) == 1
assert log[0].msg == 'added foo.txt'
finally:
killproc(pid)
def test_switch(self):
port, pid = self._start_svnserve()
try:
wc = py.path.svnwc(self.temppath, auth=self.auth)
svnurl = 'svn://localhost:%s/%s' % (port, self.repopath.basename)
wc.checkout(svnurl)
wc.ensure('foo', dir=True).ensure('foo.txt').write('foo')
wc.commit('added foo dir with foo.txt file')
wc.ensure('bar', dir=True)
wc.commit('added bar dir')
bar = wc.join('bar')
bar.switch(svnurl + '/foo')
assert bar.join('foo.txt')
finally:
killproc(pid)
def test_update(self):
port, pid = self._start_svnserve()
try:
wc1 = py.path.svnwc(self.temppath.ensure('wc1', dir=True),
auth=self.auth)
wc2 = py.path.svnwc(self.temppath.ensure('wc2', dir=True),
auth=self.auth)
wc1.checkout(
'svn://localhost:%s/%s' % (port, self.repopath.basename))
wc2.checkout(
'svn://localhost:%s/%s' % (port, self.repopath.basename))
wc1.ensure('foo', dir=True)
wc1.commit('added foo dir')
wc2.update()
assert wc2.join('foo').check()
auth = py.path.SvnAuth('unknown', 'unknown', interactive=False)
wc2.auth = auth
py.test.raises(Exception, 'wc2.update()')
finally:
killproc(pid)
def test_lock_unlock_status(self):
port, pid = self._start_svnserve()
try:
wc = py.path.svnwc(self.temppath, auth=self.auth)
wc.checkout(
'svn://localhost:%s/%s' % (port, self.repopath.basename,))
wc.ensure('foo', file=True)
wc.commit('added foo file')
foo = wc.join('foo')
foo.lock()
status = foo.status()
assert status.locked
foo.unlock()
status = foo.status()
assert not status.locked
auth = py.path.SvnAuth('unknown', 'unknown', interactive=False)
foo.auth = auth
py.test.raises(Exception, 'foo.lock()')
py.test.raises(Exception, 'foo.unlock()')
finally:
killproc(pid)
def test_diff(self):
port, pid = self._start_svnserve()
try:
wc = py.path.svnwc(self.temppath, auth=self.auth)
wc.checkout(
'svn://localhost:%s/%s' % (port, self.repopath.basename,))
wc.ensure('foo', file=True)
wc.commit('added foo file')
wc.update()
rev = int(wc.status().rev)
foo = wc.join('foo')
foo.write('bar')
diff = foo.diff()
assert '\n+bar\n' in diff
foo.commit('added some content')
diff = foo.diff()
assert not diff
diff = foo.diff(rev=rev)
assert '\n+bar\n' in diff
auth = py.path.SvnAuth('unknown', 'unknown', interactive=False)
foo.auth = auth
py.test.raises(Exception, 'foo.diff(rev=rev)')
finally:
killproc(pid)
class TestSvnURLAuthFunctional(SvnAuthFunctionalTestBase):
def test_listdir(self):
port, pid = self._start_svnserve()
try:
u = py.path.svnurl(
'svn://localhost:%s/%s' % (port, self.repopath.basename),
auth=self.auth)
u.ensure('foo')
paths = u.listdir()
assert len(paths) == 1
assert paths[0].auth is self.auth
auth = SvnAuth('foo', 'bar', interactive=False)
u = py.path.svnurl(
'svn://localhost:%s/%s' % (port, self.repopath.basename),
auth=auth)
py.test.raises(Exception, 'u.listdir()')
finally:
killproc(pid)
def test_copy(self):
port, pid = self._start_svnserve()
try:
u = py.path.svnurl(
'svn://localhost:%s/%s' % (port, self.repopath.basename),
auth=self.auth)
foo = u.ensure('foo')
bar = u.join('bar')
foo.copy(bar)
assert bar.check()
assert bar.auth is self.auth
auth = SvnAuth('foo', 'bar', interactive=False)
u = py.path.svnurl(
'svn://localhost:%s/%s' % (port, self.repopath.basename),
auth=auth)
foo = u.join('foo')
bar = u.join('bar')
py.test.raises(Exception, 'foo.copy(bar)')
finally:
killproc(pid)
def test_write_read(self):
port, pid = self._start_svnserve()
try:
u = py.path.svnurl(
'svn://localhost:%s/%s' % (port, self.repopath.basename),
auth=self.auth)
foo = u.ensure('foo')
fp = foo.open()
try:
data = fp.read()
finally:
fp.close()
assert data == ''
auth = SvnAuth('foo', 'bar', interactive=False)
u = py.path.svnurl(
'svn://localhost:%s/%s' % (port, self.repopath.basename),
auth=auth)
foo = u.join('foo')
py.test.raises(Exception, 'foo.open()')
finally:
killproc(pid)
# XXX rinse, repeat... :|

View File

@ -1,13 +1,27 @@
import py
import sys
from py.__.path.svn.testing.svntestbase import CommonSvnTests, getrepowc
from py.__.path.svn.wccommand import InfoSvnWCCommand
from py.__.path.svn.wccommand import parse_wcinfotime
from py.__.path.svn import svncommon
if py.path.local.sysfind('svn') is None:
py.test.skip("cannot test py.path.svn, 'svn' binary not found")
if sys.platform != 'win32':
def normpath(p):
return p
else:
try:
import win32api
except ImportError:
def normpath(p):
py.test.skip('this test requires win32api to run on windows')
else:
import os
def normpath(p):
p = win32api.GetShortPathName(p)
return os.path.normpath(os.path.normcase(p))
class TestWCSvnCommandPath(CommonSvnTests):
@ -253,7 +267,7 @@ class TestWCSvnCommandPath(CommonSvnTests):
try:
locked = root.status().locked
assert len(locked) == 1
assert str(locked[0]) == str(somefile)
assert normpath(str(locked[0])) == normpath(str(somefile))
#assert somefile.locked()
py.test.raises(Exception, 'somefile.lock()')
finally:

View File

@ -21,10 +21,11 @@ class SvnCommandPath(svncommon.SvnPathBase):
_lsrevcache = BuildcostAccessCache(maxentries=128)
_lsnorevcache = AgingCache(maxentries=1000, maxseconds=60.0)
def __new__(cls, path, rev=None):
def __new__(cls, path, rev=None, auth=None):
self = object.__new__(cls)
if isinstance(path, cls):
rev = path.rev
auth = path.auth
path = path.strpath
proto, uri = path.split("://", 1)
host, uripath = uri.split('/', 1)
@ -36,6 +37,7 @@ class SvnCommandPath(svncommon.SvnPathBase):
path = path.rstrip('/')
self.strpath = path
self.rev = rev
self.auth = auth
return self
def __repr__(self):
@ -44,7 +46,8 @@ class SvnCommandPath(svncommon.SvnPathBase):
else:
return 'svnurl(%r, %r)' % (self.strpath, self.rev)
def _svn(self, cmd, *args):
def _svnwithrev(self, cmd, *args):
""" execute an svn command, append our own url and revision """
if self.rev is None:
return self._svnwrite(cmd, *args)
else:
@ -52,16 +55,28 @@ class SvnCommandPath(svncommon.SvnPathBase):
return self._svnwrite(cmd, *args)
def _svnwrite(self, cmd, *args):
""" execute an svn command, append our own url """
l = ['svn %s' % cmd]
args = ['"%s"' % self._escape(item) for item in args]
l.extend(args)
l.append('"%s"' % self._encodedurl())
# fixing the locale because we can't otherwise parse
string = svncommon.fixlocale() + " ".join(l)
string = " ".join(l)
if DEBUG:
print "execing", string
out = self._svncmdexecauth(string)
return out
def _svncmdexecauth(self, cmd):
""" execute an svn command 'as is' """
cmd = svncommon.fixlocale() + cmd
if self.auth is not None:
cmd += ' ' + self.auth.makecmdoptions()
return self._cmdexec(cmd)
def _cmdexec(self, cmd):
try:
out = process.cmdexec(string)
out = process.cmdexec(cmd)
except py.process.cmdexec.Error, e:
if (e.err.find('File Exists') != -1 or
e.err.find('File already exists') != -1):
@ -69,21 +84,33 @@ class SvnCommandPath(svncommon.SvnPathBase):
raise
return out
def _svnpopenauth(self, cmd):
""" execute an svn command, return a pipe for reading stdin """
cmd = svncommon.fixlocale() + cmd
if self.auth is not None:
cmd += ' ' + self.auth.makecmdoptions()
return self._popen(cmd)
def _popen(self, cmd):
return os.popen(cmd)
def _encodedurl(self):
return self._escape(self.strpath)
def _norev_delentry(self, path):
auth = self.auth and self.auth.makecmdoptions() or None
self._lsnorevcache.delentry((str(path), auth))
def open(self, mode='r'):
""" return an opened file with the given mode. """
assert 'w' not in mode and 'a' not in mode, "XXX not implemented for svn cmdline"
assert self.check(file=1) # svn cat returns an empty file otherwise
def popen(cmd):
return os.popen(cmd)
if self.rev is None:
return popen(svncommon.fixlocale() +
'svn cat "%s"' % (self._escape(self.strpath), ))
return self._svnpopenauth('svn cat "%s"' % (
self._escape(self.strpath), ))
else:
return popen(svncommon.fixlocale() +
'svn cat -r %s "%s"' % (self.rev, self._escape(self.strpath)))
return self._svnpopenauth('svn cat -r %s "%s"' % (
self.rev, self._escape(self.strpath)))
def dirpath(self, *args, **kwargs):
""" return the directory path of the current path joined
@ -104,33 +131,33 @@ a checkin message by giving a keyword argument 'msg'"""
commit_msg=kwargs.get('msg', "mkdir by py lib invocation")
createpath = self.join(*args)
createpath._svnwrite('mkdir', '-m', commit_msg)
self._lsnorevcache.delentry(createpath.dirpath().strpath)
self._norev_delentry(createpath.dirpath())
return createpath
def copy(self, target, msg='copied by py lib invocation'):
""" copy path to target with checkin message msg."""
if getattr(target, 'rev', None) is not None:
raise py.error.EINVAL(target, "revisions are immutable")
process.cmdexec('svn copy -m "%s" "%s" "%s"' %(msg,
self._svncmdexecauth('svn copy -m "%s" "%s" "%s"' %(msg,
self._escape(self), self._escape(target)))
self._lsnorevcache.delentry(target.dirpath().strpath)
self._norev_delentry(target.dirpath())
def rename(self, target, msg="renamed by py lib invocation"):
""" rename this path to target with checkin message msg. """
if getattr(self, 'rev', None) is not None:
raise py.error.EINVAL(self, "revisions are immutable")
py.process.cmdexec('svn move -m "%s" --force "%s" "%s"' %(
self._svncmdexecauth('svn move -m "%s" --force "%s" "%s"' %(
msg, self._escape(self), self._escape(target)))
self._lsnorevcache.delentry(self.dirpath().strpath)
self._lsnorevcache.delentry(self.strpath)
self._norev_delentry(self.dirpath())
self._norev_delentry(self)
def remove(self, rec=1, msg='removed by py lib invocation'):
""" remove a file or directory (or a directory tree if rec=1) with
checkin message msg."""
if self.rev is not None:
raise py.error.EINVAL(self, "revisions are immutable")
process.cmdexec('svn rm -m "%s" "%s"' %(msg, self._escape(self)))
self._lsnorevcache.delentry(self.dirpath().strpath)
self._svncmdexecauth('svn rm -m "%s" "%s"' %(msg, self._escape(self)))
self._norev_delentry(self.dirpath())
def export(self, topath):
""" export to a local path
@ -143,7 +170,7 @@ checkin message msg."""
'"%s"' % (self._escape(topath),)]
if self.rev is not None:
args = ['-r', str(self.rev)] + args
process.cmdexec('svn export %s' % (' '.join(args),))
self._svncmdexecauth('svn export %s' % (' '.join(args),))
return topath
def ensure(self, *args, **kwargs):
@ -173,19 +200,19 @@ checkin message msg."""
"ensure %s" % self._escape(tocreate),
self._escape(tempdir.join(basename)),
x.join(basename)._encodedurl())
process.cmdexec(cmd)
self._lsnorevcache.delentry(x.strpath) # !!!
self._svncmdexecauth(cmd)
self._norev_delentry(x)
finally:
tempdir.remove()
return target
# end of modifying methods
def _propget(self, name):
res = self._svn('propget', name)
res = self._svnwithrev('propget', name)
return res[:-1] # strip trailing newline
def _proplist(self):
res = self._svn('proplist')
res = self._svnwithrev('proplist')
lines = res.split('\n')
lines = map(str.strip, lines[1:])
return svncommon.PropListDict(self, lines)
@ -194,7 +221,7 @@ checkin message msg."""
""" return sequence of name-info directory entries of self """
def builder():
try:
res = self._svn('ls', '-v')
res = self._svnwithrev('ls', '-v')
except process.cmdexec.Error, e:
if e.err.find('non-existent in that revision') != -1:
raise py.error.ENOENT(self, e.err)
@ -214,10 +241,13 @@ checkin message msg."""
info = InfoSvnCommand(lsline)
nameinfo_seq.append((info._name, info))
return nameinfo_seq
auth = self.auth and self.auth.makecmdoptions() or None
if self.rev is not None:
return self._lsrevcache.getorbuild((self.strpath, self.rev), builder)
return self._lsrevcache.getorbuild((self.strpath, self.rev, auth),
builder)
else:
return self._lsnorevcache.getorbuild(self.strpath, builder)
return self._lsnorevcache.getorbuild((self.strpath, auth),
builder)
def log(self, rev_start=None, rev_end=1, verbose=False):
""" return a list of LogEntry instances for this path.
@ -234,8 +264,7 @@ if verbose is True, then the LogEntry instances also know which files changed.
else:
rev_opt = "-r %s:%s" % (rev_start, rev_end)
verbose_opt = verbose and "-v" or ""
xmlpipe = os.popen(svncommon.fixlocale() +
'svn log --xml %s %s "%s"' %
xmlpipe = self._svnpopenauth('svn log --xml %s %s "%s"' %
(rev_opt, verbose_opt, self.strpath))
from xml.dom import minidom
tree = minidom.parse(xmlpipe)
@ -254,7 +283,7 @@ class InfoSvnCommand:
# the '0?' part in the middle is an indication of whether the resource is
# locked, see 'svn help ls'
lspattern = re.compile(
r'^ *(?P<rev>\d+) +(?P<author>\S+) +(0? *(?P<size>\d+))? '
r'^ *(?P<rev>\d+) +(?P<author>.+?) +(0? *(?P<size>\d+))? '
'*(?P<date>\w+ +\d{2} +[\d:]+) +(?P<file>.*)$')
def __init__(self, line):
# this is a typical line from 'svn ls http://...'

View File

@ -25,7 +25,7 @@ class SvnWCCommandPath(common.FSPathBase):
"""
sep = os.sep
def __new__(cls, wcpath=None):
def __new__(cls, wcpath=None, auth=None):
self = object.__new__(cls)
if isinstance(wcpath, cls):
if wcpath.__class__ == cls:
@ -35,6 +35,7 @@ class SvnWCCommandPath(common.FSPathBase):
svncommon.ALLOWED_CHARS):
raise ValueError("bad char in wcpath %s" % (wcpath, ))
self.localpath = py.path.local(wcpath)
self.auth = auth
return self
strpath = property(lambda x: str(x.localpath), None, None, "string path")
@ -63,13 +64,22 @@ class SvnWCCommandPath(common.FSPathBase):
info = self.info()
return py.path.svnurl(info.url)
def __repr__(self):
return "svnwc(%r)" % (self.strpath) # , self._url)
def __str__(self):
return str(self.localpath)
def _makeauthoptions(self):
if self.auth is None:
return ''
return self.auth.makecmdoptions()
def _authsvn(self, cmd, args=None):
args = args and list(args) or []
args.append(self._makeauthoptions())
return self._svn(cmd, *args)
def _svn(self, cmd, *args):
l = ['svn %s' % cmd]
args = [self._escape(item) for item in args]
@ -103,7 +113,7 @@ class SvnWCCommandPath(common.FSPathBase):
def switch(self, url):
""" switch to given URL. """
self._svn('switch', url)
self._authsvn('switch', [url])
def checkout(self, url=None, rev=None):
""" checkout from url to local wcpath. """
@ -119,11 +129,12 @@ class SvnWCCommandPath(common.FSPathBase):
url += "@%d" % rev
else:
args.append('-r' + str(rev))
self._svn('co', url, *args)
args.append(url)
self._authsvn('co', args)
def update(self, rev = 'HEAD'):
""" update working copy item to given revision. (None -> HEAD). """
self._svn('up -r %s' % rev)
self._authsvn('up', ['-r', rev])
def write(self, content, mode='wb'):
""" write content into local filesystem wc. """
@ -131,7 +142,7 @@ class SvnWCCommandPath(common.FSPathBase):
def dirpath(self, *args):
""" return the directory Path of the current Path. """
return self.__class__(self.localpath.dirpath(*args))
return self.__class__(self.localpath.dirpath(*args), auth=self.auth)
def _ensuredirs(self):
parent = self.dirpath()
@ -197,18 +208,21 @@ class SvnWCCommandPath(common.FSPathBase):
""" rename this path to target. """
py.process.cmdexec("svn move --force %s %s" %(str(self), str(target)))
_rex_status = re.compile(r'\s+(\d+|-)\s+(\S+)\s+(\S+)\s+(.*)')
# XXX a bit scary to assume there's always 2 spaces between username and
# path, however with win32 allowing spaces in user names there doesn't
# seem to be a more solid approach :(
_rex_status = re.compile(r'\s+(\d+|-)\s+(\S+)\s+(.+?)\s{2,}(.*)')
def lock(self):
""" set a lock (exclusive) on the resource """
out = self._svn('lock').strip()
out = self._authsvn('lock').strip()
if not out:
# warning or error, raise exception
raise Exception(out[4:])
def unlock(self):
""" unset a previously set lock """
out = self._svn('unlock').strip()
out = self._authsvn('unlock').strip()
if out.startswith('svn:'):
# warning or error, raise exception
raise Exception(out[4:])
@ -248,7 +262,8 @@ class SvnWCCommandPath(common.FSPathBase):
update_rev = None
out = self._svn('status -v %s %s %s' % (updates, rec, externals))
cmd = 'status -v %s %s %s' % (updates, rec, externals)
out = self._authsvn(cmd)
rootstatus = WCStatus(self)
for line in out.split('\n'):
if not line.strip():
@ -266,7 +281,8 @@ class SvnWCCommandPath(common.FSPathBase):
wcpath = self.join(fn, abs=1)
rootstatus.unknown.append(wcpath)
elif c0 == 'X':
wcpath = self.__class__(self.localpath.join(fn, abs=1))
wcpath = self.__class__(self.localpath.join(fn, abs=1),
auth=self.auth)
rootstatus.external.append(wcpath)
elif c0 == 'I':
wcpath = self.join(fn, abs=1)
@ -334,10 +350,10 @@ class SvnWCCommandPath(common.FSPathBase):
""" return a diff of the current path against revision rev (defaulting
to the last one).
"""
if rev is None:
out = self._svn('diff')
else:
out = self._svn('diff -r %d' % rev)
args = []
if rev is not None:
args.append("-r %d" % rev)
out = self._authsvn('diff', args)
return out
def blame(self):
@ -365,7 +381,7 @@ class SvnWCCommandPath(common.FSPathBase):
cmd = 'commit -m "%s" --force-log' % (msg.replace('"', '\\"'),)
if not rec:
cmd += ' -N'
out = self._svn(cmd)
out = self._authsvn(cmd)
try:
del cache.info[self]
except KeyError:
@ -431,7 +447,7 @@ recursively. """
localpath = self.localpath.new(**kw)
else:
localpath = self.localpath
return self.__class__(localpath)
return self.__class__(localpath, auth=self.auth)
def join(self, *args, **kwargs):
""" return a new Path (with the same revision) which is composed
@ -440,7 +456,7 @@ recursively. """
if not args:
return self
localpath = self.localpath.join(*args, **kwargs)
return self.__class__(localpath)
return self.__class__(localpath, auth=self.auth)
def info(self, usecache=1):
""" return an Info structure with svn-provided information. """
@ -483,7 +499,7 @@ recursively. """
paths = []
for localpath in self.localpath.listdir(notsvn):
p = self.__class__(localpath)
p = self.__class__(localpath, auth=self.auth)
paths.append(p)
if fil or sort:
@ -534,10 +550,12 @@ if verbose is True, then the LogEntry instances also know which files changed.
else:
rev_opt = "-r %s:%s" % (rev_start, rev_end)
verbose_opt = verbose and "-v" or ""
s = svncommon.fixlocale()
locale_env = svncommon.fixlocale()
# some blather on stderr
stdin, stdout, stderr = os.popen3(s + 'svn log --xml %s %s "%s"' % (
rev_opt, verbose_opt,
auth_opt = self._makeauthoptions()
stdin, stdout, stderr = os.popen3(locale_env +
'svn log --xml %s %s %s "%s"' % (
rev_opt, verbose_opt, auth_opt,
self.strpath))
from xml.dom import minidom
from xml.parsers.expat import ExpatError
@ -562,7 +580,7 @@ if verbose is True, then the LogEntry instances also know which files changed.
return self.info().mtime
def __hash__(self):
return hash((self.strpath, self.__class__))
return hash((self.strpath, self.__class__, self.auth))
class WCStatus: