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)

archive.py 13.4 KB
Newer Older
Johan Samyn's avatar
Johan Samyn committed
1 2 3 4 5 6 7 8
# archive.py - TortoiseHg's dialog for archiving a repo revision
#
# Copyright 2009 Emmanuel Rosa <goaway1000@gmail.com>
# Copyright 2010 Johan Samyn <johan.samyn@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.

9 10
from __future__ import absolute_import

Johan Samyn's avatar
Johan Samyn committed
11 12
import os

13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
from .qtcore import (
    pyqtSlot,
)
from .qtgui import (
    QButtonGroup,
    QCheckBox,
    QComboBox,
    QFileDialog,
    QFormLayout,
    QHBoxLayout,
    QLineEdit,
    QPushButton,
    QRadioButton,
    QSizePolicy,
    QVBoxLayout,
)
Johan Samyn's avatar
Johan Samyn committed
29

30 31
from mercurial import (
    error,
32
    pycompat,
33 34
    scmutil,
)
Johan Samyn's avatar
Johan Samyn committed
35

36 37 38 39 40 41 42
from ..util import hglib
from ..util.i18n import _
from . import (
    cmdcore,
    cmdui,
    qtlib,
)
Johan Samyn's avatar
Johan Samyn committed
43 44 45

WD_PARENT = _('= Working Directory Parent =')

46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
_ARCHIVE_TYPES = [
    {'type': 'files', 'ext': '', 'label': _('Directory of files'),
     'desc': _('Directory of files')},
    {'type': 'tar', 'ext': '.tar', 'label': _('Tar archives'),
     'desc': _('Uncompressed tar archive')},
    {'type': 'tbz2', 'ext': '.tar.bz2', 'label': _('Bzip2 tar archives'),
     'desc': _('Tar archive compressed using bzip2')},
    {'type': 'tgz', 'ext': '.tar.gz', 'label': _('Gzip tar archives'),
     'desc': _('Tar archive compressed using gzip')},
    {'type': 'uzip', 'ext': '.zip', 'label': _('Zip archives'),
     'desc': _('Uncompressed zip archive')},
    {'type': 'zip', 'ext': '.zip', 'label': _('Zip archives'),
     'desc': _('Zip archive compressed using deflate')},
    ]

61
class ArchiveWidget(cmdui.AbstractCmdWidget):
62
    """Command widget to archive a particular Mercurial revision"""
63

64 65 66 67
    _archive_content_all_files = 0
    _archive_content_touched_files = 1
    _archive_content_touched_since = 2

68
    def __init__(self, repoagent, rev, parent=None, minrev=None):
69
        super(ArchiveWidget, self).__init__(parent)
70
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
71
        self._repoagent = repoagent
72
        if minrev is None:
73
            minrev = rev
74 75 76
            archive_since = False
        else:
            archive_since = True
77 78 79

        possibleroots = []
        if minrev is not None:
80
            parents = scmutil.revsymbol(self.repo, str(minrev)).parents()
81 82
            if parents:
                for p in parents:
83
                    possibleroots.append(u'%d' % p.rev())
84 85
            else:
                possibleroots.append('null')
Johan Samyn's avatar
Johan Samyn committed
86

87
        form = QFormLayout()
88 89
        form.setContentsMargins(0, 0, 0, 0)
        self.setLayout(form)
Johan Samyn's avatar
Johan Samyn committed
90

91 92
        ### content selection
        ## revision selection
Johan Samyn's avatar
Johan Samyn committed
93 94 95
        self.rev_combo = QComboBox()
        self.rev_combo.setEditable(True)
        self.rev_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
96
        form.addRow(_('Revision:'), self.rev_combo)
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114

        ### content type
        ## Selection of the content mode
        self.content_mode = QButtonGroup(self)
        # All files
        box = QVBoxLayout()
        w = QRadioButton(_('All files in this revision'), self)
        self.content_mode.addButton(w, self._archive_content_all_files)
        box.addWidget(w)
        # Touched in this revision
        w = QRadioButton(_('Only files modified/created in this revision'), self)
        self.content_mode.addButton(w, self._archive_content_touched_files)
        box.addWidget(w)
        # Touched since
        sincebox = QHBoxLayout()
        self.rootrev_combo = QComboBox(self)
        self.rootrev_combo.setEditable(True)
        self.rootrev_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
115 116 117
        if minrev is not None:
            for text in possibleroots:
                self.rootrev_combo.addItem(text)
118 119 120 121 122 123
            self.rootrev_combo.setCurrentIndex(0)
        w = QRadioButton(_('Only files modified/created since:'), self)
        self.content_mode.addButton(w, self._archive_content_touched_since)
        sincebox.addWidget(w)
        sincebox.addWidget(self.rootrev_combo)
        box.addLayout(sincebox)
124
        if archive_since:
125 126
            # default to "touched since X" if the input is a range
            self.content_mode.button(self._archive_content_touched_since).setChecked(True)
127 128
        else:
            self.content_mode.button(self._archive_content_all_files).setChecked(True)
129 130 131 132
        form.addRow(_('Archive Content:'), box)

        # subrepository option
        self.subrepos_chk = QCheckBox(_('Recurse into subrepositories'))
133
        form.addRow('', self.subrepos_chk)
Johan Samyn's avatar
Johan Samyn committed
134

Johan Samyn's avatar
Johan Samyn committed
135
        # selecting a destination
Johan Samyn's avatar
Johan Samyn committed
136 137 138 139
        self.dest_edit = QLineEdit()
        self.dest_edit.setMinimumWidth(300)
        self.dest_btn = QPushButton(_('Browse...'))
        self.dest_btn.setAutoDefault(False)
140 141 142 143
        box = QHBoxLayout()
        box.addWidget(self.dest_edit)
        box.addWidget(self.dest_btn)
        form.addRow(_('Destination path:'), box)
Johan Samyn's avatar
Johan Samyn committed
144

Johan Samyn's avatar
Johan Samyn committed
145
        # archive type selection
146
        self._typesradios = QButtonGroup(self)
147
        box = QVBoxLayout()
148 149
        for i, spec in enumerate(_ARCHIVE_TYPES):
            w = QRadioButton(spec['desc'], self)
150 151
            self._typesradios.addButton(w, i)
            box.addWidget(w)
152
        form.addRow(_('Archive types:'), box)
Johan Samyn's avatar
Johan Samyn committed
153 154 155 156

        # some extras
        self.hgcmd_txt = QLineEdit()
        self.hgcmd_txt.setReadOnly(True)
157
        form.addRow(_('Hg command:'), self.hgcmd_txt)
Johan Samyn's avatar
Johan Samyn committed
158 159 160

        # set default values
        self.prevtarget = None
161
        self.rev_combo.addItem(WD_PARENT)
162
        self.rev_combo.addItems(pycompat.maplist(hglib.tounicode,
163
                                    hglib.namedbranches(self.repo)))
164
        for t in sorted(self.repo.tags(), reverse=True):
165
            self.rev_combo.addItem(hglib.tounicode(t))
166 167
        if rev:
            text = hglib.tounicode(str(rev))
168
            selectindex = self.rev_combo.findText(text)
169 170 171
            if selectindex >= 0:
                self.rev_combo.setCurrentIndex(selectindex)
            else:
172
                self.rev_combo.insertItem(0, text)
173
                self.rev_combo.setCurrentIndex(0)
174
        self.rev_combo.setMaxVisibleItems(self.rev_combo.count())
175
        self.subrepos_chk.setChecked(self.get_subrepos_present())
176
        self.dest_edit.setText(hglib.tounicode(self.repo.root))
177
        self._typesradios.button(0).setChecked(True)
Johan Samyn's avatar
Johan Samyn committed
178 179
        self.update_path()

180
        # connecting slots
181
        self.dest_edit.textEdited.connect(self.compose_command)
Yuya Nishihara's avatar
Yuya Nishihara committed
182
        self.rev_combo.editTextChanged.connect(self.rev_combo_changed)
183
        self.dest_btn.clicked.connect(self.browse_clicked)
184 185
        self.content_mode.buttonClicked.connect(self.compose_command)
        self.rootrev_combo.editTextChanged.connect(self.compose_command)
186
        self.subrepos_chk.toggled.connect(self.compose_command)
187
        self._typesradios.buttonClicked.connect(self.update_path)
188

189 190 191 192
    @property
    def repo(self):
        return self._repoagent.rawRepo()

193
    def rev_combo_changed(self):
194
        self.subrepos_chk.setChecked(self.get_subrepos_present())
Johan Samyn's avatar
Johan Samyn committed
195 196 197 198
        self.update_path()

    def browse_clicked(self):
        """Select the destination directory or file"""
199
        dest = pycompat.unicode(self.dest_edit.text())
Johan Samyn's avatar
Johan Samyn committed
200 201 202 203 204 205
        if not os.path.exists(dest):
            dest = os.path.dirname(dest)
        select = self.get_selected_archive_type()
        FD = QFileDialog
        if select['type'] == 'files':
            caption = _('Select Destination Folder')
206
            filter = ''
Johan Samyn's avatar
Johan Samyn committed
207
        else:
208
            caption = _('Select Destination File')
Johan Samyn's avatar
Johan Samyn committed
209
            ext = '*' + select['ext']
210
            filter = ';;'.join(['%s (%s)' % (select['label'], ext),
211
                                _('All files (*)')])
212 213
        response, _filter = FD.getSaveFileName(
            self, caption, dest, filter, None, FD.ReadOnly)
Johan Samyn's avatar
Johan Samyn committed
214 215 216 217
        if response:
            self.dest_edit.setText(response)
            self.update_path()

218 219
    def get_subrepos_present(self):
        rev = self.get_selected_rev()
220
        try:
221
            ctx = scmutil.revsymbol(self.repo, rev)
222 223
        except (error.LookupError, error.RepoLookupError):
            return False
224
        return '.hgsubstate' in ctx
225 226 227 228 229 230 231 232 233

    def get_selected_rev(self):
        rev = self.rev_combo.currentText()
        if rev == WD_PARENT:
            rev = '.'
        else:
            rev = hglib.fromunicode(rev)
        return rev

234 235 236 237
    def get_selected_rootrev(self):
        rev = self.rootrev_combo.currentText()
        return hglib.fromunicode(rev)

Johan Samyn's avatar
Johan Samyn committed
238 239
    def get_selected_archive_type(self):
        """Return a dictionary describing the selected archive type"""
240
        return _ARCHIVE_TYPES[self._typesradios.checkedId()]
Johan Samyn's avatar
Johan Samyn committed
241 242 243 244 245 246 247 248 249

    def update_path(self):
        def remove_ext(path):
            for ext in ('.tar', '.tar.bz2', '.tar.gz', '.zip'):
                if path.endswith(ext):
                    return path.replace(ext, '')
            return path
        def remove_rev(path):
            l = ''
250
            for i in pycompat.xrange(self.rev_combo.count() - 1):
251
                l += pycompat.unicode(self.rev_combo.itemText(i))
Johan Samyn's avatar
Johan Samyn committed
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
            revs = [rev[0] for rev in l]
            revs.append(wdrev)
            if not self.prevtarget is None:
                revs.append(self.prevtarget)
            for rev in ['_' + rev for rev in revs]:
                if path.endswith(rev):
                    return path.replace(rev, '')
            return path
        def add_rev(path, rev):
            return '%s_%s' % (path, rev)
        def add_ext(path):
            select = self.get_selected_archive_type()
            if select['type'] != 'files':
                path += select['ext']
            return path
267
        text = pycompat.unicode(self.rev_combo.currentText())
Johan Samyn's avatar
Johan Samyn committed
268
        if len(text) == 0:
269
            self.commandChanged.emit()
Johan Samyn's avatar
Johan Samyn committed
270
            return
271
        wdrev = str(self.repo[b'.'].rev())
Johan Samyn's avatar
Johan Samyn committed
272 273 274 275
        if text == WD_PARENT:
            text = wdrev
        else:
            try:
276
                scmutil.revsymbol(self.repo, hglib.fromunicode(text))
Johan Samyn's avatar
Johan Samyn committed
277
            except (error.RepoError, error.LookupError):
278
                self.commandChanged.emit()
Johan Samyn's avatar
Johan Samyn committed
279
                return
280
        path = pycompat.unicode(self.dest_edit.text())
Johan Samyn's avatar
Johan Samyn committed
281 282
        path = remove_ext(path)
        path = remove_rev(path)
283
        path = add_rev(path, text)
Johan Samyn's avatar
Johan Samyn committed
284 285 286
        path = add_ext(path)
        self.dest_edit.setText(path)
        self.prevtarget = text
287
        self.compose_command()
Johan Samyn's avatar
Johan Samyn committed
288

289 290
    @pyqtSlot()
    def compose_command(self):
291 292 293 294 295
        content = self.content_mode.checkedId()
        targetrev = hglib.tounicode(self.get_selected_rev())
        if content == self._archive_content_all_files:
            incl = None
        elif content == self._archive_content_touched_files:
296
            incl = 'set:added() or modified()'
297
        elif content == self._archive_content_touched_since:
298
            expr = 'set:status(%s, %s, added() or modified())'
299
            rootrev = hglib.tounicode(self.get_selected_rootrev())
300
            incl = hglib.formatfilespec(expr, rootrev, targetrev)
301
        else:
302
            assert False
303
        cmdline = hglib.buildcmdargs('archive', self.dest_edit.text(),
304
                                     r=targetrev,
305 306
                                     S=self.subrepos_chk.isChecked(), I=incl,
                                     t=self.get_selected_archive_type()['type'])
307
        self.hgcmd_txt.setText('hg ' + hglib.prettifycmdline(cmdline))
308
        self.commandChanged.emit()
Johan Samyn's avatar
Johan Samyn committed
309 310
        return cmdline

311
    def canRunCommand(self):
312 313 314 315
        rev = self.get_selected_rev()
        if not rev or not self.dest_edit.text():
            return False
        try:
316
            return scmutil.isrevsymbol(self.repo, rev)
317 318 319
        except error.LookupError:
            # ambiguous changeid
            return False
320

321
    def runCommand(self):
Johan Samyn's avatar
Johan Samyn committed
322 323
        # verify input
        type = self.get_selected_archive_type()['type']
324
        dest = pycompat.unicode(self.dest_edit.text())
Johan Samyn's avatar
Johan Samyn committed
325 326 327
        if os.path.exists(dest):
            if type == 'files':
                if os.path.isfile(dest):
328
                    qtlib.WarningMsgBox(_('Duplicate Name'),
Yuki KODAMA's avatar
Yuki KODAMA committed
329
                            _('The destination "%s" already exists as '
330
                              'a file!') % dest)
331
                    return cmdcore.nullCmdSession()
Johan Samyn's avatar
Johan Samyn committed
332
                elif os.listdir(dest):
333
                    if not qtlib.QuestionMsgBox(_('Confirm Overwrite'),
334 335 336
                                 _('The directory "%s" is not empty!\n\n'
                                   'Do you want to overwrite it?') % dest,
                                 parent=self):
337
                        return cmdcore.nullCmdSession()
Johan Samyn's avatar
Johan Samyn committed
338 339
            else:
                if os.path.isfile(dest):
340
                    if not qtlib.QuestionMsgBox(_('Confirm Overwrite'),
341 342 343
                                 _('The file "%s" already exists!\n\n'
                                   'Do you want to overwrite it?') % dest,
                                 parent=self):
344
                        return cmdcore.nullCmdSession()
Johan Samyn's avatar
Johan Samyn committed
345
                else:
346
                    qtlib.WarningMsgBox(_('Duplicate Name'),
Yuki KODAMA's avatar
Yuki KODAMA committed
347
                          _('The destination "%s" already exists as '
348
                            'a folder!') % dest)
349
                    return cmdcore.nullCmdSession()
350

351
        cmdline = self.compose_command()
352 353
        return self._repoagent.runCommand(cmdline, self)

354

355
def createArchiveDialog(repoagent, rev=None, parent=None, minrev=None):
356
    dlg = cmdui.CmdControlDialog(parent)
357
    dlg.setWindowTitle(_('Archive - %s') % repoagent.displayName())
358
    dlg.setWindowIcon(qtlib.geticon('hg-archive'))
359 360
    dlg.setObjectName('archive')
    dlg.setRunButtonText(_('&Archive'))
361
    dlg.setCommandWidget(ArchiveWidget(repoagent, rev, dlg, minrev))
362
    return dlg