2
svn-Command based Implementation of a Subversion WorkingCopy Path.
4
SvnWCCommandPath is the main class.
8
import os, sys, time, re, calendar
11
from py._path import common
13
#-----------------------------------------------------------
14
# Caching latest repository revision and repo-paths
15
# (getting them is slow with the current implementations)
18
#-----------------------------------------------------------
27
def __init__(self, url, rev, timestamp):
30
self.timestamp = timestamp
33
return "repo: %s;%s %s" %(self.url, self.rev, self.timestamp)
36
""" The Repocache manages discovered repository paths
37
and their revisions. If inside a timeout the cache
38
will even return the revision of the root.
40
timeout = 20 # seconds after which we forget that we know the last revision
48
def put(self, url, rev, timestamp=None):
52
timestamp = time.time()
54
for entry in self.repos:
56
entry.timestamp = timestamp
58
#print "set repo", entry
61
entry = RepoEntry(url, rev, timestamp)
62
self.repos.append(entry)
63
#print "appended repo", entry
67
for entry in self.repos:
68
if url.startswith(entry.url):
69
if now < entry.timestamp + self.timeout:
70
#print "returning immediate Etrny", entry
71
return entry.url, entry.rev
75
repositories = RepoCache()
80
ALLOWED_CHARS = "_ -/\\=$.~+%" #add characters as necessary when tested
81
if sys.platform == "win32":
83
ALLOWED_CHARS_HOST = ALLOWED_CHARS + '@:'
85
def _getsvnversion(ver=[]):
89
v = py.process.cmdexec("svn -q --version")
91
v = '.'.join(v.split('.')[:2])
95
def _escape_helper(text):
97
if py.std.sys.platform != 'win32':
98
text = str(text).replace('$', '\\$')
101
def _check_for_bad_chars(text, allowed_chars=ALLOWED_CHARS):
105
if c in allowed_chars:
110
def checkbadchars(url):
111
# (hpk) not quite sure about the exact purpose, guido w.?
112
proto, uri = url.split("://", 1)
114
host, uripath = uri.split('/', 1)
115
# only check for bad chars in the non-protocol parts
116
if (_check_for_bad_chars(host, ALLOWED_CHARS_HOST) \
117
or _check_for_bad_chars(uripath, ALLOWED_CHARS)):
118
raise ValueError("bad char in %r" % (url, ))
121
#_______________________________________________________________
123
class SvnPathBase(common.PathBase):
124
""" Base implementation for SvnPath implementations. """
129
url = property(_geturl, None, None, "url of this svn-path.")
132
""" return a string representation (including rev-number) """
136
return hash(self.strpath)
139
""" create a modified version of this path. A 'rev' argument
140
indicates a new revision.
141
the following keyword arguments modify various path parts::
143
http://host.com/repo/path/file.ext
144
|-----------------------| dirname
149
obj = object.__new__(self.__class__)
150
obj.rev = kw.get('rev', self.rev)
151
obj.auth = kw.get('auth', self.auth)
152
dirname, basename, purebasename, ext = self._getbyspec(
153
"dirname,basename,purebasename,ext")
155
if 'purebasename' in kw or 'ext' in kw:
156
raise ValueError("invalid specification %r" % kw)
158
pb = kw.setdefault('purebasename', purebasename)
159
ext = kw.setdefault('ext', ext)
160
if ext and not ext.startswith('.'):
162
kw['basename'] = pb + ext
164
kw.setdefault('dirname', dirname)
165
kw.setdefault('sep', self.sep)
167
obj.strpath = "%(dirname)s%(sep)s%(basename)s" % kw
169
obj.strpath = "%(dirname)s" % kw
172
def _getbyspec(self, spec):
173
""" get specified parts of the path. 'arg' is a string
174
with comma separated path parts. The parts are returned
175
in exactly the order of the specification.
177
you may specify the following parts:
179
http://host.com/repo/path/file.ext
180
|-----------------------| dirname
186
parts = self.strpath.split(self.sep)
187
for name in spec.split(','):
189
if name == 'dirname':
190
res.append(self.sep.join(parts[:-1]))
191
elif name == 'basename':
192
res.append(parts[-1])
195
i = basename.rfind('.')
197
purebasename, ext = basename, ''
199
purebasename, ext = basename[:i], basename[i:]
200
if name == 'purebasename':
201
res.append(purebasename)
205
raise NameError("Don't know part %r" % name)
208
def __eq__(self, other):
209
""" return true if path and rev attributes each match """
210
return (str(self) == str(other) and
211
(self.rev == other.rev or self.rev == other.rev))
213
def __ne__(self, other):
214
return not self == other
216
def join(self, *args):
217
""" return a new Path (with the same revision) which is composed
218
of the self Path followed by 'args' path components.
223
args = tuple([arg.strip(self.sep) for arg in args])
224
parts = (self.strpath, ) + args
225
newpath = self.__class__(self.sep.join(parts), self.rev, self.auth)
228
def propget(self, name):
229
""" return the content of the given property. """
230
value = self._propget(name)
234
""" list all property names. """
235
content = self._proplist()
239
""" Return the size of the file content of the Path. """
240
return self.info().size
243
""" Return the last modification time of the file. """
244
return self.info().mtime
246
# shared help methods
248
def _escape(self, cmd):
249
return _escape_helper(cmd)
252
#def _childmaxrev(self):
253
# """ return maximum revision number of childs (or self.rev if no childs) """
255
# for name, info in self._listdir_nameinfo():
256
# rev = max(rev, info.created_rev)
259
#def _getlatestrevision(self):
260
# """ return latest repo-revision for this path. """
262
# path = self.__class__(url, None)
264
# # we need a long walk to find the root-repo and revision
267
# rev = max(rev, path._childmaxrev())
269
# path = path.dirpath()
270
# except (IOError, process.cmdexec.Error):
273
# raise IOError, "could not determine newest repo revision for %s" % self
276
class Checkers(common.Checkers):
279
return self.path.info().kind == 'dir'
280
except py.error.Error:
281
return self._listdirworks()
283
def _listdirworks(self):
286
except py.error.ENOENT:
293
return self.path.info().kind == 'file'
294
except py.error.ENOENT:
299
return self.path.info()
300
except py.error.ENOENT:
301
return self._listdirworks()
303
def parse_apr_time(timestr):
304
i = timestr.rfind('.')
306
raise ValueError("could not parse %s" % timestr)
307
timestr = timestr[:i]
308
parsedtime = time.strptime(timestr, "%Y-%m-%dT%H:%M:%S")
309
return time.mktime(parsedtime)
311
class PropListDict(dict):
312
""" a Dictionary which fetches values (InfoSvnCommand instances) lazily"""
313
def __init__(self, path, keynames):
314
dict.__init__(self, [(x, None) for x in keynames])
317
def __getitem__(self, key):
318
value = dict.__getitem__(self, key)
320
value = self.path.propget(key)
321
dict.__setitem__(self, key, value)
325
if sys.platform != 'win32':
329
# some nasty chunk of code to solve path and url conversion and quoting issues
330
ILLEGAL_CHARS = '* | \ / : < > ? \t \n \x0b \x0c \r'.split(' ')
331
if os.sep in ILLEGAL_CHARS:
332
ILLEGAL_CHARS.remove(os.sep)
333
ISWINDOWS = sys.platform == 'win32'
334
_reg_allow_disk = re.compile(r'^([a-z]\:\\)?[^:]+$', re.I)
335
def _check_path(path):
336
illegal = ILLEGAL_CHARS[:]
340
if not _reg_allow_disk.match(sp):
341
raise ValueError('path may not contain a colon (:)')
343
if char not in string.printable or char in illegal:
344
raise ValueError('illegal character %r in path' % (char,))
346
def path_to_fspath(path, addat=True):
349
if addat and path.rev != -1:
350
sp = '%s@%s' % (sp, path.rev)
352
sp = '%s@HEAD' % (sp,)
355
def url_from_path(path):
356
fspath = path_to_fspath(path, False)
357
quote = py.std.urllib.quote
359
match = _reg_allow_disk.match(fspath)
360
fspath = fspath.replace('\\', '/')
362
fspath = '/%s%s' % (match.group(1).replace('\\', '/'),
363
quote(fspath[len(match.group(1)):]))
365
fspath = quote(fspath)
367
fspath = quote(fspath)
369
fspath = '%s@%s' % (fspath, path.rev)
371
fspath = '%s@HEAD' % (fspath,)
372
return 'file://%s' % (fspath,)
374
class SvnAuth(object):
375
""" container for auth information for Subversion """
376
def __init__(self, username, password, cache_auth=True, interactive=True):
377
self.username = username
378
self.password = password
379
self.cache_auth = cache_auth
380
self.interactive = interactive
382
def makecmdoptions(self):
383
uname = self.username.replace('"', '\\"')
384
passwd = self.password.replace('"', '\\"')
387
ret.append('--username="%s"' % (uname,))
389
ret.append('--password="%s"' % (passwd,))
390
if not self.cache_auth:
391
ret.append('--no-auth-cache')
392
if not self.interactive:
393
ret.append('--non-interactive')
397
return "<SvnAuth username=%s ...>" %(self.username,)
399
rex_blame = re.compile(r'\s*(\d+)\s*(\S+) (.*)')
401
class SvnWCCommandPath(common.PathBase):
402
""" path implementation offering access/modification to svn working copies.
403
It has methods similar to the functions in os.path and similar to the
404
commands of the svn client.
408
def __new__(cls, wcpath=None, auth=None):
409
self = object.__new__(cls)
410
if isinstance(wcpath, cls):
411
if wcpath.__class__ == cls:
413
wcpath = wcpath.localpath
414
if _check_for_bad_chars(str(wcpath),
416
raise ValueError("bad char in wcpath %s" % (wcpath, ))
417
self.localpath = py.path.local(wcpath)
421
strpath = property(lambda x: str(x.localpath), None, None, "string path")
422
rev = property(lambda x: x.info(usecache=0).rev, None, None, "revision")
424
def __eq__(self, other):
425
return self.localpath == getattr(other, 'localpath', None)
428
if getattr(self, '_url', None) is None:
430
self._url = info.url #SvnPath(info.url, info.rev)
431
assert isinstance(self._url, py.builtin._basestring)
434
url = property(_geturl, None, None, "url of this WC item")
436
def _escape(self, cmd):
437
return _escape_helper(cmd)
440
""" pickle object into path location"""
441
return self.localpath.dump(obj)
444
""" return current SvnPath for this WC-item. """
446
return py.path.svnurl(info.url)
449
return "svnwc(%r)" % (self.strpath) # , self._url)
452
return str(self.localpath)
454
def _makeauthoptions(self):
455
if self.auth is None:
457
return self.auth.makecmdoptions()
459
def _authsvn(self, cmd, args=None):
460
args = args and list(args) or []
461
args.append(self._makeauthoptions())
462
return self._svn(cmd, *args)
464
def _svn(self, cmd, *args):
466
args = [self._escape(item) for item in args]
468
l.append('"%s"' % self._escape(self.strpath))
469
# try fixing the locale because we can't otherwise parse
470
string = fixlocale() + " ".join(l)
474
hold = os.environ.get(key)
475
os.environ[key] = 'C'
476
out = py.process.cmdexec(string)
479
os.environ[key] = hold
482
except py.process.cmdexec.Error:
483
e = sys.exc_info()[1]
484
strerr = e.err.lower()
485
if strerr.find('not found') != -1:
486
raise py.error.ENOENT(self)
487
elif strerr.find("E200009:") != -1:
488
raise py.error.ENOENT(self)
489
if (strerr.find('file exists') != -1 or
490
strerr.find('file already exists') != -1 or
491
strerr.find('w150002:') != -1 or
492
strerr.find("can't create directory") != -1):
493
raise py.error.EEXIST(strerr) #self)
497
def switch(self, url):
498
""" switch to given URL. """
499
self._authsvn('switch', [url])
501
def checkout(self, url=None, rev=None):
502
""" checkout from url to local wcpath. """
506
if rev is None or rev == -1:
507
if (py.std.sys.platform != 'win32' and
508
_getsvnversion() == '1.3'):
511
if _getsvnversion() == '1.3':
514
args.append('-r' + str(rev))
516
self._authsvn('co', args)
518
def update(self, rev='HEAD', interactive=True):
519
""" update working copy item to given revision. (None -> HEAD). """
522
opts.append("--non-interactive")
523
self._authsvn('up', opts)
525
def write(self, content, mode='w'):
526
""" write content into local filesystem wc. """
527
self.localpath.write(content, mode)
529
def dirpath(self, *args):
530
""" return the directory Path of the current Path. """
531
return self.__class__(self.localpath.dirpath(*args), auth=self.auth)
533
def _ensuredirs(self):
534
parent = self.dirpath()
535
if parent.check(dir=0):
537
if self.check(dir=0):
541
def ensure(self, *args, **kwargs):
542
""" ensure that an args-joined path exists (by default as
543
a file). if you specify a keyword argument 'directory=True'
544
then the path is forced to be a directory path.
548
if p.check(versioned=False):
551
if kwargs.get('dir', 0):
552
return p._ensuredirs()
559
def mkdir(self, *args):
560
""" create & return the directory joined with args. """
562
return self.join(*args).mkdir()
568
""" add ourself to svn """
571
def remove(self, rec=1, force=1):
572
""" remove a file or a directory tree. 'rec'ursive is
573
ignored and considered always true (because of
574
underlying svn semantics.
576
assert rec, "svn cannot remove non-recursively"
577
if not self.check(versioned=True):
578
# not added to svn (anymore?), just remove
579
py.path.local(self).remove()
583
flags.append('--force')
584
self._svn('remove', *flags)
586
def copy(self, target):
587
""" copy path to target."""
588
py.process.cmdexec("svn copy %s %s" %(str(self), str(target)))
590
def rename(self, target):
591
""" rename this path to target. """
592
py.process.cmdexec("svn move --force %s %s" %(str(self), str(target)))
595
""" set a lock (exclusive) on the resource """
596
out = self._authsvn('lock').strip()
598
# warning or error, raise exception
599
raise ValueError("unknown error in svn lock command")
602
""" unset a previously set lock """
603
out = self._authsvn('unlock').strip()
604
if out.startswith('svn:'):
605
# warning or error, raise exception
606
raise Exception(out[4:])
609
""" remove any locks from the resource """
610
# XXX should be fixed properly!!!
616
def status(self, updates=0, rec=0, externals=0):
617
""" return (collective) Status object for this file. """
618
# http://svnbook.red-bean.com/book.html#svn-ch-3-sect-4.3.1
622
raise ValueError("XXX cannot perform status() "
623
"on external items yet")
625
#1.2 supports: externals = '--ignore-externals'
630
rec = '--non-recursive'
632
# XXX does not work on all subversion versions
634
# externals = '--ignore-externals'
642
cmd = 'status -v --xml --no-ignore %s %s %s' % (
643
updates, rec, externals)
644
out = self._authsvn(cmd)
645
except py.process.cmdexec.Error:
646
cmd = 'status -v --no-ignore %s %s %s' % (
647
updates, rec, externals)
648
out = self._authsvn(cmd)
649
rootstatus = WCStatus(self).fromstring(out, self)
651
rootstatus = XMLWCStatus(self).fromstring(out, self)
654
def diff(self, rev=None):
655
""" return a diff of the current path against revision rev (defaulting
660
args.append("-r %d" % rev)
661
out = self._authsvn('diff', args)
665
""" return a list of tuples of three elements:
666
(revision, commiter, line)
668
out = self._svn('blame')
670
blamelines = out.splitlines()
671
reallines = py.path.svnurl(self.url).readlines()
672
for i, (blameline, line) in enumerate(
673
zip(blamelines, reallines)):
674
m = rex_blame.match(blameline)
676
raise ValueError("output line %r of svn blame does not match "
677
"expected format" % (line, ))
678
rev, name, _ = m.groups()
679
result.append((int(rev), name, line))
682
_rex_commit = re.compile(r'.*Committed revision (\d+)\.$', re.DOTALL)
683
def commit(self, msg='', rec=1):
684
""" commit with support for non-recursive commits """
685
# XXX i guess escaping should be done better here?!?
686
cmd = 'commit -m "%s" --force-log' % (msg.replace('"', '\\"'),)
689
out = self._authsvn(cmd)
695
m = self._rex_commit.match(out)
696
return int(m.group(1))
698
def propset(self, name, value, *args):
699
""" set property name to value on this path. """
700
d = py.path.local.mkdtemp()
704
self._svn('propset', name, '--file', str(p), *args)
708
def propget(self, name):
709
""" get property name on this path. """
710
res = self._svn('propget', name)
711
return res[:-1] # strip trailing newline
713
def propdel(self, name):
714
""" delete property name on this path. """
715
res = self._svn('propdel', name)
716
return res[:-1] # strip trailing newline
718
def proplist(self, rec=0):
719
""" return a mapping of property names to property values.
720
If rec is True, then return a dictionary mapping sub-paths to such mappings.
723
res = self._svn('proplist -R')
724
return make_recursive_propdict(self, res)
726
res = self._svn('proplist')
727
lines = res.split('\n')
728
lines = [x.strip() for x in lines[1:]]
729
return PropListDict(self, lines)
731
def revert(self, rec=0):
732
""" revert the local changes of this path. if rec is True, do so
735
result = self._svn('revert -R')
737
result = self._svn('revert')
741
""" create a modified version of this path. A 'rev' argument
742
indicates a new revision.
743
the following keyword arguments modify various path parts:
745
http://host.com/repo/path/file.ext
746
|-----------------------| dirname
752
localpath = self.localpath.new(**kw)
754
localpath = self.localpath
755
return self.__class__(localpath, auth=self.auth)
757
def join(self, *args, **kwargs):
758
""" return a new Path (with the same revision) which is composed
759
of the self Path followed by 'args' path components.
763
localpath = self.localpath.join(*args, **kwargs)
764
return self.__class__(localpath, auth=self.auth)
766
def info(self, usecache=1):
767
""" return an Info structure with svn-provided information. """
768
info = usecache and cache.info.get(self)
771
output = self._svn('info')
772
except py.process.cmdexec.Error:
773
e = sys.exc_info()[1]
774
if e.err.find('Path is not a working copy directory') != -1:
775
raise py.error.ENOENT(self, e.err)
776
elif e.err.find("is not under version control") != -1:
777
raise py.error.ENOENT(self, e.err)
779
# XXX SVN 1.3 has output on stderr instead of stdout (while it does
780
# return 0!), so a bit nasty, but we assume no output is output
782
if (output.strip() == '' or
783
output.lower().find('not a versioned resource') != -1):
784
raise py.error.ENOENT(self, output)
785
info = InfoSvnWCCommand(output)
787
# Can't reliably compare on Windows without access to win32api
788
if py.std.sys.platform != 'win32':
789
if info.path != self.localpath:
790
raise py.error.ENOENT(self, "not a versioned resource:" +
791
" %s != %s" % (info.path, self.localpath))
792
cache.info[self] = info
795
def listdir(self, fil=None, sort=None):
796
""" return a sequence of Paths.
798
listdir will return either a tuple or a list of paths
799
depending on implementation choices.
801
if isinstance(fil, str):
802
fil = common.FNMatcher(fil)
803
# XXX unify argument naming with LocalPath.listdir
805
return path.basename != '.svn'
808
for localpath in self.localpath.listdir(notsvn):
809
p = self.__class__(localpath, auth=self.auth)
810
if notsvn(p) and (not fil or fil(p)):
812
self._sortlist(paths, sort)
815
def open(self, mode='r'):
816
""" return an opened file with the given mode. """
817
return open(self.strpath, mode)
819
def _getbyspec(self, spec):
820
return self.localpath._getbyspec(spec)
822
class Checkers(py.path.local.Checkers):
823
def __init__(self, path):
824
self.svnwcpath = path
825
self.path = path.localpath
828
s = self.svnwcpath.info()
829
except (py.error.ENOENT, py.error.EEXIST):
831
except py.process.cmdexec.Error:
832
e = sys.exc_info()[1]
833
if e.err.find('is not a working copy')!=-1:
835
if e.err.lower().find('not a versioned resource') != -1:
841
def log(self, rev_start=None, rev_end=1, verbose=False):
842
""" return a list of LogEntry instances for this path.
843
rev_start is the starting revision (defaulting to the first one).
844
rev_end is the last revision (defaulting to HEAD).
845
if verbose is True, then the LogEntry instances also know which files changed.
847
assert self.check() # make it simpler for the pipe
848
rev_start = rev_start is None and "HEAD" or rev_start
849
rev_end = rev_end is None and "HEAD" or rev_end
850
if rev_start == "HEAD" and rev_end == 1:
853
rev_opt = "-r %s:%s" % (rev_start, rev_end)
854
verbose_opt = verbose and "-v" or ""
855
locale_env = fixlocale()
856
# some blather on stderr
857
auth_opt = self._makeauthoptions()
858
#stdin, stdout, stderr = os.popen3(locale_env +
859
# 'svn log --xml %s %s %s "%s"' % (
860
# rev_opt, verbose_opt, auth_opt,
862
cmd = locale_env + 'svn log --xml %s %s %s "%s"' % (
863
rev_opt, verbose_opt, auth_opt, self.strpath)
865
popen = subprocess.Popen(cmd,
866
stdout=subprocess.PIPE,
867
stderr=subprocess.PIPE,
870
stdout, stderr = popen.communicate()
871
stdout = py.builtin._totext(stdout, sys.getdefaultencoding())
872
minidom,ExpatError = importxml()
874
tree = minidom.parseString(stdout)
876
raise ValueError('no such revision')
878
for logentry in filter(None, tree.firstChild.childNodes):
879
if logentry.nodeType == logentry.ELEMENT_NODE:
880
result.append(LogEntry(logentry))
884
""" Return the size of the file content of the Path. """
885
return self.info().size
888
""" Return the last modification time of the file. """
889
return self.info().mtime
892
return hash((self.strpath, self.__class__, self.auth))
896
attrnames = ('modified','added', 'conflict', 'unchanged', 'external',
897
'deleted', 'prop_modified', 'unknown', 'update_available',
898
'incomplete', 'kindmismatch', 'ignored', 'locked', 'replaced'
901
def __init__(self, wcpath, rev=None, modrev=None, author=None):
907
for name in self.attrnames:
908
setattr(self, name, [])
910
def allpath(self, sort=True, **kw):
912
for name in self.attrnames:
913
if name not in kw or kw[name]:
914
for path in getattr(self, name):
921
# XXX a bit scary to assume there's always 2 spaces between username and
922
# path, however with win32 allowing spaces in user names there doesn't
923
# seem to be a more solid approach :(
924
_rex_status = re.compile(r'\s+(\d+|-)\s+(\S+)\s+(.+?)\s{2,}(.*)')
926
def fromstring(data, rootwcpath, rev=None, modrev=None, author=None):
927
""" return a new WCStatus object from data 's'
929
rootstatus = WCStatus(rootwcpath, rev, modrev, author)
931
for line in data.split('\n'):
934
#print "processing %r" % line
935
flags, rest = line[:8], line[8:]
937
c0,c1,c2,c3,c4,c5,x6,c7 = flags
939
# print "flags", repr(flags), "rest", repr(rest)
942
fn = line.split(None, 1)[1]
944
wcpath = rootwcpath.join(fn, abs=1)
945
rootstatus.unknown.append(wcpath)
947
wcpath = rootwcpath.__class__(
948
rootwcpath.localpath.join(fn, abs=1),
949
auth=rootwcpath.auth)
950
rootstatus.external.append(wcpath)
952
wcpath = rootwcpath.join(fn, abs=1)
953
rootstatus.ignored.append(wcpath)
957
#elif c0 in '~!' or c4 == 'S':
958
# raise NotImplementedError("received flag %r" % c0)
960
m = WCStatus._rex_status.match(rest)
964
wcpath = rootwcpath.join(fn, abs=1)
965
rootstatus.update_available.append(wcpath)
967
if line.lower().find('against revision:')!=-1:
968
update_rev = int(rest.split(':')[1].strip())
970
if line.lower().find('status on external') > -1:
971
# XXX not sure what to do here... perhaps we want to
972
# store some state instead of just continuing, as right
973
# now it makes the top-level external get added twice
974
# (once as external, once as 'normal' unchanged item)
975
# because of the way SVN presents external items
978
raise ValueError("could not parse line %r" % line)
980
rev, modrev, author, fn = m.groups()
981
wcpath = rootwcpath.join(fn, abs=1)
982
#assert wcpath.check()
984
assert wcpath.check(file=1), "didn't expect a directory with changed content here"
985
rootstatus.modified.append(wcpath)
986
elif c0 == 'A' or c3 == '+' :
987
rootstatus.added.append(wcpath)
989
rootstatus.deleted.append(wcpath)
991
rootstatus.conflict.append(wcpath)
993
rootstatus.kindmismatch.append(wcpath)
995
rootstatus.incomplete.append(wcpath)
997
rootstatus.replaced.append(wcpath)
999
rootstatus.unchanged.append(wcpath)
1001
raise NotImplementedError("received flag %r" % c0)
1004
rootstatus.prop_modified.append(wcpath)
1005
# XXX do we cover all client versions here?
1006
if c2 == 'L' or c5 == 'K':
1007
rootstatus.locked.append(wcpath)
1009
rootstatus.update_available.append(wcpath)
1011
if wcpath == rootwcpath:
1012
rootstatus.rev = rev
1013
rootstatus.modrev = modrev
1014
rootstatus.author = author
1016
rootstatus.update_rev = update_rev
1019
fromstring = staticmethod(fromstring)
1021
class XMLWCStatus(WCStatus):
1022
def fromstring(data, rootwcpath, rev=None, modrev=None, author=None):
1023
""" parse 'data' (XML string as outputted by svn st) into a status obj
1025
# XXX for externals, the path is shown twice: once
1026
# with external information, and once with full info as if
1027
# the item was a normal non-external... the current way of
1028
# dealing with this issue is by ignoring it - this does make
1029
# externals appear as external items as well as 'normal',
1030
# unchanged ones in the status object so this is far from ideal
1031
rootstatus = WCStatus(rootwcpath, rev, modrev, author)
1033
minidom, ExpatError = importxml()
1035
doc = minidom.parseString(data)
1037
e = sys.exc_info()[1]
1038
raise ValueError(str(e))
1039
urevels = doc.getElementsByTagName('against')
1041
rootstatus.update_rev = urevels[-1].getAttribute('revision')
1042
for entryel in doc.getElementsByTagName('entry'):
1043
path = entryel.getAttribute('path')
1044
statusel = entryel.getElementsByTagName('wc-status')[0]
1045
itemstatus = statusel.getAttribute('item')
1047
if itemstatus == 'unversioned':
1048
wcpath = rootwcpath.join(path, abs=1)
1049
rootstatus.unknown.append(wcpath)
1051
elif itemstatus == 'external':
1052
wcpath = rootwcpath.__class__(
1053
rootwcpath.localpath.join(path, abs=1),
1054
auth=rootwcpath.auth)
1055
rootstatus.external.append(wcpath)
1057
elif itemstatus == 'ignored':
1058
wcpath = rootwcpath.join(path, abs=1)
1059
rootstatus.ignored.append(wcpath)
1061
elif itemstatus == 'incomplete':
1062
wcpath = rootwcpath.join(path, abs=1)
1063
rootstatus.incomplete.append(wcpath)
1066
rev = statusel.getAttribute('revision')
1067
if itemstatus == 'added' or itemstatus == 'none':
1072
elif itemstatus == "replaced":
1075
#print entryel.toxml()
1076
commitel = entryel.getElementsByTagName('commit')[0]
1078
modrev = commitel.getAttribute('revision')
1080
author_els = commitel.getElementsByTagName('author')
1082
for c in author_els[0].childNodes:
1083
author += c.nodeValue
1085
for c in commitel.getElementsByTagName('date')[0]\
1089
wcpath = rootwcpath.join(path, abs=1)
1091
assert itemstatus != 'modified' or wcpath.check(file=1), (
1092
'did\'t expect a directory with changed content here')
1095
'normal': 'unchanged',
1096
'unversioned': 'unknown',
1097
'conflicted': 'conflict',
1099
}.get(itemstatus, itemstatus)
1101
attr = getattr(rootstatus, itemattrname)
1104
propsstatus = statusel.getAttribute('props')
1105
if propsstatus not in ('none', 'normal'):
1106
rootstatus.prop_modified.append(wcpath)
1108
if wcpath == rootwcpath:
1109
rootstatus.rev = rev
1110
rootstatus.modrev = modrev
1111
rootstatus.author = author
1112
rootstatus.date = date
1114
# handle repos-status element (remote info)
1115
rstatusels = entryel.getElementsByTagName('repos-status')
1117
rstatusel = rstatusels[0]
1118
ritemstatus = rstatusel.getAttribute('item')
1119
if ritemstatus in ('added', 'modified'):
1120
rootstatus.update_available.append(wcpath)
1122
lockels = entryel.getElementsByTagName('lock')
1124
rootstatus.locked.append(wcpath)
1127
fromstring = staticmethod(fromstring)
1129
class InfoSvnWCCommand:
1130
def __init__(self, output):
1132
# URL: http://codespeak.net/svn/std.path/trunk/dist/std.path/test
1133
# Repository UUID: fd0d7bf2-dfb6-0310-8d31-b7ecfe96aada
1135
# Node Kind: directory
1137
# Last Changed Author: hpk
1138
# Last Changed Rev: 2100
1139
# Last Changed Date: 2003-10-27 20:43:14 +0100 (Mon, 27 Oct 2003)
1140
# Properties Last Updated: 2003-11-03 14:47:48 +0100 (Mon, 03 Nov 2003)
1143
for line in output.split('\n'):
1144
if not line.strip():
1146
key, value = line.split(':', 1)
1147
key = key.lower().replace(' ', '')
1148
value = value.strip()
1153
raise ValueError("Not a versioned resource")
1154
#raise ValueError, "Not a versioned resource %r" % path
1155
self.kind = d['nodekind'] == 'directory' and 'dir' or d['nodekind']
1157
self.rev = int(d['revision'])
1161
self.path = py.path.local(d['path'])
1162
self.size = self.path.size()
1163
if 'lastchangedrev' in d:
1164
self.created_rev = int(d['lastchangedrev'])
1165
if 'lastchangedauthor' in d:
1166
self.last_author = d['lastchangedauthor']
1167
if 'lastchangeddate' in d:
1168
self.mtime = parse_wcinfotime(d['lastchangeddate'])
1169
self.time = self.mtime * 1000000
1171
def __eq__(self, other):
1172
return self.__dict__ == other.__dict__
1174
def parse_wcinfotime(timestr):
1175
""" Returns seconds since epoch, UTC. """
1176
# example: 2003-10-27 20:43:14 +0100 (Mon, 27 Oct 2003)
1177
m = re.match(r'(\d+-\d+-\d+ \d+:\d+:\d+) ([+-]\d+) .*', timestr)
1179
raise ValueError("timestring %r does not match" % timestr)
1180
timestr, timezone = m.groups()
1181
# do not handle timezone specially, return value should be UTC
1182
parsedtime = time.strptime(timestr, "%Y-%m-%d %H:%M:%S")
1183
return calendar.timegm(parsedtime)
1185
def make_recursive_propdict(wcroot,
1187
rex = re.compile("Properties on '(.*)':")):
1188
""" Return a dictionary of path->PropListDict mappings. """
1189
lines = [x for x in output.split('\n') if x]
1195
raise ValueError("could not parse propget-line: %r" % line)
1196
path = m.groups()[0]
1197
wcpath = wcroot.join(path, abs=1)
1199
while lines and lines[0].startswith(' '):
1200
propname = lines.pop(0).strip()
1201
propnames.append(propname)
1202
assert propnames, "must have found properties!"
1203
pdict[wcpath] = PropListDict(wcpath, propnames)
1207
def importxml(cache=[]):
1210
from xml.dom import minidom
1211
from xml.parsers.expat import ExpatError
1212
cache.extend([minidom, ExpatError])
1216
def __init__(self, logentry):
1217
self.rev = int(logentry.getAttribute('revision'))
1218
for lpart in filter(None, logentry.childNodes):
1219
if lpart.nodeType == lpart.ELEMENT_NODE:
1220
if lpart.nodeName == 'author':
1221
self.author = lpart.firstChild.nodeValue
1222
elif lpart.nodeName == 'msg':
1223
if lpart.firstChild:
1224
self.msg = lpart.firstChild.nodeValue
1227
elif lpart.nodeName == 'date':
1228
#2003-07-29T20:05:11.598637Z
1229
timestr = lpart.firstChild.nodeValue
1230
self.date = parse_apr_time(timestr)
1231
elif lpart.nodeName == 'paths':
1233
for ppart in filter(None, lpart.childNodes):
1234
if ppart.nodeType == ppart.ELEMENT_NODE:
1235
self.strpaths.append(PathEntry(ppart))
1237
return '<Logentry rev=%d author=%s date=%s>' % (
1238
self.rev, self.author, self.date)