This instance will be upgraded to Heptapod 0.21.0rc2 on 2021-04-16 at 11:00 UTC+2 (a few minutes of down time)

hgignore.py 11.6 KB
Newer Older
1 2 3 4 5 6 7
# hgignore.py - TortoiseHg's dialog for editing .hgignore
#
# Copyright 2010 Steve Borho <steve@borho.org>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2, incorporated herein by reference.

8 9
from __future__ import absolute_import

10 11 12
import os
import re

13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
from .qtcore import (
    QEvent,
    QSettings,
    QTimer,
    Qt,
    pyqtSignal,
)
from .qtgui import (
    QAbstractItemView,
    QComboBox,
    QDialog,
    QDialogButtonBox,
    QGroupBox,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QListWidget,
    QMenu,
    QPushButton,
    QSplitter,
    QVBoxLayout,
)
35

36 37 38 39
from hgext.largefiles import (
    lfutil,
)

40 41 42 43 44 45
from mercurial import (
    commands,
    error,
    match,
    util,
)
46

47 48 49 50 51 52 53 54 55
from ..util import (
    hglib,
    shlib,
)
from ..util.i18n import _
from . import (
    qscilib,
    qtlib,
)
56 57

class HgignoreDialog(QDialog):
58
    'Edit a repository .hgignore file'
59 60 61

    ignoreFilterUpdated = pyqtSignal()

62 63
    contextmenu = None

64
    def __init__(self, repoagent, parent=None, *pats):
65 66
        'Initialize the Dialog'
        QDialog.__init__(self, parent)
67
        self.setWindowFlags(self.windowFlags()
68 69
            & ~Qt.WindowContextHelpButtonHint
            | Qt.WindowMaximizeButtonHint)
70

71
        self._repoagent = repoagent
72
        self.pats = pats
73
        self.setWindowTitle(_('Ignore filter - %s') % repoagent.displayName())
74
        self.setWindowIcon(qtlib.geticon('thg-ignore'))
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90

        vbox = QVBoxLayout()
        self.setLayout(vbox)

        # layer 1
        hbox = QHBoxLayout()
        vbox.addLayout(hbox)
        recombo = QComboBox()
        recombo.addItems([_('Glob'), _('Regexp')])
        hbox.addWidget(recombo)

        le = QLineEdit()
        hbox.addWidget(le, 1)
        le.returnPressed.connect(self.addEntry)

        add = QPushButton(_('Add'))
91
        add.setAutoDefault(False)
92 93 94 95
        add.clicked.connect(self.addEntry)
        hbox.addWidget(add, 0)

        # layer 2
96
        repo = repoagent.rawRepo()
97 98
        hbox = QHBoxLayout()
        vbox.addLayout(hbox)
kiilerix's avatar
kiilerix committed
99
        ignorefiles = [repo.wjoin(b'.hgignore')]
100 101
        for name, value in repo.ui.configitems(b'ui'):
            if name == b'ignore' or name.startswith(b'ignore.'):
102
                ignorefiles.append(util.expandpath(value))
103 104 105 106 107 108 109 110 111

        filecombo = QComboBox()
        hbox.addWidget(filecombo)
        for f in ignorefiles:
            filecombo.addItem(hglib.tounicode(f))
        filecombo.currentIndexChanged.connect(self.fileselect)
        self.ignorefile = ignorefiles[0]

        edit = QPushButton(_('Edit File'))
112
        edit.setAutoDefault(False)
113 114 115 116 117 118 119 120
        edit.clicked.connect(self.editClicked)
        hbox.addWidget(edit)
        hbox.addStretch(1)

        # layer 3 - main widgets
        split = QSplitter()
        vbox.addWidget(split, 1)

121
        ignoregb = QGroupBox()
122 123 124 125 126 127
        ivbox = QVBoxLayout()
        ignoregb.setLayout(ivbox)
        lbl = QLabel(_('<b>Ignore Filter</b>'))
        ivbox.addWidget(lbl)
        split.addWidget(ignoregb)

128
        unknowngb = QGroupBox()
129 130 131 132 133 134 135 136
        uvbox = QVBoxLayout()
        unknowngb.setLayout(uvbox)
        lbl = QLabel(_('<b>Untracked Files</b>'))
        uvbox.addWidget(lbl)
        split.addWidget(unknowngb)

        ignorelist = QListWidget()
        ivbox.addWidget(ignorelist)
137
        ignorelist.setSelectionMode(QAbstractItemView.ExtendedSelection)
138 139
        unknownlist = QListWidget()
        uvbox.addWidget(unknownlist)
140
        unknownlist.setSelectionMode(QAbstractItemView.ExtendedSelection)
141 142
        unknownlist.currentTextChanged.connect(self.setGlobFilter)
        unknownlist.setContextMenuPolicy(Qt.CustomContextMenu)
Steve Borho's avatar
Steve Borho committed
143
        unknownlist.customContextMenuRequested.connect(self.menuRequest)
144
        unknownlist.itemDoubleClicked.connect(self.unknownDoubleClicked)
145
        lbl = QLabel(_('Backspace or Del to remove row(s)'))
146
        ivbox.addWidget(lbl)
147 148 149

        # layer 4 - dialog buttons
        BB = QDialogButtonBox
150
        bb = QDialogButtonBox(BB.Close)
151
        bb.button(BB.Close).setAutoDefault(False)
Steve Borho's avatar
Steve Borho committed
152 153
        bb.accepted.connect(self.accept)
        bb.rejected.connect(self.reject)
154 155 156 157 158 159
        vbox.addWidget(bb)
        self.bb = bb

        le.setFocus()
        self.le, self.recombo, self.filecombo = le, recombo, filecombo
        self.ignorelist, self.unknownlist = ignorelist, unknownlist
160
        ignorelist.installEventFilter(self)
161 162 163
        QTimer.singleShot(0, self.refresh)

        s = QSettings()
164
        self.restoreGeometry(qtlib.readByteArray(s, 'hgignore/geom'))
165

166 167 168 169
    @property
    def repo(self):
        return self._repoagent.rawRepo()

170 171 172 173 174 175 176
    def eventFilter(self, obj, event):
        if obj != self.ignorelist:
            return False
        if event.type() != QEvent.KeyPress:
            return False
        elif event.key() not in (Qt.Key_Backspace, Qt.Key_Delete):
            return False
177
        if obj.currentRow() < 0:
178
            return False
179 180
        for idx in sorted(obj.selectedIndexes(), reverse=True):
            self.ignorelines.pop(idx.row())
181 182 183 184
        self.writeIgnoreFile()
        self.refresh()
        return True

Steve Borho's avatar
Steve Borho committed
185
    def menuRequest(self, point):
186
        'context menu request for unknown list'
187
        point = self.unknownlist.viewport().mapToGlobal(point)
188 189 190
        selected = [self.lclunknowns[i.row()]
                    for i in sorted(self.unknownlist.selectedIndexes())]
        if len(selected) == 0:
191
            return
192 193 194 195 196
        if not self.contextmenu:
            self.contextmenu = QMenu(self)
            self.contextmenu.setTitle(_('Add ignore filter...'))
        else:
            self.contextmenu.clear()
197 198 199 200 201 202 203 204 205 206 207
        filters = []
        if len(selected) == 1:
            local = selected[0]
            filters.append([local])
            dirname = os.path.dirname(local)
            while dirname:
                filters.append([dirname])
                dirname = os.path.dirname(dirname)
            base, ext = os.path.splitext(local)
            if ext:
                filters.append(['*'+ext])
208
                filters.append(['**'+ext])
209 210
        else:
            filters.append(selected)
211
        for f in filters:
212 213 214 215
            n = len(f) == 1 and f[0] or _('selected files')
            a = self.contextmenu.addAction(_('Ignore ') + hglib.tounicode(n))
            a._patterns = f
            a.triggered.connect(self.insertFilters)
216
        self.contextmenu.exec_(point)
217

218
    def unknownDoubleClicked(self, item):
219
        self.insertFilters([hglib.fromunicode(item.text())])
220

221 222 223
    def insertFilters(self, pats=False, isregexp=False):
        if pats is False:
            pats = self.sender()._patterns
224 225 226 227 228
        h = isregexp and 'syntax: regexp' or 'syntax: glob'
        if h in self.ignorelines:
            l = self.ignorelines.index(h)
            for i, line in enumerate(self.ignorelines[l+1:]):
                if line.startswith('syntax:'):
229 230
                    for pat in pats:
                        self.ignorelines.insert(l+i+1, pat)
231 232
                    break
            else:
233
                self.ignorelines.extend(pats)
234
        else:
235
            self.ignorelines.append(h)
236
            self.ignorelines.extend(pats)
237 238 239
        self.writeIgnoreFile()
        self.refresh()

240 241 242 243
    def setGlobFilter(self, qstr):
        'user selected an unknown file; prep a glob filter'
        self.recombo.setCurrentIndex(0)
        self.le.setText(qstr)
244 245 246

    def fileselect(self):
        'user selected another ignore file'
Yuki KODAMA's avatar
Yuki KODAMA committed
247
        self.ignorefile = hglib.fromunicode(self.filecombo.currentText())
248 249 250
        self.refresh()

    def editClicked(self):
251
        if qscilib.fileEditor(self.ignorefile) == QDialog.Accepted:
252 253 254 255
            self.refresh()

    def addEntry(self):
        newfilter = hglib.fromunicode(self.le.text()).strip()
256
        if newfilter == b'':
257
            return
258
        self.le.clear()
259
        if self.recombo.currentIndex() == 0:
260
            test = b'glob:' + newfilter
261
            try:
262
                match.match(self.repo.root, b'', [], [test])
263
                self.insertFilters([newfilter], False)
264
            except error.Abort as inst:
265 266 267 268
                qtlib.WarningMsgBox(_('Invalid glob expression'), str(inst),
                                    parent=self)
                return
        else:
269
            test = b'relre:' + newfilter
270
            try:
271
                match.match(self.repo.root, b'', [], [test])
272
                re.compile(test)
273
                self.insertFilters([newfilter], True)
274
            except (error.Abort, re.error) as inst:
275 276 277 278 279
                qtlib.WarningMsgBox(_('Invalid regexp expression'), str(inst),
                                    parent=self)
                return

    def refresh(self):
280
        try:
281 282
            with open(self.ignorefile, 'rb') as fp:
                l = fp.readlines()
283
            self.doseoln = l[0].endswith(b'\r\n')
284 285 286 287 288 289
        except (IOError, ValueError, IndexError):
            self.doseoln = os.name == 'nt'
            l = []
        self.ignorelines = [line.strip() for line in l]
        self.ignorelist.clear()

290
        uni = hglib.tounicode
291

292 293
        self.ignorelist.addItems([uni(l) for l in self.ignorelines])

294
        try:
Steve Borho's avatar
Steve Borho committed
295
            self.repo.thginvalidate()
296 297
            with lfutil.lfstatus(self.repo):
                self.lclunknowns = self.repo.status(unknown=True).unknown
298
        except (EnvironmentError, error.RepoError) as e:
299 300
            qtlib.WarningMsgBox(_('Unable to read repository status'),
                                uni(str(e)), parent=self)
301
        except error.Abort as e:
302 303 304 305 306 307
            if e.hint:
                err = _('%s (hint: %s)') % (uni(str(e)), uni(e.hint))
            else:
                err = uni(str(e))
            qtlib.WarningMsgBox(_('Unable to read repository status'),
                                err, parent=self)
308
            self.lclunknowns = []
309
            return
310

311
        if not self.pats:
312 313
            try:
                self.pats = [self.lclunknowns[i.row()]
314
                         for i in self.unknownlist.selectedIndexes()]
315 316
            except IndexError:
                self.pats = []
317
        self.unknownlist.clear()
318
        self.unknownlist.addItems([uni(u) for u in self.lclunknowns])
319 320 321
        for i, u in enumerate(self.lclunknowns):
            if u in self.pats:
                item = self.unknownlist.item(i)
322
                item.setSelected(True)
323
                self.unknownlist.setCurrentItem(item)
324
                self.le.setText(u)
325
        self.pats = []
326 327 328 329

    def writeIgnoreFile(self):
        eol = self.doseoln and '\r\n' or '\n'
        out = eol.join(self.ignorelines) + eol
330
        hasignore = os.path.exists(self.repo.vfs.join(self.ignorefile))
331 332

        try:
333
            f = util.atomictempfile(self.ignorefile, b'wb', createmode=None)
334
            f.write(out)
335
            f.close()
336 337 338 339 340 341 342
            if not hasignore:
                ret = qtlib.QuestionMsgBox(_('New file created'),
                                           _('TortoiseHg has created a new '
                                             '.hgignore file.  Would you like to '
                                             'add this file to the source code '
                                             'control repository?'), parent=self)
                if ret:
343
                    commands.add(hglib.loadui(), self.repo, self.ignorefile)
344
            shlib.shell_notify([self.ignorefile])
Steve Borho's avatar
Steve Borho committed
345
            self.ignoreFilterUpdated.emit()
346
        except EnvironmentError as e:
347 348 349 350 351 352 353 354 355 356 357 358
            qtlib.WarningMsgBox(_('Unable to write .hgignore file'),
                                hglib.tounicode(str(e)), parent=self)

    def accept(self):
        s = QSettings()
        s.setValue('hgignore/geom', self.saveGeometry())
        QDialog.accept(self)

    def reject(self):
        s = QSettings()
        s.setValue('hgignore/geom', self.saveGeometry())
        QDialog.reject(self)