[TUI] phases support (closes #87899)

Customize commit character in the graphlog using the changeset phase information.

Remove special character for:

  • merge: already graphically visible by branch junction
  • curdir parent: also highlighted, so no need for a special char
authorAlain Leufroy <alain.leufroy@logilab.fr>
changeset4017b4155c6a
branchdefault
phasepublic
hiddenno
parent revision#3db520bf9455 [qt/hgrepoview] try probable encodings rather than just impose one (author, commit msg)
child revision#68ab533747a4 [qt] mimic the curses layout by adding "description" to the files list (wip #83294)
files modified by this revision
hgviewlib/curses/graphlog.py
hgviewlib/curses/hgrepoviewer.py
hgviewlib/hgpatches/__init__.py
hgviewlib/hgpatches/mqsupport.py
hgviewlib/hgpatches/phases.py
hgviewlib/qt4/hgrepomodel.py
hgviewlib/qt4/hgrepoview.py
# HG changeset patch
# User Alain Leufroy <alain.leufroy@logilab.fr>
# Date 1327604151 -3600
# Thu Jan 26 19:55:51 2012 +0100
# Node ID 4017b4155c6a66287574fa6dd9aece97b5b0739e
# Parent 3db520bf945535f70771baf07fbb42ac41baeb3b
[TUI] phases support (closes #87899)

Customize commit character in the graphlog using the changeset phase information.

Remove special character for:

* merge: already graphically visible by branch junction
* curdir parent: also highlighted, so no need for a special char

diff --git a/hgviewlib/curses/graphlog.py b/hgviewlib/curses/graphlog.py
@@ -16,11 +16,13 @@
1  '''
2  Contains a listbox definition that walk the repo log and display an ascii graph
3  '''
4 
5  from itertools import izip_longest as zzip
6 +from logging import warn
7 
8 +from mercurial.node import short
9  from urwid import AttrMap, Text, ListWalker, Columns, WidgetWrap, emit_signal
10 
11  from hgext.graphlog import (fix_long_right_edges, get_nodeline_edges_tail,
12                              draw_edges, get_padding_line)
13 
@@ -41,10 +43,11 @@
14      'Author': lambda m, c, g: tounicode(c.user().split('<',1)[0]),
15      'Date': getdate,
16      'Tags': gettags,
17      'Branch': lambda m, c, g: c.branch() != 'default' and c.branch(),
18      'Filename': lambda m, c, g: g.extra[0],
19 +    'Phase': lambda model, ctx, gnode: ctx.phasestr(),
20      }
21  GRAPH_MIN_WIDTH = 6
22 
23  # ____________________________________________________________________ classes
24 
@@ -164,22 +167,24 @@
25 
26      def graphlog(self, gnode, ctx):
27          """Return a generator that get lines of graph log for the node
28          """
29          # define node symbol
30 -        char = 'o'
31          if gnode.rev is None:
32              char = '!' # pending changes
33 -        elif gnode.rev in self.walker.wd_revs:
34 -            char = '@'
35 -
36 -        if len(ctx.parents()) > 1:
37 -            char = 'M' # merge
38          elif not getattr(ctx, 'applied', True):
39              char = ' '
40          elif set(ctx.tags()).intersection(self.walker.mqueues):
41              char = '*'
42 +        else:
43 +            phase = ctx.phase()
44 +            try:
45 +                char = 'o#^'[phase]
46 +            except IndexError:
47 +                warn('"%(node)s" has an unknown phase: %(phase)i',
48 +                     {'node':short(ctx.node()), 'phase':phase})
49 +                char = '?'
50 
51          # build the column data for the graphlogger from data given by hgview
52          curcol = gnode.x
53          curedges = [(start, end) for start, end, _ in gnode.bottomlines
54                      if start == curcol]
diff --git a/hgviewlib/curses/hgrepoviewer.py b/hgviewlib/curses/hgrepoviewer.py
@@ -54,11 +54,14 @@
55          """update title depending on the given context ``ctx``."""
56          if ctx.node() is None:
57              hex_ = 'WORKING DIRECTORY'
58          else:
59              hex_ = ctx.hex()
60 -        self.title = '%s [%s]' % (self.walker.repo.root, hex_)
61 +        self.title = '%(root)s [%(hex)s] %(phase)s' % {
62 +            'root':self.walker.repo.root,
63 +            'hex':hex_,
64 +            'phase':ctx.phasestr()}
65 
66      def register_commands(self):
67          '''Register commands and connect commands for bodies'''
68          register_command(
69                  ('goto', 'g'), 'Set focus on a particular revision',
diff --git a/hgviewlib/hgpatches/__init__.py b/hgviewlib/hgpatches/__init__.py
@@ -30,8 +30,15 @@
70      iterhunks_orig = patch.iterhunks
71      ui = type('UI', (), {'debug':lambda *x: None})()
72      iterhunks = partial(iterhunks_orig, ui)
73      patch.iterhunks = iterhunks
74 
75 -#  mercurial ~< 1.8.3
76 +# mercurial ~< 1.8.3
77  if not hasattr(context.filectx, 'p1'):
78      context.filectx.p1 = lambda self: self.parents()[0]
79 +
80 +# mercurial < 2.1
81 +if not hasattr(context.changectx, 'phase'):
82 +    from hgviewlib.hgpatches.phases import phasenames
83 +    context.changectx.phase = lambda self: 0
84 +    context.changectx.phasestr = lambda self: phasenames[self.phase()]
85 +    context.workingctx.phase = lambda self: 1
diff --git a/hgviewlib/hgpatches/mqsupport.py b/hgviewlib/hgpatches/mqsupport.py
@@ -41,10 +41,12 @@
86  from collections import namedtuple
87 
88  from mercurial import error, node, patch, context, manifest
89  from hgext.mq import patchheader
90 
91 +from hgviewlib.hgpatches import phases
92 +
93  MODIFY, ADD, REMOVE, DELETE, UNKNOWN, RENAME = range(6) # order is important for status
94 
95  PatchMetaData = namedtuple('Meta', 'path oldpath op')
96 
97  class MqLookupError(error.LookupError):
@@ -172,10 +174,13 @@
98          return self.name
99 
100      def hidden(self):
101          return True
102 
103 +    def phase(self):
104 +        return phases.secret
105 +
106      def manifest(self):
107          return manifest.manifestdict.fromkeys(self.files(), '=')
108 
109      def node(self):
110          '''Return the name of the patch'''
diff --git a/hgviewlib/hgpatches/phases.py b/hgviewlib/hgpatches/phases.py
@@ -0,0 +1,22 @@
111 +# Copyright (c) 2003-2011 LOGILAB S.A. (Paris, FRANCE).
112 +# http://www.logilab.fr/ -- mailto:contact@logilab.fr
113 +#
114 +# This program is free software; you can redistribute it and/or modify it under
115 +# the terms of the GNU General Public License as published by the Free Software
116 +# Foundation; either version 2 of the License, or (at your option) any later
117 +# version.
118 +#
119 +# This program is distributed in the hope that it will be useful, but WITHOUT
120 +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
121 +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
122 +#
123 +# You should have received a copy of the GNU General Public License along with
124 +# this program.  If not, see <http://www.gnu.org/licenses/>.
125 +"""
126 +add a phases mock to mercurial
127 +"""
128 +try:
129 +    from mercurial.phases import *
130 +except ImportError:
131 +    allphases = public, draft, secret = range(3)
132 +    phasenames = ['public', 'draft', 'secret']
diff --git a/hgviewlib/qt4/hgrepomodel.py b/hgviewlib/qt4/hgrepomodel.py
@@ -28,10 +28,11 @@
133  from hgviewlib.hggraph import revision_grapher, filelog_grapher, getlog, gettags
134  from hgviewlib.config import HgConfig
135  from hgviewlib.util import tounicode, isbfile, Curry
136  from hgviewlib.qt4 import icon as geticon
137  from hgviewlib.decorators import timeit
138 +from hgviewlib.hgpatches import phases
139 
140  from PyQt4 import QtCore, QtGui
141  connect = QtCore.QObject.connect
142  SIGNAL = QtCore.SIGNAL
143  nullvariant = QtCore.QVariant()
@@ -42,11 +43,10 @@
144             "darkcyan", "gray", "yellow", ]
145  COLORS = [str(QtGui.QColor(x).name()) for x in COLORS]
146  #COLORS = [str(color) for color in QtGui.QColor.colorNames()]
147 
148 
149 -PUBLIC_PHASE, DRAFT_PHASE, SECRET_PHASE = 0, 1, 2
150 
151  def cvrt_date(date):
152      """
153      Convert a date given the hg way, ie. couple (date, tz), into a
154      formatted QString
@@ -64,10 +64,11 @@
155                'Author': lambda model, ctx, gnode: tounicode(ctx.user()),
156                'Date': lambda model, ctx, gnode: cvrt_date(ctx.date()),
157                'Tags': gettags,
158                'Branch': lambda model, ctx, gnode: ctx.branch(),
159                'Filename': lambda model, ctx, gnode: gnode.extra[0],
160 +              'Phase': lambda model, ctx, gnode: ctx.phasestr(),
161                }
162 
163  _tooltips = {'ID': lambda model, ctx, gnode: ctx.rev() is not None and ctx.hex() or "Working Directory",
164               }
165 
@@ -78,17 +79,16 @@
166      return sorted(auths, cmp=lambda x,y: cmp(len(x), len(y)))[-1]
167 
168  # in following lambdas, r is a hg repo
169  _maxwidth = {'ID': lambda self, r: str(len(r.changelog)),
170               'Date': lambda self, r: cvrt_date(r.changectx(0).date()),
171 -             'Tags': lambda self, r: sorted(r.tags().keys(),
172 -                                            key=lambda x: len(x))[-1][:10],
173 -             'Branch': lambda self, r: sorted(r.branchtags().keys(),
174 -                                              key=lambda x: len(x))[-1]
175 +             'Tags': lambda self, r: sorted(r.tags().keys(), key=len)[-1][:10],
176 +             'Branch': lambda self, r: sorted(r.branchtags().keys(), key=len)[-1]
177                                                if r.branchtags().keys() else None,
178               'Author': lambda self, r: 'author name',
179               'Filename': lambda self, r: self.filename,
180 +             'Phase': lambda self, r: sorted(phases.phasenames, key=len)[-1]
181               }
182 
183  def datacached(meth):
184      """
185      decorator used to cache 'data' method of Qt models. It will *not*
@@ -198,10 +198,11 @@
186                  msg = _columnmap[column](self, ctx, gnode)
187                  return QtCore.QVariant(msg)
188              return QtCore.QVariant(_columnmap[column](self, ctx, gnode))
189          elif role == QtCore.Qt.ToolTipRole:
190              msg = "<b>Branch:</b> %s<br>\n" % ctx.branch()
191 +            msg += "<b>Phase:</b> %s<br>\n" % ctx.phasestr()
192              if gnode.rev in self.wd_revs:
193                  msg += " <i>Working Directory position"
194                  states = 'modified added removed deleted'.split()
195                  status = self.wd_status[self.wd_revs.index(gnode.rev)]
196                  status = [state for st, state in zip(status, states) if st]
@@ -274,35 +275,35 @@
197                      atwd = True
198                      status = self.wd_status[self.wd_revs.index(gnode.rev)]
199                      if [True for st in status if st]:
200                          modified = True
201 
202 -                phase = getattr(ctx, 'phase', lambda : PUBLIC_PHASE)()
203 +                phase = ctx.phase()
204 
205                  if gnode.rev is None:
206                      # WD is displayed only if there are local
207                      # modifications, so let's use the modified icon
208                      icn = geticon('modified')
209                  elif tags.intersection(self.mqueues):
210                      icn = geticon('mqpatch')
211                  #elif modified:
212                  #    icn = geticon('modified')
213                  elif atwd:
214 -                    if phase > PUBLIC_PHASE:
215 +                    if phase > phases.public:
216                          pen_color = QtCore.Qt.red
217                          pen = QtGui.QPen(pen_color)
218                          pen.setWidth(penradius)
219                          painter.setPen(pen)
220                      else:
221                          icn = geticon('clean')
222 
223 
224                  if icn:
225                      icn.paint(painter, dot_x-5, dot_y-5, 17, 17)
226 -                elif phase == DRAFT_PHASE:
227 +                elif phase == phases.draft:
228                      painter.drawRect(dot_x, dot_y, radius, radius)
229 -                elif phase == SECRET_PHASE:
230 +                elif phase == phases.secret:
231                      P = QtCore.QPointF
232                      painter.drawPolygon(
233                          P(dot_x + (radius//2), dot_y),
234                          P(dot_x, dot_y + radius),
235                          P(dot_x + radius, dot_y+radius)
diff --git a/hgviewlib/qt4/hgrepoview.py b/hgviewlib/qt4/hgrepoview.py
@@ -424,11 +424,11 @@
236          rev = ctx.rev()
237          cfg = HgConfig(ctx._repo.ui)
238          buf = "<table width=100%>\n"
239          if self.mqpatch:
240              buf += '<tr bgcolor=%s>' % cfg.getMQFGColor()
241 -            buf += '<td colspan=3 width=100%><b>Patch queue:</b>&nbsp;'
242 +            buf += '<td colspan=4 width=100%><b>Patch queue:</b>&nbsp;'
243              for p in self.mqseries:
244                  if p in self.mqunapplied:
245                      p = "<i>%s</i>" % p
246                  elif p == self.mqpatch:
247                      p = "<b>%s</b>" % p
@@ -437,30 +437,30 @@
248 
249          buf += '<tr>'
250          if rev is None:
251              buf += "<td><b>Working Directory</b></td>\n"
252          else:
253 -            buf += '<td><b>Revision:</b>&nbsp;'\
254 +            buf += '<td title="Revision"><b>'\
255                     '<span class="rev_number">%s</span>:'\
256 -                   '<span class="rev_hash">%s</span></td>'\
257 -                   '\n' % (ctx.rev(), short_hex(ctx.node()))
258 +                   '<span class="rev_hash">%s</span>'\
259 +                   '</b></td>\n' % (ctx.rev(), short_hex(ctx.node()))
260 
261 -        buf += '<td><b>Author:</b>&nbsp;'\
262 -               '%s</td>'\
263 -               '\n' %  tounicode(ctx.user())
264 -        buf += '<td><b>Branch:</b>&nbsp;%s</td>' % ctx.branch()
265 +        buf += '<td title="Author">%s</td>\n' \
266 +               % tounicode(ctx.user())
267 +        buf += '<td title="Branch name">%s</td>\n' % ctx.branch()
268 +        buf += '<td title="Phase name">%s</td>\n' % ctx.phasestr()
269          buf += '</tr>'
270          buf += "</table>\n"
271          buf += "<table width=100%>\n"
272          parents = [p for p in ctx.parents() if p]
273          for p in parents:
274              if p.rev() > -1:
275                  short = short_hex(p.node()) if getattr(p, 'applied', True) else p.node()
276                  desc = format_desc(p.description(), self.descwidth)
277                  p_rev = p.rev()
278                  p_fmt = '<span class="rev_number">%s</span>:'\
279 -                        '<a href="%s" class="rev_hash">%s</a>'
280 +                        '<a title="go to" href="%s" class="rev_hash">%s</a>'
281                  if p_rev == self.diffrev:
282                      p_rev = '<b>%s</b>' % (p_fmt % (p_rev, p_rev, short))
283                  else:
284                      p_rev = p_fmt % ('<a href="diff_%s" class="rev_diff">%s</a>' % (p_rev, p_rev), p_rev, short)
285                  buf += '<tr><td width=50 class="label"><b>Parent:</b></td>'\
@@ -471,15 +471,15 @@
286              p = parents[0].ancestor(parents[1])
287              short = short_hex(p.node())
288              desc = format_desc(p.description(), self.descwidth)
289              p_rev = p.rev()
290              p_fmt = '<span class="rev_number">%s</span>:'\
291 -                    '<a href="%s" class="rev_hash">%s</a>'
292 +                    '<a title="go to" href="%s" class="rev_hash">%s</a>'
293              if p_rev == self.diffrev:
294                  p_rev = '<b>%s</b>' % (p_fmt % (p_rev, p_rev, short))
295              else:
296 -                p_rev = p_fmt % ('<a href="diff_%s" class="rev_diff">%s</a>' % (p_rev, p_rev), p_rev, short)
297 +                p_rev = p_fmt % ('<a title="go to" href="diff_%s" class="rev_diff">%s</a>' % (p_rev, p_rev), p_rev, short)
298              buf += '<tr><td width=50 class="label"><b>Ancestor:</b></td>'\
299                     '<td colspan=5>%s&nbsp;'\
300                     '<span class="short_desc"><i>%s</i></span></td></tr>'\
301                     '\n' % (p_rev, desc)
302 
@@ -487,11 +487,11 @@
303              if p.rev() > -1:
304                  short = short_hex(p.node()) if getattr(p, 'applied', True) else p.node()
305                  desc = format_desc(p.description(), self.descwidth)
306                  buf += '<tr><td class="label"><b>Child:</b></td>'\
307                         '<td colspan=5><span class="rev_number">%s</span>:'\
308 -                       '<a href="%s" class="rev_hash">%s</a>&nbsp;'\
309 +                       '<a title="go to" href="%s" class="rev_hash">%s</a>&nbsp;'\
310                         '<span class="short_desc"><i>%s</i></span></td></tr>'\
311                         '\n' % (p.rev(), p.rev(), short, desc)
312 
313          buf += "</table>\n"
314          desc = tounicode(ctx.description())