Commits

Yuya Nishihara committed a9effc2

implementing test utilities

Comments (0)

Files changed (19)

dirtyrepocacheclear.diff

+# HG changeset patch
+# Parent 30f3760dc4eff3b31a3dad5ddabac41cd72f9d04
+diff --git a/tortoisehg/hgqt/workbench.py b/tortoisehg/hgqt/workbench.py
+--- a/tortoisehg/hgqt/workbench.py
++++ b/tortoisehg/hgqt/workbench.py
+@@ -635,6 +635,7 @@ class Workbench(QMainWindow):
+             w = tw.widget(index)
+             try:
+                 reporoot = w.repo.root
++                del thgrepo._repocache[reporoot]
+             except:
+                 reporoot = ''
+             if w and w.closeRepoWidget():
+# HG changeset patch
+# Parent 6d79eb06ae0d824077ce8938529fa2d66dbb4313
+diff --git a/tortoisehg/hgqt/run.py b/tortoisehg/hgqt/run.py
+--- a/tortoisehg/hgqt/run.py
++++ b/tortoisehg/hgqt/run.py
+@@ -534,6 +534,9 @@ class _QtRunner(QObject):
+             self._installtranslator()
+             qtlib.setup_font_substitutions()
+             qtlib.fix_application_font()
++            f = QFont('Helvetica')
++            f.setPixelSize(12)
++            self._mainapp.setFont(f)
+             qtlib.configstyles(ui)
+             qtlib.initfontcache(ui)
+             self._mainapp.setWindowIcon(qtlib.geticon('thg-logo'))
+# HG changeset patch
+# Parent 1df0f23af4d2c230ac9f1cbac8f8d5e07bec3150
+diff --git a/tortoisehg/hgqt/run.py b/tortoisehg/hgqt/run.py
+--- a/tortoisehg/hgqt/run.py
++++ b/tortoisehg/hgqt/run.py
+@@ -306,6 +306,10 @@ def runcommand(ui, args):
+     if options['quiet']:
+         ui.quiet = True
+ 
++    # best to set style before the QApplication constructor
++    if options['guistyle']:
++        QApplication.setStyle(options['guistyle'])
++
+     if cmd not in nonrepo_commands.split() and not path:
+         raise error.RepoError(_("There is no Mercurial repository here"
+                                 " (.hg not found)"))
+@@ -1040,6 +1044,7 @@ globalopts = [
+     ('', 'fork', None, _('always fork GUI process')),
+     ('', 'listfile', '', _('read file list from file')),
+     ('', 'listfileutf8', '', _('read file list from file encoding utf-8')),
++    ('', 'guistyle', '', _('set application GUI style'))
+ ]
+ 
+ table = {

less-except-expodiff.diff

+# HG changeset patch
+# Parent 2628b874d234ba279dd6cf430bd034271d3aad7e
+diff --git a/tortoisehg/hgqt/repowidget.py b/tortoisehg/hgqt/repowidget.py
+--- a/tortoisehg/hgqt/repowidget.py
++++ b/tortoisehg/hgqt/repowidget.py
+@@ -1347,7 +1347,7 @@ class RepoWidget(QWidget):
+                     f.write(diff)
+                 finally:
+                     f.close()
+-            except Exception, e:
++            except (IOError, OSError):
+                 WarningMsgBox(_('Repository Error'),
+                               _('Unable to write diff file'))
+         def exportDagRange():

lockstillheld.diff

-# HG changeset patch
-# Parent db9c2a41db426b0e708a8cbc7a36de51475d61bd
-thgrepo: exit from 'lock still held' case by exception
-
-diff --git a/tortoisehg/hgqt/thgrepo.py b/tortoisehg/hgqt/thgrepo.py
---- a/tortoisehg/hgqt/thgrepo.py
-+++ b/tortoisehg/hgqt/thgrepo.py
-@@ -63,6 +63,9 @@ def repository(_ui=None, path='', create
-         raise error.RepoError('%s is not a valid repository' % path)
-     return _repocache[path]
- 
-+class _LockStillHeld(Exception):
-+    'Raised to abort status check due to lock existence'
-+
- class ThgRepoWrapper(QObject):
- 
-     configChanged = pyqtSignal()
-@@ -139,11 +142,14 @@ class ThgRepoWrapper(QObject):
-         if self.locked():
-             dbgoutput('locked, aborting')
-             return
--        if self._checkdirstate():
--            dbgoutput('dirstate changed, exiting')
--            return
--        self._checkrepotime()
--        self._checkuimtime()
-+        try:
-+            if self._checkdirstate():
-+                dbgoutput('dirstate changed, exiting')
-+                return
-+            self._checkrepotime()
-+            self._checkuimtime()
-+        except _LockStillHeld:
-+            dbgoutput('lock still held - ignoring for now')
- 
-     def locked(self):
-         if os.path.lexists(self.repo.join('wlock')):
-@@ -198,8 +204,7 @@ class ThgRepoWrapper(QObject):
-         if self._repomtime < self._getrepomtime():
-             dbgoutput('detected repository change')
-             if self.locked():
--                dbgoutput('lock still held - ignoring for now')
--                return
-+                raise _LockStillHeld
-             self.recordState()
-             self.repo.thginvalidate()
-             self.repositoryChanged.emit()
-@@ -222,8 +227,7 @@ class ThgRepoWrapper(QObject):
-         if nodes != self._parentnodes:
-             dbgoutput('dirstate change found')
-             if self.locked():
--                dbgoutput('lock still held - ignoring for now')
--                return True
-+                raise _LockStillHeld
-             self.recordState()
-             self.repo.thginvalidate()
-             self.repositoryChanged.emit()
-@@ -248,8 +252,7 @@ class ThgRepoWrapper(QObject):
-         if newbranch != self._rawbranch:
-             dbgoutput('branch time change')
-             if self.locked():
--                dbgoutput('lock still held - ignoring for now')
--                return True
-+                raise _LockStillHeld
-             self._rawbranch = newbranch
-             self.repo.thginvalidate()
-             self.workingBranchChanged.emit()

lookuperror.diff

-# HG changeset patch
-# Parent 9ad1fb0da376ab68cac5c36047f9e808e0275336
-run: catch LookupError to show instruction message box
-
-I saw LookupError when qpush/pop/refreshing heavily.
-
-
-diff --git a/tortoisehg/hgqt/run.py b/tortoisehg/hgqt/run.py
---- a/tortoisehg/hgqt/run.py
-+++ b/tortoisehg/hgqt/run.py
-@@ -356,6 +356,7 @@ class _QtRunner(object):
-     # It doesn't check the hierarchy of exception classes for simplicity.
-     _recoverableexc = {
-         error.RepoLookupError: _('Try refreshing your repository.'),
-+        error.LookupError: _('Try refreshing your repository.'),
-         }
- 
-     def __init__(self):

mtimeatlast.diff

-# HG changeset patch
-# Parent 837ce92001c2f0fe0187633659bf28ee10fae7fb
-thgrepo: update timestamp only if changes detected successfully (fixes #1728)
-
-This fixes the following case:
-
- 1. hg: change dirstate
- 2. hg: unlock
- 3. thgrepo: detect change of .hg directory
- 4. hg: another lock
- 5. thgrepo: update _dirstatemtime
- 6. thgrepo: exit by lock still held
- 7. hg: unlock
- 8. thgrepo: detect change of .hg directory
- 9. thgrepo: ignore change because _dirstatemtime is already up-to-date
-
-diff --git a/tortoisehg/hgqt/thgrepo.py b/tortoisehg/hgqt/thgrepo.py
---- a/tortoisehg/hgqt/thgrepo.py
-+++ b/tortoisehg/hgqt/thgrepo.py
-@@ -219,8 +219,9 @@ class ThgRepoWrapper(QObject):
-             return False
-         if mtime <= self._dirstatemtime:
-             return False
-+        changed = self._checkparentchanges() or self._checkbranch()
-         self._dirstatemtime = mtime
--        return self._checkparentchanges() or self._checkbranch()
-+        return changed
- 
-     def _checkparentchanges(self):
-         nodes = self._getrawparents()
-@@ -241,8 +242,9 @@ class ThgRepoWrapper(QObject):
-             return False
-         if mtime <= self._branchmtime:
-             return False
-+        changed = self._checkbranchcontent()
-         self._branchmtime = mtime
--        return self._checkbranchcontent()
-+        return changed
- 
-     def _checkbranchcontent(self):
-         try:
+# HG changeset patch
+# Parent f19fa7275815f0a8f26ac2dd6d1fb44c73066c2f
+diff --git a/tortoisehg/hgqt/run.py b/tortoisehg/hgqt/run.py
+--- a/tortoisehg/hgqt/run.py
++++ b/tortoisehg/hgqt/run.py
+@@ -389,6 +389,8 @@ class GarbageCollector(QObject):
+         #gc.set_debug(gc.DEBUG_SAVEALL)
+ 
+     def check(self):
++        from tortoisehg.hgqt import thgrepo
++        print thgrepo._repocache.keys()
+         #return self.debug_cycles()
+         l0, l1, l2 = gc.get_count()
+         if self.debug:

repoviewlookuperror.diff

+# HG changeset patch
+# Parent 909c9cf02673f72588d4efef0ccf900445df0137
+diff --git a/tortoisehg/hgqt/repoview.py b/tortoisehg/hgqt/repoview.py
+--- a/tortoisehg/hgqt/repoview.py
++++ b/tortoisehg/hgqt/repoview.py
+@@ -275,7 +275,7 @@ class HgRepoView(QTableView):
+         except error.RepoError:
+             self.showMessage.emit(_("Can't find revision '%s'")
+                                   % hglib.tounicode(str(rev)))
+-        except LookupError, e:
++        except error.LookupError, e:
+             self.showMessage.emit(hglib.tounicode(str(e)))
+         else:
+             idx = self.model().indexFromRev(rev)
+# HG changeset patch
+# Parent 0b54af210d9ea2b0c3700c7a4a5bcb1729d78df2
+tests: add a little script to run nosetests with the specified HGPATH
+
+diff --git a/tests/run-tests.py b/tests/run-tests.py
+new file mode 100755
+--- /dev/null
++++ b/tests/run-tests.py
+@@ -0,0 +1,13 @@
++#!/usr/bin/env python
++import os, sys
++import nose
++
++HGPATH = os.environ.get('HGPATH')
++if HGPATH and os.path.isdir(os.path.join(HGPATH, 'mercurial')):
++    sys.path.insert(1, HGPATH)
++
++THGPATH = os.path.join(os.path.dirname(__file__), '..')
++sys.path.insert(1, THGPATH)
++
++if __name__ == '__main__':
++    nose.main()
+run-tests.diff
+tests-tmpdir.diff
+tests-withencoding.diff
+tests-nofixture.diff
+tests-nomodule.diff
+try-guitest.diff
+sync-savedialog-notedit.diff
+similar.eml
+less-except-expodiff.diff
+guistyle.diff
+forcefont.diff
+repoviewlookuperror.diff
+repocache-trace.diff
+dirtyrepocacheclear.diff
 dbgwatcher.diff
 dbgdirstatedelay.diff
-splitfun.diff
-lockstillheld.diff
-mtimeatlast.diff
 historyexp.diff
 qscieol.mbox
 trynewwindow.diff
 cmdui-objtree-deletable.diff
 annot-by-manifest.diff
 mani-filetree-visibility.diff
-lookuperror.diff
 error-mutiple.diff
 revdesc-template.diff
 qtui-nothread.diff
+Delivered-To: youjah@gmail.com
+Received: by 10.64.33.168 with SMTP id s8csp3285iei;
+        Sat, 28 Apr 2012 04:18:33 -0700 (PDT)
+Return-Path: <thg-dev+bncCJv02ajiEhCIpO_8BBoEmf5LbQ@googlegroups.com>
+Received-SPF: pass (google.com: domain of thg-dev+bncCJv02ajiEhCIpO_8BBoEmf5LbQ@googlegroups.com designates 10.216.134.7 as permitted sender) client-ip=10.216.134.7;
+Authentication-Results: mr.google.com; spf=pass (google.com: domain of thg-dev+bncCJv02ajiEhCIpO_8BBoEmf5LbQ@googlegroups.com designates 10.216.134.7 as permitted sender) smtp.mail=thg-dev+bncCJv02ajiEhCIpO_8BBoEmf5LbQ@googlegroups.com; dkim=pass header.i=thg-dev+bncCJv02ajiEhCIpO_8BBoEmf5LbQ@googlegroups.com
+Received: from mr.google.com ([10.216.134.7])
+        by 10.216.134.7 with SMTP id r7mr1221475wei.58.1335611912668 (num_hops = 1);
+        Sat, 28 Apr 2012 04:18:32 -0700 (PDT)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+        d=googlegroups.com; s=beta;
+        h=x-beenthere:received-spf:mime-version:subject:x-mercurial-node
+         :message-id:user-agent:date:from:to:x-original-sender
+         :x-original-authentication-results:reply-to:precedence:mailing-list
+         :list-id:x-google-group-id:list-post:list-help:list-archive:sender
+         :list-subscribe:list-unsubscribe:content-type
+         :content-transfer-encoding;
+        bh=dTiYesQvGPgVThyczB3gHziZllwhRyqeXhFaGKA5OCw=;
+        b=Mp+W6RMpv6o+hM9o+R+4XutBWlp+icIWlOAzfVTohYPdArD4as5v8C1FhDRfAfIEUd
+         mQxzxjc+WVuVCE3dPBsX/ZPb7HO5A+SyV5gL73EZJCJew8osFX9ONmYV4FFzpBqxHFlB
+         qiMzOTbJrgPsQN18upWJUsB1bWQxXRjf71Kz8=
+Received: by 10.216.134.7 with SMTP id r7mr105491wei.58.1335611912565;
+        Sat, 28 Apr 2012 04:18:32 -0700 (PDT)
+X-BeenThere: thg-dev@googlegroups.com
+Received: by 10.180.73.74 with SMTP id j10ls1238823wiv.0.gmail; Sat, 28 Apr
+ 2012 04:18:31 -0700 (PDT)
+Received: by 10.180.87.165 with SMTP id az5mr797538wib.4.1335611911273;
+        Sat, 28 Apr 2012 04:18:31 -0700 (PDT)
+Received: by 10.180.87.165 with SMTP id az5mr797537wib.4.1335611911251;
+        Sat, 28 Apr 2012 04:18:31 -0700 (PDT)
+Received: from mail-wi0-f170.google.com (mail-wi0-f170.google.com [209.85.212.170])
+        by gmr-mx.google.com with ESMTPS id fm5si2004212wib.2.2012.04.28.04.18.31
+        (version=TLSv1/SSLv3 cipher=OTHER);
+        Sat, 28 Apr 2012 04:18:31 -0700 (PDT)
+Received-SPF: pass (google.com: domain of angel.ezquerra@gmail.com designates 209.85.212.170 as permitted sender) client-ip=209.85.212.170;
+Received: by wibhr17 with SMTP id hr17so1198351wib.1
+        for <thg-dev@googlegroups.com>; Sat, 28 Apr 2012 04:18:31 -0700 (PDT)
+Received: by 10.216.135.206 with SMTP id u56mr9075076wei.29.1335611911027;
+        Sat, 28 Apr 2012 04:18:31 -0700 (PDT)
+Received: from [192.168.1.34] (61.Red-79-152-149.dynamicIP.rima-tde.net. [79.152.149.61])
+        by mx.google.com with ESMTPS id ff9sm11822684wib.2.2012.04.28.04.18.27
+        (version=TLSv1/SSLv3 cipher=OTHER);
+        Sat, 28 Apr 2012 04:18:28 -0700 (PDT)
+MIME-Version: 1.0
+Subject: [thg-dev] [PATCH REBASED] repowidget: add "Similar revisions..."
+ command to revision context menu
+X-Mercurial-Node: 61d9a6d0acb72d133bf5ac0140148b07014c8170
+Message-Id: <61d9a6d0acb72d133bf5.1335611907@Lucia-PC>
+User-Agent: Mercurial-patchbomb/2.2-rc+18-cbf2ea2f5ca1
+Date: Sat, 28 Apr 2012 13:18:27 +0200
+From: Angel Ezquerra <angel.ezquerra@gmail.com>
+To: thg-dev@googlegroups.com
+X-Original-Sender: angel.ezquerra@gmail.com
+X-Original-Authentication-Results: gmr-mx.google.com; spf=pass (google.com:
+ domain of angel.ezquerra@gmail.com designates 209.85.212.170 as permitted
+ sender) smtp.mail=angel.ezquerra@gmail.com; dkim=pass header.i=@gmail.com
+Reply-To: thg-dev@googlegroups.com
+Precedence: list
+Mailing-list: list thg-dev@googlegroups.com; contact thg-dev+owners@googlegroups.com
+List-ID: <thg-dev.googlegroups.com>
+X-Google-Group-Id: 695465234280
+List-Post: <http://groups.google.com/group/thg-dev/post?hl=en_US>, <mailto:thg-dev@googlegroups.com>
+List-Help: <http://groups.google.com/support/?hl=en_US>, <mailto:thg-dev+help@googlegroups.com>
+List-Archive: <http://groups.google.com/group/thg-dev?hl=en_US>
+Sender: thg-dev@googlegroups.com
+List-Subscribe: <http://groups.google.com/group/thg-dev/subscribe?hl=en_US>, <mailto:thg-dev+subscribe@googlegroups.com>
+List-Unsubscribe: <http://groups.google.com/group/thg-dev/subscribe?hl=en_US>, <mailto:googlegroups-manage+695465234280+unsubscribe@googlegroups.com>
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+
+# HG changeset patch
+# User Angel Ezquerra <angel.ezquerra@gmail.com>
+# Date 1334267637 -7200
+# Branch stable
+# Node ID 61d9a6d0acb72d133bf5ac0140148b07014c8170
+# Parent 8fa00f5368e20c057c362841eaa5a9739afb68bb
+repowidget: add "Similar revisions..." command to revision context menu
+
+This command lets you find revisions that are similar to (i.e. that match some
+of the fields of) a given revision.
+
+This requires a recent (above 2.2) mercurial version.
+
+diff --git a/tortoisehg/hgqt/matching.py b/tortoisehg/hgqt/matching.py
+new file mode 100644
+--- /dev/null
++++ b/tortoisehg/hgqt/matching.py
+@@ -0,0 +1,254 @@
++# matching.py - Find similar (matching) revisions dialog for TortoiseHg
++#
++# Copyright 2012 Angel Ezquerra <angel.ezquerra@gmail.com>
++#
++# This software may be used and distributed according to the terms of the
++# GNU General Public License version 2, incorporated herein by reference.
++
++from mercurial import error, revset
++
++from tortoisehg.util import hglib, paths
++from tortoisehg.hgqt.i18n import _
++from tortoisehg.hgqt import cmdui, csinfo, qtlib, thgrepo, resolve
++
++from PyQt4.QtCore import *
++from PyQt4.QtGui import *
++
++class MatchDialog(QDialog):
++
++    revmatch = pyqtSignal(QString)
++
++    def __init__(self, repo, rev=None, parent=None, opts={}):
++        super(MatchDialog, self).__init__(parent)
++        self.setWindowFlags(self.windowFlags() & \
++                            ~Qt.WindowContextHelpButtonHint)
++
++        self.revsetexpression = ''
++        self._finished = False
++        self.repo = repo
++
++        # base layout box
++        box = QVBoxLayout()
++        box.setSpacing(6)
++
++        ## main layout grid
++        self.grid = QGridLayout()
++        self.grid.setSpacing(6)
++        box.addLayout(self.grid)
++
++        ### matched revision combo
++        self.rev_combo = combo = QComboBox()
++        combo.setEditable(True)
++        self.grid.addWidget(QLabel(_('Find revisions matching fields of:')), 0, 0)
++        self.grid.addWidget(combo, 0, 1)
++
++        # Give the combo box a minimum width that will ensure that the dialog is
++        # large enough to fit the additional progress bar that will appear when
++        # updating subrepositories.
++        combo.setMinimumWidth(450)
++
++        if rev is None:
++            rev = self.repo.dirstate.branch()
++        else:
++            rev = str(rev)
++        combo.addItem(hglib.tounicode(rev))
++        combo.setCurrentIndex(0)
++        # make it easy to match the workding directory parent revision
++        combo.addItem(hglib.tounicode('.'))
++
++        tags = list(self.repo.tags()) + repo._bookmarks.keys()
++        tags.sort(reverse=True)
++        for tag in tags:
++            combo.addItem(hglib.tounicode(tag))
++
++        ### matched revision info
++        self.rev_to_match_info_text = QLabel()
++        self.rev_to_match_info_text.setShown(False)
++        style = csinfo.panelstyle(contents=('cset', 'branch', 'close', 'user',
++               'dateage', 'parents', 'children', 'tags', 'transplant',
++               'p4', 'svn', 'converted'), selectable=True,
++               expandable=True)
++        factory = csinfo.factory(self.repo, style=style)
++        self.rev_to_match_info = factory()
++        self.rev_to_match_info_lbl = QLabel(_('Revision to Match:'))
++        self.grid.addWidget(self.rev_to_match_info_lbl, 1, 0, Qt.AlignLeft | Qt.AlignTop)
++        self.grid.addWidget(self.rev_to_match_info, 1, 1)
++        self.grid.addWidget(self.rev_to_match_info_text, 1, 1)
++
++        ### fields that will be matched
++        self.optbox = QVBoxLayout()
++        self.optbox.setSpacing(6)
++        expander = qtlib.ExpanderLabel(_('Fields to match:'), False)
++        expander.expanded.connect(self.show_options)
++        row = self.grid.rowCount()
++        self.grid.addWidget(expander, row, 0, Qt.AlignLeft | Qt.AlignTop)
++        self.grid.addLayout(self.optbox, row, 1)
++
++        self.summary_chk = QCheckBox(_('Summary (first description line)'))
++        self.description_chk = QCheckBox(_('Description'))
++        self.desc_btngroup = QButtonGroup()
++        self.desc_btngroup.setExclusive(False)
++        self.desc_btngroup.addButton(self.summary_chk)
++        self.desc_btngroup.addButton(self.description_chk)
++        self.desc_btngroup.buttonClicked.connect(self._selectSummaryOrDescription)
++
++        self.author_chk = QCheckBox(_('Author'))
++        self.date_chk = QCheckBox(_('Date'))
++        self.files_chk = QCheckBox(_('Files'))
++        self.substate_chk = QCheckBox(_('Subrepo states'))
++        self.branch_chk = QCheckBox(_('Branch'))
++        self.parents_chk = QCheckBox(_('Parents'))
++        self.phase_chk = QCheckBox(_('Phase'))
++        self._hideable_chks = (self.branch_chk, self.phase_chk, self.parents_chk,)
++
++        self.optbox.addWidget(self.summary_chk)
++        self.optbox.addWidget(self.description_chk)
++        self.optbox.addWidget(self.author_chk)
++        self.optbox.addWidget(self.date_chk)
++        self.optbox.addWidget(self.files_chk)
++        self.optbox.addWidget(self.substate_chk)
++        self.optbox.addWidget(self.branch_chk)
++        self.optbox.addWidget(self.parents_chk)
++        self.optbox.addWidget(self.phase_chk)
++
++        s = QSettings()
++
++        #### Persisted Options
++        self.summary_chk.setChecked(s.value('matching/summary', False).toBool())
++        self.description_chk.setChecked(s.value('matching/description', True).toBool())
++        self.author_chk.setChecked(s.value('matching/author', True).toBool())
++        self.branch_chk.setChecked(s.value('matching/branch', False).toBool())
++        self.date_chk.setChecked(s.value('matching/date', True).toBool())
++        self.files_chk.setChecked(s.value('matching/files', False).toBool())
++        self.parents_chk.setChecked(s.value('matching/parents', False).toBool())
++        self.phase_chk.setChecked(s.value('matching/phase', False).toBool())
++        self.substate_chk.setChecked(s.value('matching/substate', False).toBool())
++
++        ## bottom buttons
++        buttons = QDialogButtonBox()
++        self.close_btn = buttons.addButton(QDialogButtonBox.Close)
++        self.close_btn.clicked.connect(self.reject)
++        self.close_btn.setAutoDefault(False)
++        self.match_btn = buttons.addButton(_('&Match'),
++                                            QDialogButtonBox.ActionRole)
++        self.match_btn.clicked.connect(self.match)
++        box.addWidget(buttons)
++
++        # signal handlers
++        self.rev_combo.editTextChanged.connect(self.update_info)
++
++        # dialog setting
++        self.setLayout(box)
++        self.layout().setSizeConstraint(QLayout.SetFixedSize)
++        self.setWindowTitle(_('Find matches - %s') % self.repo.displayname)
++        self.setWindowIcon(qtlib.geticon('hg-update'))
++
++        # prepare to show
++        self.update_info()
++        if not self.match_btn.isEnabled():
++            self.rev_combo.lineEdit().selectAll()  # need to change rev
++
++        # expand options if a hidden one is checked
++        hiddenOptionsChecked = self.hiddenSettingIsChecked()
++        self.show_options(hiddenOptionsChecked)
++        expander.set_expanded(hiddenOptionsChecked)
++
++    ### Private Methods ###
++    def hiddenSettingIsChecked(self):
++        for chk in self._hideable_chks:
++            if chk.isChecked():
++                return True
++        return False
++
++    def saveSettings(self):
++        s = QSettings()
++        s.setValue('matching/summary', self.summary_chk.isChecked())
++        s.setValue('matching/description', self.description_chk.isChecked())
++        s.setValue('matching/author', self.author_chk.isChecked())
++        s.setValue('matching/branch', self.branch_chk.isChecked())
++        s.setValue('matching/date', self.date_chk.isChecked())
++        s.setValue('matching/files', self.files_chk.isChecked())
++        s.setValue('matching/parents', self.parents_chk.isChecked())
++        s.setValue('matching/phase', self.phase_chk.isChecked())
++        s.setValue('matching/substate', self.substate_chk.isChecked())
++
++    def update_info(self, *args):
++        def set_csinfo_mode(mode):
++            """Show the csinfo widget or the info text label"""
++            # hide first, then show
++            if mode:
++                self.rev_to_match_info_text.setShown(False)
++                self.rev_to_match_info.setShown(True)
++            else:
++                self.rev_to_match_info.setShown(False)
++                self.rev_to_match_info_text.setShown(True)
++        def csinfo_update(ctx):
++            self.rev_to_match_info.update(ctx)
++            set_csinfo_mode(True)
++        def csinfo_set_text(text):
++            self.rev_to_match_info_text.setText(text)
++            set_csinfo_mode(False)
++
++        self.rev_to_match_info_lbl.setText(_('Revision to Match:'))
++        new_rev = hglib.fromunicode(self.rev_combo.currentText())
++        if new_rev.lower() == 'null':
++            self.match_btn.setEnabled(True)
++            return
++        try:
++            csinfo_update(self.repo[new_rev])
++            return
++        except (error.LookupError, error.RepoLookupError, error.RepoError):
++            pass
++        # If we get this far, assume we are matching a revision set
++        try:
++            func = revset.match(self.repo.ui, new_rev)
++            rset = [c for c in func(self.repo, range(len(self.repo)))]
++            if len(rset) > 1:
++                self.rev_to_match_info_lbl.setText(_('Revisions to Match:'))
++                csinfo_set_text(_('Match any of <b><i>%d</i></b> revisions') \
++                    % len(rset))
++            else:
++                self.rev_to_match_info_lbl.setText(_('Revision to Match:'))
++                csinfo_update(rset[0])
++            self.match_btn.setEnabled(True)
++        except (error.LookupError, error.RepoLookupError):
++            csinfo_set_text(_('<b>Unknown revision!</b>'))
++            self.match_btn.setDisabled(True)
++
++    def match(self):
++        self.saveSettings()
++        fieldmap = {
++            'summary': self.summary_chk,
++            'description': self.description_chk,
++            'author': self.author_chk,
++            'branch': self.branch_chk,
++            'date': self.date_chk,
++            'files': self.files_chk,
++            'parents': self.parents_chk,
++            'phase': self.phase_chk,
++            'substate': self.substate_chk,
++        }
++        fields = []
++        for (field, chk) in fieldmap.items():
++            if chk.isChecked():
++                fields.append(field)
++
++        rev = hglib.fromunicode(self.rev_combo.currentText())
++        if fields:
++            self.revsetexpression = "matching(%s, '%s')" % (rev, ' '.join(fields))
++        else:
++            self.revsetexpression = "matching(%s)" % (rev)
++        self.accept()
++
++    ### Signal Handlers ###
++
++    def show_options(self, visible):
++        for chk in self._hideable_chks:
++            chk.setShown(visible)
++
++    @pyqtSlot(object)
++    def _selectSummaryOrDescription(self, btn):
++        # Uncheck all other buttons
++        for b in self.desc_btngroup.buttons():
++            if not (b is btn):
++                b.setChecked(False)
+diff --git a/tortoisehg/hgqt/repowidget.py b/tortoisehg/hgqt/repowidget.py
+--- a/tortoisehg/hgqt/repowidget.py
++++ b/tortoisehg/hgqt/repowidget.py
+@@ -25,6 +25,7 @@ from tortoisehg.hgqt import cmdui, updat
+ from tortoisehg.hgqt import archive, thgimport, thgstrip, run, purge, bookmark
+ from tortoisehg.hgqt import bisect, rebase, resolve, thgrepo, compress, mq
+ from tortoisehg.hgqt import qdelete, qreorder, qfold, qrename, shelve
++from tortoisehg.hgqt import matching
+ 
+ from tortoisehg.hgqt.repofilter import RepoFilterBar
+ from tortoisehg.hgqt.repoview import HgRepoView
+@@ -426,6 +427,12 @@ class RepoWidget(QWidget):
+             self.showIcon.emit(QIcon())
+ 
+     @pyqtSlot(QString, QString)
++    def setFilter(self, filter):
++        self.filterbar.revsetle.setText(filter)
++        self.filterbar.setVisible(True)
++        self.filterbar.returnPressed()
++
++    @pyqtSlot(QString, QString)
+     def setBundle(self, bfile, bsource=None):
+         if self.bundle:
+             self.clearBundle()
+@@ -1217,6 +1224,8 @@ class RepoWidget(QWidget):
+               self.visualDiffToLocal)
+         entry(menu, None, isctx, _('Browse at rev...'), 'hg-annotate',
+               self.manifestRevision)
++        entry(menu, None, isrev, _('Similar revisions...'), 'view-filter',
++              self.matchRevision)
+         entry(menu)
+         entry(menu, None, fixed, _('Merge with local...'), 'hg-merge',
+               self.mergeWithRevision)
+@@ -1669,6 +1678,24 @@ class RepoWidget(QWidget):
+         dlg.finished.connect(dlg.deleteLater)
+         dlg.exec_()
+ 
++    def matchRevision(self):
++        hasmatching = True
++        try:
++            # hg >= 2.2
++            from mercurial.revset import matching as matchingkeyword
++        except:
++            hasmatching = False
++        if hasmatching:
++            dlg = matching.MatchDialog(self.repo, self.rev, self)
++            if dlg.exec_():
++                self.setFilter(dlg.revsetexpression)
++        else:
++            # We cannot find similar revisions
++            # without the matching revset keyword
++            qtlib.WarningMsgBox(_('Incorrect Mercurial version'),
++                _('In order to use the "Find Similar revisions" '
++                'functionality, you must use a mercurial version above 2.1.'))
++
+     def pushAll(self):
+         self.syncDemand.forward('push', True)
+ 

splitfun.diff

-# HG changeset patch
-# Parent b2f8d7a55b5a2384f3167b40a2ed301546b71467
-thgrepo: split _checkdirstate() in terms of mtime condition
-
-It will be rearranged to skip mtime updates if lock still held.
-
-diff --git a/tortoisehg/hgqt/thgrepo.py b/tortoisehg/hgqt/thgrepo.py
---- a/tortoisehg/hgqt/thgrepo.py
-+++ b/tortoisehg/hgqt/thgrepo.py
-@@ -215,6 +215,9 @@ class ThgRepoWrapper(QObject):
-         if mtime <= self._dirstatemtime:
-             return False
-         self._dirstatemtime = mtime
-+        return self._checkparentchanges() or self._checkbranch()
-+
-+    def _checkparentchanges(self):
-         nodes = self._getrawparents()
-         if nodes != self._parentnodes:
-             dbgoutput('dirstate change found')
-@@ -225,6 +228,9 @@ class ThgRepoWrapper(QObject):
-             self.repo.thginvalidate()
-             self.repositoryChanged.emit()
-             return True
-+        return False
-+
-+    def _checkbranch(self):
-         try:
-             mtime = os.path.getmtime(self.repo.join('branch'))
-         except EnvironmentError:
-@@ -232,6 +238,9 @@ class ThgRepoWrapper(QObject):
-         if mtime <= self._branchmtime:
-             return False
-         self._branchmtime = mtime
-+        return self._checkbranchcontent()
-+
-+    def _checkbranchcontent(self):
-         try:
-             newbranch = self.repo.opener('branch').read()
-         except EnvironmentError:

sync-savedialog-notedit.diff

+# HG changeset patch
+# Parent 8c8bf25eeef78fafbd09a45b7bf7974066851836
+sync: disable 'remove auth' check on edit path dialog (fixes #1866)
+
+It's useful only if the URL is immutable.
+
+diff --git a/tortoisehg/hgqt/sync.py b/tortoisehg/hgqt/sync.py
+--- a/tortoisehg/hgqt/sync.py
++++ b/tortoisehg/hgqt/sync.py
+@@ -1230,7 +1230,7 @@ class SaveDialog(QDialog):
+             self.layout().addRow(_('URL'), self.urllabel)
+ 
+         user, host, port, folder, passwd, scheme = parseurl(origurl)
+-        if (user or passwd) and scheme in ('http', 'https'):
++        if not edit and (user or passwd) and scheme in ('http', 'https'):
+             cleanurl = util.removeauth(origurl)
+             def showurl(showclean):
+                 newurl = showclean and cleanurl or safeurl
+# HG changeset patch
+# Parent 05047be8fdd0b00fd50dbfc0ec451d37f401537c
+
+diff --git a/tests/__init__.py b/tests/__init__.py
+--- a/tests/__init__.py
++++ b/tests/__init__.py
+@@ -4,27 +4,4 @@ from mercurial import ui, commands, erro
+ from tortoisehg.util import hglib
+ from tortoisehg.hgqt import thgrepo
+ 
+-import helpers
+-
+ FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixtures')
+-
+-def setup():
+-    global _reposdir
+-    _reposdir = helpers.mktmpdir('repos')
+-
+-
+-def create_fixture_repo(name, dirname=None):
+-    """Create the fixture repo and return thgrepo object"""
+-    path = os.path.join(_reposdir, dirname or name)
+-    repo = thgrepo.repository(ui.ui(), path, create=True)
+-    commands.import_(repo.ui, repo, os.path.join(FIXTURES_DIR, name + '.diff'),
+-                     base='', strip=1, exact=True)
+-    return repo
+-
+-def get_fixture_repo(name):
+-    """Return the thgrepo object for the specified fixture repo"""
+-    path = os.path.join(_reposdir, name)
+-    try:
+-        return thgrepo.repository(ui.ui(), path)
+-    except error.RepoError:
+-        return create_fixture_repo(name)
+diff --git a/tests/fixtures/euc-jp-path.diff b/tests/fixtures/euc-jp-path.diff
+deleted file mode 100644
+--- a/tests/fixtures/euc-jp-path.diff
++++ /dev/null
+@@ -1,9 +0,0 @@
+-# HG changeset patch
+-# User test
+-# Date 0 0
+-# Node ID b124d3c17d0d93e83d26c00d7a176c02c42d73d1
+-# Parent  0000000000000000000000000000000000000000
+-add aloha
+-
+-diff --git a/����ϡ� b/����ϡ�
+-new file mode 100644
+diff --git a/tests/fixtures/subdirs.diff b/tests/fixtures/subdirs.diff
+deleted file mode 100644
+--- a/tests/fixtures/subdirs.diff
++++ /dev/null
+@@ -1,31 +0,0 @@
+-# HG changeset patch
+-# User test
+-# Date 0 0
+-# Node ID 5b65a3d842610f25b192d26e3dc9161ff53208d0
+-# Parent  0000000000000000000000000000000000000000
+-foobar
+-
+-diff --git a/bar b/bar
+-new file mode 100644
+-diff --git a/baz/bax b/baz/bax
+-new file mode 100644
+-diff --git a/baz/box b/baz/box
+-new file mode 100644
+-diff --git a/foo b/foo
+-new file mode 100644
+-# HG changeset patch
+-# User test
+-# Date 0 0
+-# Node ID 8651fa2fe75d6fdaec4a477fb29a88f7dc97a7e2
+-# Parent  5b65a3d842610f25b192d26e3dc9161ff53208d0
+-remove baz/box, add zzz, modify bar
+-
+-diff --git a/bar b/bar
+---- a/bar
+-+++ b/bar
+-@@ -0,0 +1,1 @@
+-+hello
+-diff --git a/baz/box b/baz/box
+-deleted file mode 100644
+-diff --git a/zzz b/zzz
+-new file mode 100644
+diff --git a/tests/qt_manifestmodel_test.py b/tests/qt_manifestmodel_test.py
+--- a/tests/qt_manifestmodel_test.py
++++ b/tests/qt_manifestmodel_test.py
+@@ -1,8 +1,10 @@
++import os
+ from nose.tools import *
+ from PyQt4.QtCore import QModelIndex, QString
+ from PyQt4.QtGui import QApplication
++from mercurial import commands
++from tortoisehg.hgqt import thgrepo
+ from tortoisehg.hgqt.manifestmodel import ManifestModel
+-from tests import get_fixture_repo
+ 
+ import helpers
+ 
+@@ -12,8 +14,30 @@ def setup():
+     global _app, _repos
+     _app = QApplication([])  # for style().standardIcon()
+     _repos = {}
+-    for name in ('subdirs', 'euc-jp-path'):
+-        _repos[name] = get_fixture_repo(name)
++
++    tmpdir = helpers.mktmpdir(__name__)
++
++    # XXX helper class
++    _repos['subdirs'] = repo = thgrepo.repository(path=os.path.join(tmpdir, 'subdirs'),
++                                                  create=True)
++    open(repo.wjoin('foo'), 'w').close()
++    open(repo.wjoin('bar'), 'w').close()
++    os.mkdir(repo.wjoin('baz'))
++    open(repo.wjoin('baz/bax'), 'w').close()
++    open(repo.wjoin('baz/box'), 'w').close()
++    commands.addremove(repo.ui, repo)
++    commands.commit(repo.ui, repo, message='foobar')
++    open(repo.wjoin('bar'), 'w').write('hello\n')  # XXX close()
++    commands.remove(repo.ui, repo, repo.wjoin('baz/box'))
++    open(repo.wjoin('zzz'), 'w').close()
++    commands.addremove(repo.ui, repo)
++    commands.commit(repo.ui, repo, message='remove baz/box, add zzz, modify bar')
++
++    _repos['euc-jp-path'] = repo = thgrepo.repository(path=os.path.join(tmpdir, 'euc-jp-path'),
++                                                      create=True)
++    open(repo.wjoin(_aloha_ja.encode('euc-jp')), 'w').close()
++    commands.add(repo.ui, repo, repo.wjoin(_aloha_ja.encode('euc-jp')))
++    commands.commit(repo.ui, repo, message='add aloha')
+ 
+ def teardown():
+     global _app
+# HG changeset patch
+# Parent 01ed8e26a4712bbec2b5e1b702ab8d6d3ec8dc2e
+
+diff --git a/tests/__init__.py b/tests/__init__.py
+deleted file mode 100644
+--- a/tests/__init__.py
++++ /dev/null
+@@ -1,7 +0,0 @@
+-import os, tempfile, shutil
+-from nose.tools import *
+-from mercurial import ui, commands, error
+-from tortoisehg.util import hglib
+-from tortoisehg.hgqt import thgrepo
+-
+-FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixtures')
+diff --git a/tests/qt_repotreemodel_test.py b/tests/qt_repotreemodel_test.py
+--- a/tests/qt_repotreemodel_test.py
++++ b/tests/qt_repotreemodel_test.py
+@@ -2,7 +2,8 @@ import os
+ from nose.tools import *
+ from mercurial import node
+ from tortoisehg.hgqt.repotreemodel import *
+-from tests import FIXTURES_DIR
++
++FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixtures')
+ 
+ def openfixture(name, mode=QIODevice.ReadOnly):
+     path = os.path.join(FIXTURES_DIR, name)
+# HG changeset patch
+# Parent 7e12f68a82751b9691224d2d926bb913d3d63fae
+tests: use single temporary directory and add option not to clean up
+
+diff --git a/tests/__init__.py b/tests/__init__.py
+--- a/tests/__init__.py
++++ b/tests/__init__.py
+@@ -4,16 +4,13 @@ from mercurial import ui, commands, erro
+ from tortoisehg.util import hglib
+ from tortoisehg.hgqt import thgrepo
+ 
++import helpers
++
+ FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixtures')
+ 
+ def setup():
+-    global _tempdir, _reposdir
+-    _tempdir = tempfile.mkdtemp()
+-    _reposdir = os.path.join(_tempdir, 'repos')
+-    os.mkdir(_reposdir)
+-
+-def teardown():
+-    shutil.rmtree(_tempdir)
++    global _reposdir
++    _reposdir = helpers.mktmpdir('repos')
+ 
+ 
+ def create_fixture_repo(name, dirname=None):
+diff --git a/tests/helpers.py b/tests/helpers.py
+new file mode 100644
+--- /dev/null
++++ b/tests/helpers.py
+@@ -0,0 +1,35 @@
++import os, shutil, tempfile
++from nose import plugins
++
++class HgTmpPlugin(plugins.Plugin):
++    """Set up temporary environment"""
++    enabled = True
++    name = 'hgtmp'
++
++    def options(self, parser, env):
++        parser.add_option('--keep-tmpdir', action='store_true', default=False,
++                          help='keep temporary directory after running tests')
++        parser.add_option('--tmpdir',
++                          help=('put temporary files in the given directory '
++                                '(implies --keep-tmpdir)'))
++
++    def configure(self, options, conf):
++        self.keep_tmpdir = options.keep_tmpdir or bool(options.tmpdir)
++        self.tmpdir = options.tmpdir
++
++    def begin(self):
++        if self.tmpdir:
++            if os.path.exists(self.tmpdir):
++                raise Exception('temp dir %r already exists' % self.tmpdir)
++            os.makedirs(self.tmpdir)
++        else:
++            self.tmpdir = tempfile.mkdtemp('', 'thgtests.')
++        os.environ['HGTMP'] = self.tmpdir
++
++    def finalize(self, result):
++        if not self.keep_tmpdir:
++            shutil.rmtree(self.tmpdir)
++
++def mktmpdir(prefix):
++    """Create temporary directory under HGTMP"""
++    return tempfile.mkdtemp('', prefix, os.environ['HGTMP'])
+diff --git a/tests/run-tests.py b/tests/run-tests.py
+--- a/tests/run-tests.py
++++ b/tests/run-tests.py
+@@ -9,5 +9,7 @@ if HGPATH and os.path.isdir(os.path.join
+ THGPATH = os.path.join(os.path.dirname(__file__), '..')
+ sys.path.insert(1, THGPATH)
+ 
++import helpers
++
+ if __name__ == '__main__':
+-    nose.main()
++    nose.main(plugins=[helpers.HgTmpPlugin()])
+diff --git a/tests/wconfig_test.py b/tests/wconfig_test.py
+--- a/tests/wconfig_test.py
++++ b/tests/wconfig_test.py
+@@ -1,16 +1,15 @@
+-import os, tempfile, shutil
++import os, tempfile
+ from nose.tools import *
+ from nose.plugins.skip import SkipTest
+ from StringIO import StringIO
+ from mercurial import config, error
+ from tortoisehg.util import wconfig
+ 
++import helpers
++
+ def setup():
+     global _tempdir
+-    _tempdir = tempfile.mkdtemp()
+-
+-def teardown():
+-    shutil.rmtree(_tempdir)
++    _tempdir = helpers.mktmpdir(__name__)
+ 
+ 
+ def newrconfig(vals={}):

tests-withencoding.diff

+# HG changeset patch
+# Parent 25b9051807eb68b62570e4036fc5b7dc34eb27d5
+tests: move with_encoding decorator to helpers module
+
+diff --git a/tests/__init__.py b/tests/__init__.py
+--- a/tests/__init__.py
++++ b/tests/__init__.py
+@@ -28,19 +28,3 @@ def get_fixture_repo(name):
+         return thgrepo.repository(ui.ui(), path)
+     except error.RepoError:
+         return create_fixture_repo(name)
+-
+-
+-def with_encoding(encoding, fallbackencoding=None):
+-    """Change locale encoding temporarily"""
+-    orig_encoding = hglib._encoding
+-    orig_fallbackencoding = hglib._fallbackencoding
+-
+-    def setenc():
+-        hglib._encoding = encoding
+-        hglib._fallbackencoding = fallbackencoding or encoding
+-
+-    def restoreenc():
+-        hglib._encoding = orig_encoding
+-        hglib._fallbackencoding = orig_fallbackencoding
+-
+-    return with_setup(setenc, restoreenc)
+diff --git a/tests/helpers.py b/tests/helpers.py
+--- a/tests/helpers.py
++++ b/tests/helpers.py
+@@ -1,5 +1,6 @@
+ import os, shutil, tempfile
+-from nose import plugins
++from nose import plugins, tools
++from tortoisehg.util import hglib
+ 
+ class HgTmpPlugin(plugins.Plugin):
+     """Set up temporary environment"""
+@@ -33,3 +34,18 @@ class HgTmpPlugin(plugins.Plugin):
+ def mktmpdir(prefix):
+     """Create temporary directory under HGTMP"""
+     return tempfile.mkdtemp('', prefix, os.environ['HGTMP'])
++
++def with_encoding(encoding, fallbackencoding=None):
++    """Change locale encoding temporarily"""
++    orig_encoding = hglib._encoding
++    orig_fallbackencoding = hglib._fallbackencoding
++
++    def setenc():
++        hglib._encoding = encoding
++        hglib._fallbackencoding = fallbackencoding or encoding
++
++    def restoreenc():
++        hglib._encoding = orig_encoding
++        hglib._fallbackencoding = orig_fallbackencoding
++
++    return tools.with_setup(setenc, restoreenc)
+diff --git a/tests/hglib_encoding_test.py b/tests/hglib_encoding_test.py
+--- a/tests/hglib_encoding_test.py
++++ b/tests/hglib_encoding_test.py
+@@ -1,11 +1,12 @@
+ """Test for encoding helper functions of tortoisehg.util.hglib"""
+ from nose.tools import *
+ from tortoisehg.util import hglib
+-from tests import with_encoding
++
++import helpers
+ 
+ JAPANESE_KANA_I = u'\u30a4'  # Japanese katakana "i"
+ 
+-@with_encoding('utf-8')
++@helpers.with_encoding('utf-8')
+ def test_none():
+     """None shouldn't be touched"""
+     for e in ('fromunicode', 'fromutf', 'tounicode', 'toutf'):
+@@ -13,12 +14,12 @@ def test_none():
+         assert_equals(None, f(None))
+ 
+ 
+-@with_encoding('utf-8')
++@helpers.with_encoding('utf-8')
+ def test_fromunicode():
+     assert_equals(JAPANESE_KANA_I.encode('utf-8'),
+                   hglib.fromunicode(JAPANESE_KANA_I))
+ 
+-@with_encoding('utf-8')
++@helpers.with_encoding('utf-8')
+ def test_fromunicode_unicodableobj():
+     """fromunicode() accepts unicode-able obj like QString"""
+     class Unicodable(object):
+@@ -28,54 +29,54 @@ def test_fromunicode_unicodableobj():
+     assert_equals(JAPANESE_KANA_I.encode('utf-8'),
+                   hglib.fromunicode(Unicodable()))
+ 
+-@with_encoding('ascii', 'utf-8')
++@helpers.with_encoding('ascii', 'utf-8')
+ def test_fromunicode_fallback():
+     assert_equals(JAPANESE_KANA_I.encode('utf-8'),
+                   hglib.fromunicode(JAPANESE_KANA_I))
+ 
+-@with_encoding('ascii')
++@helpers.with_encoding('ascii')
+ def test_fromunicode_replace():
+     assert_equals('?', hglib.fromunicode(JAPANESE_KANA_I,
+                                          errors='replace'))
+ 
+-@with_encoding('ascii')
++@helpers.with_encoding('ascii')
+ def test_fromunicode_strict():
+     assert_raises(UnicodeEncodeError,
+                   lambda: hglib.fromunicode(JAPANESE_KANA_I))
+ 
+ 
+-@with_encoding('euc-jp')
++@helpers.with_encoding('euc-jp')
+ def test_fromutf():
+     assert_equals(JAPANESE_KANA_I.encode('euc-jp'),
+                   hglib.fromutf(JAPANESE_KANA_I.encode('utf-8')))
+ 
+-@with_encoding('ascii', 'euc-jp')
++@helpers.with_encoding('ascii', 'euc-jp')
+ def test_fromutf_fallback():
+     assert_equals(JAPANESE_KANA_I.encode('euc-jp'),
+                   hglib.fromutf(JAPANESE_KANA_I.encode('utf-8')))
+ 
+-@with_encoding('ascii')
++@helpers.with_encoding('ascii')
+ def test_fromutf_replace():
+     assert_equals('?', hglib.fromutf(JAPANESE_KANA_I.encode('utf-8')))
+ 
+ 
+-@with_encoding('euc-jp')
++@helpers.with_encoding('euc-jp')
+ def test_tounicode():
+     assert_equals(JAPANESE_KANA_I,
+                   hglib.tounicode(JAPANESE_KANA_I.encode('euc-jp')))
+ 
+-@with_encoding('ascii', 'euc-jp')
++@helpers.with_encoding('ascii', 'euc-jp')
+ def test_tounicode_fallback():
+     assert_equals(JAPANESE_KANA_I,
+                   hglib.tounicode(JAPANESE_KANA_I.encode('euc-jp')))
+ 
+ 
+-@with_encoding('euc-jp')
++@helpers.with_encoding('euc-jp')
+ def test_toutf():
+     assert_equals(JAPANESE_KANA_I.encode('utf-8'),
+                   hglib.toutf(JAPANESE_KANA_I.encode('euc-jp')))
+ 
+-@with_encoding('ascii', 'euc-jp')
++@helpers.with_encoding('ascii', 'euc-jp')
+ def test_toutf_fallback():
+     assert_equals(JAPANESE_KANA_I.encode('utf-8'),
+                   hglib.toutf(JAPANESE_KANA_I.encode('euc-jp')))
+diff --git a/tests/qt_manifestmodel_test.py b/tests/qt_manifestmodel_test.py
+--- a/tests/qt_manifestmodel_test.py
++++ b/tests/qt_manifestmodel_test.py
+@@ -2,7 +2,9 @@ from nose.tools import *
+ from PyQt4.QtCore import QModelIndex, QString
+ from PyQt4.QtGui import QApplication
+ from tortoisehg.hgqt.manifestmodel import ManifestModel
+-from tests import get_fixture_repo, with_encoding
++from tests import get_fixture_repo
++
++import helpers
+ 
+ _aloha_ja = u'\u3042\u308d\u306f\u30fc'
+ 
+@@ -36,7 +38,7 @@ def test_data_inexistent():
+     assert_equals(None, m.data(QModelIndex()))
+     assert_equals(None, m.data(m.index(0, 0, m.index(1, 0))))
+ 
+-@with_encoding('euc-jp')
++@helpers.with_encoding('euc-jp')
+ def test_data_eucjp():
+     m = newmodel(name='euc-jp-path')
+     assert_equals(_aloha_ja, m.data(m.index(0, 0)))
+@@ -66,7 +68,7 @@ def test_pathfromindex():
+     assert_equals('baz', m.filePath(m.index(0, 0)))
+     assert_equals('baz/bax', m.filePath(m.index(0, 0, m.index(0, 0))))
+ 
+-@with_encoding('euc-jp')
++@helpers.with_encoding('euc-jp')
+ def test_pathfromindex_eucjp():
+     m = newmodel(name='euc-jp-path')
+     assert_equals(_aloha_ja, m.filePath(m.index(0, 0)))
+@@ -82,7 +84,7 @@ def test_indexfrompath_qstr():
+     m = newmodel()
+     assert_equals(m.index(1, 0), m.indexFromPath(QString('bar')))
+ 
+-@with_encoding('euc-jp')
++@helpers.with_encoding('euc-jp')
+ def test_indexfrompath_eucjp():
+     m = newmodel(name='euc-jp-path')
+     assert_equals(m.index(0, 0), m.indexFromPath(_aloha_ja))
+# HG changeset patch
+# Parent 0b54af210d9ea2b0c3700c7a4a5bcb1729d78df2
+
+diff --git a/tests/qtwidgets/__init__.py b/tests/qtwidgets/__init__.py
+new file mode 100644
+diff --git a/tests/qtwidgets/sync_save_dialog_test.py b/tests/qtwidgets/sync_save_dialog_test.py
+new file mode 100644
+--- /dev/null
++++ b/tests/qtwidgets/sync_save_dialog_test.py
+@@ -0,0 +1,39 @@
++from nose.tools import *
++from PyQt4.QtGui import QApplication
++from mercurial import demandimport; demandimport.enable()  # XXX
++from tortoisehg.hgqt import sync
++
++def setup():
++    global _app
++    _app = QApplication([])
++
++def teardown():
++    global _app
++    del _app
++
++def test_clearcb_save():
++    repo = None  # XXX
++    origurl = 'http://foo:bar@example.org/baz'
++    safeurl = 'http://foo:***@example.org/baz'
++    cleanurl = 'http://example.org/baz'
++    dlg = sync.SaveDialog(repo, 'default', origurl, safeurl, parent=None,
++                          edit=False)
++    assert dlg.clearcb.isChecked()
++    assert_equals(cleanurl, dlg.urllabel.text())
++
++    dlg.clearcb.setChecked(False)
++    assert_equals(safeurl, dlg.urllabel.text())
++
++def test_clearcb_save_noauth():
++    repo = None  # XXX
++    url = 'http://example.org/'
++    dlg = sync.SaveDialog(repo, 'default', url, url, parent=None, edit=False)
++    assert not dlg.clearcb
++    assert_equals(url, dlg.urllabel.text())
++
++def test_clearcb_edit():
++    repo = None  # XXX
++    url = 'http://foo:bar@example.org/'
++    dlg = sync.SaveDialog(repo, 'default', url, url, parent=None, edit=True)
++    assert not dlg.clearcb
++    assert_equals(url, dlg.urlentry.text())