[core] improve mq support (closes #19194)

Make unapplied mq patch looks like "normal" changeset.

authorAlain Leufroy <alain.leufroy@logilab.fr>
changeset29e1b3984822
branchdefault
phasepublic
hiddenno
parent revision#33d334a26e40 [console] add history and completion for command (closes #84733)
child revision#46242942780f [packaging] update FSF address in py files (closes #75295)
files modified by this revision
hgviewlib/config.py
hgviewlib/curses/application.py
hgviewlib/curses/graphlog.py
hgviewlib/curses/hgrepoviewer.py
hgviewlib/hggraph.py
hgviewlib/hgpatches/__init__.py
hgviewlib/hgpatches/mqsupport.py
hgviewlib/qt4/hgfiledialog.py
hgviewlib/qt4/hgrepomodel.py
hgviewlib/qt4/hgrepoview.py
# HG changeset patch
# User Alain Leufroy <alain.leufroy@logilab.fr>
# Date 1323618621 -3600
# Sun Dec 11 16:50:21 2011 +0100
# Node ID 29e1b39848221346c30890e53aee6b4e89289005
# Parent 33d334a26e40dec8eb95fddc69aa1518a9deaf1f
[core] improve mq support (closes #19194)

Make unapplied mq patch looks like "normal" changeset.

diff --git a/hgviewlib/config.py b/hgviewlib/config.py
@@ -239,10 +239,17 @@
1          mqhidetags: hide mq tags
2          """
3          return self.ui.config(self.section, 'mqhidetags', default)
4 
5      @cached
6 +    def getMQHideUnapplieds(self, default=False):
7 +        """
8 +        mqhideunapplieds: hide unapplied mq patches
9 +        """
10 +        return self.ui.config(self.section, 'mqhideunapplieds', default)
11 +
12 +    @cached
13      def getInterface(self, default=None):
14          """
15          interface: which GUI interface to use (among "qt", "raw" and "curses")
16          """
17          return self.ui.config(self.section, 'interface', default)
diff --git a/hgviewlib/curses/application.py b/hgviewlib/curses/application.py
@@ -298,11 +298,10 @@
18      ('Author', 'dark blue', 'default', 'bold'),
19      ('Date', 'dark green', 'default', 'bold'),
20      ('Tags', 'yellow', 'dark red', 'bold'),
21      ('Branch', 'yellow', 'default', 'bold'),
22      ('Filename', 'white', 'default', 'bold'),
23 -    ('Unapplied', 'light cyan', 'black'),
24 
25      # filelist
26      ('+', 'dark green', 'default'),
27      ('-', 'dark red', 'default'),
28      ('=', 'default', 'default'),
diff --git a/hgviewlib/curses/graphlog.py b/hgviewlib/curses/graphlog.py
@@ -17,56 +17,40 @@
29  '''
30  Contains a listbox definition that walk the repo log and display an ascii graph
31  '''
32 
33  from itertools import izip_longest as zzip
34 -from time import strftime, localtime
35 
36  from urwid import AttrMap, Text, ListWalker, Columns, WidgetWrap, emit_signal
37 
38  from hgext.graphlog import (fix_long_right_edges, get_nodeline_edges_tail,
39                              draw_edges, get_padding_line)
40 
41  from hgviewlib.util import tounicode
42 -from hgviewlib.hggraph import getlog, gettags, HgRepoListWalker
43 +from hgviewlib.hggraph import getlog, gettags, getdate, HgRepoListWalker
44  from hgviewlib.curses import connect_command, SelectableText
45 
46  # __________________________________________________________________ constants
47 
48  COLORS = ["brown", "dark red", "dark magenta", "dark blue", "dark cyan",
49            "dark green", "yellow", "light red", "light magenta", "light blue",
50            "light cyan", "light green"]
51 
52 -DATE_FMT = '%F %R'
53 
54  _COLUMNMAP = {
55      'ID': lambda m, c, g: c.rev() is not None and str(c.rev()) or "",
56      'Log': getlog,
57      'Author': lambda m, c, g: tounicode(c.user().split('<',1)[0]),
58 -    'Date': lambda m, c, g: strftime(DATE_FMT, localtime(int(c.date()[0]))),
59 +    'Date': getdate,
60      'Tags': gettags,
61      'Branch': lambda m, c, g: c.branch() != 'default' and c.branch(),
62      'Filename': lambda m, c, g: g.extra[0],
63      }
64  GRAPH_MIN_WIDTH = 6
65 
66  # ____________________________________________________________________ classes
67 
68 -class AppliedItem(WidgetWrap):
69 -    """Wrap widget that displays basic changeset"""
70 -    def __init__(self, widget, gnode, ctx, *args, **kwargs):
71 -        self.gnode = gnode
72 -        self.ctx = ctx
73 -        super(AppliedItem, self).__init__(widget, *args, **kwargs)
74 -
75 -class UnappliedItem(WidgetWrap):
76 -    """Wrap widget that diplays unapplied mq patch"""
77 -    def __init__(self, widget, idx, name, *args, **kwargs):
78 -        self.idx = idx
79 -        self.name = name
80 -        super(UnappliedItem, self).__init__(widget, *args, **kwargs)
81 -
82  class RevisionsWalker(ListWalker):
83      """ListWalker-compatible class for browsing log changeset.
84      """
85 
86      signals = ['focus changed']
@@ -76,26 +60,20 @@
87      _allcolumns = (('Date', 16), ('Author', 20), ('ID', 6),)
88 
89      def __init__(self, walker, branch='', fromhead=None, follow=False,
90                   *args, **kwargs):
91          self._data_cache = {}
92 -        self._focus = 0L
93 +        self._focus = 0
94          self.walker = walker
95          super(RevisionsWalker, self).__init__(*args, **kwargs)
96 -        if hasattr(self.walker.repo, "mq"): # focus on first mq patch if any
97 -            self._focus = -len(self._get_unapplied())
98          self.asciistate = [0, 0] # graphlog.asciistate()
99 
100      def connect_commands(self):
101          """Connect usefull commands to callbacks"""
102          connect_command('goto', self.set_rev)
103          connect_command('refresh', self.refresh)
104 
105 -    def _get_unapplied(self):
106 -        """return unapplied mq patches"""
107 -        return self.walker.repo.mq.unapplied(self.walker.repo)
108 -
109      def _modified(self):
110          """obsolete widget content"""
111          super(RevisionsWalker, self)._modified()
112 
113      def refresh(self):
@@ -125,49 +103,18 @@
114          """Return a widget and the position passed."""
115          # cache may be very hudge on very big repo
116          # (cpython for instance: >1.5GB)
117          if pos in self._data_cache: # speed up rendering
118              return self._data_cache[pos], pos
119 -        if pos < 0:
120 -            widget = self.get_unapplied_widget(pos)
121 -        else:
122 -            widget = self.get_applied_widget(pos)
123 +        widget = self.get_widget(pos)
124          if widget is None:
125              return None, None
126          self._data_cache[pos] = widget
127          return widget, pos
128 
129 -    def get_unapplied_widget(self, pos):
130 -        """return a widget for unapplied patch"""
131 -        # blank columns
132 -        idx, name = self._get_unapplied()[-pos - 1]
133 -        info = {'Branch':'[Unapplied patches]', 'ID':str(idx), 'Log':name}
134 -        # prepare the last columns content
135 -        txts = ['.' + ' ' * GRAPH_MIN_WIDTH] # mock graph log
136 -        for fields in self._allfields:
137 -            if not fields:
138 -                continue
139 -            for field in fields:
140 -                if field not in info:
141 -                    continue
142 -                txts.append(('Unapplied', info.get(field)))
143 -                txts.append(('default', ' '))
144 -            txts.append('\n')
145 -        txt = SelectableText(txts[:-1], wrap='clip')
146 -        # prepare other columns
147 -        columns = [('fixed', sz, Text(('Unapplied', info.get(col, '')),
148 -                                      align='right', wrap='clip'))
149 -                   for col, sz in self._allcolumns
150 -                   if col in self._columns]
151 -        txt = Columns(columns + [txt], 1)
152 -        txt = AttrMap(txt, {}, {'Unapplied':'focus'})
153 -        txt = UnappliedItem(txt, idx, name)
154 -        return txt
155 -
156 -    def get_applied_widget(self, pos):
157 -        """Return a widget for changeset, applied patches and working
158 -        directory state"""
159 +    def get_widget(self, pos):
160 +        """Return a widget for the node"""
161          if pos in self._data_cache: # speed up rendering
162              return self._data_cache[pos], pos
163 
164          try:
165              self.walker.ensureBuilt(row=pos)
@@ -212,11 +159,10 @@
166          foc_style = dict.fromkeys(self._columns + ('GraphLog', None,),
167                                    style or 'focus')
168          foc_style['GraphLog.node'] = 'focus.alternate'
169          # wrap widget with style modified
170          widget = AttrMap(Columns(columns, 1), spec_style, foc_style)
171 -        widget = AppliedItem(widget, gnode, ctx)
172          return widget
173 
174      def graphlog(self, gnode, ctx):
175          """Return a generator that get lines of graph log for the node
176          """
@@ -227,12 +173,14 @@
177          elif gnode.rev in self.walker.wd_revs:
178              char = '@'
179 
180          if len(ctx.parents()) > 1:
181              char = 'M' # merge
182 +        elif not getattr(ctx, 'applied', True):
183 +            char = ' '
184          elif set(ctx.tags()).intersection(self.walker.mqueues):
185 -            char = '*' # applied patch from mq
186 +            char = '*'
187 
188          # build the column data for the graphlogger from data given by hgview
189          curcol = gnode.x
190          curedges = [(start, end) for start, end, _ in gnode.bottomlines
191                      if start == curcol]
@@ -252,11 +200,11 @@
192              return self.data(self._focus)
193          except IndexError:
194              if self._focus > 0:
195                  self._focus = 0
196              else:
197 -                self._focus = -len(self._get_unapplied())
198 +                self._focus = 0
199          try:
200              return self.data(self._focus)
201          except:
202              return None, None
203 
@@ -297,10 +245,12 @@
204              return None, None
205 
206      def get_prev(self, start_from):
207          """get the previous widget to display"""
208          focus = start_from - 1
209 +        if focus < 0:
210 +            return None, None
211          try:
212              return self.data(focus)
213          except IndexError:
214              return None, None
215 
diff --git a/hgviewlib/curses/hgrepoviewer.py b/hgviewlib/curses/hgrepoviewer.py
@@ -50,13 +50,11 @@
216          signals.connect_signal(self.graphlog_walker, 'focus changed',
217                                 self.update_title)
218 
219      def update_title(self, ctx):
220          """update title depending on the given context ``ctx``."""
221 -        if ctx is None:
222 -            hex_ = 'UNAPPLIED MQ PATCH'
223 -        elif ctx.node() is None:
224 +        if ctx.node() is None:
225              hex_ = 'WORKING DIRECTORY'
226          else:
227              hex_ = ctx.hex()
228          self.title = '%s [%s]' % (self.walker.repo.root, hex_)
229 
@@ -253,13 +251,10 @@
230          super(RepoViewer, self).__init__(widget_list=widget_list, focus_item=0,
231                                           *args, **kwargs)
232 
233      def update_context(self, ctx):
234          """Change the current displayed context"""
235 -        if ctx is None: # unapplied patch
236 -            self.context.clear()
237 -            return
238          self.context.manifest_walker.ctx = ctx
239 
240      def register_commands(self):
241          """Register commands and commands of bodies"""
242          register_command('hide-context', 'Hide context pane.')
diff --git a/hgviewlib/hggraph.py b/hgviewlib/hggraph.py
@@ -18,21 +18,27 @@
243 
244  Based on graphlog's algorithm, with insipration stolen to TortoiseHg
245  revision grapher.
246  """
247 
248 +import os
249  from cStringIO import StringIO
250  import difflib
251  from itertools import chain
252 +from time import strftime, localtime
253 
254  from mercurial.node import nullrev
255  from mercurial import patch, util, match, error, hg
256 
257  import hgviewlib.hgpatches # force apply patches to mercurial
258 +from hgviewlib.hgpatches import mqsupport
259 +
260  from hgviewlib.util import tounicode, isbfile
261  from hgviewlib.config import HgConfig
262 
263 +DATE_FMT = '%F %R'
264 +
265  def diff(repo, ctx1, ctx2=None, files=None):
266      """
267      Compute the diff of ``files`` between the 2 contexts ``ctx1`` and ``ctx2``.
268 
269      :Note: context may be a changectx or a filectx.
@@ -41,10 +47,12 @@
270        file ctx, the parent is the first ancestor that contains modification
271        on the given file
272      * If ``files`` is None, return the diff for all files.
273 
274      """
275 +    if not getattr(ctx1, 'applied', True): #no diff vs ctx2 on unapplied patch
276 +        return ''.join(chain(ctx1.filectx(fname).data() for fname in files))
277      if ctx2 is None:
278          ctx2 = ctx1.p1()
279      if files is None:
280          matchfn = match.always(repo.root, repo.getcwd())
281      else:
@@ -99,10 +107,15 @@
282      tags = ctx.tags()
283      if model.hide_mq_tags:
284          tags = [t for t in tags if t not in mqtags]
285      return ",".join(tags)
286 
287 +def getdate(model, ctx, gnode):
288 +    if not ctx.date():
289 +        return ""
290 +    return strftime(DATE_FMT, localtime(int(ctx.date()[0])))
291 +
292  def ismerge(ctx):
293      """
294      Return True if the changecontext ctx is a merge mode (should work
295      with hg 1.0 and 1.2)
296      """
@@ -123,14 +136,26 @@
297        - color of the node (?)
298        - lines; a list of (col, next_col, color) indicating the edges between
299          the current row and the next row
300        - parent revisions of current revision
301 
302 +    If start_rev is -1, shown unapplied mq patches (if available)
303 +
304 +    If start_rev is None, started from working directory node
305 +
306      If follow is True, only generated the subtree from the start_rev head.
307 
308      If branch is set, only generated the subtree for the given named branch.
309 +
310      """
311 +    if start_rev == -1 and hasattr(repo, 'mq'):
312 +        series = list(reversed(repo.mq.series))
313 +        for patchname in series:
314 +            if not repo.mq.isapplied(patchname):
315 +                yield (patchname, 0, 0, [(0,0,0)], [])
316 +        start_rev = None
317 +
318      if start_rev is None and repo.status() == ([],)*7:
319          start_rev = len(repo.changelog)
320      assert start_rev is None or start_rev >= stop_rev
321      curr_rev = start_rev
322      revs = []
@@ -309,11 +334,10 @@
323 
324      def build_nodes(self, nnodes=None, rev=None):
325          """
326          Build up to `nnodes` more nodes in our graph, or build as many
327          nodes required to reach `rev`.
328 -
329          If both rev and nnodes are set, build as many nodes as
330          required to reach rev plus nnodes more.
331          """
332          if self.grapher is None:
333              return False
@@ -321,11 +345,11 @@
334          mcol = [self.max_cols]
335          for vnext in self.grapher:
336              if vnext is None:
337                  continue
338              nrev, xpos, color, lines, parents = vnext[:5]
339 -            if nrev >= self.maxlog:
340 +            if isinstance(nrev, int) and nrev >= self.maxlog:
341                  continue
342              gnode = GraphNode(nrev, xpos, color, lines, parents,
343                                extra=vnext[5:])
344              if self.nodes:
345                  gnode.toplines = self.nodes[-1].bottomlines
@@ -431,25 +455,27 @@
346          # XXX This really begins to be a dirty mess...
347          data = ""
348          if flag is None:
349              flag = self.fileflag(filename, rev)
350          ctx = self.repo.changectx(rev)
351 +        filesize = 0
352          try:
353              fctx = ctx.filectx(filename)
354 +            filesize = fctx.size() # compute size here to lookup data securely
355          except LookupError:
356 -            fctx = None # may happen for renamed files?
357 +            fctx = None # may happen for renamed files or mq patch ?
358 
359          if isbfile(filename):
360              data = "[bfile]\n"
361              if fctx:
362                  data = fctx.data()
363                  data += "footprint: %s\n" % data
364              return "+", data
365          if flag not in ('-', '?'):
366              if fctx is None:# or fctx.node() is None:
367                  return '', None
368 -            if fctx.size() > self.maxfilesize:
369 +            if filesize > self.maxfilesize:
370                  data = "file too big"
371                  return flag, data
372              if flag == "+" or mode == 'file':
373                  flag = '+'
374                  # return the whole file
@@ -463,11 +489,11 @@
375                  if isinstance(mode, int):
376                      parentctx = self.repo.changectx(mode)
377                  else:
378                      parentctx = self.repo[self._fileparent(fctx)]
379                  data = diff(self.repo, ctx, parentctx, files=[filename])
380 -                data = u'\n'.join(data.splitlines()[3:])
381 +                data = data.split(os.linesep, 3)[-1]
382              elif flag == '':
383                  data = ''
384              else: # file renamed
385                  oldname, node = flag
386                  newdata = fctx.data().splitlines()
@@ -520,10 +546,13 @@
387          self.setRepo(repo, branch=branch, fromhead=fromhead, follow=follow)
388 
389      def setRepo(self, repo=None, branch='', fromhead=None, follow=False):
390          if repo is None:
391              repo = hg.repository(self.repo.ui, self.repo.root)
392 +        self._hasmq = hasattr(self.repo, "mq")
393 +        if not getattr(repo, '__hgview__', False) and self._hasmq:
394 +            mqsupport.reposetup(repo.ui, repo)
395          oldrepo = self.repo
396          self.repo = repo
397          if oldrepo.root != repo.root:
398              self.load_config()
399          self._datacache = {}
@@ -531,20 +560,21 @@
400              wdctxs = self.repo.changectx(None).parents()
401          except error.Abort:
402              # might occur if reloading during a mq operation (or
403              # whatever operation playing with hg history)
404              return
405 -        self._hasmq = hasattr(self.repo, "mq")
406          if self._hasmq:
407              self.mqueues = self.repo.mq.series[:]
408          self.wd_revs = [ctx.rev() for ctx in wdctxs]
409          self.wd_status = [self.repo.status(ctx.node(), None)[:4] for ctx in wdctxs]
410          self._user_colors = {}
411          self._branch_colors = {}
412          # precompute named branch color for stable value.
413          for branch_name in chain(['default', 'stable'], sorted(repo.branchtags().keys())):
414              self.namedbranch_color(branch_name)
415 +        if fromhead is None and not self.hide_mq_unapplieds:
416 +            fromhead = -1
417          grapher = revision_grapher(self.repo, start_rev=fromhead,
418                                     follow=follow, branch=branch)
419          self.graph = Graph(self.repo, grapher, self.max_file_size)
420          self.rowcount = 0
421          self.heads = [self.repo.changectx(x).rev() for x in self.repo.heads()]
@@ -593,10 +623,11 @@
422          self.dot_radius = cfg.getDotRadius(default=8)
423          self.rowheight = cfg.getRowHeight()
424          self.fill_step = cfg.getFillingStep()
425          self.max_file_size = cfg.getMaxFileSize()
426          self.hide_mq_tags = cfg.getMQHideTags()
427 +        self.hide_mq_unapplieds = cfg.getMQHideUnapplieds()
428 
429          cols = getattr(cfg, self._getcolumns)()
430          if cols is not None:
431              validcols = [col for col in cols if col in self._allcolumns]
432              if len(validcols) != len(cols):
diff --git a/hgviewlib/hgpatches/__init__.py b/hgviewlib/hgpatches/__init__.py
@@ -16,14 +16,23 @@
433  """
434  This modules contains monkey patches for Mercurial allowing hgview to support
435  older versions
436  """
437 
438 -from mercurial import changelog, filelog
439 +from functools import partial
440 +from mercurial import changelog, filelog, patch, context
441 +
442  if not hasattr(changelog.changelog, '__len__'):
443      changelog.changelog.__len__ = changelog.changelog.count
444  if not hasattr(filelog.filelog, '__len__'):
445      filelog.filelog.__len__ = filelog.filelog.count
446 
447 -from mercurial import context
448 +# mercurial ~< 1.8.4
449 +if patch.iterhunks.func_code.co_varnames[0] == 'ui':
450 +    iterhunks_orig = patch.iterhunks
451 +    ui = type('UI', (), {'debug':lambda *x: None})()
452 +    iterhunks = partial(iterhunks_orig, ui)
453 +    patch.iterhunks = iterhunks
454 +
455 +#  mercurial ~< 1.8.3
456  if not hasattr(context.filectx, 'p1'):
457      context.filectx.p1 = lambda self: self.parents()[0]
diff --git a/hgviewlib/hgpatches/mqsupport.py b/hgviewlib/hgpatches/mqsupport.py
@@ -0,0 +1,336 @@
458 +# Copyright (c) 2003-2011 LOGILAB S.A. (Paris, FRANCE).
459 +# http://www.logilab.fr/ -- mailto:contact@logilab.fr
460 +#
461 +# This program is free software; you can redistribute it and/or modify it under
462 +# the terms of the GNU General Public License as published by the Free Software
463 +# Foundation; either version 2 of the License, or (at your option) any later
464 +# version.
465 +#
466 +# This program is distributed in the hope that it will be useful, but WITHOUT
467 +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
468 +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
469 +#
470 +# You should have received a copy of the GNU General Public License along with
471 +# this program; if not, write to the Free Software Foundation, Inc.,
472 +# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
473 +
474 +"""
475 +The main goal of this module is to fakse mercurial change context classes from
476 +data information available in mq patch files.
477 +
478 +Only methods that are requiered by hgview had been implemented.
479 +They may have special features to help hgview, So use it with care.
480 +
481 +The main differences are:
482 +
483 +* node, rev, hex are all strings (patch name)
484 +* files within patches are always displayed as modified files
485 +* manifest only shows files modified by the mq patch.
486 +* data may be empty (date, description, status, tags, branch, etc.)
487 +* the parent of a patch may by the last appied on or previous patch or nullid
488 +* the child of a patch is the next patch
489 +* patches are hidden
490 +"""
491 +
492 +from __future__ import with_statement
493 +
494 +import re
495 +import os
496 +import os.path as osp
497 +from operator import or_
498 +from itertools import chain
499 +from collections import namedtuple
500 +
501 +from mercurial import error, node, patch, context, manifest
502 +from hgext.mq import patchheader
503 +
504 +MODIFY, ADD, REMOVE, DELETE, UNKNOWN, RENAME = range(6) # order is important for status
505 +
506 +PatchMetaData = namedtuple('Meta', 'path oldpath op')
507 +
508 +class MqLookupError(error.LookupError):
509 +    """Specific exception dedicated to mq patches"""
510 +
511 +class MqCtx(context.changectx):
512 +    """Base class of mq patch context (changectx, filectx, etc.)"""
513 +    def __init__(self, repo, patch_name):
514 +        self.name = patch_name
515 +        self._rev = self.name
516 +        self._repo = repo
517 +        self._queue = self._repo.mq
518 +
519 +        self.path = self._queue.join(self.name)
520 +
521 +    @property
522 +    def applied(self):
523 +        return bool(self._queue.isapplied(self.name))
524 +
525 +    def __contains__(self, filename):
526 +        return filename in self.files()
527 +
528 +    def __iter__(self):
529 +        for filename in self.files():
530 +            yield filename
531 +
532 +    def __getitem__(self, filename):
533 +        return self.filectx(filename)
534 +
535 +    def files(self):
536 +        """Return the list of related files"""
537 +        raise NotImplementedError
538 +
539 +    def filectx(self, path, **kwargs):
540 +        """Return the context related to the filename"""
541 +        raise NotImplementedError
542 +
543 +class MqChangeCtx(MqCtx):
544 +    """
545 +    A Mercurial change context fake for unapplied mq patch.
546 +    Use with care as methodes may be missing or have special features.
547 +    """
548 +
549 +    def __init__(self, repo, patch_name):
550 +        super(MqChangeCtx, self).__init__(repo, patch_name)
551 +        if patch_name is None:
552 +            raise ValueError
553 +        self._header_cache = None
554 +        self._diffs_cache = None
555 +        self._files_cache = None
556 +
557 +    def __repr__(self):
558 +        return '<MQchangectx (unapplied) %s>' % self.name
559 +
560 +    @property
561 +    def _header(self):
562 +        if self._header_cache is not None:
563 +            return self._header_cache
564 +        self._header_cache = patchheader(self.path) 
565 +        return self._header_cache
566 +
567 +    @property
568 +    def _diffs(self):
569 +        # cache on first access only to speed up the process
570 +        if self._diffs_cache is not None:
571 +            return self._diffs_cache
572 +        self._diffs_cache = []
573 +        hunks = None
574 +        meta = None
575 +        data = None
576 +        with open(self.path) as fid:
577 +            for event, data in patch.iterhunks(fid):
578 +                if event == 'file':
579 +                    if hunks:
580 +                        self._diffs_cache.append(MqFileCtx(hunks, meta, self))
581 +                    hunks = []
582 +                    meta = data[-1]
583 +                    if not hasattr(meta, 'path'):
584 +                        new, old = data[:2]
585 +                        meta = PatchMetaData(new[2:], old[2:], 'UNKNOWN') # [2:] = remove a/
586 +                elif event == 'hunk' and data:
587 +                    hunks.append(data)
588 +            if hunks:
589 +                self._diffs_cache.append(MqFileCtx(hunks, meta, self))
590 +        return self._diffs_cache
591 +
592 +    def branch(self):
593 +        return getattr(self._header, 'branch', '')
594 +
595 +    def children(self):
596 +        series = self._queue.series
597 +        try:
598 +            idx = series.index(self.name)
599 +            return [self._repo.changectx(series[idx + 1]) if idx else self._repo[None]]
600 +        except IndexError:
601 +            return [self._repo[node.nullid]]
602 +
603 +    def date(self):
604 +        date = self._header.date
605 +        if not date:
606 +            return ()
607 +        date, timezone = date.split()
608 +        return float(date), int(timezone)
609 +
610 +    def description(self):
611 +        return '\n'.join(self._header.message)
612 +
613 +    def filectx(self, filename, _cache=[], **kwargs):
614 +        for diff in self._diffs:
615 +            if diff.path == filename:
616 +                return diff
617 +        raise MqLookupError(self.name, filename, 'file not in manifest.')
618 +
619 +    def files(self):
620 +        if self._files_cache is not None:
621 +            return self._files_cache
622 +        out = list(set(chain(*(diff.files() for diff in self._diffs))))
623 +        self._files_cache = out
624 +        return out
625 +
626 +    def flags(self, path):
627 +        return ''
628 +
629 +    def hex(self):
630 +        return self.name
631 +
632 +    def hidden(self):
633 +        return True
634 +
635 +    def manifest(self):
636 +        return manifest.manifestdict.fromkeys(self.files(), '=')
637 +
638 +    def node(self):
639 +        '''Return the name of the patch'''
640 +        return self.name
641 +
642 +    def parents(self):
643 +        if self._header.parent:
644 +            try:
645 +                return [self._repo[self._header.parent]]
646 +            except error.RepoLookupError:
647 +                pass
648 +        series = self._queue.series
649 +        if not self.name in series:
650 +            return []
651 +        idx = series.index(self.name)
652 +        return [self._repo.changectx(series[idx - 1]) if idx else self._repo[None]]
653 +
654 +    def rev(self):
655 +        return self.name
656 +
657 +    def status(self):
658 +        return ()
659 +
660 +    def tags(self):
661 +        return [self.name]
662 +
663 +    def user(self):
664 +        return self._header.user or ''
665 +
666 +class _MqMissingPatch_Header(object):
667 +    """Patch header fake for missing file"""
668 +    message = (':ERROR: patch file is missing !!!',)
669 +    date, breanch, user, parent = ('',) * 4
670 +
671 +class MqMissingChangeCtx(MqChangeCtx):
672 +    """Changeset class for patch in series without file."""
673 +    def __init__(self, repo, patch_name):
674 +        super(MqMissingChangeCtx, self).__init__(repo, patch_name)
675 +        self._header_cache = _MqMissingPatch_Header()
676 +        self._diffs_cache = ()
677 +        self._files_cache = ()
678 +
679 +    def __repr__(self):
680 +        return '<MQchangectx (missing file) %s>' % self.name
681 +
682 +class MqFileCtx(context.filectx):
683 +    """Mq Fake for file context"""
684 +
685 +    def __init__(self, hunks, meta, changectx):
686 +        self._changectx = changectx
687 +        self._repo = changectx._repo
688 +        self._path = meta.path
689 +        self._oldpath = meta.oldpath
690 +        self._operation = meta.op
691 +        self._data = '\n\n\n'
692 +        self._data += ''.join(l for h in hunks for l in h.hunk if h)
693 +
694 +    @property
695 +    def path(self):
696 +        return self._path
697 +
698 +    @property
699 +    def oldpath(self):
700 +        return self._oldpath
701 +
702 +    def files(self):
703 +        """List of modified files"""
704 +        return tuple(path for path in (self._path, self._oldpath) if path)
705 +
706 +    def data(self):
707 +        """ return the patch hunks"""
708 +        return self._data
709 +    __str__ = data
710 +
711 +    def isexec(self):
712 +        return False #  XXX
713 +
714 +    def __repr__(self):
715 +        return ('<mqfilectx (unapplied) %s@%s>' %
716 +                (self._path, self._changectx.name))
717 +
718 +    def flags(self):
719 +        return ''
720 +
721 +    def renamed(self):
722 +        if self.state == 'RENAME':
723 +            return self._oldpath, self._path
724 +        return False
725 +
726 +    def parents(self):
727 +        try:
728 +            return [self._changectx._repo[self._changectx._header.parent]]
729 +        except error.RepoLookupError:
730 +            return [self]
731 +
732 +    def size(self):
733 +        return len(self._data)
734 +
735 +    @property
736 +    def state(self):
737 +        return self._operation or 'UNKNOWN'
738 +
739 +    def filelog(self):
740 +        return None
741 +
742 +# ___________________________________________________________________________
743 +def reposetup(ui, repo):
744 +    """
745 +    extend repo class with special mq logic
746 +    """
747 +    if (not repo.local()) or (not hasattr(repo, "mq")):
748 +        return
749 +
750 +    repo.unapplieds = filter(repo.mq.unapplied, repo.mq.series)
751 +
752 +    getitem_orig = repo.__getitem__
753 +    status_orig = repo.status
754 +    lookup_orig = repo.lookup
755 +
756 +    class MqRepository(repo.__class__):
757 +        __hgview__ = True
758 +
759 +        def __getitem__(self, changeid):
760 +            if changeid not in self.unapplieds:
761 +                return getitem_orig(changeid)
762 +            patch = MqChangeCtx(repo, changeid)
763 +            if os.path.exists(patch.path):
764 +                return patch
765 +            return MqMissingChangeCtx(repo, changeid)
766 +
767 +        def status(self, node1='.', node2=None, match=None, *args, **kwargs):
768 +            if isinstance(node1, context.changectx):
769 +                ctx1 = node1
770 +            else:
771 +                ctx1 = self[node1]
772 +            if isinstance(node2, context.changectx):
773 +                ctx2 = node2
774 +            else:
775 +                ctx2 = self[node2]
776 +            if not isinstance(ctx1, MqCtx) and not isinstance(ctx2, MqCtx):
777 +                return status_orig(ctx1, ctx2, match, *args, **kwargs)
778 +            # modified, added, removed, deleted, unknown
779 +            status = ([], [], [], [], [], [], [])
780 +            if match is None:
781 +                match = lambda x: x
782 +            # force patch content as MODIFY which is close to what a patch is :D
783 +            status[MODIFY][:] = [path for path in ctx2.files() if match(path)]
784 +            return status
785 +
786 +        def lookup(self, key):
787 +            if isinstance(key, MqCtx):
788 +                return key.node()
789 +            if key in repo.unapplieds:
790 +                return key
791 +            return lookup_orig(key)
792 +    # common way for hg extensions
793 +    repo.__class__ = MqRepository
diff --git a/hgviewlib/qt4/hgfiledialog.py b/hgviewlib/qt4/hgfiledialog.py
@@ -91,11 +91,11 @@
794          self.lexer = lexer
795 
796      def modelFilled(self):
797          disconnect(self.filerevmodel, SIGNAL('filled'),
798                     self.modelFilled)
799 -        if self._show_rev is not None:
800 +        if isinstance(self._show_rev, int):
801              index = self.filerevmodel.indexFromRev(self._show_rev)
802              self._show_rev = None
803          else:
804              index = self.filerevmodel.index(0,0)
805          self.tableView_revisions.setCurrentIndex(index)
diff --git a/hgviewlib/qt4/hgrepomodel.py b/hgviewlib/qt4/hgrepomodel.py
@@ -48,10 +48,12 @@
806  def cvrt_date(date):
807      """
808      Convert a date given the hg way, ie. couple (date, tz), into a
809      formatted QString
810      """
811 +    if not date:
812 +        return QtCore.QString(u'')
813      date, tzdelay = date
814      return QtCore.QDateTime.fromTime_t(int(date)).toString(QtCore.Qt.LocaleDate)
815 
816 
817  # XXX maybe it's time to make these methods of the model...
@@ -212,10 +214,12 @@
818                  return QtCore.QVariant(QtGui.QColor(self.user_color(ctx.user())))
819              if column == 'Branch': #branch
820                  return QtCore.QVariant(QtGui.QColor(self.namedbranch_color(ctx.branch())))
821          elif role == QtCore.Qt.DecorationRole:
822              if column == 'Log':
823 +                if not getattr(ctx, 'applied', True):
824 +                    return nullvariant
825                  radius = self.dot_radius
826                  w = (gnode.cols)*(1*radius + 0) + 20
827                  h = self.rowheight
828 
829                  dot_x = self.col2x(gnode.x) - radius / 2
@@ -517,18 +521,18 @@
830      def _fill(self):
831          # the generator used to fill file stats as a background process
832          for row, desc in enumerate(self._files):
833              filename = desc['path']
834              if desc['flag'] == '=' and self._displaydiff:
835 -                diff = revdiff(self.repo, self.current_ctx, desc['parent'],
836 -                               files=[filename])
837                  try:
838 +                    diff = revdiff(self.repo, self.current_ctx, None, files=[filename])
839                      tot = self.current_ctx.filectx(filename).data().count('\n')
840 -                except LookupError:
841 -                    tot = 0
842 -                add = len(replus.findall(diff))
843 -                rem = len(reminus.findall(diff))
844 +                    add = len(replus.findall(diff))
845 +                    rem = len(reminus.findall(diff))
846 +                except (LookupError, TypeError): # unknown revision and mq support
847 +                    tot, add, rem = 0, 0, 0
848 +
849                  if tot == 0:
850                      tot = max(add + rem, 1)
851                  desc['stats'] = (tot, add, rem)
852                  yield row, 1
853 
diff --git a/hgviewlib/qt4/hgrepoview.py b/hgviewlib/qt4/hgrepoview.py
@@ -363,11 +363,11 @@
854              self.diffrev = int(rev[5:])
855              self.refreshDisplay()
856              # TODO: emit a signal to recompute the diff
857              self.emit(SIGNAL('parentRevisionSelected'), self.diffrev)
858          else:
859 -            self.emit(SIGNAL('revisionSelected'), int(rev))
860 +            self.emit(SIGNAL('revisionSelected'), rev)
861 
862      def setDiffRevision(self, rev):
863          if rev != self.diffrev:
864              self.diffrev = rev
865              self.refreshDisplay()
@@ -435,11 +435,11 @@
866          buf += '<tr>'
867          if rev is None:
868              buf += "<td><b>Working Directory</b></td>\n"
869          else:
870              buf += '<td><b>Revision:</b>&nbsp;'\
871 -                   '<span class="rev_number">%d</span>:'\
872 +                   '<span class="rev_number">%s</span>:'\
873                     '<span class="rev_hash">%s</span></td>'\
874                     '\n' % (ctx.rev(), short_hex(ctx.node()))
875 
876          buf += '<td><b>Author:</b>&nbsp;'\
877                 '%s</td>'\
@@ -449,11 +449,11 @@
878          buf += "</table>\n"
879          buf += "<table width=100%>\n"
880          parents = [p for p in ctx.parents() if p]
881          for p in parents:
882              if p.rev() > -1:
883 -                short = short_hex(p.node())
884 +                short = short_hex(p.node()) if getattr(p, 'applied', True) else p.node()
885                  desc = format_desc(p.description(), self.descwidth)
886                  p_rev = p.rev()
887                  p_fmt = '<span class="rev_number">%s</span>:'\
888                          '<a href="%s" class="rev_hash">%s</a>'
889                  if p_rev == self.diffrev:
@@ -480,14 +480,14 @@
890                     '<span class="short_desc"><i>%s</i></span></td></tr>'\
891                     '\n' % (p_rev, desc)
892 
893          for p in ctx.children():
894              if p.rev() > -1:
895 -                short = short_hex(p.node())
896 +                short = short_hex(p.node()) if getattr(p, 'applied', True) else p.node()
897                  desc = format_desc(p.description(), self.descwidth)
898                  buf += '<tr><td class="label"><b>Child:</b></td>'\
899 -                       '<td colspan=5><span class="rev_number">%d</span>:'\
900 +                       '<td colspan=5><span class="rev_number">%s</span>:'\
901                         '<a href="%s" class="rev_hash">%s</a>&nbsp;'\
902                         '<span class="short_desc"><i>%s</i></span></td></tr>'\
903                         '\n' % (p.rev(), p.rev(), short, desc)
904 
905          buf += "</table>\n"
@@ -531,11 +531,11 @@
906      view = HgRepoView(w)
907      view.setModel(model)
908      view.setWindowTitle("Simple Hg List Model")
909 
910      disp = RevDisplay(w)
911 -    connect(view, SIGNAL('revisionSelected'), lambda rev: disp.displayRevision(repo.changectx(rev)))
912 +    connect(view, SIGNAL('revisionSelected'), lambda rev: disp.displayRevision(repo.changectx(int(rev) if rev.isdigit() else rev)))
913      connect(disp, SIGNAL('revisionSelected'), view.goto)
914      #connect(view, SIGNAL('revisionActivated'), rev_act)
915 
916      l.addWidget(view, 2)
917      l.addWidget(disp)