/tortoisehg/hgtk/logview/revgraph.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 556 lines · 434 code · 46 blank · 76 comment · 46 complexity · 5c58ffcb353faae772092331b7666766 MD5 · raw file

  1. """Directed graph production.
  2. This module contains the code to produce an ordered directed graph of a
  3. Mercurial repository, such as we display in the tree view at the top of the
  4. history window. Original code was from graphlog extension.
  5. The generator functions walks through the revision history and for the
  6. selected revision emits tuples with the following elements:
  7. (curr_rev, node, lines, parents)
  8. - Current revision.
  9. - node; defined as tuple (rev_column, rev_color)
  10. - lines; a list of (col, next_col, color, type) indicating the edges between
  11. the current row and the next row
  12. - parent revisions of current revision
  13. The node tuple has the following elements:
  14. - rev_column: Column for the node "circle"
  15. - rev_color: Color used to render the circle
  16. - rev_status:
  17. 0 for normal circle,
  18. 1 for add up arrow (outgoing cset)
  19. 2 for add star (new cset)
  20. 3 for add down arrow (incoming cset)
  21. All above +4 for larger circle = current workdir parent
  22. The lines tuple has the following elements:
  23. - col: Column for the upper end of the line.
  24. - nextcol: Column for the lower end of the line.
  25. - color: Colour used for line
  26. - type: 0 for plain line, 1 for loose upper end, 2 for loose lower end
  27. loose ends are rendered as dashed lines. They indicate that the
  28. edge of the graph does not really end here. This is used to render
  29. a branch view where the focus is on branches instead of revisions.
  30. The data is used in treeview.populate with the following signature
  31. (rev, node, lines, parents) = self.grapher.next()
  32. and stored in treeview.graphdata
  33. The treeview.model is an instance of treemodel which references
  34. treeview.graphdata
  35. treemodel stores it in self.line_graph_data, and extracts it
  36. in on_get_value, where it is mapped to several columns.
  37. REVID, NODE, LINES, PARENTS, LAST_LINES
  38. LAST_LINES is a copy of LINES from the previous row
  39. treeview maps columns
  40. treemodel.NODE, treemodel.LAST_LINES, treemodel.LINES
  41. to CellRendererGraph attributes
  42. "node", "in-lines", "out-lines"
  43. which stores it in varables
  44. node, in_lines, out_lines
  45. """
  46. __copyright__ = "Copyright 2007 Joel Rosdahl, 2008 Steve Borho"
  47. __author__ = "Joel Rosdahl <joel@rosdahl.net>, Steve Borho <steve@borho.org>"
  48. import re
  49. from mercurial.node import nullrev
  50. from mercurial import cmdutil, util, match
  51. from tortoisehg.util import hglib
  52. def __get_parents(repo, rev):
  53. return [x for x in repo.changelog.parentrevs(rev) if x != nullrev]
  54. known_branch_colors = {} # cache for repository colour configuration
  55. def _get_known_branch_colors(repo):
  56. global known_branch_colors
  57. repo_setting = repo.ui.config('tortoisehg', 'branchcolors')
  58. if known_branch_colors:
  59. branch_colors, setting = known_branch_colors
  60. if repo_setting == setting:
  61. return branch_colors
  62. branchcolors = repo_setting
  63. if not branchcolors:
  64. return {}
  65. branchcolors = hglib.tounicode(branchcolors)
  66. branchcolors = [x for x in re.split(r'(?:(?<=\\\\)|(?<!\\)) ', branchcolors) if x]
  67. values = {}
  68. for branchcolor in branchcolors:
  69. parts = re.split(r'(?:(?<=\\\\)|(?<!\\)):', branchcolor)
  70. if len(parts) != 2:
  71. continue # ignore badly formed entry
  72. # Mercurial branch names are encoded in utf-8 so we must
  73. # make sure to encode back to that after having unescaped
  74. # the string.
  75. branch_name = hglib.toutf(parts[0].replace('\\:', ':').replace('\\ ', ' ').decode('unicode_escape'))
  76. values[branch_name] = hglib.toutf(parts[1])
  77. known_branch_colors = values, repo_setting
  78. return values
  79. def _color_of_branch(repo, rev):
  80. branch = repo[rev].branch()
  81. candidates = _get_known_branch_colors(repo)
  82. if branch in candidates:
  83. return candidates[branch]
  84. if branch == 'default':
  85. color = 0
  86. else:
  87. color = sum([ord(c) for c in branch])
  88. return color
  89. def _color_of(repo, rev, nextcolor, preferredcolor, branch_color=False):
  90. if not branch_color:
  91. if preferredcolor[0]:
  92. rv = preferredcolor[0]
  93. preferredcolor[0] = None
  94. else:
  95. rv = nextcolor[0]
  96. nextcolor[0] = nextcolor[0] + 1
  97. return rv
  98. else:
  99. return _color_of_branch(repo, rev)
  100. type_PLAIN = 0
  101. type_LOOSE_LOW = 1
  102. type_LOOSE_HIGH = 2
  103. def revision_grapher(repo, start_rev=-1, stop_rev=-1, branch=None, noheads=False,
  104. branch_color=False, only_revs=[]):
  105. """incremental revision grapher
  106. This grapher generates a full graph where every edge is visible.
  107. This means that repeated merges between two branches may make
  108. the graph very wide.
  109. if branch is set to the name of a branch, only this branch is shown.
  110. if noheads is True, other heads than start_rev is hidden. Use this
  111. to show ancestors of a revision.
  112. if branch_color is True, the branch colour is determined by a hash
  113. of the branch tip, and will thus always be the same.
  114. if only_revs is not [], only those revisions will be shown.
  115. """
  116. shown_revs = None
  117. if only_revs:
  118. shown_revs = set([repo[repo.lookup(r)].rev() for r in only_revs])
  119. start_rev = max(shown_revs)
  120. stop_rev = min(shown_revs)
  121. assert start_rev >= stop_rev
  122. curr_rev = start_rev
  123. revs = []
  124. rev_color = {}
  125. nextcolor = [0]
  126. def hidden(rev):
  127. return (shown_revs and (rev not in shown_revs)) or False
  128. while curr_rev >= stop_rev:
  129. if hidden(curr_rev):
  130. curr_rev -= 1
  131. continue
  132. # Compute revs and next_revs.
  133. if curr_rev not in revs:
  134. if noheads and curr_rev != start_rev:
  135. curr_rev -= 1
  136. continue
  137. if branch:
  138. ctx = repo.changectx(curr_rev)
  139. if ctx.branch() != branch:
  140. curr_rev -= 1
  141. continue
  142. # New head.
  143. revs.append(curr_rev)
  144. rev_color[curr_rev] = _color_of(repo, curr_rev, nextcolor, [None], branch_color)
  145. r = __get_parents(repo, curr_rev)
  146. while r:
  147. r0 = r[0]
  148. if r0 < stop_rev: break
  149. if r0 in rev_color: break
  150. if not branch_color:
  151. rev_color[r0] = rev_color[curr_rev]
  152. else:
  153. rev_color[r0] = _color_of(repo, r0, None, None, True)
  154. r = __get_parents(repo, r0)
  155. rev_index = revs.index(curr_rev)
  156. next_revs = revs[:]
  157. # Add parents to next_revs.
  158. parents = [p for p in __get_parents(repo, curr_rev) if not hidden(p)]
  159. parents_to_add = []
  160. preferred_color = [rev_color[curr_rev]]
  161. for parent in parents:
  162. if parent not in next_revs:
  163. parents_to_add.append(parent)
  164. if parent not in rev_color:
  165. rev_color[parent] = _color_of(repo, parent, nextcolor, preferred_color, branch_color)
  166. next_revs[rev_index:rev_index + 1] = parents_to_add
  167. lines = []
  168. for i, rev in enumerate(revs):
  169. if rev in next_revs:
  170. color = rev_color[rev]
  171. lines.append( (i, next_revs.index(rev), color, type_PLAIN) )
  172. elif rev == curr_rev:
  173. for parent in parents:
  174. color = rev_color[parent]
  175. lines.append( (i, next_revs.index(parent), color, type_PLAIN) )
  176. yield (curr_rev, (rev_index, rev_color[curr_rev]), lines, None)
  177. revs = next_revs
  178. curr_rev -= 1
  179. class BranchGrapher:
  180. """Incremental branch grapher
  181. This generator function produces a graph that uses loose ends to keep
  182. focus on branches. All revisions in the range are shown, but not all edges.
  183. The function identifies branches and may use loose ends if an edge links
  184. two branches.
  185. """
  186. def __init__(self, repo, start_rev, stop_rev, branch_filter, branch_color):
  187. '''
  188. start_rev - first (newest) changeset to cover
  189. stop_rev - last (oldest) changeset to cover
  190. branch_filter - if not None, show this branch and all its ancestors
  191. branch_color - true if branch name determines colours
  192. '''
  193. assert start_rev >= stop_rev
  194. self.repo = repo
  195. #
  196. # Iterator content
  197. # Variables that keep track of the iterator
  198. #
  199. # The current revision to process
  200. self.curr_rev = start_rev
  201. # Process all revs from start_rev to and including stop_rev
  202. self.stop_rev = stop_rev
  203. # List current branches. For each branch the next rev is listed
  204. # The order of branches determine their columns
  205. self.curr_branches = [start_rev]
  206. # For each branch, tell the next version that will show up
  207. self.next_in_branch = {}
  208. #
  209. # Graph variables
  210. # These hold information related to the graph. Most are computed lazily.
  211. # The graph is split into a number of "branches", defined as paths within
  212. # a named branch.
  213. #
  214. # Map rev to next rev in branch = parent with same branch name
  215. # If two parents, use first that has same branch name.
  216. # The parent of last rev in a branch is undefined,
  217. # even if the revsion has a parent rev.
  218. self.parent_of = {}
  219. # Map rev to newest rev in branch. This identifies the branch that the rev
  220. # is part of
  221. self.branch4rev = {}
  222. # Last revision in branch
  223. self.branch_tail = {}
  224. # Map branch-id (head-rev of branch) to color
  225. self.color4branch = {}
  226. # Next colour used. for branches
  227. self.nextcolor = 0
  228. # If set, show only this branch and all descendants.
  229. self.branch_filter = branch_filter
  230. # Flag to indicate if coloring is done pr micro-branch or pr named branch
  231. self.branch_color = branch_color
  232. def _get_parents(self, rev):
  233. return [x for x in self.repo.changelog.parentrevs(rev) if x != nullrev]
  234. def _branch_name(self, rev):
  235. return self.repo[rev].branch()
  236. def _covered_rev(self, rev):
  237. """True if rev is inside the revision range for the iterator"""
  238. return self.stop_rev <= rev
  239. def _new_branch(self, branch_head):
  240. """Mark all unknown revisions in range that are direct ancestors
  241. of branch_head as part of the same branch. Stops when stop_rev
  242. is passed or a known revision is found"""
  243. assert not branch_head in self.branch4rev
  244. if self.branch_color:
  245. self.color4branch[branch_head] = \
  246. _color_of_branch(self.repo, branch_head)
  247. else:
  248. self.color4branch[branch_head] = self.nextcolor
  249. self.nextcolor += 1
  250. self.next_in_branch[branch_head] = branch_head
  251. branch_name = self._branch_name(branch_head)
  252. rev = branch_head
  253. while not rev in self.branch4rev:
  254. # Add rev to branch
  255. self.branch4rev[rev] = branch_head
  256. self.branch_tail[branch_head] = rev
  257. # Exit if rev is outside visible range
  258. if not self._covered_rev(rev):
  259. # rev is outside visible range, so we don't know tail location
  260. self.branch_tail[branch_head] = 0 # Prev revs wasn't tail
  261. return
  262. # Find next revision in branch
  263. self.parent_of[rev] = None
  264. for parent in self._get_parents(rev):
  265. if self._branch_name(parent) == branch_name:
  266. self.parent_of[rev] = parent
  267. break
  268. if self.parent_of[rev] is None:
  269. return
  270. rev = self.parent_of[rev]
  271. def _get_rev_branch(self, rev):
  272. """Find revision branch or create a new branch"""
  273. branch = self.branch4rev.get(rev)
  274. if branch is None:
  275. branch = rev
  276. self._new_branch(branch)
  277. assert rev in self.branch4rev
  278. assert branch in self.branch_tail
  279. return branch
  280. def _compute_next_branches(self):
  281. """Compute next row of branches"""
  282. next_branches = self.curr_branches[:]
  283. # Find branch used by current revision
  284. curr_branch = self._get_rev_branch(self.curr_rev)
  285. self.next_in_branch[curr_branch] = self.parent_of[self.curr_rev]
  286. branch_index = self.curr_branches.index(curr_branch)
  287. # Insert branches if parents point to new branches
  288. new_parents = 0
  289. for parent in self._get_parents(self.curr_rev):
  290. branch = self._get_rev_branch(parent)
  291. if not branch in next_branches:
  292. new_parents += 1
  293. next_branches.insert(branch_index + new_parents, branch)
  294. # Delete branch if last revision
  295. if self.curr_rev == self.branch_tail[curr_branch]:
  296. del next_branches[branch_index]
  297. # Return result
  298. return next_branches
  299. def _rev_color(self, rev):
  300. """Map a revision to a color"""
  301. return self.color4branch[self.branch4rev[rev]]
  302. def _compute_lines(self, parents, next_branches):
  303. # Compute lines (from CUR to NEXT branch row)
  304. lines = []
  305. curr_rev_branch = self.branch4rev[self.curr_rev]
  306. for curr_column, branch in enumerate(self.curr_branches):
  307. if branch == curr_rev_branch:
  308. # Add lines from current branch to parents
  309. for par_rev in parents:
  310. par_branch = self._get_rev_branch(par_rev)
  311. color = self.color4branch[par_branch]
  312. next_column = next_branches.index(par_branch)
  313. line_type = type_PLAIN
  314. if par_rev != self.next_in_branch.get(par_branch):
  315. line_type = type_LOOSE_LOW
  316. lines.append( (curr_column, next_column, color, line_type) )
  317. else:
  318. # Continue unrelated branch
  319. color = self.color4branch[branch]
  320. next_column = next_branches.index(branch)
  321. lines.append( (curr_column, next_column, color, type_PLAIN) )
  322. return lines
  323. def more(self):
  324. return self.curr_rev >= self.stop_rev
  325. def next(self):
  326. """Perform one iteration of the branch grapher"""
  327. # Compute revision (on CUR branch row)
  328. while self.more():
  329. rev = self.curr_rev
  330. rev_branch = self._get_rev_branch(rev)
  331. if rev_branch in self.curr_branches:
  332. # Follow parent from known child
  333. break
  334. elif self.branch_filter is None:
  335. # New head - no branch name filter
  336. self.curr_branches.append(rev_branch)
  337. break
  338. elif self._branch_name(rev) == self.branch_filter:
  339. # New head - matches branch name filter
  340. self.curr_branches.append(rev_branch)
  341. break
  342. else:
  343. # Skip this revision
  344. self.curr_rev -= 1
  345. # Compute parents (indicates the branches on NEXT branch row that curr_rev links to)
  346. parents = self._get_parents(rev)
  347. # BUG: __get_parents is not defined - why?
  348. # Compute lines (from CUR to NEXT branch row)
  349. next_branches = self._compute_next_branches()
  350. lines = self._compute_lines(parents, next_branches)
  351. # Compute node info (for CUR branch row)
  352. rev_column = self.curr_branches.index(rev_branch)
  353. rev_color = self.color4branch[rev_branch]
  354. node = (rev_column, rev_color)
  355. # Next loop
  356. self.curr_branches = next_branches
  357. self.curr_rev -= 1
  358. # Return result
  359. return (rev, node, lines, None)
  360. def branch_grapher(repo, start_rev, stop_rev, branch=None, branch_color=False):
  361. grapher = BranchGrapher(repo, start_rev, stop_rev, branch, branch_color)
  362. while grapher.more():
  363. yield grapher.next()
  364. def filelog_grapher(repo, path):
  365. '''
  366. Graph the ancestry of a single file (log). Deletions show
  367. up as breaks in the graph.
  368. '''
  369. fctx = repo.file(path)
  370. if not len(fctx):
  371. return
  372. paths = { path : len(fctx)-1 }
  373. revs = []
  374. rev_color = {}
  375. nextcolor = 0
  376. type = type_PLAIN
  377. while paths:
  378. # pick largest revision from known filenames
  379. fctx = None
  380. for path, filerev in paths.iteritems():
  381. f = repo.filectx(path, fileid=filerev)
  382. if not fctx or f.rev() > fctx.rev():
  383. fctx = f
  384. # Compute revs and next_revs.
  385. if fctx.rev() not in revs:
  386. revs.append(fctx.rev())
  387. rev_color[fctx.rev()] = nextcolor ; nextcolor += 1
  388. curcolor = rev_color[fctx.rev()]
  389. index = revs.index(fctx.rev())
  390. next_revs = revs[:]
  391. # Add parents to next_revs.
  392. parents_to_add = []
  393. for pfctx in fctx.parents():
  394. if pfctx.path() != fctx.path():
  395. continue
  396. if pfctx.rev() not in next_revs:
  397. parents_to_add.append(pfctx.rev())
  398. if len(fctx.parents()) > 1:
  399. rev_color[pfctx.rev()] = nextcolor ; nextcolor += 1
  400. else:
  401. rev_color[pfctx.rev()] = curcolor
  402. ret = fctx.renamed()
  403. if ret:
  404. renpath, fnode = ret
  405. flog = repo.file(renpath)
  406. filerev = flog.rev(fnode)
  407. paths[renpath] = filerev
  408. rfctx = repo.filectx(renpath, fileid=filerev)
  409. if rfctx.rev() not in revs:
  410. parents_to_add.append(rfctx.rev())
  411. rev_color[rfctx.rev()] = nextcolor ; nextcolor += 1
  412. parents_to_add.sort()
  413. next_revs[index:index + 1] = parents_to_add
  414. lines = []
  415. for i, rev in enumerate(revs):
  416. if rev in next_revs:
  417. color = rev_color[rev]
  418. lines.append((i, next_revs.index(rev), color, type))
  419. elif rev == fctx.rev():
  420. for pfctx in fctx.parents():
  421. color = rev_color[pfctx.rev()]
  422. lines.append((i, next_revs.index(pfctx.rev()), color, type))
  423. if fctx.filerev() == 0:
  424. del paths[fctx.path()]
  425. else:
  426. paths[fctx.path()] = fctx.filerev()-1
  427. revs = next_revs
  428. yield (fctx.rev(), (index, curcolor), lines, fctx.path())
  429. def dumb_log_generator(repo, revs):
  430. for revname in revs:
  431. node = repo.lookup(revname)
  432. rev = repo.changelog.rev(node)
  433. yield (rev, (0,0), [], None)
  434. def filtered_log_generator(repo, pats, opts):
  435. '''Fill view model iteratively
  436. repo - Mercurial repository object
  437. pats - list of file names or patterns
  438. opts - command line options for log command
  439. '''
  440. matching_revs = []
  441. only_branch = opts.get('branch', None)
  442. df = False
  443. if opts['date']:
  444. df = util.matchdate(opts['date'])
  445. def prep(ctx, fns):
  446. if only_branch and ctx.branch() != only_branch:
  447. return
  448. if opts['no_merges'] and len(ctx.parents()) == 2:
  449. return
  450. if opts['only_merges'] and len(ctx.parents()) != 2:
  451. return
  452. if df and not df(ctx.date()[0]):
  453. return
  454. if opts['user'] and not [k for k in opts['user'] if k in ctx.user()]:
  455. return
  456. if opts['keyword']:
  457. for k in [kw.lower() for kw in opts['keyword']]:
  458. if (k in ctx.user().lower() or
  459. k in ctx.description().lower() or
  460. k in " ".join(ctx.files()).lower()):
  461. break
  462. else:
  463. return
  464. matching_revs.append(ctx.rev())
  465. m = match.match(repo.root, repo.root, pats)
  466. for ctx in cmdutil.walkchangerevs(repo, m, opts, prep):
  467. if ctx.rev() in matching_revs:
  468. yield (ctx.rev(), (0,0), [], None)