diff --git a/.hgignore b/.hgignore index 632eea61d6b8a3ff2e3ed6e8d09b6ebccfb0e25d..c130c8ffb6800226c977528a7de0498e0a4996d0 100644 --- a/.hgignore +++ b/.hgignore @@ -55,3 +55,4 @@ contrib/packaging/macos/toolchain/downloads contrib/packaging/macos/toolchain/toolchain-qt4 contrib/packaging/macos/toolchain/toolchain-qt5 contrib/packaging/macos/releases +.pytest_cache diff --git a/Makefile b/Makefile index 13cfe2d391dd8274ebb7ddfd38b156c40340954a..7a37976ce474f0f5fa30f69c17a6057a4f9c8248 100644 --- a/Makefile +++ b/Makefile @@ -32,8 +32,8 @@ local: tests: @[ -n "$(HGPATH)" ] || { echo "HGPATH not specified"; false; } @[ -d "$(HGPATH)" ] || { echo "HGPATH not found: $(HGPATH)"; false; } - $(PYTHON) tests/run-tests.py -a '!extensions' --with-doctest - $(PYTHON) tests/run-tests.py -a extensions=largefiles + $(PYTHON) tests/run-tests.py -m 'not largefiles' --doctest-modules tests + $(PYTHON) tests/run-tests.py -m largefiles tests $(PYTHON) tests/run-hgtests.py .PHONY: pytype diff --git a/tests/colormap_test.py b/tests/colormap_test.py index c39ba6e140fc1ddf9625057e3198ae4efa44f76b..872c1887f2010737c9f9bb395beca0843296ecdf 100644 --- a/tests/colormap_test.py +++ b/tests/colormap_test.py @@ -3,12 +3,12 @@ import sys from nose.tools import * from tortoisehg.util import colormap -def setup(): +def setup_module(): global _origmaxint _origmaxint = sys.maxsize sys.maxsize = int((1 << 31) - 1) -def teardown(): +def teardown_module(): sys.maxsize = _origmaxint def fakectx(userhash, date, tz=0): diff --git a/tests/filedata_test.py b/tests/filedata_test.py index 41e2ac856454261d1034864871063ee32b8ec991..705400e813298de167dc18045d9aaa0d0849e4dd 100644 --- a/tests/filedata_test.py +++ b/tests/filedata_test.py @@ -1,11 +1,13 @@ +import functools import os from nose.tools import * +import pytest from tortoisehg.hgqt import filedata, thgrepo import helpers -def setup(): +def setup_module(): global _tmpdir _tmpdir = helpers.mktmpdir(__name__) @@ -40,30 +42,32 @@ def loaddata(reponame, rev, path, prev=None): return fd -def check_error(fd, message): +@pytest.mark.parametrize('get_fd, message', [ + (functools.partial(loaddata, 'status', rev, path + sfx), msg) + for rev, sfx in [(0, ''), (1, '.copied'), (2, '.renamed')] + for path, msg in [('binary', 'is binary'), + ('text-over1kb', 'is larger than')] +]) +def test_error_added(get_fd, message): + fd = get_fd() assert message in fd.error, (message, fd.error) # no data should be loaded assert_false(fd.contents) assert_false(fd.olddata) assert_false(fd.diff) -def test_error_added(): - for rev, sfx in [(0, ''), (1, '.copied'), (2, '.renamed')]: - for path, msg in [('binary', 'is binary'), - ('text-over1kb', 'is larger than')]: - fd = loaddata('status', rev, path + sfx) - yield check_error, fd, msg - def check_flabel(fd, message): assert message in fd.flabel, (message, fd.flabel) -def test_flabel_added(): - for path in ['text', 'binary', 'text-over1kb']: - for rev, sfx, msg in [(0, '', 'added'), - (1, '.copied', 'copied from'), - (2, '.renamed', 'renamed from')]: - fd = loaddata('status', rev, path + sfx) - yield check_flabel, fd, msg +@pytest.mark.parametrize('get_fd, message', [ + (functools.partial(loaddata, 'status', rev, path + sfx), msg) + for path in ['text', 'binary', 'text-over1kb'] + for rev, sfx, msg in [(0, '', 'added'), + (1, '.copied', 'copied from'), + (2, '.renamed', 'renamed from')] +]) +def test_flabel_added(get_fd, message): + check_flabel(get_fd(), message) def test_flabel_far_rename(): fd = loaddata('status', 2, 'text.copied', 0) diff --git a/tests/graph_test.py b/tests/graph_test.py index 0aeb96f26cb4970dec60a829d28f1fc2dc83dec9..cc262213c93804ccb7b0cdc64a32379a6557a67c 100644 --- a/tests/graph_test.py +++ b/tests/graph_test.py @@ -10,7 +10,7 @@ from tortoisehg.util import hglib import helpers -def setup(): +def setup_module(): global _tmpdir _tmpdir = helpers.mktmpdir(__name__) diff --git a/tests/graphbuilder_test.py b/tests/graphbuilder_test.py index c83b02cd48c3dfdb278abacb5be96c256c8a2a71..03f2567843026862d3c9abef884725aba48d9ae3 100644 --- a/tests/graphbuilder_test.py +++ b/tests/graphbuilder_test.py @@ -8,7 +8,7 @@ from tortoisehg.util import hglib import helpers -def setup(): +def setup_module(): global _tmpdir _tmpdir = helpers.mktmpdir(__name__) diff --git a/tests/hglib_repo_test.py b/tests/hglib_repo_test.py index fe831c4b95955f6429300a68007562e86aa1edea..902aacc070531877dbeebcd8aa8a142af216d82a 100644 --- a/tests/hglib_repo_test.py +++ b/tests/hglib_repo_test.py @@ -2,13 +2,14 @@ import os from nose.tools import * +import pytest from mercurial import hg from tortoisehg.util import hglib import helpers -def setup(): +def setup_module(): global _tmpdir _tmpdir = helpers.mktmpdir(__name__) @@ -44,15 +45,14 @@ def setup_repoid(): hg = helpers.HgClient(os.path.join(_tmpdir, 'repoid-rev0hidden')) hg.debugobsolete(_repoidnodes[0]) -def check_repoid(reponame, rootrev): +@pytest.mark.parametrize('reponame, rootrev', [ + ('empty', -1), + ('repoid-trivial', 0), + ('repoid-allhidden', -1), + ('repoid-rev0hidden', 1) +]) +def test_repoid(reponame, rootrev): repo = openrepo(reponame) nodehex = _repoidnodes[rootrev] assert_equal(nodehex[:12], hglib.shortrepoid(repo)) assert_equal(nodehex.decode('hex'), hglib.repoidnode(repo)) - -def test_repoid(): - for reponame, rev in [('empty', -1), - ('repoid-trivial', 0), - ('repoid-allhidden', -1), - ('repoid-rev0hidden', 1)]: - yield check_repoid, reponame, rev diff --git a/tests/mercurialapi_test.py b/tests/mercurialapi_test.py index c7d4074aacd0496bca6b8886f0999dcfbe30eebf..993bf7968f5e91f4e24452cb5a9cc8ba2e2728a3 100644 --- a/tests/mercurialapi_test.py +++ b/tests/mercurialapi_test.py @@ -1,11 +1,13 @@ """Test for compatibility with Mercurial API""" import inspect + +import pytest from nose.tools import * from tortoisehg.util import hglib, pipeui -def assert_same_argspec(f, g): +def test_same_argspec(f, g): fa, ga = inspect.getargspec(f), inspect.getargspec(g) assert_equals(fa, ga, '%s != %s' % (inspect.formatargspec(*fa), @@ -13,13 +15,13 @@ def assert_same_argspec(f, g): def overridden_methods(cls): basemethods = inspect.getmembers(cls.__base__, inspect.ismethod) - return [(name, basemeth, getattr(cls, name)) + return [(basemeth, getattr(cls, name)) for name, basemeth in basemethods if basemeth.__func__ is not getattr(cls, name).__func__] -def test_pipeui(): +def pytest_generate_tests(metafunc): ui = hglib.loadui() pipeui.uisetup(ui) - for name, basemeth, meth in overridden_methods(ui.__class__): - yield assert_same_argspec, basemeth, meth + if metafunc.function == test_same_argspec: + metafunc.parametrize('f, g', overridden_methods(ui.__class__)) diff --git a/tests/nosecaptureexc.py b/tests/nosecaptureexc.py deleted file mode 100644 index b6cf79b58d565fa095c49c07465e1d78f36a2341..0000000000000000000000000000000000000000 --- a/tests/nosecaptureexc.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Nose plugin to capture unhandled exception""" -import sys -from nose import plugins - -class CaptureExcPlugin(plugins.Plugin): - """Capture unhandled exception (probably raised inside event loop)""" - enabled = False - name = 'capture-exc' - - def options(self, parser, env): - parser.add_option('--no-capture-exc', dest='capture_exc', - action='store_false', default=True, - help='Catch unhandled exception to report as error') - - def configure(self, options, conf): - self.enabled = options.capture_exc - - def prepareTestResult(self, result): - # dirty hack to access result.addError() - self._result = result - - def startTest(self, test): - self._origexcepthook = sys.excepthook - sys.excepthook = self._excepthook - self._excepts = [] - - def stopTest(self, test): - sys.excepthook = self._origexcepthook - del self._origexcepthook - if self._excepts: - # BUG: because the corresponding addSuccess/Failure/Error for the - # given test is already called, this increases the test counts. - self._result.addError(test, self._excepts.pop(0)) - - def _excepthook(self, type, value, traceback): - self._excepts.append((type, value, traceback)) diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 0000000000000000000000000000000000000000..a316b26ee3f480770c49fe8fa9ed97582129cb7c --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +markers = + largefiles: enable largefiles extensions + diff --git a/tests/pytestcaptureexc.py b/tests/pytestcaptureexc.py new file mode 100644 index 0000000000000000000000000000000000000000..e0187afd592057acb2a6f02a9962e624fa86d213 --- /dev/null +++ b/tests/pytestcaptureexc.py @@ -0,0 +1,48 @@ +"""Pytest plugin to capture unhandled exception""" +from __future__ import print_function + +try: + from cStringIO import StringIO +except ImportError: + try: + from StringIO import StringIO + except ImportError: + from io import StringIO +import sys +import traceback + +import pytest + +class CaptureExcPlugin(object): + """Capture unhandled exception (probably raised inside event loop)""" + + def pytest_addoption(self, parser): + parser.addoption('--no-capture-exc', dest='capture_exc', + action='store_false', default=True, + help='Catch unhandled exception to report as error') + + def pytest_configure(self, config): + options = config.known_args_namespace + self.enabled = options.capture_exc + + @pytest.mark.hookwrapper + def pytest_runtest_call(self, item): + self._origexcepthook = sys.excepthook + sys.excepthook = self._excepthook + self._excepts = [] + try: + yield + finally: + sys.excepthook = self._origexcepthook + del self._origexcepthook + if self._excepts: + out = StringIO() + print("%s uncaught exception(s):" % len(self._excepts), file=out) + for exc_info in self._excepts: + print(file=out) + traceback.print_exception(*exc_info, file=out) + del self._excepts[:] + pytest.fail(out.getvalue(), pytrace=False) + + def _excepthook(self, type, value, traceback): + self._excepts.append((type, value, traceback)) diff --git a/tests/nosehgenv.py b/tests/pytesthgenv.py similarity index 86% rename from tests/nosehgenv.py rename to tests/pytesthgenv.py index 563ceaa342ad353620db2153833faf98a85be0ce..f33956c6b42ba4a507148633462ae9aa595f4fc1 100644 --- a/tests/nosehgenv.py +++ b/tests/pytesthgenv.py @@ -1,4 +1,4 @@ -"""Nose plugin to set up test environment""" +"""Pytest plugin to set up test environment""" from __future__ import absolute_import @@ -7,27 +7,28 @@ import shutil import sys import tempfile -from nose import plugins +import pytest + # don't import mercurial or tortoisehg before setting up test environment -class HgEnvPlugin(plugins.Plugin): +class HgEnvPlugin(object): """Set up temporary environment""" - enabled = True - name = 'hgenv' - 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 pytest_addoption(self, parser): + parser.addoption('--keep-tmpdir', action='store_true', default=False, + help='Keep temporary directory after running tests') + parser.addoption('--tmpdir', + help=('Put temporary files in the given directory ' + '(implies --keep-tmpdir)')) - def configure(self, options, conf): + def pytest_configure(self, config): + options = config.known_args_namespace self.keep_tmpdir = options.keep_tmpdir or bool(options.tmpdir) self.tmpdir = options.tmpdir - def begin(self): + @pytest.hookimpl(tryfirst=True) + def pytest_collection(self): if 'mercurial' in sys.modules: raise Exception('loaded mercurial module before setting up ' 'test environment') @@ -124,7 +125,7 @@ class HgEnvPlugin(plugins.Plugin): QSettings.setPath(QSettings.IniFormat, QSettings.UserScope, self.configdir) - def finalize(self, result): + def pytest_sessionfinish(self): del self._qapp if not self.keep_tmpdir: diff --git a/tests/qt_cmdagent_test.py b/tests/qt_cmdagent_test.py index 13d031262771555fef228c1ef2a774ddd41f099c..063dba4f6c350a18c141cf8ef6784dde16c0c93b 100644 --- a/tests/qt_cmdagent_test.py +++ b/tests/qt_cmdagent_test.py @@ -55,7 +55,7 @@ def waitForCmdStarted(session, timeout=5000): loop.exec_() -class _CmdAgentTestBase(unittest.TestCase): +class _CmdAgentTestBase(object): @classmethod def setUpClass(cls): tmpdir = helpers.mktmpdir(cls.__name__) @@ -247,11 +247,11 @@ class _CmdAgentTestBase(unittest.TestCase): self.assertFalse(self.agent.isBusy()) -class CmdAgentProcTest(_CmdAgentTestBase): +class CmdAgentProcTest(_CmdAgentTestBase, unittest.TestCase): workername = 'proc' -class CmdAgentServerTest(_CmdAgentTestBase): +class CmdAgentServerTest(_CmdAgentTestBase, unittest.TestCase): workername = 'server' @classmethod diff --git a/tests/qt_manifestmodel_test.py b/tests/qt_manifestmodel_test.py index 476132ae78ff1a46d52be74e6b7a5f4443377d8c..0ccf1a5ba3ee86c402d6fadda10456bc409d1852 100644 --- a/tests/qt_manifestmodel_test.py +++ b/tests/qt_manifestmodel_test.py @@ -11,6 +11,8 @@ from mercurial import ( from nose.plugins.skip import SkipTest +import pytest + from tortoisehg.hgqt.qtcore import ( QModelIndex, Qt, @@ -27,7 +29,7 @@ from tortoisehg.hgqt.manifestmodel import ManifestModel import helpers -def setup(): +def setup_module(): # necessary for style().standardIcon() if not isinstance(QApplication.instance(), QApplication): raise SkipTest @@ -637,9 +639,8 @@ class ManifestModelReloadTest(unittest.TestCase): self.assertEqual(['bar', 'foo'], alldata(m)) +@pytest.mark.largefiles class ManifestModelLargeFilesTest(unittest.TestCase): - extensions = ['largefiles'] - @classmethod def setUpClass(cls): cls.hg = hg = helpers.HgClient(os.path.join(_tmpdir, cls.__name__)) diff --git a/tests/qt_repoagent_test.py b/tests/qt_repoagent_test.py index f297a097eb9257df5f264e58cbf55c8f4d69fd85..e4e79708c48a30f7a01dfe22a26537494d9e72ee 100644 --- a/tests/qt_repoagent_test.py +++ b/tests/qt_repoagent_test.py @@ -50,7 +50,7 @@ class RepoAgentAttributeTest(unittest.TestCase): self.assertFalse(self.agent.hiddenRevsIncluded()) -class _RepoAgentChangedSignalTestBase(unittest.TestCase): +class _RepoAgentChangedSignalTestBase(object): @classmethod def setUpClass(cls): tmpdir = helpers.mktmpdir(cls.__name__) @@ -198,7 +198,7 @@ class _RepoAgentChangedSignalTestBase(unittest.TestCase): self.wait_changed(thgrepo.LogChanged) -class RepoAgentChangedSignalWithMonitorTest(_RepoAgentChangedSignalTestBase): +class RepoAgentChangedSignalWithMonitorTest(_RepoAgentChangedSignalTestBase, unittest.TestCase): monitorrepo = b'always' def test_filesystem_watcher(self): @@ -225,5 +225,5 @@ class RepoAgentChangedSignalWithMonitorTest(_RepoAgentChangedSignalTestBase): | thgrepo.WorkingStateChanged) -class RepoAgentChangedSignalWithoutMonitorTest(_RepoAgentChangedSignalTestBase): +class RepoAgentChangedSignalWithoutMonitorTest(_RepoAgentChangedSignalTestBase, unittest.TestCase): monitorrepo = b'never' diff --git a/tests/run-tests.py b/tests/run-tests.py index feb552b15c4b580ff080e99d1560c827c5639009..1cd728331bcead9e3a6d8ecc7118e507c4165bea 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -1,38 +1,25 @@ #!/usr/bin/env python -"""Run a set of tests by nosetests +"""Run a set of tests by pytest To run all tests:: % export HGPATH=path/to/mercurial - % ./run-tests.py -a '!extensions' --with-doctest - % ./run-tests.py -a 'extensions=largefiles' + % ./run-tests.py -m 'not largefiles' --doctest-modules tests + % ./run-tests.py -m largefiles tests For details, please see: - `./run-tests.py --help` -- http://readthedocs.org/docs/nose/en/latest/ +- https://docs.pytest.org/en/stable/ - http://docs.python.org/library/unittest.html """ -import nose, os - -import nosecaptureexc, nosehgenv - -ignorefiles = [ - r'^[._]', - r'^setup\.py$', - r'^TortoiseHgOverlayServer\.py$', - # exclude platform-dependent modules - r'^bugtraq\.py$', - r'^shellconf\.py$', - ] +import sys +import pytest +import pytestcaptureexc, pytesthgenv def main(): - env = os.environ.copy() - if 'NOSE_IGNORE_FILES' not in env: - env['NOSE_IGNORE_FILES'] = ignorefiles - nose.main(env=env, - addplugins=[nosecaptureexc.CaptureExcPlugin(), - nosehgenv.HgEnvPlugin()]) + sys.exit(pytest.main(plugins=[pytestcaptureexc.CaptureExcPlugin(), + pytesthgenv.HgEnvPlugin()])) if __name__ == '__main__': main() diff --git a/tests/wconfig_test.py b/tests/wconfig_test.py index 2ab6b2854a6abb44053057967ead7a5f145e922d..a5b4bc2c58820da05bbeb3f7dc27a7cc688561ae 100644 --- a/tests/wconfig_test.py +++ b/tests/wconfig_test.py @@ -1,17 +1,17 @@ import os, tempfile from nose.tools import * -from nose.plugins.skip import SkipTest from mercurial import ( config, error, pycompat, ) +import pytest from tortoisehg.util import wconfig import helpers -def setup(): +def setup_module(): global _tempdir _tempdir = helpers.mktmpdir(__name__) @@ -41,33 +41,40 @@ def writetempfile(s): os.close(fd) return path -class _Collector(list): - def __call__(self, func): - self.append(func) - return func -with_rconfig = _Collector() -with_wconfig = _Collector() -with_both = _Collector() +def with_rconfig(f): + f.__test__ = True + return pytest.mark.usefixtures('rconfig')(f) +def with_wconfig(f): + f.__test__ = True + return pytest.mark.usefixtures('wconfig')(f) -def test_both(): - for e in with_wconfig + with_both: - if wconfig._hasiniparse: - yield e - else: - def skipped(): - raise SkipTest - yield skipped +def with_both(f): + f.__test__ = True + return pytest.mark.usefixtures('both')(f) + +@pytest.fixture(name='wconfig') +def fixture_wconfig(): + if wconfig._hasiniparse: + yield + else: + pytest.skip() + +@pytest.fixture(name='rconfig') +def fixture_rconfig(): orighasiniparse = wconfig._hasiniparse wconfig._hasiniparse = False try: - for e in with_rconfig + with_both: - yield e + yield finally: wconfig._hasiniparse = orighasiniparse +@pytest.fixture(name='both', params=['wconfig', 'rconfig']) +def fixture_both(request): + request.getfixturevalue(request.param) + @with_both def check_copy(): diff --git a/tests/widget/__init__.py b/tests/widget/__init__.py index 66e10014df874e20ccc17854185532666360e43f..0ef29179da4187a6ea46bb3c04f727d72a8a0053 100644 --- a/tests/widget/__init__.py +++ b/tests/widget/__init__.py @@ -8,6 +8,6 @@ from tortoisehg.hgqt.qtgui import ( QApplication, ) -def setup(): +def setup_module(): if not isinstance(QApplication.instance(), QApplication): raise SkipTest diff --git a/tests/widget/repofilterbar_branch_test.py b/tests/widget/repofilterbar_branch_test.py index 1af96e0438a03ea690cb81515a0138435f674eca..e6bb8b1582425f3dd0b4b5889d45d8bce88017f1 100644 --- a/tests/widget/repofilterbar_branch_test.py +++ b/tests/widget/repofilterbar_branch_test.py @@ -16,7 +16,7 @@ from tortoisehg.hgqt import ( import helpers -def setup(): +def setup_module(): global _tmpdir _tmpdir = helpers.mktmpdir(__name__) diff --git a/tortoisehg/hgqt/archive.py b/tortoisehg/hgqt/archive.py index 9add61a1a5648db64e0505d6b5d671d595bb243b..04ff3d457d5679855b7af339f5092c77fc5a157c 100644 --- a/tortoisehg/hgqt/archive.py +++ b/tortoisehg/hgqt/archive.py @@ -161,9 +161,7 @@ class ArchiveWidget(cmdui.AbstractCmdWidget): self.rev_combo.addItem(WD_PARENT) self.rev_combo.addItems(pycompat.maplist(hglib.tounicode, hglib.namedbranches(self.repo))) - tags = list(self.repo.tags()) - tags.sort(reverse=True) - for t in tags: + for t in sorted(self.repo.tags(), reverse=True): self.rev_combo.addItem(hglib.tounicode(t)) if rev: text = hglib.tounicode(str(rev)) diff --git a/tortoisehg/hgqt/cmdcore.py b/tortoisehg/hgqt/cmdcore.py index 418956b78db94e392ca7c3d4e90ed51491d167ff..595e23c000c13c86ff67fbcabbcbcd0e6ccc1dda 100644 --- a/tortoisehg/hgqt/cmdcore.py +++ b/tortoisehg/hgqt/cmdcore.py @@ -256,6 +256,9 @@ def _proccmdline(ui, exts): for section, name, value in ui.walkconfig() if ui.configsource(section, name) == b'--config'] configs.extend(('extensions', e, '') for e in exts) + # Override user aliases (for example "alias.log=log --graph") that could + # break searching through the filter toolbar (CTRL+S). + configs.append(('alias', 'log', 'log')) cmdline = list(paths.get_hg_command()) for section, name, value in configs: cmdline.extend(('--config', '%s.%s=%s' % (section, name, value))) diff --git a/tortoisehg/hgqt/commit.py b/tortoisehg/hgqt/commit.py index 63c250646c2003af7acc2bd63cf6636c6b7980e4..b42848c6f18b3c5d6263392ca943e447cdc773c9 100644 --- a/tortoisehg/hgqt/commit.py +++ b/tortoisehg/hgqt/commit.py @@ -326,8 +326,8 @@ class CommitWidget(QWidget, qtlib.TaskWidget): QSizePolicy.Preferred) vbox.addWidget(self.optionslabel, 0) - self.pcsinfo = revpanel.ParentWidget(repo) - vbox.addWidget(self.pcsinfo, 0) + self.wdirinfo = revpanel.WDirInfoWidget(repo) + vbox.addWidget(self.wdirinfo, 0) msgte = MessageEntry(self, self.stwidget.getChecked) msgte.installEventFilter(qscilib.KeyPressInterceptor(self)) @@ -551,9 +551,9 @@ class CommitWidget(QWidget, qtlib.TaskWidget): self.stwidget.partials = {} if refreshwctx: self.stwidget.refreshWctx() - custom = self.pcsinfo.custom.copy() + custom = self.wdirinfo.custom.copy() custom['isAmend'] = curraction._name == 'amend' - self.pcsinfo.update(custom=custom) + self.wdirinfo.update(custom=custom) self.committb.setText(curraction._text) self.lastAction = curraction._name @@ -781,18 +781,18 @@ class CommitWidget(QWidget, qtlib.TaskWidget): False)) self.optionslabel.setVisible(bool(opts)) - # Update parent csinfo widget - self.pcsinfo.set_revision(None) - self.pcsinfo.update() + # Update wdir csinfo widget + self.wdirinfo.set_revision(None) + self.wdirinfo.update() # This is ugly, but want pnlabel to have the same alignment/style/etc - # as pcsinfo, so extract the needed parts of pcsinfo's markup. Would + # as wdirinfo, so extract the needed parts of wdirinfo's markup. Would # be nicer if csinfo exposed this information, or if csinfo could hold # widgets like pnlabel. if self.hasmqbutton: parent = _('Parent:') patchname = _('Patch name:') - text = pycompat.unicode(self.pcsinfo.revlabel.text()) + text = pycompat.unicode(self.wdirinfo.revlabel.text()) cellend = '' firstidx = text.find(cellend) + len(cellend) secondidx = text[firstidx:].rfind('') diff --git a/tortoisehg/hgqt/csinfo.py b/tortoisehg/hgqt/csinfo.py index d99ba7f732522e6a2da5c7a321e0843f3b97c3d3..745a42d34d56eca3d36f06b4f9556d48d5b01792 100644 --- a/tortoisehg/hgqt/csinfo.py +++ b/tortoisehg/hgqt/csinfo.py @@ -249,8 +249,7 @@ class SummaryInfo(object): else: return None elif item == 'ishead': - childbranches = [cctx.branch() for cctx in ctx.children()] - return ctx.branch() not in childbranches + return ctx.node() in ctx.repo().branchheads(ctx.branch()) elif item == 'mqoriginalparent': target = ctx.thgmqoriginalparent() if not target: diff --git a/tortoisehg/hgqt/fileview.py b/tortoisehg/hgqt/fileview.py index 4262723fe8c9d072316832ce636635ae362ba0cd..6fd47d8418619fb8c44accd4d497771036e68d4a 100644 --- a/tortoisehg/hgqt/fileview.py +++ b/tortoisehg/hgqt/fileview.py @@ -566,10 +566,7 @@ class HgFileView(QFrame): if lines: # assume that the longest line has the largest width; # fm.width() is too slow to apply to each line. - try: - longestline = max(lines, key=len) - except TypeError: # Python<2.5 has no key support - longestline = max((len(l), l) for l in lines)[1] + longestline = max(lines, key=len) self.maxWidth += fm.width(longestline) self._updateScrollBar() diff --git a/tortoisehg/hgqt/merge.py b/tortoisehg/hgqt/merge.py index 7c93a38de208d339555a46742dbef087b84861a7..a2cd30ba711e7ee47c537f9c8c162524641876ac 100644 --- a/tortoisehg/hgqt/merge.py +++ b/tortoisehg/hgqt/merge.py @@ -183,11 +183,11 @@ class SummaryPage(BasePage): self.setLayout(QVBoxLayout()) repo = self.repo - contents = ('ishead',) + csinfo.PANEL_DEFAULT + contents = ('createsnewhead',) + csinfo.PANEL_DEFAULT style = csinfo.panelstyle(contents=contents) def markup_func(widget, item, value): - if item == 'ishead' and value is False: - text = _('Not a head revision!') + if item == 'createsnewhead' and value is True: + text = _('Creates new head!') return qtlib.markup(text, fg='red', weight='bold') raise csinfo.UnknownItem(item) custom = csinfo.custom(markup=markup_func) diff --git a/tortoisehg/hgqt/repomodel.py b/tortoisehg/hgqt/repomodel.py index 473a98d5febffee28752603f28b56dcc43080f73..878fd5eef9839bdc7710c60cf0d8514aeda89b84 100644 --- a/tortoisehg/hgqt/repomodel.py +++ b/tortoisehg/hgqt/repomodel.py @@ -794,9 +794,8 @@ class HgRepoListModel(QAbstractTableModel): self._branchheads[branch] = branchheads if ctx.rev() is None: - for pctx in ctx.parents(): - if branchheads and pctx.node() not in branchheads: - labels.append((_('Not a head revision!'), 'log.warning')) + if hglib.createsnewhead(ctx, branchheads): + labels.append((_('Creates new head!'), 'log.warning')) return labels if self._show_branch_head_label: diff --git a/tortoisehg/hgqt/repotreeitem.py b/tortoisehg/hgqt/repotreeitem.py index e2ba0a56b320abd16b8e0637f3fef7f645c9c880..36427b2342a6ae7813fa30ee8df529448fac9fc9 100644 --- a/tortoisehg/hgqt/repotreeitem.py +++ b/tortoisehg/hgqt/repotreeitem.py @@ -422,9 +422,6 @@ class RepoItem(RepoTreeItem): # If any other sort of exception happens, show the corresponding # error message, but do not crash! # Note that we _also_ will mark the offending repos as invalid - # It is unfortunate that Python 2.4, which we target does not - # support combined try/except/finally clauses, forcing us - # to duplicate some code here self._valid = False if sri: sri._valid = False diff --git a/tortoisehg/hgqt/repowidget.py b/tortoisehg/hgqt/repowidget.py index b099d5a7c67a142694c139d6027e84e7d222a7e8..cab907b171dd7963dee764e3273a38429937f5eb 100644 --- a/tortoisehg/hgqt/repowidget.py +++ b/tortoisehg/hgqt/repowidget.py @@ -88,7 +88,7 @@ from . import ( tag, thgimport, thgstrip, - topics, + topic, update, visdiff, ) @@ -1261,7 +1261,7 @@ class RepoWidget(QWidget): entry(items, menu, None, enablefuncs['isrev'], _('Boo&kmark...'), 'hg-bookmarks', self.bookmarkRevision) entry(items, menu, 'topic', enablefuncs['isdraftorwd'], _('Top&ic...'), - 'topic', self.topicsRevision) + 'topic', self.topicRevision) entry(items, menu, 'gpg', enablefuncs['fixed'], _('Sig&n...'), 'hg-sign', self.signRevision) entry(items, menu) @@ -1290,7 +1290,7 @@ class RepoWidget(QWidget): submenu = menu.addMenu(_('Change &Phase to')) submenu.triggered.connect(self._changePhaseByMenu) # TODO: filter out hidden names better - for pnum, pname in enumerate(phases.phasenames[:3]): + for pnum, pname in enumerate(phases.cmdphasenames): a = entry(items, submenu, None, enablefuncs['isrev'], pycompat.sysstr(pname)) assert a is not None # help pytype @@ -1858,8 +1858,8 @@ class RepoWidget(QWidget): dlg = bookmark.BookmarkDialog(self._repoagent, self.rev, self) dlg.exec_() - def topicsRevision(self): - dlg = topics.TopicsDialog(self._repoagent, self.rev, self) + def topicRevision(self): + dlg = topic.TopicDialog(self._repoagent, self.rev, self) dlg.exec_() def signRevision(self): diff --git a/tortoisehg/hgqt/revpanel.py b/tortoisehg/hgqt/revpanel.py index 14175d15e53eee229fbaec4b40cf59ef34570cb8..835cd090a18de77758ddcaf11af4a324d89f0c53 100644 --- a/tortoisehg/hgqt/revpanel.py +++ b/tortoisehg/hgqt/revpanel.py @@ -91,15 +91,20 @@ def data_func(widget, item, ctx): return revline_data(tctx) except (error.LookupError, error.RepoLookupError, error.RepoError): return ts - elif item == 'ishead': - if ctx.rev() is None: - ctx = ctx.p1() - childbranches = [cctx.branch() for cctx in ctx.children()] - return ctx.branch() not in childbranches - elif item == 'isclose': - if ctx.rev() is None: - ctx = ctx.p1() - return ctx.extra().get(b'close') is not None + elif item == 'createsnewhead': + # Strictly speaking, amend can create a new head in the case when + # amending a revision which is not a topological head, as then the + # original amended revision is kept alive by its orphan ancestors. + # However, as the original amended revision along with its orphan + # ancestors are eventually going to be evolved, we should not warn the + # user. Instead, we should show that the amend will create orphans. + return not widget.custom['isAmend'] and hglib.createsnewhead(ctx) + elif item == 'createsorphans': + return widget.custom['isAmend'] and any(p.children() + for p in ctx.parents()) + elif item == 'reopensbranchhead': + return any(p.closesbranch() and p.branch() == ctx.branch() + for p in ctx.parents()) elif item == 'predecessors': ctxlist = obsoleteutil.first_known_predecessors(ctx) return format_ctxlist(ctxlist) @@ -196,18 +201,19 @@ def nomarkup(widget, item, value): revnum = qtlib.markup(revnum) return '%s - %s' % (revnum, summary) csets = [] - if item == 'ishead': - if value is False: - if widget.custom['isAmend']: - text = _('Not a head revision.') - return qtlib.markup(text, weight='bold') - else: - text = _('Not a head revision!') - return qtlib.markup(text, fg='red', weight='bold') + if item == 'createsnewhead': + if value is True: + text = _('Creates new head!') + return qtlib.markup(text, fg='red', weight='bold') + raise csinfo.UnknownItem(item) + elif item == 'createsorphans': + if value is True: + text = _('Creates orphans.') + return qtlib.markup(text, weight='bold') raise csinfo.UnknownItem(item) - elif item == 'isclose': + elif item == 'reopensbranchhead': if value is True: - text = _('Head is closed!') + text = _('Reopens closed branch head!') return qtlib.markup(text, fg='red', weight='bold') raise csinfo.UnknownItem(item) for cset in value: @@ -217,9 +223,10 @@ def nomarkup(widget, item, value): csets.append(revline_markup(*cset)) return csets -def ParentWidget(repo): - 'creates a parent rev widget and returns it' +def WDirInfoWidget(repo): + 'creates a wdir info widget and returns it' custom = csinfo.custom(data=data_func, label=label_func, markup=nomarkup) - style = csinfo.panelstyle(contents=('parents', 'ishead', 'isclose'), - selectable=True) + style = csinfo.panelstyle(contents=('parents', 'createsnewhead', + 'createsorphans', 'reopensbranchhead'), + selectable=True) return csinfo.create(repo, style=style, custom=custom) diff --git a/tortoisehg/hgqt/run.py b/tortoisehg/hgqt/run.py index 98b98eb3c9cbe14adde28c9d70ddc6b4905a4550..2cc6c3e6460e1ed7e6f751d75c8073ec1f6c03b1 100644 --- a/tortoisehg/hgqt/run.py +++ b/tortoisehg/hgqt/run.py @@ -223,7 +223,7 @@ def get_lines_from_listfile(filename, isutf8): global _linesutf8 try: if filename == b'-': - lines = [ x.replace(b"\n", b"") for x in pycompat.stdin.readlines() ] + lines = [x.replace(b"\n", b"") for x in procutil.stdin.readlines()] else: fd = open(filename, "r") lines = [ x.replace("\n", "") for x in fd.readlines() ] @@ -1353,15 +1353,15 @@ def thgstatus(ui, *pats, **opts): [(b'r', b'rev', b'', _('revision'))], _('thg topics [-r REV] [NAME]')) def topics(ui, repoagent, *names, **opts): - """add, remove or change a topic""" + """set or clear a topic""" repo = repoagent.rawRepo() if b'topic' not in repo.extensions(): raise error.Abort(_('Please enable the Topic extension first.')) - from tortoisehg.hgqt import topics as topicsmod + from tortoisehg.hgqt import topic as topicmod rev = scmutil.revsingle(repo, opts.get('rev')).rev() if len(names) > 1: raise error.Abort(_('only one new topic name allowed')) - dlg = topicsmod.TopicsDialog(repoagent, rev) + dlg = topicmod.TopicDialog(repoagent, rev) if names: dlg.setTopicName(hglib.tounicode(names[0])) return dlg diff --git a/tortoisehg/hgqt/settings.py b/tortoisehg/hgqt/settings.py index 672fc3a4e0c92bee931815b818629878d84e08ca..dce0ad346e96303d48f7f28ae87dcdd81b6ac4d7 100644 --- a/tortoisehg/hgqt/settings.py +++ b/tortoisehg/hgqt/settings.py @@ -845,7 +845,7 @@ INFO = ( 'Default: False')), _fi(_('New Commit Phase'), 'phases.new-commit', (genDefaultCombo, - [pycompat.sysstr(ph) for ph in phases.phasenames[:3]]), + [pycompat.sysstr(ph) for ph in phases.cmdphasenames]), _('The phase of new commits. Default: draft')), _fi(_('Secret MQ Patches'), 'mq.secret', genBoolRBGroup, _('Make MQ patches secret (instead of draft). ' diff --git a/tortoisehg/hgqt/status.py b/tortoisehg/hgqt/status.py index 4d70f73c6db74e646580085e60d087f729043cc3..ca0146a0af10711dfd7ed9f2477d26db5ab23989 100644 --- a/tortoisehg/hgqt/status.py +++ b/tortoisehg/hgqt/status.py @@ -1068,7 +1068,7 @@ class WctxModel(QAbstractTableModel): # However, for files which have the same status or extension, etc, # we want them to be sorted alphabetically (without taking into account # the case) - # Since Python 2.3 the sort function is guaranteed to be stable. + # The sort function is guaranteed to be stable. # Thus we can perform the sort in two passes: # 1.- Perform a secondary sort by path # 2.- Perform a primary sort by the actual column that we are sorting on diff --git a/tortoisehg/hgqt/tag.py b/tortoisehg/hgqt/tag.py index 78fb0d5cdf84deabb70b5ab25673c75ae21f9111..9fd0f2418c517ee68a26e632c595781b3486fb6c 100644 --- a/tortoisehg/hgqt/tag.py +++ b/tortoisehg/hgqt/tag.py @@ -162,10 +162,8 @@ class TagDialog(QDialog): """ update display on dialog with recent repo data """ cur = self.tagCombo.currentText() - tags = list(self.repo.tags()) - tags.sort(reverse=True) self.tagCombo.clear() - for tag in tags: + for tag in sorted(self.repo.tags(), reverse=True): if tag in (b'tip', b'qbase', b'qtip', b'qparent'): continue self.tagCombo.addItem(hglib.tounicode(tag)) diff --git a/tortoisehg/hgqt/thgstrip.py b/tortoisehg/hgqt/thgstrip.py index 3c98768a6ee638a5fae96e93045d5ea33ffa4984..0bcfc3e9ee6438998d9014734b858cb2d955ebb8 100644 --- a/tortoisehg/hgqt/thgstrip.py +++ b/tortoisehg/hgqt/thgstrip.py @@ -68,9 +68,7 @@ class StripWidget(cmdui.AbstractCmdWidget): for name in hglib.namedbranches(self.repo): combo.addItem(hglib.tounicode(name)) - tags = list(self.repo.tags()) - tags.sort(reverse=True) - for tag in tags: + for tag in sorted(self.repo.tags(), reverse=True): combo.addItem(hglib.tounicode(tag)) ### preview box, contained in scroll area, contains preview grid diff --git a/tortoisehg/hgqt/topics.py b/tortoisehg/hgqt/topic.py similarity index 50% rename from tortoisehg/hgqt/topics.py rename to tortoisehg/hgqt/topic.py index 37d00b94b635da7b05dfb45360b8d975a5aca9f9..0589ce6cdbe2a4fc64978aca964c6cfbb6cc02dd 100644 --- a/tortoisehg/hgqt/topics.py +++ b/tortoisehg/hgqt/topic.py @@ -1,4 +1,4 @@ -# topics.py - Topic dialog for TortoiseHg +# topic.py - Topic dialog for TortoiseHg # # Copyright 2010 Michal De Wildt # @@ -17,6 +17,7 @@ from .qtgui import ( QComboBox, QDialog, QDialogButtonBox, + QFontMetrics, QFormLayout, QFrame, QLabel, @@ -38,10 +39,10 @@ from . import ( qtlib, ) -class TopicsDialog(QDialog): +class TopicDialog(QDialog): def __init__(self, repoagent, rev, parent=None): - super(TopicsDialog, self).__init__(parent) + super(TopicDialog, self).__init__(parent) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self._repoagent = repoagent @@ -63,8 +64,8 @@ class TopicsDialog(QDialog): formwidget.setLayout(form) base.addWidget(formwidget) - if rev: - form.addRow(_('Revision:'), QLabel('%d (%s)' % (rev, repo[rev]))) + self.revLabel = QLabel() # text is set in self.revUpdated() + form.addRow(_('Revision:'), self.revLabel) # topic combo self.topicsCombo = QComboBox() @@ -78,16 +79,14 @@ class TopicsDialog(QDialog): # bottom buttons BB = QDialogButtonBox bbox = QDialogButtonBox() - self.addBtn = bbox.addButton(_('&Add'), BB.ActionRole) - self.removeBtn = bbox.addButton(_('&Remove'), BB.ActionRole) - self.renameBtn = bbox.addButton(_('&Rename'), BB.ActionRole) + self.setBtn = bbox.addButton(_('&Set'), BB.ActionRole) + self.clearBtn = bbox.addButton(_('&Clear'), BB.ActionRole) bbox.addButton(BB.Close) bbox.rejected.connect(self.reject) form.addRow(bbox) - self.addBtn.clicked.connect(self.add_topic) - self.removeBtn.clicked.connect(self.remove_topic) - self.renameBtn.clicked.connect(self.rename_topic) + self.setBtn.clicked.connect(self.set_topic) + self.clearBtn.clicked.connect(self.clear_topic) # horizontal separator self.sep = QFrame() @@ -107,91 +106,46 @@ class TopicsDialog(QDialog): # prepare to show self.clear_status() + self.revUpdated() self.refresh() self._repoagent.repositoryChanged.connect(self.refresh) self.topicsCombo.setFocus() self.topicTextChanged() + def revUpdated(self): + if self.rev is not None: + revText = '%d (%s)' % (self.rev, self.repo[self.rev]) + else: + hasunicodestar = QFontMetrics(self.font()).inFont(u'\u2605') + if hasunicodestar: + # The Unicode symbol is a black star: + revText = u'\u2605 ' + _('Working Directory') + u' \u2605' + else: + revText = '*** ' + _('Working Directory') + ' ***' + self.revLabel.setText(revText) + self.topicsCombo.setEditText(self._current_topic) + @property def repo(self): return self._repoagent.rawRepo() - def _allTopics(self): - return (pycompat.maplist(hglib.tounicode, self.repo._topics) - if self.repo._topics else []) - @pyqtSlot() def refresh(self): - """Update display on dialog with recent repo data""" - # add topics to drop-down list - + """Update drop-down list if repo changed.""" cur = self.topicsCombo.currentText() self.topicsCombo.clear() - self.topicsCombo.addItems(sorted(self._allTopics())) - if cur and not self._is_wd: - self.topicsCombo.setEditText(cur) - elif self._is_wd: - self.topicsCombo.setEditText('') - else: - cs_topic = self._current_topic - if self.repo.currenttopic == cs_topic: - tp = hglib.tounicode(self.repo.currenttopic) - self.topicsCombo.setEditText(tp) - elif cs_topic: - tp = hglib.tounicode(cs_topic) - self.topicsCombo.setEditText(tp) - else: - self.topicTextChanged() + self.topicsCombo.addItems(sorted(map(hglib.tounicode, + self.repo.topics))) + self.topicsCombo.setEditText(cur) @pyqtSlot() def topicTextChanged(self): topic = self.topicsCombo.currentText() - topiclocal = hglib.fromunicode(topic) - current_topic = self._current_topic - if (self.repo._topics and topiclocal in self.repo._topics - and not self._is_wd): - if topiclocal != current_topic: - self.addBtn.setEnabled(False) - self.removeBtn.setEnabled(False) - self.renameBtn.setEnabled(True) - else: - self.addBtn.setEnabled(False) - self.removeBtn.setEnabled(True) - self.renameBtn.setEnabled(False) - else: - self.addBtn.setEnabled(bool(topic)) - self.removeBtn.setEnabled(False) - self.renameBtn.setEnabled(False) - - @property - def _is_wd(self): - try: - ctx = self.repo[self.rev] - except error.FilteredRepoLookupError as exc: - self._set_successor(exc) - ctx = self.repo[self.rev] - return ctx.rev() is None + self.setBtn.setEnabled(bool(topic)) @property def _current_topic(self): - try: - ctx = self.repo[self.rev] - except error.FilteredRepoLookupError as exc: - self._set_successor(exc) - ctx = self.repo[self.rev] - return ctx.topic() - - def _set_successor(self, exc=error.FilteredRepoLookupError): - repo = self.repo.unfiltered() - ctx = repo[self.rev] - if ctx.extinct(): - changes = [x for x in first_known_successors(ctx)] - if changes: - self.rev = changes[0].rev() - else: - raise exc - else: - raise exc + return hglib.tounicode(self.repo[self.rev].topic()) def setTopicName(self, name): self.topicsCombo.setEditText(name) @@ -205,53 +159,33 @@ class TopicsDialog(QDialog): self.status.setHidden(True) self.sep.setHidden(True) - def _runTopics(self, *args, **opts): + def _runTopic(self, *args, **opts): self._finishmsg = opts.pop('finishmsg') - cmdline = hglib.buildcmdargs('topics', *args, **opts) + cmdline = hglib.buildcmdargs('topic', *args, **opts) self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) - sess.commandFinished.connect(self._onTopicsFinished) + sess.commandFinished.connect(self._onTopicFinished) @pyqtSlot(int) - def _onTopicsFinished(self, ret): + def _onTopicFinished(self, ret): if ret == 0: - self.topicsCombo.clearEditText() self.set_status(self._finishmsg, True) else: self.set_status(self._cmdsession.errorString(), False) + repo = self.repo.unfiltered() + ctx = repo[self.rev] + if ctx.extinct(): + changes = [x for x in first_known_successors(ctx)] + if changes: + self.rev = changes[0].rev() + self.revUpdated() @pyqtSlot() - def add_topic(self): - topic = pycompat.unicode(self.topicsCombo.currentText()) - if topic in self._allTopics(): - self.set_status(_('A topic named "%s" already exists') % - topic, False) - return - - finishmsg = _("Topic '%s' has been added") % topic - rev = None - if self.rev: - rev = self.rev - self._runTopics(topic, rev=rev, finishmsg=finishmsg) - - @pyqtSlot() - def remove_topic(self): + def set_topic(self): topic = pycompat.unicode(self.topicsCombo.currentText()) - if topic not in self._allTopics(): - self.set_status(_("Topic '%s' does not exist") % topic, False) - return - - finishmsg = _("Topic '%s' has been removed") % topic - rev = None - if self.rev: - rev = self.rev - self._runTopics(rev=rev, clear=True, finishmsg=finishmsg) + finishmsg = _("Set topic to '%s'") % topic + self._runTopic(topic, rev=self.rev, finishmsg=finishmsg) @pyqtSlot() - def rename_topic(self): - topic = pycompat.unicode(self.topicsCombo.currentText()) - finishmsg = (_("Topic '%s' has been renamed to %s") - % (self._current_topic, topic)) - rev = None - if self.rev: - rev = self.rev - self._runTopics(topic, rev=rev, finishmsg=finishmsg) + def clear_topic(self): + finishmsg = _("Cleared current topic '%s'") % self._current_topic + self._runTopic(rev=self.rev, clear=True, finishmsg=finishmsg) diff --git a/tortoisehg/util/hgdispatch.py b/tortoisehg/util/hgdispatch.py index 2edee83dd8b37213c37a20e66dd21785c74e38fc..77aa1b5dc64e55cd73f7f39f04141847e8d6d244 100644 --- a/tortoisehg/util/hgdispatch.py +++ b/tortoisehg/util/hgdispatch.py @@ -6,6 +6,8 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. +import ssl + from mercurial import encoding, error, extensions, subrepo, util from mercurial import dispatch as dispatchmod @@ -38,20 +40,16 @@ def _dispatch(orig, req): ui.write_err(err + b'\n', label=b'ui.error') except util.urlerr.urlerror as e: err = _('URLError: %s') % encoding.strtolocal(str(e.reason)) - try: - import ssl # Python 2.6 or backport for 2.5 - if isinstance(e.args[0], ssl.SSLError): - parts = encoding.strtolocal(e.args[0].strerror).split(b':') - if len(parts) == 7: - file, line, level, _errno, lib, func, reason = parts - if func == b'SSL3_GET_SERVER_CERTIFICATE': - err = _('SSL: Server certificate verify failed') - elif _errno == b'00000000': - err = _('SSL: unknown error %s:%s') % (file, line) - else: - err = _('SSL error: %s') % reason - except ImportError: - pass + if isinstance(e.args[0], ssl.SSLError): + parts = encoding.strtolocal(e.args[0].strerror).split(b':') + if len(parts) == 7: + file, line, level, _errno, lib, func, reason = parts + if func == b'SSL3_GET_SERVER_CERTIFICATE': + err = _('SSL: Server certificate verify failed') + elif _errno == b'00000000': + err = _('SSL: unknown error %s:%s') % (file, line) + else: + err = _('SSL error: %s') % reason ui.write_err(err + b'\n', label=b'ui.error') return -1 diff --git a/tortoisehg/util/hglib.py b/tortoisehg/util/hglib.py index 9b9af67e7d0a7e8242e3fcc2e880b225d38ef6cc..8f5b2234a89250ae6b94b56700d25c8fd2c79eb0 100644 --- a/tortoisehg/util/hglib.py +++ b/tortoisehg/util/hglib.py @@ -341,7 +341,13 @@ def readgraftstate(repo): return None -readmergestate = mergemod.mergestate.read +try: + from mercurial import mergestate as mergestatemod + mergestate = mergestatemod.mergestate +except (AttributeError, ImportError): + # hg<5.5 b7808443ed6a + mergestate = mergemod.mergestate +readmergestate = mergestate.read def readundodesc(repo): # type: (...) -> Tuple[Text, int] @@ -1296,7 +1302,7 @@ def parsecmdline(cmdline, cwd): lex = shlex.shlex(src, posix=True) decode_token = pycompat.identity else: - # shlex can't process unicode on Python < 2.7.3 + # shlex can't process non-ASCII unicode on Python 2 cmdline = cmdline.encode('utf-8') src = pycompat.bytesio(cmdline) lex = shlex.shlex(src, posix=True) # pytype: disable=wrong-arg-types @@ -1330,3 +1336,12 @@ def parsecmdline(cmdline, cwd): args.extend(p[len(cwd) + 1:] for p in expanded) else: args.append(e) + + +def createsnewhead(ctx, branchheads=None): + branch = ctx.branch() + if branchheads is None: + branchheads = set(ctx.repo().branchheads(branch)) + return branchheads and not any( + p.node() in branchheads and p.branch() == branch for p in ctx.parents() + )