PageRenderTime 54ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/mercurial/bookmarks.py

https://bitbucket.org/mirror/mercurial/
Python | 429 lines | 398 code | 12 blank | 19 comment | 22 complexity | dc3fb6a481a35c4de63f3e65390d939d MD5 | raw file
Possible License(s): GPL-2.0
  1. # Mercurial bookmark support code
  2. #
  3. # Copyright 2008 David Soria Parra <dsp@php.net>
  4. #
  5. # This software may be used and distributed according to the terms of the
  6. # GNU General Public License version 2 or any later version.
  7. from mercurial.i18n import _
  8. from mercurial.node import hex, bin
  9. from mercurial import encoding, error, util, obsolete
  10. import errno
  11. class bmstore(dict):
  12. """Storage for bookmarks.
  13. This object should do all bookmark reads and writes, so that it's
  14. fairly simple to replace the storage underlying bookmarks without
  15. having to clone the logic surrounding bookmarks.
  16. This particular bmstore implementation stores bookmarks as
  17. {hash}\s{name}\n (the same format as localtags) in
  18. .hg/bookmarks. The mapping is stored as {name: nodeid}.
  19. This class does NOT handle the "current" bookmark state at this
  20. time.
  21. """
  22. def __init__(self, repo):
  23. dict.__init__(self)
  24. self._repo = repo
  25. try:
  26. for line in repo.vfs('bookmarks'):
  27. line = line.strip()
  28. if not line:
  29. continue
  30. if ' ' not in line:
  31. repo.ui.warn(_('malformed line in .hg/bookmarks: %r\n')
  32. % line)
  33. continue
  34. sha, refspec = line.split(' ', 1)
  35. refspec = encoding.tolocal(refspec)
  36. try:
  37. self[refspec] = repo.changelog.lookup(sha)
  38. except LookupError:
  39. pass
  40. except IOError, inst:
  41. if inst.errno != errno.ENOENT:
  42. raise
  43. def write(self):
  44. '''Write bookmarks
  45. Write the given bookmark => hash dictionary to the .hg/bookmarks file
  46. in a format equal to those of localtags.
  47. We also store a backup of the previous state in undo.bookmarks that
  48. can be copied back on rollback.
  49. '''
  50. repo = self._repo
  51. if repo._bookmarkcurrent not in self:
  52. unsetcurrent(repo)
  53. wlock = repo.wlock()
  54. try:
  55. file = repo.vfs('bookmarks', 'w', atomictemp=True)
  56. for name, node in self.iteritems():
  57. file.write("%s %s\n" % (hex(node), encoding.fromlocal(name)))
  58. file.close()
  59. # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
  60. try:
  61. repo.svfs.utime('00changelog.i', None)
  62. except OSError:
  63. pass
  64. finally:
  65. wlock.release()
  66. def readcurrent(repo):
  67. '''Get the current bookmark
  68. If we use gittish branches we have a current bookmark that
  69. we are on. This function returns the name of the bookmark. It
  70. is stored in .hg/bookmarks.current
  71. '''
  72. mark = None
  73. try:
  74. file = repo.opener('bookmarks.current')
  75. except IOError, inst:
  76. if inst.errno != errno.ENOENT:
  77. raise
  78. return None
  79. try:
  80. # No readline() in osutil.posixfile, reading everything is cheap
  81. mark = encoding.tolocal((file.readlines() or [''])[0])
  82. if mark == '' or mark not in repo._bookmarks:
  83. mark = None
  84. finally:
  85. file.close()
  86. return mark
  87. def setcurrent(repo, mark):
  88. '''Set the name of the bookmark that we are currently on
  89. Set the name of the bookmark that we are on (hg update <bookmark>).
  90. The name is recorded in .hg/bookmarks.current
  91. '''
  92. if mark not in repo._bookmarks:
  93. raise AssertionError('bookmark %s does not exist!' % mark)
  94. current = repo._bookmarkcurrent
  95. if current == mark:
  96. return
  97. wlock = repo.wlock()
  98. try:
  99. file = repo.opener('bookmarks.current', 'w', atomictemp=True)
  100. file.write(encoding.fromlocal(mark))
  101. file.close()
  102. finally:
  103. wlock.release()
  104. repo._bookmarkcurrent = mark
  105. def unsetcurrent(repo):
  106. wlock = repo.wlock()
  107. try:
  108. try:
  109. repo.vfs.unlink('bookmarks.current')
  110. repo._bookmarkcurrent = None
  111. except OSError, inst:
  112. if inst.errno != errno.ENOENT:
  113. raise
  114. finally:
  115. wlock.release()
  116. def iscurrent(repo, mark=None, parents=None):
  117. '''Tell whether the current bookmark is also active
  118. I.e., the bookmark listed in .hg/bookmarks.current also points to a
  119. parent of the working directory.
  120. '''
  121. if not mark:
  122. mark = repo._bookmarkcurrent
  123. if not parents:
  124. parents = [p.node() for p in repo[None].parents()]
  125. marks = repo._bookmarks
  126. return (mark in marks and marks[mark] in parents)
  127. def updatecurrentbookmark(repo, oldnode, curbranch):
  128. try:
  129. return update(repo, oldnode, repo.branchtip(curbranch))
  130. except error.RepoLookupError:
  131. if curbranch == "default": # no default branch!
  132. return update(repo, oldnode, repo.lookup("tip"))
  133. else:
  134. raise util.Abort(_("branch %s not found") % curbranch)
  135. def deletedivergent(repo, deletefrom, bm):
  136. '''Delete divergent versions of bm on nodes in deletefrom.
  137. Return True if at least one bookmark was deleted, False otherwise.'''
  138. deleted = False
  139. marks = repo._bookmarks
  140. divergent = [b for b in marks if b.split('@', 1)[0] == bm.split('@', 1)[0]]
  141. for mark in divergent:
  142. if mark == '@' or '@' not in mark:
  143. # can't be divergent by definition
  144. continue
  145. if mark and marks[mark] in deletefrom:
  146. if mark != bm:
  147. del marks[mark]
  148. deleted = True
  149. return deleted
  150. def calculateupdate(ui, repo, checkout):
  151. '''Return a tuple (targetrev, movemarkfrom) indicating the rev to
  152. check out and where to move the active bookmark from, if needed.'''
  153. movemarkfrom = None
  154. if checkout is None:
  155. curmark = repo._bookmarkcurrent
  156. if iscurrent(repo):
  157. movemarkfrom = repo['.'].node()
  158. elif curmark:
  159. ui.status(_("updating to active bookmark %s\n") % curmark)
  160. checkout = curmark
  161. return (checkout, movemarkfrom)
  162. def update(repo, parents, node):
  163. deletefrom = parents
  164. marks = repo._bookmarks
  165. update = False
  166. cur = repo._bookmarkcurrent
  167. if not cur:
  168. return False
  169. if marks[cur] in parents:
  170. new = repo[node]
  171. divs = [repo[b] for b in marks
  172. if b.split('@', 1)[0] == cur.split('@', 1)[0]]
  173. anc = repo.changelog.ancestors([new.rev()])
  174. deletefrom = [b.node() for b in divs if b.rev() in anc or b == new]
  175. if validdest(repo, repo[marks[cur]], new):
  176. marks[cur] = new.node()
  177. update = True
  178. if deletedivergent(repo, deletefrom, cur):
  179. update = True
  180. if update:
  181. marks.write()
  182. return update
  183. def listbookmarks(repo):
  184. # We may try to list bookmarks on a repo type that does not
  185. # support it (e.g., statichttprepository).
  186. marks = getattr(repo, '_bookmarks', {})
  187. d = {}
  188. hasnode = repo.changelog.hasnode
  189. for k, v in marks.iteritems():
  190. # don't expose local divergent bookmarks
  191. if hasnode(v) and ('@' not in k or k.endswith('@')):
  192. d[k] = hex(v)
  193. return d
  194. def pushbookmark(repo, key, old, new):
  195. w = repo.wlock()
  196. try:
  197. marks = repo._bookmarks
  198. if hex(marks.get(key, '')) != old:
  199. return False
  200. if new == '':
  201. del marks[key]
  202. else:
  203. if new not in repo:
  204. return False
  205. marks[key] = repo[new].node()
  206. marks.write()
  207. return True
  208. finally:
  209. w.release()
  210. def compare(repo, srcmarks, dstmarks,
  211. srchex=None, dsthex=None, targets=None):
  212. '''Compare bookmarks between srcmarks and dstmarks
  213. This returns tuple "(addsrc, adddst, advsrc, advdst, diverge,
  214. differ, invalid)", each are list of bookmarks below:
  215. :addsrc: added on src side (removed on dst side, perhaps)
  216. :adddst: added on dst side (removed on src side, perhaps)
  217. :advsrc: advanced on src side
  218. :advdst: advanced on dst side
  219. :diverge: diverge
  220. :differ: changed, but changeset referred on src is unknown on dst
  221. :invalid: unknown on both side
  222. Each elements of lists in result tuple is tuple "(bookmark name,
  223. changeset ID on source side, changeset ID on destination
  224. side)". Each changeset IDs are 40 hexadecimal digit string or
  225. None.
  226. Changeset IDs of tuples in "addsrc", "adddst", "differ" or
  227. "invalid" list may be unknown for repo.
  228. This function expects that "srcmarks" and "dstmarks" return
  229. changeset ID in 40 hexadecimal digit string for specified
  230. bookmark. If not so (e.g. bmstore "repo._bookmarks" returning
  231. binary value), "srchex" or "dsthex" should be specified to convert
  232. into such form.
  233. If "targets" is specified, only bookmarks listed in it are
  234. examined.
  235. '''
  236. if not srchex:
  237. srchex = lambda x: x
  238. if not dsthex:
  239. dsthex = lambda x: x
  240. if targets:
  241. bset = set(targets)
  242. else:
  243. srcmarkset = set(srcmarks)
  244. dstmarkset = set(dstmarks)
  245. bset = srcmarkset ^ dstmarkset
  246. for b in srcmarkset & dstmarkset:
  247. if srchex(srcmarks[b]) != dsthex(dstmarks[b]):
  248. bset.add(b)
  249. results = ([], [], [], [], [], [], [])
  250. addsrc = results[0].append
  251. adddst = results[1].append
  252. advsrc = results[2].append
  253. advdst = results[3].append
  254. diverge = results[4].append
  255. differ = results[5].append
  256. invalid = results[6].append
  257. for b in sorted(bset):
  258. if b not in srcmarks:
  259. if b in dstmarks:
  260. adddst((b, None, dsthex(dstmarks[b])))
  261. else:
  262. invalid((b, None, None))
  263. elif b not in dstmarks:
  264. addsrc((b, srchex(srcmarks[b]), None))
  265. else:
  266. scid = srchex(srcmarks[b])
  267. dcid = dsthex(dstmarks[b])
  268. if scid in repo and dcid in repo:
  269. sctx = repo[scid]
  270. dctx = repo[dcid]
  271. if sctx.rev() < dctx.rev():
  272. if validdest(repo, sctx, dctx):
  273. advdst((b, scid, dcid))
  274. else:
  275. diverge((b, scid, dcid))
  276. else:
  277. if validdest(repo, dctx, sctx):
  278. advsrc((b, scid, dcid))
  279. else:
  280. diverge((b, scid, dcid))
  281. else:
  282. # it is too expensive to examine in detail, in this case
  283. differ((b, scid, dcid))
  284. return results
  285. def _diverge(ui, b, path, localmarks):
  286. if b == '@':
  287. b = ''
  288. # find a unique @ suffix
  289. for x in range(1, 100):
  290. n = '%s@%d' % (b, x)
  291. if n not in localmarks:
  292. break
  293. # try to use an @pathalias suffix
  294. # if an @pathalias already exists, we overwrite (update) it
  295. for p, u in ui.configitems("paths"):
  296. if path == u:
  297. n = '%s@%s' % (b, p)
  298. return n
  299. def updatefromremote(ui, repo, remotemarks, path):
  300. ui.debug("checking for updated bookmarks\n")
  301. localmarks = repo._bookmarks
  302. (addsrc, adddst, advsrc, advdst, diverge, differ, invalid
  303. ) = compare(repo, remotemarks, localmarks, dsthex=hex)
  304. changed = []
  305. for b, scid, dcid in addsrc:
  306. if scid in repo: # add remote bookmarks for changes we already have
  307. changed.append((b, bin(scid), ui.status,
  308. _("adding remote bookmark %s\n") % (b)))
  309. for b, scid, dcid in advsrc:
  310. changed.append((b, bin(scid), ui.status,
  311. _("updating bookmark %s\n") % (b)))
  312. for b, scid, dcid in diverge:
  313. db = _diverge(ui, b, path, localmarks)
  314. changed.append((db, bin(scid), ui.warn,
  315. _("divergent bookmark %s stored as %s\n") % (b, db)))
  316. if changed:
  317. for b, node, writer, msg in sorted(changed):
  318. localmarks[b] = node
  319. writer(msg)
  320. localmarks.write()
  321. def pushtoremote(ui, repo, remote, targets):
  322. (addsrc, adddst, advsrc, advdst, diverge, differ, invalid
  323. ) = compare(repo, repo._bookmarks, remote.listkeys('bookmarks'),
  324. srchex=hex, targets=targets)
  325. if invalid:
  326. b, scid, dcid = invalid[0]
  327. ui.warn(_('bookmark %s does not exist on the local '
  328. 'or remote repository!\n') % b)
  329. return 2
  330. def push(b, old, new):
  331. r = remote.pushkey('bookmarks', b, old, new)
  332. if not r:
  333. ui.warn(_('updating bookmark %s failed!\n') % b)
  334. return 1
  335. return 0
  336. failed = 0
  337. for b, scid, dcid in sorted(addsrc + advsrc + advdst + diverge + differ):
  338. ui.status(_("exporting bookmark %s\n") % b)
  339. if dcid is None:
  340. dcid = ''
  341. failed += push(b, dcid, scid)
  342. for b, scid, dcid in adddst:
  343. # treat as "deleted locally"
  344. ui.status(_("deleting remote bookmark %s\n") % b)
  345. failed += push(b, dcid, '')
  346. if failed:
  347. return 1
  348. def diff(ui, dst, src):
  349. ui.status(_("searching for changed bookmarks\n"))
  350. smarks = src.listkeys('bookmarks')
  351. dmarks = dst.listkeys('bookmarks')
  352. diff = sorted(set(smarks) - set(dmarks))
  353. for k in diff:
  354. mark = ui.debugflag and smarks[k] or smarks[k][:12]
  355. ui.write(" %-25s %s\n" % (k, mark))
  356. if len(diff) <= 0:
  357. ui.status(_("no changed bookmarks found\n"))
  358. return 1
  359. return 0
  360. def validdest(repo, old, new):
  361. """Is the new bookmark destination a valid update from the old one"""
  362. repo = repo.unfiltered()
  363. if old == new:
  364. # Old == new -> nothing to update.
  365. return False
  366. elif not old:
  367. # old is nullrev, anything is valid.
  368. # (new != nullrev has been excluded by the previous check)
  369. return True
  370. elif repo.obsstore:
  371. return new.node() in obsolete.foreground(repo, [old.node()])
  372. else:
  373. # still an independent clause as it is lazyer (and therefore faster)
  374. return old.descendant(new)