|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog to offer the worktree management functionality. |
|
8 """ |
|
9 |
|
10 import os |
|
11 |
|
12 from PyQt6.QtCore import QDateTime, QProcess, QSize, Qt, QTime, QTimer, pyqtSlot |
|
13 from PyQt6.QtWidgets import ( |
|
14 QAbstractButton, |
|
15 QDialog, |
|
16 QDialogButtonBox, |
|
17 QHeaderView, |
|
18 QInputDialog, |
|
19 QLineEdit, |
|
20 QMenu, |
|
21 QTreeWidgetItem, |
|
22 QWidget, |
|
23 ) |
|
24 |
|
25 from eric7 import Preferences |
|
26 from eric7.EricGui import EricPixmapCache |
|
27 from eric7.EricWidgets import EricMessageBox, EricPathPickerDialog |
|
28 |
|
29 from .GitDialog import GitDialog |
|
30 from .Ui_GitWorktreeDialog import Ui_GitWorktreeDialog |
|
31 |
|
32 |
|
33 class GitWorktreeDialog(QWidget, Ui_GitWorktreeDialog): |
|
34 """ |
|
35 Class implementing a dialog to offer the worktree management functionality. |
|
36 """ |
|
37 |
|
38 StatusRole = Qt.ItemDataRole.UserRole |
|
39 |
|
40 def __init__(self, vcs, parent=None): |
|
41 """ |
|
42 Constructor |
|
43 |
|
44 @param vcs reference to the vcs object |
|
45 @type Git |
|
46 @param parent reference to the parent widget (defaults to None) |
|
47 @type QWidget (optional) |
|
48 """ |
|
49 super().__init__(parent) |
|
50 self.setupUi(self) |
|
51 |
|
52 self.__nameColumn = 0 |
|
53 self.__pathColumn = 1 |
|
54 self.__commitColumn = 2 |
|
55 self.__branchColumn = 3 |
|
56 |
|
57 self.worktreeList.header().setSortIndicator(0, Qt.SortOrder.AscendingOrder) |
|
58 |
|
59 self.__refreshButton = self.buttonBox.addButton( |
|
60 self.tr("Refresh"), QDialogButtonBox.ButtonRole.ActionRole |
|
61 ) |
|
62 self.__refreshButton.setToolTip(self.tr("Press to refresh the status display")) |
|
63 self.__refreshButton.setEnabled(False) |
|
64 self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(False) |
|
65 self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setDefault(True) |
|
66 |
|
67 self.__vcs = vcs |
|
68 self.__process = QProcess() |
|
69 self.__process.finished.connect(self.__procFinished) |
|
70 self.__process.readyReadStandardOutput.connect(self.__readStdout) |
|
71 self.__process.readyReadStandardError.connect(self.__readStderr) |
|
72 |
|
73 self.__initActionsMenu() |
|
74 |
|
75 def __initActionsMenu(self): |
|
76 """ |
|
77 Private method to initialize the actions menu. |
|
78 """ |
|
79 self.__actionsMenu = QMenu() |
|
80 self.__actionsMenu.setTearOffEnabled(True) |
|
81 self.__actionsMenu.setToolTipsVisible(True) |
|
82 self.__actionsMenu.aboutToShow.connect(self.__showActionsMenu) |
|
83 |
|
84 self.__addAct = self.__actionsMenu.addAction( |
|
85 self.tr("Add..."), self.__worktreeAdd |
|
86 ) |
|
87 self.__addAct.setToolTip(self.tr("Add a new linked worktree")) |
|
88 self.__actionsMenu.addSeparator() |
|
89 self.__lockAct = self.__actionsMenu.addAction( |
|
90 self.tr("Lock..."), self.__worktreeLock |
|
91 ) |
|
92 self.__lockAct.setToolTip(self.tr("Lock the selected worktree")) |
|
93 self.__unlockAct = self.__actionsMenu.addAction( |
|
94 self.tr("Unlock"), self.__worktreeUnlock |
|
95 ) |
|
96 self.__unlockAct.setToolTip(self.tr("Unlock the selected worktree")) |
|
97 self.__actionsMenu.addSeparator() |
|
98 self.__moveAct = self.__actionsMenu.addAction( |
|
99 self.tr("Move..."), self.__worktreeMove |
|
100 ) |
|
101 self.__moveAct.setToolTip( |
|
102 self.tr("Move the selected worktree to a new location") |
|
103 ) |
|
104 self.__actionsMenu.addSeparator() |
|
105 self.__removeAct = self.__actionsMenu.addAction( |
|
106 self.tr("Remove"), self.__worktreeRemove |
|
107 ) |
|
108 self.__removeAct.setToolTip(self.tr("Remove the selected worktree")) |
|
109 self.__removeForcedAct = self.__actionsMenu.addAction( |
|
110 self.tr("Forced Remove"), self.__worktreeRemoveForced |
|
111 ) |
|
112 self.__removeForcedAct.setToolTip( |
|
113 self.tr("Remove the selected worktree forcefully") |
|
114 ) |
|
115 self.__actionsMenu.addSeparator() |
|
116 self.__pruneAct = self.__actionsMenu.addAction( |
|
117 self.tr("Prune..."), self.__worktreePrune |
|
118 ) |
|
119 self.__pruneAct.setToolTip(self.tr("Prune worktree information")) |
|
120 self.__actionsMenu.addSeparator() |
|
121 self.__repairAct = self.__actionsMenu.addAction( |
|
122 self.tr("Repair"), self.__worktreeRepair |
|
123 ) |
|
124 self.__repairAct.setToolTip(self.tr("Repair worktree administrative files")) |
|
125 self.__repairMultipleAct = self.__actionsMenu.addAction( |
|
126 self.tr("Repair Multiple"), self.__worktreeRepairMultiple |
|
127 ) |
|
128 self.__repairMultipleAct.setToolTip( |
|
129 self.tr("Repair administrative files of multiple worktrees") |
|
130 ) |
|
131 |
|
132 self.actionsButton.setIcon(EricPixmapCache.getIcon("actionsToolButton")) |
|
133 self.actionsButton.setMenu(self.__actionsMenu) |
|
134 |
|
135 def closeEvent(self, e): |
|
136 """ |
|
137 Protected slot implementing a close event handler. |
|
138 |
|
139 @param e close event (QCloseEvent) |
|
140 """ |
|
141 if ( |
|
142 self.__process is not None |
|
143 and self.__process.state() != QProcess.ProcessState.NotRunning |
|
144 ): |
|
145 self.__process.terminate() |
|
146 QTimer.singleShot(2000, self.__process.kill) |
|
147 self.__process.waitForFinished(3000) |
|
148 |
|
149 self.__vcs.getPlugin().setPreferences( |
|
150 "WorktreeDialogGeometry", self.saveGeometry() |
|
151 ) |
|
152 |
|
153 e.accept() |
|
154 |
|
155 def show(self): |
|
156 """ |
|
157 Public slot to show the dialog. |
|
158 """ |
|
159 super().show() |
|
160 |
|
161 geom = self.__vcs.getPlugin().getPreferences("WorktreeDialogGeometry") |
|
162 if geom.isEmpty(): |
|
163 s = QSize(900, 600) |
|
164 self.resize(s) |
|
165 else: |
|
166 self.restoreGeometry(geom) |
|
167 |
|
168 def __resort(self): |
|
169 """ |
|
170 Private method to resort the tree. |
|
171 """ |
|
172 self.worktreeList.sortItems( |
|
173 self.worktreeList.sortColumn(), |
|
174 self.worktreeList.header().sortIndicatorOrder(), |
|
175 ) |
|
176 |
|
177 def __resizeColumns(self): |
|
178 """ |
|
179 Private method to resize the list columns. |
|
180 """ |
|
181 self.worktreeList.header().resizeSections( |
|
182 QHeaderView.ResizeMode.ResizeToContents |
|
183 ) |
|
184 self.worktreeList.header().setStretchLastSection(True) |
|
185 |
|
186 def __generateItem(self, dataLines): |
|
187 """ |
|
188 Private method to generate a worktree entry with the given data. |
|
189 |
|
190 @param dataLines lines extracted from the git worktree list output |
|
191 with porcelain format |
|
192 @type list of str |
|
193 """ |
|
194 checkoutPath = worktreeName = commit = branch = status = "" |
|
195 iconName = tooltip = "" |
|
196 for line in dataLines: |
|
197 if " " in line: |
|
198 option, value = line.split(None, 1) |
|
199 else: |
|
200 option, value = line, "" |
|
201 |
|
202 if option == "worktree": |
|
203 checkoutPath = value |
|
204 worktreeName = os.path.basename(value) |
|
205 elif option == "HEAD": |
|
206 commit = value[: self.__commitIdLength] |
|
207 elif option == "branch": |
|
208 branch = value.rsplit("/", 1)[-1] |
|
209 elif option == "bare": |
|
210 branch = self.tr("(bare)") |
|
211 elif option == "detached": |
|
212 branch = self.tr("(detached HEAD)") |
|
213 elif option == "prunable": |
|
214 iconName = "trash" |
|
215 tooltip = value |
|
216 status = option |
|
217 elif option == "locked": |
|
218 iconName = "locked" |
|
219 tooltip = value |
|
220 status = option |
|
221 |
|
222 itm = QTreeWidgetItem( |
|
223 self.worktreeList, [worktreeName, checkoutPath, commit, branch] |
|
224 ) |
|
225 if iconName: |
|
226 itm.setIcon(0, EricPixmapCache.getIcon(iconName)) |
|
227 if tooltip: |
|
228 itm.setToolTip(0, tooltip) |
|
229 |
|
230 if self.worktreeList.topLevelItemCount() == 1: |
|
231 # the first item is the main worktree |
|
232 status = "main" |
|
233 font = itm.font(0) |
|
234 font.setBold(True) |
|
235 if checkoutPath == self.__projectDir: |
|
236 # it is the current project as well |
|
237 status = "main+current" |
|
238 font.setItalic(True) |
|
239 for col in range(self.worktreeList.columnCount()): |
|
240 itm.setFont(col, font) |
|
241 elif checkoutPath == self.__projectDir: |
|
242 # it is the current project |
|
243 if not status: |
|
244 status = "current" |
|
245 elif status == "locked": |
|
246 status = "locked+current" |
|
247 font = itm.font(0) |
|
248 font.setItalic(True) |
|
249 for col in range(self.worktreeList.columnCount()): |
|
250 itm.setFont(col, font) |
|
251 itm.setData(0, GitWorktreeDialog.StatusRole, status) |
|
252 |
|
253 def start(self, projectDir): |
|
254 """ |
|
255 Public slot to start the git worktree list command. |
|
256 |
|
257 @param projectDir name of the project directory |
|
258 @type str |
|
259 """ |
|
260 self.errorGroup.hide() |
|
261 self.worktreeList.clear() |
|
262 |
|
263 self.__ioEncoding = Preferences.getSystem("IOEncoding") |
|
264 |
|
265 args = self.__vcs.initCommand("worktree") |
|
266 args += ["list", "--porcelain"] |
|
267 if self.expireCheckBox.isChecked(): |
|
268 args += [ |
|
269 "--expire", |
|
270 self.expireDateTimeEdit.dateTime().toString(Qt.DateFormat.ISODate), |
|
271 ] |
|
272 |
|
273 self.__projectDir = projectDir |
|
274 |
|
275 # find the root of the repo |
|
276 self.__repodir = self.__vcs.findRepoRoot(projectDir) |
|
277 if not self.__repodir: |
|
278 return |
|
279 |
|
280 self.__outputLines = [] |
|
281 self.__commitIdLength = self.__vcs.getPlugin().getPreferences("CommitIdLength") |
|
282 |
|
283 self.__process.kill() |
|
284 self.__process.setWorkingDirectory(self.__repodir) |
|
285 |
|
286 self.__process.start("git", args) |
|
287 procStarted = self.__process.waitForStarted(5000) |
|
288 if not procStarted: |
|
289 EricMessageBox.critical( |
|
290 self, |
|
291 self.tr("Process Generation Error"), |
|
292 self.tr( |
|
293 "The process {0} could not be started. " |
|
294 "Ensure, that it is in the search path." |
|
295 ).format("git"), |
|
296 ) |
|
297 else: |
|
298 self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled( |
|
299 False |
|
300 ) |
|
301 self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setEnabled( |
|
302 True |
|
303 ) |
|
304 self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setDefault( |
|
305 True |
|
306 ) |
|
307 |
|
308 self.__refreshButton.setEnabled(False) |
|
309 |
|
310 def __finish(self): |
|
311 """ |
|
312 Private slot called when the process finished or the user pressed |
|
313 the button. |
|
314 """ |
|
315 if ( |
|
316 self.__process is not None |
|
317 and self.__process.state() != QProcess.ProcessState.NotRunning |
|
318 ): |
|
319 self.__process.terminate() |
|
320 QTimer.singleShot(2000, self.__process.kill) |
|
321 self.__process.waitForFinished(3000) |
|
322 |
|
323 self.__refreshButton.setEnabled(True) |
|
324 |
|
325 self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(True) |
|
326 self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setEnabled(False) |
|
327 self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setDefault(True) |
|
328 self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setFocus( |
|
329 Qt.FocusReason.OtherFocusReason |
|
330 ) |
|
331 |
|
332 self.__resort() |
|
333 self.__resizeColumns() |
|
334 |
|
335 @pyqtSlot(QAbstractButton) |
|
336 def on_buttonBox_clicked(self, button): |
|
337 """ |
|
338 Private slot called by a button of the button box clicked. |
|
339 |
|
340 @param button button that was clicked |
|
341 @type QAbstractButton |
|
342 """ |
|
343 if button == self.buttonBox.button(QDialogButtonBox.StandardButton.Close): |
|
344 self.close() |
|
345 elif button == self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel): |
|
346 self.__finish() |
|
347 elif button == self.__refreshButton: |
|
348 self.__refreshButtonClicked() |
|
349 |
|
350 @pyqtSlot() |
|
351 def __refreshButtonClicked(self): |
|
352 """ |
|
353 Private slot to refresh the worktree display. |
|
354 """ |
|
355 self.start(self.__projectDir) |
|
356 |
|
357 def __procFinished(self, exitCode, exitStatus): |
|
358 """ |
|
359 Private slot connected to the finished signal. |
|
360 |
|
361 @param exitCode exit code of the process (integer) |
|
362 @param exitStatus exit status of the process (QProcess.ExitStatus) |
|
363 """ |
|
364 self.__finish() |
|
365 |
|
366 def __readStdout(self): |
|
367 """ |
|
368 Private slot to handle the readyReadStandardOutput signal. |
|
369 |
|
370 It reads the output of the process, formats it and inserts it into |
|
371 the contents pane. |
|
372 """ |
|
373 if self.__process is not None: |
|
374 self.__process.setReadChannel(QProcess.ProcessChannel.StandardOutput) |
|
375 |
|
376 while self.__process.canReadLine(): |
|
377 line = str( |
|
378 self.__process.readLine(), self.__ioEncoding, "replace" |
|
379 ).strip() |
|
380 if line: |
|
381 self.__outputLines.append(line) |
|
382 else: |
|
383 self.__generateItem(self.__outputLines) |
|
384 self.__outputLines = [] |
|
385 |
|
386 def __readStderr(self): |
|
387 """ |
|
388 Private slot to handle the readyReadStandardError signal. |
|
389 |
|
390 It reads the error output of the process and inserts it into the |
|
391 error pane. |
|
392 """ |
|
393 if self.__process is not None: |
|
394 s = str(self.__process.readAllStandardError(), self.__ioEncoding, "replace") |
|
395 self.errorGroup.show() |
|
396 self.errors.insertPlainText(s) |
|
397 self.errors.ensureCursorVisible() |
|
398 |
|
399 @pyqtSlot(bool) |
|
400 def on_expireCheckBox_toggled(self, checked): |
|
401 """ |
|
402 Private slot to handle a change of the expire checkbox. |
|
403 |
|
404 @param checked state of the checkbox |
|
405 @type bool |
|
406 """ |
|
407 if checked: |
|
408 now = QDateTime.currentDateTime() |
|
409 self.expireDateTimeEdit.setMaximumDateTime(now) |
|
410 self.expireDateTimeEdit.setMinimumDate(now.date().addDays(-2 * 365)) |
|
411 self.expireDateTimeEdit.setMinimumTime(QTime(0, 0, 0)) |
|
412 self.expireDateTimeEdit.setDateTime(now) |
|
413 else: |
|
414 self.__refreshButtonClicked() |
|
415 |
|
416 @pyqtSlot(QDateTime) |
|
417 def on_expireDateTimeEdit_dateTimeChanged(self, dateTime): |
|
418 """ |
|
419 Private slot to handle a change of the expire date and time. |
|
420 |
|
421 @param dateTime DESCRIPTION |
|
422 @type QDateTime |
|
423 """ |
|
424 self.__refreshButtonClicked() |
|
425 |
|
426 ########################################################################### |
|
427 ## Menu handling methods |
|
428 ########################################################################### |
|
429 |
|
430 def __showActionsMenu(self): |
|
431 """ |
|
432 Private slot to prepare the actions button menu before it is shown. |
|
433 """ |
|
434 prunableWorktrees = [] |
|
435 for row in range(self.worktreeList.topLevelItemCount()): |
|
436 itm = self.worktreeList.topLevelItem(row) |
|
437 status = itm.data(0, GitWorktreeDialog.StatusRole) |
|
438 if status == "prunable": |
|
439 prunableWorktrees.append(itm.text(self.__pathColumn)) |
|
440 |
|
441 selectedItems = self.worktreeList.selectedItems() |
|
442 enable = bool(selectedItems) |
|
443 status = ( |
|
444 selectedItems[0].data(0, GitWorktreeDialog.StatusRole) |
|
445 if selectedItems |
|
446 else "" |
|
447 ) |
|
448 |
|
449 self.__lockAct.setEnabled( |
|
450 enable |
|
451 and status not in ("locked", "locked+current", "main", "main+current") |
|
452 ) |
|
453 self.__unlockAct.setEnabled(enable and status in ("locked", "locked+current")) |
|
454 self.__moveAct.setEnabled( |
|
455 enable |
|
456 and status |
|
457 not in ( |
|
458 "prunable", |
|
459 "locked", |
|
460 "main", |
|
461 "main+current", |
|
462 "current", |
|
463 "locked+current", |
|
464 ) |
|
465 ) |
|
466 self.__removeAct.setEnabled( |
|
467 enable |
|
468 and status |
|
469 not in ("locked", "main", "main+current", "current", "locked+current") |
|
470 ) |
|
471 self.__removeForcedAct.setEnabled( |
|
472 enable |
|
473 and status not in ("main", "main+current", "current", "locked+current") |
|
474 ) |
|
475 self.__pruneAct.setEnabled(bool(prunableWorktrees)) |
|
476 |
|
477 @pyqtSlot() |
|
478 def __worktreeAdd(self): |
|
479 """ |
|
480 Private slot to add a linked worktree. |
|
481 """ |
|
482 # TODO: not yet implemented |
|
483 from .GitWorktreeAddDialog import GitWorktreeAddDialog |
|
484 |
|
485 # find current worktree and take its parent path as the parent directory |
|
486 for row in range(self.worktreeList.topLevelItemCount()): |
|
487 itm = self.worktreeList.topLevelItem(row) |
|
488 if "current" in itm.data(0, GitWorktreeDialog.StatusRole): |
|
489 parentDirectory = os.path.dirname(itm.text(self.__pathColumn)) |
|
490 break |
|
491 else: |
|
492 parentDirectory = "" |
|
493 |
|
494 dlg = GitWorktreeAddDialog( |
|
495 parentDirectory, |
|
496 self.__vcs.gitGetTagsList(self.__repodir), |
|
497 self.__vcs.gitGetBranchesList(self.__repodir, withMaster=True), |
|
498 ) |
|
499 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
500 params = dlg.getParameters() |
|
501 args = ["worktree", "add"] |
|
502 if params["force"]: |
|
503 args.append("--force") |
|
504 if params["detach"]: |
|
505 args.append("--detach") |
|
506 if params["lock"]: |
|
507 args.append("--lock") |
|
508 if params["lock_reason"]: |
|
509 args += ["--reason", params["lock_reason"]] |
|
510 if params["branch"]: |
|
511 args += ["-B" if params["force_branch"] else "-b", params["branch"]] |
|
512 args.append(params["path"]) |
|
513 if params["commit"]: |
|
514 args.append(params["commit"]) |
|
515 |
|
516 dlg = GitDialog(self.tr("Add Worktree"), self.__vcs) |
|
517 started = dlg.startProcess(args, workingDir=self.__repodir) |
|
518 if started: |
|
519 dlg.exec() |
|
520 |
|
521 self.__refreshButtonClicked() |
|
522 |
|
523 @pyqtSlot() |
|
524 def __worktreeLock(self): |
|
525 """ |
|
526 Private slot to lock a worktree. |
|
527 """ |
|
528 worktree = self.worktreeList.selectedItems()[0].text(self.__pathColumn) |
|
529 if not worktree: |
|
530 return |
|
531 |
|
532 reason, ok = QInputDialog.getText( |
|
533 self, |
|
534 self.tr("Lock Worktree"), |
|
535 self.tr("Enter a reason for the lock:"), |
|
536 QLineEdit.EchoMode.Normal, |
|
537 ) |
|
538 if not ok: |
|
539 return |
|
540 |
|
541 args = ["worktree", "lock"] |
|
542 if reason: |
|
543 args += ["--reason", reason] |
|
544 args.append(worktree) |
|
545 |
|
546 proc = QProcess() |
|
547 ok = self.__vcs.startSynchronizedProcess(proc, "git", args, self.__repodir) |
|
548 if not ok: |
|
549 err = str(proc.readAllStandardError(), self.__ioEncoding, "replace") |
|
550 EricMessageBox.critical( |
|
551 self, |
|
552 self.tr("Lock Worktree"), |
|
553 self.tr( |
|
554 "<p>Locking the selected worktree failed.</p><p>{0}</p>" |
|
555 ).format(err), |
|
556 ) |
|
557 |
|
558 self.__refreshButtonClicked() |
|
559 |
|
560 @pyqtSlot() |
|
561 def __worktreeUnlock(self): |
|
562 """ |
|
563 Private slot to unlock a worktree. |
|
564 """ |
|
565 worktree = self.worktreeList.selectedItems()[0].text(self.__pathColumn) |
|
566 if not worktree: |
|
567 return |
|
568 |
|
569 args = ["worktree", "unlock", worktree] |
|
570 |
|
571 proc = QProcess() |
|
572 ok = self.__vcs.startSynchronizedProcess(proc, "git", args, self.__repodir) |
|
573 if not ok: |
|
574 err = str(proc.readAllStandardError(), self.__ioEncoding, "replace") |
|
575 EricMessageBox.critical( |
|
576 self, |
|
577 self.tr("Unlock Worktree"), |
|
578 self.tr( |
|
579 "<p>Unlocking the selected worktree failed.</p><p>{0}</p>" |
|
580 ).format(err), |
|
581 ) |
|
582 |
|
583 self.__refreshButtonClicked() |
|
584 |
|
585 @pyqtSlot() |
|
586 def __worktreeMove(self): |
|
587 """ |
|
588 Private slot to move a worktree to a new location. |
|
589 """ |
|
590 worktree = self.worktreeList.selectedItems()[0].text(self.__pathColumn) |
|
591 if not worktree: |
|
592 return |
|
593 |
|
594 newPath, ok = EricPathPickerDialog.getStrPath( |
|
595 self, |
|
596 self.tr("Move Worktree"), |
|
597 self.tr("Enter the new path for the worktree:"), |
|
598 mode=EricPathPickerDialog.EricPathPickerModes.DIRECTORY_MODE, |
|
599 strPath=worktree, |
|
600 defaultDirectory=os.path.dirname(worktree), |
|
601 ) |
|
602 if not ok: |
|
603 return |
|
604 |
|
605 args = ["worktree", "move", worktree, newPath] |
|
606 |
|
607 proc = QProcess() |
|
608 ok = self.__vcs.startSynchronizedProcess(proc, "git", args, self.__repodir) |
|
609 if not ok: |
|
610 err = str(proc.readAllStandardError(), self.__ioEncoding, "replace") |
|
611 EricMessageBox.critical( |
|
612 self, |
|
613 self.tr("Move Worktree"), |
|
614 self.tr("<p>Moving the selected worktree failed.</p><p>{0}</p>").format( |
|
615 err |
|
616 ), |
|
617 ) |
|
618 |
|
619 self.__refreshButtonClicked() |
|
620 |
|
621 @pyqtSlot() |
|
622 def __worktreeRemove(self, force=False): |
|
623 """ |
|
624 Private slot to remove a linked worktree. |
|
625 |
|
626 @param force flag indicating a forceful remove (defaults to False) |
|
627 @type bool (optional |
|
628 """ |
|
629 worktree = self.worktreeList.selectedItems()[0].text(self.__pathColumn) |
|
630 if not worktree: |
|
631 return |
|
632 |
|
633 title = ( |
|
634 self.tr("Remove Worktree") |
|
635 if force |
|
636 else self.tr("Remove Worktree Forcefully") |
|
637 ) |
|
638 |
|
639 ok = EricMessageBox.yesNo( |
|
640 self, |
|
641 title, |
|
642 self.tr( |
|
643 "<p>Do you really want to remove the worktree <b>{0}</b>?</p>" |
|
644 ).format(worktree), |
|
645 ) |
|
646 if not ok: |
|
647 return |
|
648 |
|
649 args = ["worktree", "remove"] |
|
650 if force: |
|
651 args.append("--force") |
|
652 if ( |
|
653 self.worktreeList.selectedItems()[0].data( |
|
654 0, GitWorktreeDialog.StatusRole |
|
655 ) |
|
656 == "locked" |
|
657 ): |
|
658 # a second '--force' is needed |
|
659 args.append("--force") |
|
660 args.append(worktree) |
|
661 |
|
662 proc = QProcess() |
|
663 ok = self.__vcs.startSynchronizedProcess(proc, "git", args, self.__repodir) |
|
664 if not ok: |
|
665 err = str(proc.readAllStandardError(), self.__ioEncoding, "replace") |
|
666 EricMessageBox.critical( |
|
667 self, |
|
668 title, |
|
669 self.tr( |
|
670 "<p>Removing the selected worktree failed.</p><p>{0}</p>" |
|
671 ).format(err), |
|
672 ) |
|
673 |
|
674 self.__refreshButtonClicked() |
|
675 |
|
676 @pyqtSlot() |
|
677 def __worktreeRemoveForced(self): |
|
678 """ |
|
679 Private slot to remove a linked worktree forcefully. |
|
680 """ |
|
681 self.__worktreeRemove(force=True) |
|
682 |
|
683 @pyqtSlot() |
|
684 def __worktreePrune(self): |
|
685 """ |
|
686 Private slot to prune worktree information. |
|
687 """ |
|
688 from eric7.UI.DeleteFilesConfirmationDialog import DeleteFilesConfirmationDialog |
|
689 |
|
690 prunableWorktrees = [] |
|
691 for row in range(self.worktreeList.topLevelItemCount()): |
|
692 itm = self.worktreeList.topLevelItem(row) |
|
693 status = itm.data(0, GitWorktreeDialog.StatusRole) |
|
694 if status == "prunable": |
|
695 prunableWorktrees.append(itm.text(self.__pathColumn)) |
|
696 |
|
697 if prunableWorktrees: |
|
698 dlg = DeleteFilesConfirmationDialog( |
|
699 self, |
|
700 self.tr("Prune Worktree Information"), |
|
701 self.tr( |
|
702 "Do you really want to prune the information of these worktrees?" |
|
703 ), |
|
704 prunableWorktrees, |
|
705 ) |
|
706 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
707 args = ["worktree", "prune"] |
|
708 if self.expireCheckBox.isChecked(): |
|
709 args += [ |
|
710 "--expire", |
|
711 self.expireDateTimeEdit.dateTime().toString( |
|
712 Qt.DateFormat.ISODate |
|
713 ), |
|
714 ] |
|
715 |
|
716 proc = QProcess() |
|
717 ok = self.__vcs.startSynchronizedProcess( |
|
718 proc, "git", args, self.__repodir |
|
719 ) |
|
720 if not ok: |
|
721 err = str(proc.readAllStandardError(), self.__ioEncoding, "replace") |
|
722 EricMessageBox.critical( |
|
723 self, |
|
724 self.tr("Prune Worktree Information"), |
|
725 self.tr( |
|
726 "<p>Pruning of the worktree information failed.</p>" |
|
727 "<p>{0}</p>" |
|
728 ).format(err), |
|
729 ) |
|
730 |
|
731 self.__refreshButtonClicked() |
|
732 |
|
733 @pyqtSlot() |
|
734 def __worktreeRepair(self, worktreePaths=None): |
|
735 """ |
|
736 Private slot to repair worktree administrative files. |
|
737 |
|
738 @param worktreePaths list of worktree paths to be repaired (defaults to None) |
|
739 @type list of str (optional) |
|
740 """ |
|
741 args = ["worktree", "repair"] |
|
742 if worktreePaths: |
|
743 args += worktreePaths |
|
744 |
|
745 proc = QProcess() |
|
746 ok = self.__vcs.startSynchronizedProcess(proc, "git", args, self.__repodir) |
|
747 if ok: |
|
748 out = str(proc.readAllStandardError(), self.__ioEncoding, "replace") |
|
749 EricMessageBox.information( |
|
750 self, |
|
751 self.tr("Repair Worktree"), |
|
752 self.tr( |
|
753 "<p>Repairing of the worktree administrative files succeeded.</p>" |
|
754 "<p>{0}</p>" |
|
755 ).format(out), |
|
756 ) |
|
757 |
|
758 else: |
|
759 err = str(proc.readAllStandardError(), self.__ioEncoding, "replace") |
|
760 EricMessageBox.critical( |
|
761 self, |
|
762 self.tr("Repair Worktree"), |
|
763 self.tr( |
|
764 "<p>Repairing of the worktree administrative files failed.</p>" |
|
765 "<p>{0}</p>" |
|
766 ).format(err), |
|
767 ) |
|
768 |
|
769 self.__refreshButtonClicked() |
|
770 |
|
771 @pyqtSlot() |
|
772 def __worktreeRepairMultiple(self): |
|
773 """ |
|
774 Private slot to repair worktree administrative files for multiple worktree |
|
775 paths. |
|
776 """ |
|
777 from .GitWorktreePathsDialog import GitWorktreePathsDialog |
|
778 |
|
779 # find current worktree and take its parent path as the parent directory |
|
780 for row in range(self.worktreeList.topLevelItemCount()): |
|
781 itm = self.worktreeList.topLevelItem(row) |
|
782 if "current" in itm.data(0, GitWorktreeDialog.StatusRole): |
|
783 parentDirectory = os.path.dirname(itm.text(self.__pathColumn)) |
|
784 break |
|
785 else: |
|
786 parentDirectory = "" |
|
787 |
|
788 dlg = GitWorktreePathsDialog(parentDirectory, self) |
|
789 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
790 paths = dlg.getPathsList() |
|
791 |
|
792 if paths: |
|
793 self.__worktreeRepair(worktreePaths=paths) |