svnwc.py 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240
  1. """
  2. svn-Command based Implementation of a Subversion WorkingCopy Path.
  3. SvnWCCommandPath is the main class.
  4. """
  5. import os, sys, time, re, calendar
  6. import py
  7. import subprocess
  8. from py._path import common
  9. #-----------------------------------------------------------
  10. # Caching latest repository revision and repo-paths
  11. # (getting them is slow with the current implementations)
  12. #
  13. # XXX make mt-safe
  14. #-----------------------------------------------------------
  15. class cache:
  16. proplist = {}
  17. info = {}
  18. entries = {}
  19. prop = {}
  20. class RepoEntry:
  21. def __init__(self, url, rev, timestamp):
  22. self.url = url
  23. self.rev = rev
  24. self.timestamp = timestamp
  25. def __str__(self):
  26. return "repo: %s;%s %s" %(self.url, self.rev, self.timestamp)
  27. class RepoCache:
  28. """ The Repocache manages discovered repository paths
  29. and their revisions. If inside a timeout the cache
  30. will even return the revision of the root.
  31. """
  32. timeout = 20 # seconds after which we forget that we know the last revision
  33. def __init__(self):
  34. self.repos = []
  35. def clear(self):
  36. self.repos = []
  37. def put(self, url, rev, timestamp=None):
  38. if rev is None:
  39. return
  40. if timestamp is None:
  41. timestamp = time.time()
  42. for entry in self.repos:
  43. if url == entry.url:
  44. entry.timestamp = timestamp
  45. entry.rev = rev
  46. #print "set repo", entry
  47. break
  48. else:
  49. entry = RepoEntry(url, rev, timestamp)
  50. self.repos.append(entry)
  51. #print "appended repo", entry
  52. def get(self, url):
  53. now = time.time()
  54. for entry in self.repos:
  55. if url.startswith(entry.url):
  56. if now < entry.timestamp + self.timeout:
  57. #print "returning immediate Etrny", entry
  58. return entry.url, entry.rev
  59. return entry.url, -1
  60. return url, -1
  61. repositories = RepoCache()
  62. # svn support code
  63. ALLOWED_CHARS = "_ -/\\=$.~+%" #add characters as necessary when tested
  64. if sys.platform == "win32":
  65. ALLOWED_CHARS += ":"
  66. ALLOWED_CHARS_HOST = ALLOWED_CHARS + '@:'
  67. def _getsvnversion(ver=[]):
  68. try:
  69. return ver[0]
  70. except IndexError:
  71. v = py.process.cmdexec("svn -q --version")
  72. v.strip()
  73. v = '.'.join(v.split('.')[:2])
  74. ver.append(v)
  75. return v
  76. def _escape_helper(text):
  77. text = str(text)
  78. if sys.platform != 'win32':
  79. text = str(text).replace('$', '\\$')
  80. return text
  81. def _check_for_bad_chars(text, allowed_chars=ALLOWED_CHARS):
  82. for c in str(text):
  83. if c.isalnum():
  84. continue
  85. if c in allowed_chars:
  86. continue
  87. return True
  88. return False
  89. def checkbadchars(url):
  90. # (hpk) not quite sure about the exact purpose, guido w.?
  91. proto, uri = url.split("://", 1)
  92. if proto != "file":
  93. host, uripath = uri.split('/', 1)
  94. # only check for bad chars in the non-protocol parts
  95. if (_check_for_bad_chars(host, ALLOWED_CHARS_HOST) \
  96. or _check_for_bad_chars(uripath, ALLOWED_CHARS)):
  97. raise ValueError("bad char in %r" % (url, ))
  98. #_______________________________________________________________
  99. class SvnPathBase(common.PathBase):
  100. """ Base implementation for SvnPath implementations. """
  101. sep = '/'
  102. def _geturl(self):
  103. return self.strpath
  104. url = property(_geturl, None, None, "url of this svn-path.")
  105. def __str__(self):
  106. """ return a string representation (including rev-number) """
  107. return self.strpath
  108. def __hash__(self):
  109. return hash(self.strpath)
  110. def new(self, **kw):
  111. """ create a modified version of this path. A 'rev' argument
  112. indicates a new revision.
  113. the following keyword arguments modify various path parts::
  114. http://host.com/repo/path/file.ext
  115. |-----------------------| dirname
  116. |------| basename
  117. |--| purebasename
  118. |--| ext
  119. """
  120. obj = object.__new__(self.__class__)
  121. obj.rev = kw.get('rev', self.rev)
  122. obj.auth = kw.get('auth', self.auth)
  123. dirname, basename, purebasename, ext = self._getbyspec(
  124. "dirname,basename,purebasename,ext")
  125. if 'basename' in kw:
  126. if 'purebasename' in kw or 'ext' in kw:
  127. raise ValueError("invalid specification %r" % kw)
  128. else:
  129. pb = kw.setdefault('purebasename', purebasename)
  130. ext = kw.setdefault('ext', ext)
  131. if ext and not ext.startswith('.'):
  132. ext = '.' + ext
  133. kw['basename'] = pb + ext
  134. kw.setdefault('dirname', dirname)
  135. kw.setdefault('sep', self.sep)
  136. if kw['basename']:
  137. obj.strpath = "%(dirname)s%(sep)s%(basename)s" % kw
  138. else:
  139. obj.strpath = "%(dirname)s" % kw
  140. return obj
  141. def _getbyspec(self, spec):
  142. """ get specified parts of the path. 'arg' is a string
  143. with comma separated path parts. The parts are returned
  144. in exactly the order of the specification.
  145. you may specify the following parts:
  146. http://host.com/repo/path/file.ext
  147. |-----------------------| dirname
  148. |------| basename
  149. |--| purebasename
  150. |--| ext
  151. """
  152. res = []
  153. parts = self.strpath.split(self.sep)
  154. for name in spec.split(','):
  155. name = name.strip()
  156. if name == 'dirname':
  157. res.append(self.sep.join(parts[:-1]))
  158. elif name == 'basename':
  159. res.append(parts[-1])
  160. else:
  161. basename = parts[-1]
  162. i = basename.rfind('.')
  163. if i == -1:
  164. purebasename, ext = basename, ''
  165. else:
  166. purebasename, ext = basename[:i], basename[i:]
  167. if name == 'purebasename':
  168. res.append(purebasename)
  169. elif name == 'ext':
  170. res.append(ext)
  171. else:
  172. raise NameError("Don't know part %r" % name)
  173. return res
  174. def __eq__(self, other):
  175. """ return true if path and rev attributes each match """
  176. return (str(self) == str(other) and
  177. (self.rev == other.rev or self.rev == other.rev))
  178. def __ne__(self, other):
  179. return not self == other
  180. def join(self, *args):
  181. """ return a new Path (with the same revision) which is composed
  182. of the self Path followed by 'args' path components.
  183. """
  184. if not args:
  185. return self
  186. args = tuple([arg.strip(self.sep) for arg in args])
  187. parts = (self.strpath, ) + args
  188. newpath = self.__class__(self.sep.join(parts), self.rev, self.auth)
  189. return newpath
  190. def propget(self, name):
  191. """ return the content of the given property. """
  192. value = self._propget(name)
  193. return value
  194. def proplist(self):
  195. """ list all property names. """
  196. content = self._proplist()
  197. return content
  198. def size(self):
  199. """ Return the size of the file content of the Path. """
  200. return self.info().size
  201. def mtime(self):
  202. """ Return the last modification time of the file. """
  203. return self.info().mtime
  204. # shared help methods
  205. def _escape(self, cmd):
  206. return _escape_helper(cmd)
  207. #def _childmaxrev(self):
  208. # """ return maximum revision number of childs (or self.rev if no childs) """
  209. # rev = self.rev
  210. # for name, info in self._listdir_nameinfo():
  211. # rev = max(rev, info.created_rev)
  212. # return rev
  213. #def _getlatestrevision(self):
  214. # """ return latest repo-revision for this path. """
  215. # url = self.strpath
  216. # path = self.__class__(url, None)
  217. #
  218. # # we need a long walk to find the root-repo and revision
  219. # while 1:
  220. # try:
  221. # rev = max(rev, path._childmaxrev())
  222. # previous = path
  223. # path = path.dirpath()
  224. # except (IOError, process.cmdexec.Error):
  225. # break
  226. # if rev is None:
  227. # raise IOError, "could not determine newest repo revision for %s" % self
  228. # return rev
  229. class Checkers(common.Checkers):
  230. def dir(self):
  231. try:
  232. return self.path.info().kind == 'dir'
  233. except py.error.Error:
  234. return self._listdirworks()
  235. def _listdirworks(self):
  236. try:
  237. self.path.listdir()
  238. except py.error.ENOENT:
  239. return False
  240. else:
  241. return True
  242. def file(self):
  243. try:
  244. return self.path.info().kind == 'file'
  245. except py.error.ENOENT:
  246. return False
  247. def exists(self):
  248. try:
  249. return self.path.info()
  250. except py.error.ENOENT:
  251. return self._listdirworks()
  252. def parse_apr_time(timestr):
  253. i = timestr.rfind('.')
  254. if i == -1:
  255. raise ValueError("could not parse %s" % timestr)
  256. timestr = timestr[:i]
  257. parsedtime = time.strptime(timestr, "%Y-%m-%dT%H:%M:%S")
  258. return time.mktime(parsedtime)
  259. class PropListDict(dict):
  260. """ a Dictionary which fetches values (InfoSvnCommand instances) lazily"""
  261. def __init__(self, path, keynames):
  262. dict.__init__(self, [(x, None) for x in keynames])
  263. self.path = path
  264. def __getitem__(self, key):
  265. value = dict.__getitem__(self, key)
  266. if value is None:
  267. value = self.path.propget(key)
  268. dict.__setitem__(self, key, value)
  269. return value
  270. def fixlocale():
  271. if sys.platform != 'win32':
  272. return 'LC_ALL=C '
  273. return ''
  274. # some nasty chunk of code to solve path and url conversion and quoting issues
  275. ILLEGAL_CHARS = '* | \\ / : < > ? \t \n \x0b \x0c \r'.split(' ')
  276. if os.sep in ILLEGAL_CHARS:
  277. ILLEGAL_CHARS.remove(os.sep)
  278. ISWINDOWS = sys.platform == 'win32'
  279. _reg_allow_disk = re.compile(r'^([a-z]\:\\)?[^:]+$', re.I)
  280. def _check_path(path):
  281. illegal = ILLEGAL_CHARS[:]
  282. sp = path.strpath
  283. if ISWINDOWS:
  284. illegal.remove(':')
  285. if not _reg_allow_disk.match(sp):
  286. raise ValueError('path may not contain a colon (:)')
  287. for char in sp:
  288. if char not in string.printable or char in illegal:
  289. raise ValueError('illegal character %r in path' % (char,))
  290. def path_to_fspath(path, addat=True):
  291. _check_path(path)
  292. sp = path.strpath
  293. if addat and path.rev != -1:
  294. sp = '%s@%s' % (sp, path.rev)
  295. elif addat:
  296. sp = '%s@HEAD' % (sp,)
  297. return sp
  298. def url_from_path(path):
  299. fspath = path_to_fspath(path, False)
  300. from urllib import quote
  301. if ISWINDOWS:
  302. match = _reg_allow_disk.match(fspath)
  303. fspath = fspath.replace('\\', '/')
  304. if match.group(1):
  305. fspath = '/%s%s' % (match.group(1).replace('\\', '/'),
  306. quote(fspath[len(match.group(1)):]))
  307. else:
  308. fspath = quote(fspath)
  309. else:
  310. fspath = quote(fspath)
  311. if path.rev != -1:
  312. fspath = '%s@%s' % (fspath, path.rev)
  313. else:
  314. fspath = '%s@HEAD' % (fspath,)
  315. return 'file://%s' % (fspath,)
  316. class SvnAuth(object):
  317. """ container for auth information for Subversion """
  318. def __init__(self, username, password, cache_auth=True, interactive=True):
  319. self.username = username
  320. self.password = password
  321. self.cache_auth = cache_auth
  322. self.interactive = interactive
  323. def makecmdoptions(self):
  324. uname = self.username.replace('"', '\\"')
  325. passwd = self.password.replace('"', '\\"')
  326. ret = []
  327. if uname:
  328. ret.append('--username="%s"' % (uname,))
  329. if passwd:
  330. ret.append('--password="%s"' % (passwd,))
  331. if not self.cache_auth:
  332. ret.append('--no-auth-cache')
  333. if not self.interactive:
  334. ret.append('--non-interactive')
  335. return ' '.join(ret)
  336. def __str__(self):
  337. return "<SvnAuth username=%s ...>" %(self.username,)
  338. rex_blame = re.compile(r'\s*(\d+)\s+(\S+) (.*)')
  339. class SvnWCCommandPath(common.PathBase):
  340. """ path implementation offering access/modification to svn working copies.
  341. It has methods similar to the functions in os.path and similar to the
  342. commands of the svn client.
  343. """
  344. sep = os.sep
  345. def __new__(cls, wcpath=None, auth=None):
  346. self = object.__new__(cls)
  347. if isinstance(wcpath, cls):
  348. if wcpath.__class__ == cls:
  349. return wcpath
  350. wcpath = wcpath.localpath
  351. if _check_for_bad_chars(str(wcpath),
  352. ALLOWED_CHARS):
  353. raise ValueError("bad char in wcpath %s" % (wcpath, ))
  354. self.localpath = py.path.local(wcpath)
  355. self.auth = auth
  356. return self
  357. strpath = property(lambda x: str(x.localpath), None, None, "string path")
  358. rev = property(lambda x: x.info(usecache=0).rev, None, None, "revision")
  359. def __eq__(self, other):
  360. return self.localpath == getattr(other, 'localpath', None)
  361. def _geturl(self):
  362. if getattr(self, '_url', None) is None:
  363. info = self.info()
  364. self._url = info.url #SvnPath(info.url, info.rev)
  365. assert isinstance(self._url, py.builtin._basestring)
  366. return self._url
  367. url = property(_geturl, None, None, "url of this WC item")
  368. def _escape(self, cmd):
  369. return _escape_helper(cmd)
  370. def dump(self, obj):
  371. """ pickle object into path location"""
  372. return self.localpath.dump(obj)
  373. def svnurl(self):
  374. """ return current SvnPath for this WC-item. """
  375. info = self.info()
  376. return py.path.svnurl(info.url)
  377. def __repr__(self):
  378. return "svnwc(%r)" % (self.strpath) # , self._url)
  379. def __str__(self):
  380. return str(self.localpath)
  381. def _makeauthoptions(self):
  382. if self.auth is None:
  383. return ''
  384. return self.auth.makecmdoptions()
  385. def _authsvn(self, cmd, args=None):
  386. args = args and list(args) or []
  387. args.append(self._makeauthoptions())
  388. return self._svn(cmd, *args)
  389. def _svn(self, cmd, *args):
  390. l = ['svn %s' % cmd]
  391. args = [self._escape(item) for item in args]
  392. l.extend(args)
  393. l.append('"%s"' % self._escape(self.strpath))
  394. # try fixing the locale because we can't otherwise parse
  395. string = fixlocale() + " ".join(l)
  396. try:
  397. try:
  398. key = 'LC_MESSAGES'
  399. hold = os.environ.get(key)
  400. os.environ[key] = 'C'
  401. out = py.process.cmdexec(string)
  402. finally:
  403. if hold:
  404. os.environ[key] = hold
  405. else:
  406. del os.environ[key]
  407. except py.process.cmdexec.Error:
  408. e = sys.exc_info()[1]
  409. strerr = e.err.lower()
  410. if strerr.find('not found') != -1:
  411. raise py.error.ENOENT(self)
  412. elif strerr.find("E200009:") != -1:
  413. raise py.error.ENOENT(self)
  414. if (strerr.find('file exists') != -1 or
  415. strerr.find('file already exists') != -1 or
  416. strerr.find('w150002:') != -1 or
  417. strerr.find("can't create directory") != -1):
  418. raise py.error.EEXIST(strerr) #self)
  419. raise
  420. return out
  421. def switch(self, url):
  422. """ switch to given URL. """
  423. self._authsvn('switch', [url])
  424. def checkout(self, url=None, rev=None):
  425. """ checkout from url to local wcpath. """
  426. args = []
  427. if url is None:
  428. url = self.url
  429. if rev is None or rev == -1:
  430. if (sys.platform != 'win32' and
  431. _getsvnversion() == '1.3'):
  432. url += "@HEAD"
  433. else:
  434. if _getsvnversion() == '1.3':
  435. url += "@%d" % rev
  436. else:
  437. args.append('-r' + str(rev))
  438. args.append(url)
  439. self._authsvn('co', args)
  440. def update(self, rev='HEAD', interactive=True):
  441. """ update working copy item to given revision. (None -> HEAD). """
  442. opts = ['-r', rev]
  443. if not interactive:
  444. opts.append("--non-interactive")
  445. self._authsvn('up', opts)
  446. def write(self, content, mode='w'):
  447. """ write content into local filesystem wc. """
  448. self.localpath.write(content, mode)
  449. def dirpath(self, *args):
  450. """ return the directory Path of the current Path. """
  451. return self.__class__(self.localpath.dirpath(*args), auth=self.auth)
  452. def _ensuredirs(self):
  453. parent = self.dirpath()
  454. if parent.check(dir=0):
  455. parent._ensuredirs()
  456. if self.check(dir=0):
  457. self.mkdir()
  458. return self
  459. def ensure(self, *args, **kwargs):
  460. """ ensure that an args-joined path exists (by default as
  461. a file). if you specify a keyword argument 'directory=True'
  462. then the path is forced to be a directory path.
  463. """
  464. p = self.join(*args)
  465. if p.check():
  466. if p.check(versioned=False):
  467. p.add()
  468. return p
  469. if kwargs.get('dir', 0):
  470. return p._ensuredirs()
  471. parent = p.dirpath()
  472. parent._ensuredirs()
  473. p.write("")
  474. p.add()
  475. return p
  476. def mkdir(self, *args):
  477. """ create & return the directory joined with args. """
  478. if args:
  479. return self.join(*args).mkdir()
  480. else:
  481. self._svn('mkdir')
  482. return self
  483. def add(self):
  484. """ add ourself to svn """
  485. self._svn('add')
  486. def remove(self, rec=1, force=1):
  487. """ remove a file or a directory tree. 'rec'ursive is
  488. ignored and considered always true (because of
  489. underlying svn semantics.
  490. """
  491. assert rec, "svn cannot remove non-recursively"
  492. if not self.check(versioned=True):
  493. # not added to svn (anymore?), just remove
  494. py.path.local(self).remove()
  495. return
  496. flags = []
  497. if force:
  498. flags.append('--force')
  499. self._svn('remove', *flags)
  500. def copy(self, target):
  501. """ copy path to target."""
  502. py.process.cmdexec("svn copy %s %s" %(str(self), str(target)))
  503. def rename(self, target):
  504. """ rename this path to target. """
  505. py.process.cmdexec("svn move --force %s %s" %(str(self), str(target)))
  506. def lock(self):
  507. """ set a lock (exclusive) on the resource """
  508. out = self._authsvn('lock').strip()
  509. if not out:
  510. # warning or error, raise exception
  511. raise ValueError("unknown error in svn lock command")
  512. def unlock(self):
  513. """ unset a previously set lock """
  514. out = self._authsvn('unlock').strip()
  515. if out.startswith('svn:'):
  516. # warning or error, raise exception
  517. raise Exception(out[4:])
  518. def cleanup(self):
  519. """ remove any locks from the resource """
  520. # XXX should be fixed properly!!!
  521. try:
  522. self.unlock()
  523. except:
  524. pass
  525. def status(self, updates=0, rec=0, externals=0):
  526. """ return (collective) Status object for this file. """
  527. # http://svnbook.red-bean.com/book.html#svn-ch-3-sect-4.3.1
  528. # 2201 2192 jum test
  529. # XXX
  530. if externals:
  531. raise ValueError("XXX cannot perform status() "
  532. "on external items yet")
  533. else:
  534. #1.2 supports: externals = '--ignore-externals'
  535. externals = ''
  536. if rec:
  537. rec= ''
  538. else:
  539. rec = '--non-recursive'
  540. # XXX does not work on all subversion versions
  541. #if not externals:
  542. # externals = '--ignore-externals'
  543. if updates:
  544. updates = '-u'
  545. else:
  546. updates = ''
  547. try:
  548. cmd = 'status -v --xml --no-ignore %s %s %s' % (
  549. updates, rec, externals)
  550. out = self._authsvn(cmd)
  551. except py.process.cmdexec.Error:
  552. cmd = 'status -v --no-ignore %s %s %s' % (
  553. updates, rec, externals)
  554. out = self._authsvn(cmd)
  555. rootstatus = WCStatus(self).fromstring(out, self)
  556. else:
  557. rootstatus = XMLWCStatus(self).fromstring(out, self)
  558. return rootstatus
  559. def diff(self, rev=None):
  560. """ return a diff of the current path against revision rev (defaulting
  561. to the last one).
  562. """
  563. args = []
  564. if rev is not None:
  565. args.append("-r %d" % rev)
  566. out = self._authsvn('diff', args)
  567. return out
  568. def blame(self):
  569. """ return a list of tuples of three elements:
  570. (revision, commiter, line)
  571. """
  572. out = self._svn('blame')
  573. result = []
  574. blamelines = out.splitlines()
  575. reallines = py.path.svnurl(self.url).readlines()
  576. for i, (blameline, line) in enumerate(
  577. zip(blamelines, reallines)):
  578. m = rex_blame.match(blameline)
  579. if not m:
  580. raise ValueError("output line %r of svn blame does not match "
  581. "expected format" % (line, ))
  582. rev, name, _ = m.groups()
  583. result.append((int(rev), name, line))
  584. return result
  585. _rex_commit = re.compile(r'.*Committed revision (\d+)\.$', re.DOTALL)
  586. def commit(self, msg='', rec=1):
  587. """ commit with support for non-recursive commits """
  588. # XXX i guess escaping should be done better here?!?
  589. cmd = 'commit -m "%s" --force-log' % (msg.replace('"', '\\"'),)
  590. if not rec:
  591. cmd += ' -N'
  592. out = self._authsvn(cmd)
  593. try:
  594. del cache.info[self]
  595. except KeyError:
  596. pass
  597. if out:
  598. m = self._rex_commit.match(out)
  599. return int(m.group(1))
  600. def propset(self, name, value, *args):
  601. """ set property name to value on this path. """
  602. d = py.path.local.mkdtemp()
  603. try:
  604. p = d.join('value')
  605. p.write(value)
  606. self._svn('propset', name, '--file', str(p), *args)
  607. finally:
  608. d.remove()
  609. def propget(self, name):
  610. """ get property name on this path. """
  611. res = self._svn('propget', name)
  612. return res[:-1] # strip trailing newline
  613. def propdel(self, name):
  614. """ delete property name on this path. """
  615. res = self._svn('propdel', name)
  616. return res[:-1] # strip trailing newline
  617. def proplist(self, rec=0):
  618. """ return a mapping of property names to property values.
  619. If rec is True, then return a dictionary mapping sub-paths to such mappings.
  620. """
  621. if rec:
  622. res = self._svn('proplist -R')
  623. return make_recursive_propdict(self, res)
  624. else:
  625. res = self._svn('proplist')
  626. lines = res.split('\n')
  627. lines = [x.strip() for x in lines[1:]]
  628. return PropListDict(self, lines)
  629. def revert(self, rec=0):
  630. """ revert the local changes of this path. if rec is True, do so
  631. recursively. """
  632. if rec:
  633. result = self._svn('revert -R')
  634. else:
  635. result = self._svn('revert')
  636. return result
  637. def new(self, **kw):
  638. """ create a modified version of this path. A 'rev' argument
  639. indicates a new revision.
  640. the following keyword arguments modify various path parts:
  641. http://host.com/repo/path/file.ext
  642. |-----------------------| dirname
  643. |------| basename
  644. |--| purebasename
  645. |--| ext
  646. """
  647. if kw:
  648. localpath = self.localpath.new(**kw)
  649. else:
  650. localpath = self.localpath
  651. return self.__class__(localpath, auth=self.auth)
  652. def join(self, *args, **kwargs):
  653. """ return a new Path (with the same revision) which is composed
  654. of the self Path followed by 'args' path components.
  655. """
  656. if not args:
  657. return self
  658. localpath = self.localpath.join(*args, **kwargs)
  659. return self.__class__(localpath, auth=self.auth)
  660. def info(self, usecache=1):
  661. """ return an Info structure with svn-provided information. """
  662. info = usecache and cache.info.get(self)
  663. if not info:
  664. try:
  665. output = self._svn('info')
  666. except py.process.cmdexec.Error:
  667. e = sys.exc_info()[1]
  668. if e.err.find('Path is not a working copy directory') != -1:
  669. raise py.error.ENOENT(self, e.err)
  670. elif e.err.find("is not under version control") != -1:
  671. raise py.error.ENOENT(self, e.err)
  672. raise
  673. # XXX SVN 1.3 has output on stderr instead of stdout (while it does
  674. # return 0!), so a bit nasty, but we assume no output is output
  675. # to stderr...
  676. if (output.strip() == '' or
  677. output.lower().find('not a versioned resource') != -1):
  678. raise py.error.ENOENT(self, output)
  679. info = InfoSvnWCCommand(output)
  680. # Can't reliably compare on Windows without access to win32api
  681. if sys.platform != 'win32':
  682. if info.path != self.localpath:
  683. raise py.error.ENOENT(self, "not a versioned resource:" +
  684. " %s != %s" % (info.path, self.localpath))
  685. cache.info[self] = info
  686. return info
  687. def listdir(self, fil=None, sort=None):
  688. """ return a sequence of Paths.
  689. listdir will return either a tuple or a list of paths
  690. depending on implementation choices.
  691. """
  692. if isinstance(fil, str):
  693. fil = common.FNMatcher(fil)
  694. # XXX unify argument naming with LocalPath.listdir
  695. def notsvn(path):
  696. return path.basename != '.svn'
  697. paths = []
  698. for localpath in self.localpath.listdir(notsvn):
  699. p = self.__class__(localpath, auth=self.auth)
  700. if notsvn(p) and (not fil or fil(p)):
  701. paths.append(p)
  702. self._sortlist(paths, sort)
  703. return paths
  704. def open(self, mode='r'):
  705. """ return an opened file with the given mode. """
  706. return open(self.strpath, mode)
  707. def _getbyspec(self, spec):
  708. return self.localpath._getbyspec(spec)
  709. class Checkers(py.path.local.Checkers):
  710. def __init__(self, path):
  711. self.svnwcpath = path
  712. self.path = path.localpath
  713. def versioned(self):
  714. try:
  715. s = self.svnwcpath.info()
  716. except (py.error.ENOENT, py.error.EEXIST):
  717. return False
  718. except py.process.cmdexec.Error:
  719. e = sys.exc_info()[1]
  720. if e.err.find('is not a working copy')!=-1:
  721. return False
  722. if e.err.lower().find('not a versioned resource') != -1:
  723. return False
  724. raise
  725. else:
  726. return True
  727. def log(self, rev_start=None, rev_end=1, verbose=False):
  728. """ return a list of LogEntry instances for this path.
  729. rev_start is the starting revision (defaulting to the first one).
  730. rev_end is the last revision (defaulting to HEAD).
  731. if verbose is True, then the LogEntry instances also know which files changed.
  732. """
  733. assert self.check() # make it simpler for the pipe
  734. rev_start = rev_start is None and "HEAD" or rev_start
  735. rev_end = rev_end is None and "HEAD" or rev_end
  736. if rev_start == "HEAD" and rev_end == 1:
  737. rev_opt = ""
  738. else:
  739. rev_opt = "-r %s:%s" % (rev_start, rev_end)
  740. verbose_opt = verbose and "-v" or ""
  741. locale_env = fixlocale()
  742. # some blather on stderr
  743. auth_opt = self._makeauthoptions()
  744. #stdin, stdout, stderr = os.popen3(locale_env +
  745. # 'svn log --xml %s %s %s "%s"' % (
  746. # rev_opt, verbose_opt, auth_opt,
  747. # self.strpath))
  748. cmd = locale_env + 'svn log --xml %s %s %s "%s"' % (
  749. rev_opt, verbose_opt, auth_opt, self.strpath)
  750. popen = subprocess.Popen(cmd,
  751. stdout=subprocess.PIPE,
  752. stderr=subprocess.PIPE,
  753. shell=True,
  754. )
  755. stdout, stderr = popen.communicate()
  756. stdout = py.builtin._totext(stdout, sys.getdefaultencoding())
  757. minidom,ExpatError = importxml()
  758. try:
  759. tree = minidom.parseString(stdout)
  760. except ExpatError:
  761. raise ValueError('no such revision')
  762. result = []
  763. for logentry in filter(None, tree.firstChild.childNodes):
  764. if logentry.nodeType == logentry.ELEMENT_NODE:
  765. result.append(LogEntry(logentry))
  766. return result
  767. def size(self):
  768. """ Return the size of the file content of the Path. """
  769. return self.info().size
  770. def mtime(self):
  771. """ Return the last modification time of the file. """
  772. return self.info().mtime
  773. def __hash__(self):
  774. return hash((self.strpath, self.__class__, self.auth))
  775. class WCStatus:
  776. attrnames = ('modified','added', 'conflict', 'unchanged', 'external',
  777. 'deleted', 'prop_modified', 'unknown', 'update_available',
  778. 'incomplete', 'kindmismatch', 'ignored', 'locked', 'replaced'
  779. )
  780. def __init__(self, wcpath, rev=None, modrev=None, author=None):
  781. self.wcpath = wcpath
  782. self.rev = rev
  783. self.modrev = modrev
  784. self.author = author
  785. for name in self.attrnames:
  786. setattr(self, name, [])
  787. def allpath(self, sort=True, **kw):
  788. d = {}
  789. for name in self.attrnames:
  790. if name not in kw or kw[name]:
  791. for path in getattr(self, name):
  792. d[path] = 1
  793. l = d.keys()
  794. if sort:
  795. l.sort()
  796. return l
  797. # XXX a bit scary to assume there's always 2 spaces between username and
  798. # path, however with win32 allowing spaces in user names there doesn't
  799. # seem to be a more solid approach :(
  800. _rex_status = re.compile(r'\s+(\d+|-)\s+(\S+)\s+(.+?)\s{2,}(.*)')
  801. def fromstring(data, rootwcpath, rev=None, modrev=None, author=None):
  802. """ return a new WCStatus object from data 's'
  803. """
  804. rootstatus = WCStatus(rootwcpath, rev, modrev, author)
  805. update_rev = None
  806. for line in data.split('\n'):
  807. if not line.strip():
  808. continue
  809. #print "processing %r" % line
  810. flags, rest = line[:8], line[8:]
  811. # first column
  812. c0,c1,c2,c3,c4,c5,x6,c7 = flags
  813. #if '*' in line:
  814. # print "flags", repr(flags), "rest", repr(rest)
  815. if c0 in '?XI':
  816. fn = line.split(None, 1)[1]
  817. if c0 == '?':
  818. wcpath = rootwcpath.join(fn, abs=1)
  819. rootstatus.unknown.append(wcpath)
  820. elif c0 == 'X':
  821. wcpath = rootwcpath.__class__(
  822. rootwcpath.localpath.join(fn, abs=1),
  823. auth=rootwcpath.auth)
  824. rootstatus.external.append(wcpath)
  825. elif c0 == 'I':
  826. wcpath = rootwcpath.join(fn, abs=1)
  827. rootstatus.ignored.append(wcpath)
  828. continue
  829. #elif c0 in '~!' or c4 == 'S':
  830. # raise NotImplementedError("received flag %r" % c0)
  831. m = WCStatus._rex_status.match(rest)
  832. if not m:
  833. if c7 == '*':
  834. fn = rest.strip()
  835. wcpath = rootwcpath.join(fn, abs=1)
  836. rootstatus.update_available.append(wcpath)
  837. continue
  838. if line.lower().find('against revision:')!=-1:
  839. update_rev = int(rest.split(':')[1].strip())
  840. continue
  841. if line.lower().find('status on external') > -1:
  842. # XXX not sure what to do here... perhaps we want to
  843. # store some state instead of just continuing, as right
  844. # now it makes the top-level external get added twice
  845. # (once as external, once as 'normal' unchanged item)
  846. # because of the way SVN presents external items
  847. continue
  848. # keep trying
  849. raise ValueError("could not parse line %r" % line)
  850. else:
  851. rev, modrev, author, fn = m.groups()
  852. wcpath = rootwcpath.join(fn, abs=1)
  853. #assert wcpath.check()
  854. if c0 == 'M':
  855. assert wcpath.check(file=1), "didn't expect a directory with changed content here"
  856. rootstatus.modified.append(wcpath)
  857. elif c0 == 'A' or c3 == '+' :
  858. rootstatus.added.append(wcpath)
  859. elif c0 == 'D':
  860. rootstatus.deleted.append(wcpath)
  861. elif c0 == 'C':
  862. rootstatus.conflict.append(wcpath)
  863. elif c0 == '~':
  864. rootstatus.kindmismatch.append(wcpath)
  865. elif c0 == '!':
  866. rootstatus.incomplete.append(wcpath)
  867. elif c0 == 'R':
  868. rootstatus.replaced.append(wcpath)
  869. elif not c0.strip():
  870. rootstatus.unchanged.append(wcpath)
  871. else:
  872. raise NotImplementedError("received flag %r" % c0)
  873. if c1 == 'M':
  874. rootstatus.prop_modified.append(wcpath)
  875. # XXX do we cover all client versions here?
  876. if c2 == 'L' or c5 == 'K':
  877. rootstatus.locked.append(wcpath)
  878. if c7 == '*':
  879. rootstatus.update_available.append(wcpath)
  880. if wcpath == rootwcpath:
  881. rootstatus.rev = rev
  882. rootstatus.modrev = modrev
  883. rootstatus.author = author
  884. if update_rev:
  885. rootstatus.update_rev = update_rev
  886. continue
  887. return rootstatus
  888. fromstring = staticmethod(fromstring)
  889. class XMLWCStatus(WCStatus):
  890. def fromstring(data, rootwcpath, rev=None, modrev=None, author=None):
  891. """ parse 'data' (XML string as outputted by svn st) into a status obj
  892. """
  893. # XXX for externals, the path is shown twice: once
  894. # with external information, and once with full info as if
  895. # the item was a normal non-external... the current way of
  896. # dealing with this issue is by ignoring it - this does make
  897. # externals appear as external items as well as 'normal',
  898. # unchanged ones in the status object so this is far from ideal
  899. rootstatus = WCStatus(rootwcpath, rev, modrev, author)
  900. update_rev = None
  901. minidom, ExpatError = importxml()
  902. try:
  903. doc = minidom.parseString(data)
  904. except ExpatError:
  905. e = sys.exc_info()[1]
  906. raise ValueError(str(e))
  907. urevels = doc.getElementsByTagName('against')
  908. if urevels:
  909. rootstatus.update_rev = urevels[-1].getAttribute('revision')
  910. for entryel in doc.getElementsByTagName('entry'):
  911. path = entryel.getAttribute('path')
  912. statusel = entryel.getElementsByTagName('wc-status')[0]
  913. itemstatus = statusel.getAttribute('item')
  914. if itemstatus == 'unversioned':
  915. wcpath = rootwcpath.join(path, abs=1)
  916. rootstatus.unknown.append(wcpath)
  917. continue
  918. elif itemstatus == 'external':
  919. wcpath = rootwcpath.__class__(
  920. rootwcpath.localpath.join(path, abs=1),
  921. auth=rootwcpath.auth)
  922. rootstatus.external.append(wcpath)
  923. continue
  924. elif itemstatus == 'ignored':
  925. wcpath = rootwcpath.join(path, abs=1)
  926. rootstatus.ignored.append(wcpath)
  927. continue
  928. elif itemstatus == 'incomplete':
  929. wcpath = rootwcpath.join(path, abs=1)
  930. rootstatus.incomplete.append(wcpath)
  931. continue
  932. rev = statusel.getAttribute('revision')
  933. if itemstatus == 'added' or itemstatus == 'none':
  934. rev = '0'
  935. modrev = '?'
  936. author = '?'
  937. date = ''
  938. elif itemstatus == "replaced":
  939. pass
  940. else:
  941. #print entryel.toxml()
  942. commitel = entryel.getElementsByTagName('commit')[0]
  943. if commitel:
  944. modrev = commitel.getAttribute('revision')
  945. author = ''
  946. author_els = commitel.getElementsByTagName('author')
  947. if author_els:
  948. for c in author_els[0].childNodes:
  949. author += c.nodeValue
  950. date = ''
  951. for c in commitel.getElementsByTagName('date')[0]\
  952. .childNodes:
  953. date += c.nodeValue
  954. wcpath = rootwcpath.join(path, abs=1)
  955. assert itemstatus != 'modified' or wcpath.check(file=1), (
  956. 'did\'t expect a directory with changed content here')
  957. itemattrname = {
  958. 'normal': 'unchanged',
  959. 'unversioned': 'unknown',
  960. 'conflicted': 'conflict',
  961. 'none': 'added',
  962. }.get(itemstatus, itemstatus)
  963. attr = getattr(rootstatus, itemattrname)
  964. attr.append(wcpath)
  965. propsstatus = statusel.getAttribute('props')
  966. if propsstatus not in ('none', 'normal'):
  967. rootstatus.prop_modified.append(wcpath)
  968. if wcpath == rootwcpath:
  969. rootstatus.rev = rev
  970. rootstatus.modrev = modrev
  971. rootstatus.author = author
  972. rootstatus.date = date
  973. # handle repos-status element (remote info)
  974. rstatusels = entryel.getElementsByTagName('repos-status')
  975. if rstatusels:
  976. rstatusel = rstatusels[0]
  977. ritemstatus = rstatusel.getAttribute('item')
  978. if ritemstatus in ('added', 'modified'):
  979. rootstatus.update_available.append(wcpath)
  980. lockels = entryel.getElementsByTagName('lock')
  981. if len(lockels):
  982. rootstatus.locked.append(wcpath)
  983. return rootstatus
  984. fromstring = staticmethod(fromstring)
  985. class InfoSvnWCCommand:
  986. def __init__(self, output):
  987. # Path: test
  988. # URL: http://codespeak.net/svn/std.path/trunk/dist/std.path/test
  989. # Repository UUID: fd0d7bf2-dfb6-0310-8d31-b7ecfe96aada
  990. # Revision: 2151
  991. # Node Kind: directory
  992. # Schedule: normal
  993. # Last Changed Author: hpk
  994. # Last Changed Rev: 2100
  995. # Last Changed Date: 2003-10-27 20:43:14 +0100 (Mon, 27 Oct 2003)
  996. # Properties Last Updated: 2003-11-03 14:47:48 +0100 (Mon, 03 Nov 2003)
  997. d = {}
  998. for line in output.split('\n'):
  999. if not line.strip():
  1000. continue
  1001. key, value = line.split(':', 1)
  1002. key = key.lower().replace(' ', '')
  1003. value = value.strip()
  1004. d[key] = value
  1005. try:
  1006. self.url = d['url']
  1007. except KeyError:
  1008. raise ValueError("Not a versioned resource")
  1009. #raise ValueError, "Not a versioned resource %r" % path
  1010. self.kind = d['nodekind'] == 'directory' and 'dir' or d['nodekind']
  1011. try:
  1012. self.rev = int(d['revision'])
  1013. except KeyError:
  1014. self.rev = None
  1015. self.path = py.path.local(d['path'])
  1016. self.size = self.path.size()
  1017. if 'lastchangedrev' in d:
  1018. self.created_rev = int(d['lastchangedrev'])
  1019. if 'lastchangedauthor' in d:
  1020. self.last_author = d['lastchangedauthor']
  1021. if 'lastchangeddate' in d:
  1022. self.mtime = parse_wcinfotime(d['lastchangeddate'])
  1023. self.time = self.mtime * 1000000
  1024. def __eq__(self, other):
  1025. return self.__dict__ == other.__dict__
  1026. def parse_wcinfotime(timestr):
  1027. """ Returns seconds since epoch, UTC. """
  1028. # example: 2003-10-27 20:43:14 +0100 (Mon, 27 Oct 2003)
  1029. m = re.match(r'(\d+-\d+-\d+ \d+:\d+:\d+) ([+-]\d+) .*', timestr)
  1030. if not m:
  1031. raise ValueError("timestring %r does not match" % timestr)
  1032. timestr, timezone = m.groups()
  1033. # do not handle timezone specially, return value should be UTC
  1034. parsedtime = time.strptime(timestr, "%Y-%m-%d %H:%M:%S")
  1035. return calendar.timegm(parsedtime)
  1036. def make_recursive_propdict(wcroot,
  1037. output,
  1038. rex = re.compile("Properties on '(.*)':")):
  1039. """ Return a dictionary of path->PropListDict mappings. """
  1040. lines = [x for x in output.split('\n') if x]
  1041. pdict = {}
  1042. while lines:
  1043. line = lines.pop(0)
  1044. m = rex.match(line)
  1045. if not m:
  1046. raise ValueError("could not parse propget-line: %r" % line)
  1047. path = m.groups()[0]
  1048. wcpath = wcroot.join(path, abs=1)
  1049. propnames = []
  1050. while lines and lines[0].startswith(' '):
  1051. propname = lines.pop(0).strip()
  1052. propnames.append(propname)
  1053. assert propnames, "must have found properties!"
  1054. pdict[wcpath] = PropListDict(wcpath, propnames)
  1055. return pdict
  1056. def importxml(cache=[]):
  1057. if cache:
  1058. return cache
  1059. from xml.dom import minidom
  1060. from xml.parsers.expat import ExpatError
  1061. cache.extend([minidom, ExpatError])
  1062. return cache
  1063. class LogEntry:
  1064. def __init__(self, logentry):
  1065. self.rev = int(logentry.getAttribute('revision'))
  1066. for lpart in filter(None, logentry.childNodes):
  1067. if lpart.nodeType == lpart.ELEMENT_NODE:
  1068. if lpart.nodeName == 'author':
  1069. self.author = lpart.firstChild.nodeValue
  1070. elif lpart.nodeName == 'msg':
  1071. if lpart.firstChild:
  1072. self.msg = lpart.firstChild.nodeValue
  1073. else:
  1074. self.msg = ''
  1075. elif lpart.nodeName == 'date':
  1076. #2003-07-29T20:05:11.598637Z
  1077. timestr = lpart.firstChild.nodeValue
  1078. self.date = parse_apr_time(timestr)
  1079. elif lpart.nodeName == 'paths':
  1080. self.strpaths = []
  1081. for ppart in filter(None, lpart.childNodes):
  1082. if ppart.nodeType == ppart.ELEMENT_NODE:
  1083. self.strpaths.append(PathEntry(ppart))
  1084. def __repr__(self):
  1085. return '<Logentry rev=%d author=%s date=%s>' % (
  1086. self.rev, self.author, self.date)