eric6/Plugins/VcsPlugins/vcsGit/GitStatusDialog.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7192
a22eee00b052
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2014 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog to show the output of the git status command
8 process.
9 """
10
11 from __future__ import unicode_literals
12 try:
13 str = unicode
14 except NameError:
15 pass
16
17 import os
18 import tempfile
19
20 from PyQt5.QtCore import pyqtSlot, Qt, QProcess, QTimer, QSize
21 from PyQt5.QtGui import QTextCursor, QCursor
22 from PyQt5.QtWidgets import QWidget, QDialogButtonBox, QMenu, QHeaderView, \
23 QTreeWidgetItem, QLineEdit, QInputDialog, QToolTip
24
25 from E5Gui.E5Application import e5App
26 from E5Gui import E5MessageBox
27
28 from Globals import qVersionTuple
29
30 from .Ui_GitStatusDialog import Ui_GitStatusDialog
31
32 from .GitDiffHighlighter import GitDiffHighlighter
33 from .GitDiffGenerator import GitDiffGenerator
34 from .GitDiffParser import GitDiffParser
35 from .GitUtilities import strToQByteArray
36
37 import Preferences
38 import UI.PixmapCache
39 import Utilities
40
41
42 class GitStatusDialog(QWidget, Ui_GitStatusDialog):
43 """
44 Class implementing a dialog to show the output of the git status command
45 process.
46 """
47 ConflictStates = ["AA", "AU", "DD", "DU", "UA", "UD", "UU"]
48
49 ConflictRole = Qt.UserRole
50
51 def __init__(self, vcs, parent=None):
52 """
53 Constructor
54
55 @param vcs reference to the vcs object
56 @param parent parent widget (QWidget)
57 """
58 super(GitStatusDialog, self).__init__(parent)
59 self.setupUi(self)
60
61 self.__toBeCommittedColumn = 0
62 self.__statusWorkColumn = 1
63 self.__statusIndexColumn = 2
64 self.__pathColumn = 3
65 self.__lastColumn = self.statusList.columnCount()
66
67 self.refreshButton = self.buttonBox.addButton(
68 self.tr("Refresh"), QDialogButtonBox.ActionRole)
69 self.refreshButton.setToolTip(
70 self.tr("Press to refresh the status display"))
71 self.refreshButton.setEnabled(False)
72 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
73 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
74
75 self.diff = None
76 self.vcs = vcs
77 self.vcs.committed.connect(self.__committed)
78 self.process = QProcess()
79 self.process.finished.connect(self.__procFinished)
80 self.process.readyReadStandardOutput.connect(self.__readStdout)
81 self.process.readyReadStandardError.connect(self.__readStderr)
82
83 self.errorGroup.hide()
84 self.inputGroup.hide()
85
86 self.vDiffSplitter.setStretchFactor(0, 2)
87 self.vDiffSplitter.setStretchFactor(0, 2)
88 self.vDiffSplitter.setSizes([400, 400])
89 self.__hDiffSplitterState = None
90 self.__vDiffSplitterState = None
91
92 self.statusList.headerItem().setText(self.__lastColumn, "")
93 self.statusList.header().setSortIndicator(
94 self.__pathColumn, Qt.AscendingOrder)
95
96 font = Preferences.getEditorOtherFonts("MonospacedFont")
97 self.lDiffEdit.setFontFamily(font.family())
98 self.lDiffEdit.setFontPointSize(font.pointSize())
99 self.rDiffEdit.setFontFamily(font.family())
100 self.rDiffEdit.setFontPointSize(font.pointSize())
101 self.lDiffEdit.customContextMenuRequested.connect(
102 self.__showLDiffContextMenu)
103 self.rDiffEdit.customContextMenuRequested.connect(
104 self.__showRDiffContextMenu)
105
106 self.__lDiffMenu = QMenu()
107 self.__stageLinesAct = self.__lDiffMenu.addAction(
108 UI.PixmapCache.getIcon("vcsAdd.png"),
109 self.tr("Stage Selected Lines"),
110 self.__stageHunkOrLines)
111 self.__revertLinesAct = self.__lDiffMenu.addAction(
112 UI.PixmapCache.getIcon("vcsRevert.png"),
113 self.tr("Revert Selected Lines"),
114 self.__revertHunkOrLines)
115 self.__stageHunkAct = self.__lDiffMenu.addAction(
116 UI.PixmapCache.getIcon("vcsAdd.png"),
117 self.tr("Stage Hunk"),
118 self.__stageHunkOrLines)
119 self.__revertHunkAct = self.__lDiffMenu.addAction(
120 UI.PixmapCache.getIcon("vcsRevert.png"),
121 self.tr("Revert Hunk"),
122 self.__revertHunkOrLines)
123
124 self.__rDiffMenu = QMenu()
125 self.__unstageLinesAct = self.__rDiffMenu.addAction(
126 UI.PixmapCache.getIcon("vcsRemove.png"),
127 self.tr("Unstage Selected Lines"),
128 self.__unstageHunkOrLines)
129 self.__unstageHunkAct = self.__rDiffMenu.addAction(
130 UI.PixmapCache.getIcon("vcsRemove.png"),
131 self.tr("Unstage Hunk"),
132 self.__unstageHunkOrLines)
133
134 self.lDiffHighlighter = GitDiffHighlighter(self.lDiffEdit.document())
135 self.rDiffHighlighter = GitDiffHighlighter(self.rDiffEdit.document())
136
137 self.lDiffParser = None
138 self.rDiffParser = None
139
140 self.__selectedName = ""
141
142 self.__diffGenerator = GitDiffGenerator(vcs, self)
143 self.__diffGenerator.finished.connect(self.__generatorFinished)
144
145 self.modifiedIndicators = [
146 self.tr('added'),
147 self.tr('copied'),
148 self.tr('deleted'),
149 self.tr('modified'),
150 self.tr('renamed'),
151 ]
152 self.modifiedOnlyIndicators = [
153 self.tr('modified'),
154 ]
155
156 self.unversionedIndicators = [
157 self.tr('not tracked'),
158 ]
159
160 self.missingIndicators = [
161 self.tr('deleted'),
162 ]
163
164 self.unmergedIndicators = [
165 self.tr('unmerged'),
166 ]
167
168 self.status = {
169 ' ': self.tr("unmodified"),
170 'A': self.tr('added'),
171 'C': self.tr('copied'),
172 'D': self.tr('deleted'),
173 'M': self.tr('modified'),
174 'R': self.tr('renamed'),
175 'U': self.tr('unmerged'),
176 '?': self.tr('not tracked'),
177 '!': self.tr('ignored'),
178 }
179
180 self.__ioEncoding = Preferences.getSystem("IOEncoding")
181
182 self.__initActionsMenu()
183
184 def __initActionsMenu(self):
185 """
186 Private method to initialize the actions menu.
187 """
188 self.__actionsMenu = QMenu()
189 self.__actionsMenu.setTearOffEnabled(True)
190 if qVersionTuple() >= (5, 1, 0):
191 self.__actionsMenu.setToolTipsVisible(True)
192 else:
193 self.__actionsMenu.hovered.connect(self.__actionsMenuHovered)
194 self.__actionsMenu.aboutToShow.connect(self.__showActionsMenu)
195
196 self.__commitAct = self.__actionsMenu.addAction(
197 self.tr("Commit"), self.__commit)
198 self.__commitAct.setToolTip(self.tr("Commit the selected changes"))
199 self.__amendAct = self.__actionsMenu.addAction(
200 self.tr("Amend"), self.__amend)
201 self.__amendAct.setToolTip(self.tr(
202 "Amend the latest commit with the selected changes"))
203 self.__commitSelectAct = self.__actionsMenu.addAction(
204 self.tr("Select all for commit"), self.__commitSelectAll)
205 self.__commitDeselectAct = self.__actionsMenu.addAction(
206 self.tr("Unselect all from commit"), self.__commitDeselectAll)
207
208 self.__actionsMenu.addSeparator()
209 self.__addAct = self.__actionsMenu.addAction(
210 self.tr("Add"), self.__add)
211 self.__addAct.setToolTip(self.tr("Add the selected files"))
212 self.__stageAct = self.__actionsMenu.addAction(
213 self.tr("Stage changes"), self.__stage)
214 self.__stageAct.setToolTip(self.tr(
215 "Stages all changes of the selected files"))
216 self.__unstageAct = self.__actionsMenu.addAction(
217 self.tr("Unstage changes"), self.__unstage)
218 self.__unstageAct.setToolTip(self.tr(
219 "Unstages all changes of the selected files"))
220
221 self.__actionsMenu.addSeparator()
222
223 self.__diffAct = self.__actionsMenu.addAction(
224 self.tr("Differences"), self.__diff)
225 self.__diffAct.setToolTip(self.tr(
226 "Shows the differences of the selected entry in a"
227 " separate dialog"))
228 self.__sbsDiffAct = self.__actionsMenu.addAction(
229 self.tr("Differences Side-By-Side"), self.__sbsDiff)
230 self.__sbsDiffAct.setToolTip(self.tr(
231 "Shows the differences of the selected entry side-by-side in"
232 " a separate dialog"))
233
234 self.__actionsMenu.addSeparator()
235
236 self.__revertAct = self.__actionsMenu.addAction(
237 self.tr("Revert"), self.__revert)
238 self.__revertAct.setToolTip(self.tr(
239 "Reverts the changes of the selected files"))
240
241 self.__actionsMenu.addSeparator()
242
243 self.__forgetAct = self.__actionsMenu.addAction(
244 self.tr("Forget missing"), self.__forget)
245 self.__forgetAct.setToolTip(self.tr(
246 "Forgets about the selected missing files"))
247 self.__restoreAct = self.__actionsMenu.addAction(
248 self.tr("Restore missing"), self.__restoreMissing)
249 self.__restoreAct.setToolTip(self.tr(
250 "Restores the selected missing files"))
251
252 self.__actionsMenu.addSeparator()
253
254 self.__editAct = self.__actionsMenu.addAction(
255 self.tr("Edit file"), self.__editConflict)
256 self.__editAct.setToolTip(self.tr(
257 "Edit the selected conflicting file"))
258
259 self.__actionsMenu.addSeparator()
260
261 act = self.__actionsMenu.addAction(
262 self.tr("Adjust column sizes"), self.__resizeColumns)
263 act.setToolTip(self.tr(
264 "Adjusts the width of all columns to their contents"))
265
266 self.actionsButton.setIcon(
267 UI.PixmapCache.getIcon("actionsToolButton.png"))
268 self.actionsButton.setMenu(self.__actionsMenu)
269
270 def __actionsMenuHovered(self, action):
271 """
272 Private slot to show the tooltip for an action menu entry.
273
274 @param action action to show tooltip for
275 @type QAction
276 """
277 QToolTip.showText(
278 QCursor.pos(), action.toolTip(),
279 self.__actionsMenu, self.__actionsMenu.actionGeometry(action))
280
281 def closeEvent(self, e):
282 """
283 Protected slot implementing a close event handler.
284
285 @param e close event (QCloseEvent)
286 """
287 if self.process is not None and \
288 self.process.state() != QProcess.NotRunning:
289 self.process.terminate()
290 QTimer.singleShot(2000, self.process.kill)
291 self.process.waitForFinished(3000)
292
293 self.vcs.getPlugin().setPreferences(
294 "StatusDialogGeometry", self.saveGeometry())
295 self.vcs.getPlugin().setPreferences(
296 "StatusDialogSplitterStates", [
297 self.vDiffSplitter.saveState(),
298 self.hDiffSplitter.saveState()
299 ]
300 )
301
302 e.accept()
303
304 def show(self):
305 """
306 Public slot to show the dialog.
307 """
308 super(GitStatusDialog, self).show()
309
310 geom = self.vcs.getPlugin().getPreferences(
311 "StatusDialogGeometry")
312 if geom.isEmpty():
313 s = QSize(900, 600)
314 self.resize(s)
315 else:
316 self.restoreGeometry(geom)
317
318 states = self.vcs.getPlugin().getPreferences(
319 "StatusDialogSplitterStates")
320 if len(states) == 2:
321 # we have two splitters
322 self.vDiffSplitter.restoreState(states[0])
323 self.hDiffSplitter.restoreState(states[1])
324
325 def __resort(self):
326 """
327 Private method to resort the tree.
328 """
329 self.statusList.sortItems(
330 self.statusList.sortColumn(),
331 self.statusList.header().sortIndicatorOrder())
332
333 def __resizeColumns(self):
334 """
335 Private method to resize the list columns.
336 """
337 self.statusList.header().resizeSections(QHeaderView.ResizeToContents)
338 self.statusList.header().setStretchLastSection(True)
339
340 def __generateItem(self, status, path):
341 """
342 Private method to generate a status item in the status list.
343
344 @param status status indicator (string)
345 @param path path of the file or directory (string)
346 """
347 statusWorkText = self.status[status[1]]
348 statusIndexText = self.status[status[0]]
349 itm = QTreeWidgetItem(self.statusList, [
350 "",
351 statusWorkText,
352 statusIndexText,
353 path,
354 ])
355
356 itm.setTextAlignment(self.__statusWorkColumn, Qt.AlignHCenter)
357 itm.setTextAlignment(self.__statusIndexColumn, Qt.AlignHCenter)
358 itm.setTextAlignment(self.__pathColumn, Qt.AlignLeft)
359
360 if status not in self.ConflictStates + ["??", "!!"] and \
361 statusIndexText in self.modifiedIndicators:
362 itm.setFlags(itm.flags() | Qt.ItemIsUserCheckable)
363 itm.setCheckState(self.__toBeCommittedColumn, Qt.Checked)
364 else:
365 itm.setFlags(itm.flags() & ~Qt.ItemIsUserCheckable)
366
367 if statusWorkText not in self.__statusFilters:
368 self.__statusFilters.append(statusWorkText)
369 if statusIndexText not in self.__statusFilters:
370 self.__statusFilters.append(statusIndexText)
371
372 if status in self.ConflictStates:
373 itm.setIcon(self.__statusWorkColumn,
374 UI.PixmapCache.getIcon(
375 os.path.join("VcsPlugins", "vcsGit", "icons",
376 "conflict.png")))
377 itm.setData(0, self.ConflictRole, status in self.ConflictStates)
378
379 def start(self, fn):
380 """
381 Public slot to start the git status command.
382
383 @param fn filename(s)/directoryname(s) to show the status of
384 (string or list of strings)
385 """
386 self.errorGroup.hide()
387 self.intercept = False
388 self.args = fn
389
390 self.__ioEncoding = Preferences.getSystem("IOEncoding")
391
392 self.statusFilterCombo.clear()
393 self.__statusFilters = []
394 self.statusList.clear()
395
396 self.setWindowTitle(self.tr('Git Status'))
397
398 args = self.vcs.initCommand("status")
399 args.append('--porcelain')
400 args.append("--")
401 if isinstance(fn, list):
402 self.dname, fnames = self.vcs.splitPathList(fn)
403 self.vcs.addArguments(args, fn)
404 else:
405 self.dname, fname = self.vcs.splitPath(fn)
406 args.append(fn)
407
408 # find the root of the repo
409 self.__repodir = self.dname
410 while not os.path.isdir(
411 os.path.join(self.__repodir, self.vcs.adminDir)):
412 self.__repodir = os.path.dirname(self.__repodir)
413 if os.path.splitdrive(self.__repodir)[1] == os.sep:
414 return
415
416 self.process.kill()
417 self.process.setWorkingDirectory(self.__repodir)
418
419 self.process.start('git', args)
420 procStarted = self.process.waitForStarted(5000)
421 if not procStarted:
422 self.inputGroup.setEnabled(False)
423 self.inputGroup.hide()
424 E5MessageBox.critical(
425 self,
426 self.tr('Process Generation Error'),
427 self.tr(
428 'The process {0} could not be started. '
429 'Ensure, that it is in the search path.'
430 ).format('git'))
431 else:
432 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
433 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
434 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
435
436 self.refreshButton.setEnabled(False)
437
438 def __finish(self):
439 """
440 Private slot called when the process finished or the user pressed
441 the button.
442 """
443 if self.process is not None and \
444 self.process.state() != QProcess.NotRunning:
445 self.process.terminate()
446 QTimer.singleShot(2000, self.process.kill)
447 self.process.waitForFinished(3000)
448
449 self.inputGroup.setEnabled(False)
450 self.inputGroup.hide()
451 self.refreshButton.setEnabled(True)
452
453 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
454 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
455 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
456 self.buttonBox.button(QDialogButtonBox.Close).setFocus(
457 Qt.OtherFocusReason)
458
459 self.__statusFilters.sort()
460 self.__statusFilters.insert(0, "<{0}>".format(self.tr("all")))
461 self.statusFilterCombo.addItems(self.__statusFilters)
462
463 self.__resort()
464 self.__resizeColumns()
465
466 self.__refreshDiff()
467
468 def on_buttonBox_clicked(self, button):
469 """
470 Private slot called by a button of the button box clicked.
471
472 @param button button that was clicked (QAbstractButton)
473 """
474 if button == self.buttonBox.button(QDialogButtonBox.Close):
475 self.close()
476 elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
477 self.__finish()
478 elif button == self.refreshButton:
479 self.on_refreshButton_clicked()
480
481 def __procFinished(self, exitCode, exitStatus):
482 """
483 Private slot connected to the finished signal.
484
485 @param exitCode exit code of the process (integer)
486 @param exitStatus exit status of the process (QProcess.ExitStatus)
487 """
488 self.__finish()
489
490 def __readStdout(self):
491 """
492 Private slot to handle the readyReadStandardOutput signal.
493
494 It reads the output of the process, formats it and inserts it into
495 the contents pane.
496 """
497 if self.process is not None:
498 self.process.setReadChannel(QProcess.StandardOutput)
499
500 while self.process.canReadLine():
501 line = str(self.process.readLine(), self.__ioEncoding,
502 'replace')
503
504 status = line[:2]
505 path = line[3:].strip().split(" -> ")[-1].strip('"')
506 self.__generateItem(status, path)
507
508 def __readStderr(self):
509 """
510 Private slot to handle the readyReadStandardError signal.
511
512 It reads the error output of the process and inserts it into the
513 error pane.
514 """
515 if self.process is not None:
516 s = str(self.process.readAllStandardError(),
517 self.__ioEncoding, 'replace')
518 self.errorGroup.show()
519 self.errors.insertPlainText(s)
520 self.errors.ensureCursorVisible()
521
522 # show input in case the process asked for some input
523 self.inputGroup.setEnabled(True)
524 self.inputGroup.show()
525
526 def on_passwordCheckBox_toggled(self, isOn):
527 """
528 Private slot to handle the password checkbox toggled.
529
530 @param isOn flag indicating the status of the check box (boolean)
531 """
532 if isOn:
533 self.input.setEchoMode(QLineEdit.Password)
534 else:
535 self.input.setEchoMode(QLineEdit.Normal)
536
537 @pyqtSlot()
538 def on_sendButton_clicked(self):
539 """
540 Private slot to send the input to the git process.
541 """
542 inputTxt = self.input.text()
543 inputTxt += os.linesep
544
545 if self.passwordCheckBox.isChecked():
546 self.errors.insertPlainText(os.linesep)
547 self.errors.ensureCursorVisible()
548 else:
549 self.errors.insertPlainText(inputTxt)
550 self.errors.ensureCursorVisible()
551
552 self.process.write(strToQByteArray(inputTxt))
553
554 self.passwordCheckBox.setChecked(False)
555 self.input.clear()
556
557 def on_input_returnPressed(self):
558 """
559 Private slot to handle the press of the return key in the input field.
560 """
561 self.intercept = True
562 self.on_sendButton_clicked()
563
564 def keyPressEvent(self, evt):
565 """
566 Protected slot to handle a key press event.
567
568 @param evt the key press event (QKeyEvent)
569 """
570 if self.intercept:
571 self.intercept = False
572 evt.accept()
573 return
574 super(GitStatusDialog, self).keyPressEvent(evt)
575
576 @pyqtSlot()
577 def on_refreshButton_clicked(self):
578 """
579 Private slot to refresh the status display.
580 """
581 selectedItems = self.statusList.selectedItems()
582 if len(selectedItems) == 1:
583 self.__selectedName = selectedItems[0].text(self.__pathColumn)
584 else:
585 self.__selectedName = ""
586
587 self.start(self.args)
588
589 @pyqtSlot(str)
590 def on_statusFilterCombo_activated(self, txt):
591 """
592 Private slot to react to the selection of a status filter.
593
594 @param txt selected status filter (string)
595 """
596 if txt == "<{0}>".format(self.tr("all")):
597 for topIndex in range(self.statusList.topLevelItemCount()):
598 topItem = self.statusList.topLevelItem(topIndex)
599 topItem.setHidden(False)
600 else:
601 for topIndex in range(self.statusList.topLevelItemCount()):
602 topItem = self.statusList.topLevelItem(topIndex)
603 topItem.setHidden(
604 topItem.text(self.__statusWorkColumn) != txt and
605 topItem.text(self.__statusIndexColumn) != txt
606 )
607
608 @pyqtSlot()
609 def on_statusList_itemSelectionChanged(self):
610 """
611 Private slot to act upon changes of selected items.
612 """
613 self.__generateDiffs()
614
615 ###########################################################################
616 ## Menu handling methods
617 ###########################################################################
618
619 def __showActionsMenu(self):
620 """
621 Private slot to prepare the actions button menu before it is shown.
622 """
623 modified = len(self.__getModifiedItems())
624 modifiedOnly = len(self.__getModifiedOnlyItems())
625 unversioned = len(self.__getUnversionedItems())
626 missing = len(self.__getMissingItems())
627 commitable = len(self.__getCommitableItems())
628 commitableUnselected = len(self.__getCommitableUnselectedItems())
629 stageable = len(self.__getStageableItems())
630 unstageable = len(self.__getUnstageableItems())
631 conflicting = len(self.__getConflictingItems())
632
633 self.__commitAct.setEnabled(commitable)
634 self.__amendAct.setEnabled(commitable)
635 self.__commitSelectAct.setEnabled(commitableUnselected)
636 self.__commitDeselectAct.setEnabled(commitable)
637 self.__addAct.setEnabled(unversioned)
638 self.__stageAct.setEnabled(stageable)
639 self.__unstageAct.setEnabled(unstageable)
640 self.__diffAct.setEnabled(modified)
641 self.__sbsDiffAct.setEnabled(modifiedOnly == 1)
642 self.__revertAct.setEnabled(stageable)
643 self.__forgetAct.setEnabled(missing)
644 self.__restoreAct.setEnabled(missing)
645 self.__editAct.setEnabled(conflicting == 1)
646
647 def __amend(self):
648 """
649 Private slot to handle the Amend context menu entry.
650 """
651 self.__commit(amend=True)
652
653 def __commit(self, amend=False):
654 """
655 Private slot to handle the Commit context menu entry.
656
657 @param amend flag indicating to perform an amend operation (boolean)
658 """
659 names = [os.path.join(self.dname, itm.text(self.__pathColumn))
660 for itm in self.__getCommitableItems()]
661 if not names:
662 E5MessageBox.information(
663 self,
664 self.tr("Commit"),
665 self.tr("""There are no entries selected to be"""
666 """ committed."""))
667 return
668
669 if Preferences.getVCS("AutoSaveFiles"):
670 vm = e5App().getObject("ViewManager")
671 for name in names:
672 vm.saveEditor(name)
673 self.vcs.vcsCommit(names, commitAll=False, amend=amend)
674 # staged changes
675
676 def __committed(self):
677 """
678 Private slot called after the commit has finished.
679 """
680 if self.isVisible():
681 self.on_refreshButton_clicked()
682 self.vcs.checkVCSStatus()
683
684 def __commitSelectAll(self):
685 """
686 Private slot to select all entries for commit.
687 """
688 self.__commitSelect(True)
689
690 def __commitDeselectAll(self):
691 """
692 Private slot to deselect all entries from commit.
693 """
694 self.__commitSelect(False)
695
696 def __add(self):
697 """
698 Private slot to handle the Add context menu entry.
699 """
700 names = [os.path.join(self.dname, itm.text(self.__pathColumn))
701 for itm in self.__getUnversionedItems()]
702 if not names:
703 E5MessageBox.information(
704 self,
705 self.tr("Add"),
706 self.tr("""There are no unversioned entries"""
707 """ available/selected."""))
708 return
709
710 self.vcs.vcsAdd(names)
711 self.on_refreshButton_clicked()
712
713 project = e5App().getObject("Project")
714 for name in names:
715 project.getModel().updateVCSStatus(name)
716 self.vcs.checkVCSStatus()
717
718 def __stage(self):
719 """
720 Private slot to handle the Stage context menu entry.
721 """
722 names = [os.path.join(self.dname, itm.text(self.__pathColumn))
723 for itm in self.__getStageableItems()]
724 if not names:
725 E5MessageBox.information(
726 self,
727 self.tr("Stage"),
728 self.tr("""There are no stageable entries"""
729 """ available/selected."""))
730 return
731
732 self.vcs.vcsAdd(names)
733 self.on_refreshButton_clicked()
734
735 project = e5App().getObject("Project")
736 for name in names:
737 project.getModel().updateVCSStatus(name)
738 self.vcs.checkVCSStatus()
739
740 def __unstage(self):
741 """
742 Private slot to handle the Unstage context menu entry.
743 """
744 names = [os.path.join(self.dname, itm.text(self.__pathColumn))
745 for itm in self.__getUnstageableItems()]
746 if not names:
747 E5MessageBox.information(
748 self,
749 self.tr("Unstage"),
750 self.tr("""There are no unstageable entries"""
751 """ available/selected."""))
752 return
753
754 self.vcs.gitUnstage(names)
755 self.on_refreshButton_clicked()
756
757 project = e5App().getObject("Project")
758 for name in names:
759 project.getModel().updateVCSStatus(name)
760 self.vcs.checkVCSStatus()
761
762 def __forget(self):
763 """
764 Private slot to handle the Forget Missing context menu entry.
765 """
766 names = [os.path.join(self.dname, itm.text(self.__pathColumn))
767 for itm in self.__getMissingItems()]
768 if not names:
769 E5MessageBox.information(
770 self,
771 self.tr("Forget Missing"),
772 self.tr("""There are no missing entries"""
773 """ available/selected."""))
774 return
775
776 self.vcs.vcsRemove(names, stageOnly=True)
777 self.on_refreshButton_clicked()
778
779 def __revert(self):
780 """
781 Private slot to handle the Revert context menu entry.
782 """
783 names = [os.path.join(self.dname, itm.text(self.__pathColumn))
784 for itm in self.__getStageableItems()]
785 if not names:
786 E5MessageBox.information(
787 self,
788 self.tr("Revert"),
789 self.tr("""There are no uncommitted, unstaged changes"""
790 """ available/selected."""))
791 return
792
793 self.vcs.gitRevert(names)
794 self.raise_()
795 self.activateWindow()
796 self.on_refreshButton_clicked()
797
798 project = e5App().getObject("Project")
799 for name in names:
800 project.getModel().updateVCSStatus(name)
801 self.vcs.checkVCSStatus()
802
803 def __restoreMissing(self):
804 """
805 Private slot to handle the Restore Missing context menu entry.
806 """
807 names = [os.path.join(self.dname, itm.text(self.__pathColumn))
808 for itm in self.__getMissingItems()]
809 if not names:
810 E5MessageBox.information(
811 self,
812 self.tr("Restore Missing"),
813 self.tr("""There are no missing entries"""
814 """ available/selected."""))
815 return
816
817 self.vcs.gitRevert(names)
818 self.on_refreshButton_clicked()
819 self.vcs.checkVCSStatus()
820
821 def __editConflict(self):
822 """
823 Private slot to handle the Edit file context menu entry.
824 """
825 itm = self.__getConflictingItems()[0]
826 filename = os.path.join(self.__repodir, itm.text(self.__pathColumn))
827 if Utilities.MimeTypes.isTextFile(filename):
828 e5App().getObject("ViewManager").getEditor(filename)
829
830 def __diff(self):
831 """
832 Private slot to handle the Diff context menu entry.
833 """
834 namesW = [os.path.join(self.dname, itm.text(self.__pathColumn))
835 for itm in self.__getStageableItems()]
836 namesS = [os.path.join(self.dname, itm.text(self.__pathColumn))
837 for itm in self.__getUnstageableItems()]
838 if not namesW and not namesS:
839 E5MessageBox.information(
840 self,
841 self.tr("Differences"),
842 self.tr("""There are no uncommitted changes"""
843 """ available/selected."""))
844 return
845
846 diffMode = "work2stage2repo"
847 names = namesW + namesS
848
849 if self.diff is None:
850 from .GitDiffDialog import GitDiffDialog
851 self.diff = GitDiffDialog(self.vcs)
852 self.diff.show()
853 self.diff.start(names, diffMode=diffMode, refreshable=True)
854
855 def __sbsDiff(self):
856 """
857 Private slot to handle the Diff context menu entry.
858 """
859 itm = self.__getModifiedOnlyItems()[0]
860 workModified = (itm.text(self.__statusWorkColumn) in
861 self.modifiedOnlyIndicators)
862 stageModified = (itm.text(self.__statusIndexColumn) in
863 self.modifiedOnlyIndicators)
864 names = [os.path.join(self.dname, itm.text(self.__pathColumn))]
865
866 if workModified and stageModified:
867 # select from all three variants
868 messages = [
869 self.tr("Working Tree to Staging Area"),
870 self.tr("Staging Area to HEAD Commit"),
871 self.tr("Working Tree to HEAD Commit"),
872 ]
873 result, ok = QInputDialog.getItem(
874 None,
875 self.tr("Side-by-Side Difference"),
876 self.tr("Select the compare method."),
877 messages,
878 0, False)
879 if not ok:
880 return
881
882 if result == messages[0]:
883 revisions = ["", ""]
884 elif result == messages[1]:
885 revisions = ["HEAD", "Stage"]
886 else:
887 revisions = ["HEAD", ""]
888 elif workModified:
889 # select from work variants
890 messages = [
891 self.tr("Working Tree to Staging Area"),
892 self.tr("Working Tree to HEAD Commit"),
893 ]
894 result, ok = QInputDialog.getItem(
895 None,
896 self.tr("Side-by-Side Difference"),
897 self.tr("Select the compare method."),
898 messages,
899 0, False)
900 if not ok:
901 return
902
903 if result == messages[0]:
904 revisions = ["", ""]
905 else:
906 revisions = ["HEAD", ""]
907 else:
908 revisions = ["HEAD", "Stage"]
909
910 self.vcs.gitSbsDiff(names[0], revisions=revisions)
911
912 def __getCommitableItems(self):
913 """
914 Private method to retrieve all entries the user wants to commit.
915
916 @return list of all items, the user has checked
917 """
918 commitableItems = []
919 for index in range(self.statusList.topLevelItemCount()):
920 itm = self.statusList.topLevelItem(index)
921 if itm.checkState(self.__toBeCommittedColumn) == Qt.Checked:
922 commitableItems.append(itm)
923 return commitableItems
924
925 def __getCommitableUnselectedItems(self):
926 """
927 Private method to retrieve all entries the user may commit but hasn't
928 selected.
929
930 @return list of all items, the user has not checked
931 """
932 items = []
933 for index in range(self.statusList.topLevelItemCount()):
934 itm = self.statusList.topLevelItem(index)
935 if itm.flags() & Qt.ItemIsUserCheckable and \
936 itm.checkState(self.__toBeCommittedColumn) == Qt.Unchecked:
937 items.append(itm)
938 return items
939
940 def __getModifiedItems(self):
941 """
942 Private method to retrieve all entries, that have a modified status.
943
944 @return list of all items with a modified status
945 """
946 modifiedItems = []
947 for itm in self.statusList.selectedItems():
948 if (itm.text(self.__statusWorkColumn) in
949 self.modifiedIndicators or
950 itm.text(self.__statusIndexColumn) in
951 self.modifiedIndicators):
952 modifiedItems.append(itm)
953 return modifiedItems
954
955 def __getModifiedOnlyItems(self):
956 """
957 Private method to retrieve all entries, that have a modified status.
958
959 @return list of all items with a modified status
960 """
961 modifiedItems = []
962 for itm in self.statusList.selectedItems():
963 if (itm.text(self.__statusWorkColumn) in
964 self.modifiedOnlyIndicators or
965 itm.text(self.__statusIndexColumn) in
966 self.modifiedOnlyIndicators):
967 modifiedItems.append(itm)
968 return modifiedItems
969
970 def __getUnversionedItems(self):
971 """
972 Private method to retrieve all entries, that have an unversioned
973 status.
974
975 @return list of all items with an unversioned status
976 """
977 unversionedItems = []
978 for itm in self.statusList.selectedItems():
979 if itm.text(self.__statusWorkColumn) in self.unversionedIndicators:
980 unversionedItems.append(itm)
981 return unversionedItems
982
983 def __getStageableItems(self):
984 """
985 Private method to retrieve all entries, that have a stageable
986 status.
987
988 @return list of all items with a stageable status
989 """
990 stageableItems = []
991 for itm in self.statusList.selectedItems():
992 if itm.text(self.__statusWorkColumn) in \
993 self.modifiedIndicators + self.unmergedIndicators:
994 stageableItems.append(itm)
995 return stageableItems
996
997 def __getUnstageableItems(self):
998 """
999 Private method to retrieve all entries, that have an unstageable
1000 status.
1001
1002 @return list of all items with an unstageable status
1003 """
1004 unstageableItems = []
1005 for itm in self.statusList.selectedItems():
1006 if itm.text(self.__statusIndexColumn) in self.modifiedIndicators:
1007 unstageableItems.append(itm)
1008 return unstageableItems
1009
1010 def __getMissingItems(self):
1011 """
1012 Private method to retrieve all entries, that have a missing status.
1013
1014 @return list of all items with a missing status
1015 """
1016 missingItems = []
1017 for itm in self.statusList.selectedItems():
1018 if itm.text(self.__statusWorkColumn) in self.missingIndicators:
1019 missingItems.append(itm)
1020 return missingItems
1021
1022 def __getConflictingItems(self):
1023 """
1024 Private method to retrieve all entries, that have a conflict status.
1025
1026 @return list of all items with a conflict status
1027 """
1028 conflictingItems = []
1029 for itm in self.statusList.selectedItems():
1030 if itm.data(0, self.ConflictRole):
1031 conflictingItems.append(itm)
1032 return conflictingItems
1033
1034 def __commitSelect(self, selected):
1035 """
1036 Private slot to select or deselect all entries.
1037
1038 @param selected commit selection state to be set (boolean)
1039 """
1040 for index in range(self.statusList.topLevelItemCount()):
1041 itm = self.statusList.topLevelItem(index)
1042 if itm.flags() & Qt.ItemIsUserCheckable:
1043 if selected:
1044 itm.setCheckState(self.__toBeCommittedColumn, Qt.Checked)
1045 else:
1046 itm.setCheckState(self.__toBeCommittedColumn, Qt.Unchecked)
1047
1048 ###########################################################################
1049 ## Diff handling methods below
1050 ###########################################################################
1051
1052 def __generateDiffs(self):
1053 """
1054 Private slot to generate diff outputs for the selected item.
1055 """
1056 self.lDiffEdit.clear()
1057 self.rDiffEdit.clear()
1058 try:
1059 self.lDiffHighlighter.regenerateRules()
1060 self.rDiffHighlighter.regenerateRules()
1061 except AttributeError:
1062 # backward compatibility
1063 pass
1064
1065 selectedItems = self.statusList.selectedItems()
1066 if len(selectedItems) == 1:
1067 fn = os.path.join(self.dname,
1068 selectedItems[0].text(self.__pathColumn))
1069 self.__diffGenerator.start(fn, diffMode="work2stage2repo")
1070
1071 def __generatorFinished(self):
1072 """
1073 Private slot connected to the finished signal of the diff generator.
1074 """
1075 diff1, diff2 = self.__diffGenerator.getResult()[:2]
1076
1077 if diff1:
1078 self.lDiffParser = GitDiffParser(diff1)
1079 for line in diff1[:]:
1080 if line.startswith("@@ "):
1081 break
1082 else:
1083 diff1.pop(0)
1084 self.lDiffEdit.setPlainText("".join(diff1))
1085 else:
1086 self.lDiffParser = None
1087
1088 if diff2:
1089 self.rDiffParser = GitDiffParser(diff2)
1090 for line in diff2[:]:
1091 if line.startswith("@@ "):
1092 break
1093 else:
1094 diff2.pop(0)
1095 self.rDiffEdit.setPlainText("".join(diff2))
1096 else:
1097 self.rDiffParser = None
1098
1099 for diffEdit in [self.lDiffEdit, self.rDiffEdit]:
1100 tc = diffEdit.textCursor()
1101 tc.movePosition(QTextCursor.Start)
1102 diffEdit.setTextCursor(tc)
1103 diffEdit.ensureCursorVisible()
1104
1105 def __showLDiffContextMenu(self, coord):
1106 """
1107 Private slot to show the context menu of the status list.
1108
1109 @param coord position of the mouse pointer (QPoint)
1110 """
1111 if bool(self.lDiffEdit.toPlainText()):
1112 cursor = self.lDiffEdit.textCursor()
1113 if cursor.hasSelection():
1114 self.__stageLinesAct.setEnabled(True)
1115 self.__revertLinesAct.setEnabled(True)
1116 self.__stageHunkAct.setEnabled(False)
1117 self.__revertHunkAct.setEnabled(False)
1118 else:
1119 self.__stageLinesAct.setEnabled(False)
1120 self.__revertLinesAct.setEnabled(False)
1121 self.__stageHunkAct.setEnabled(True)
1122 self.__revertHunkAct.setEnabled(True)
1123
1124 cursor = self.lDiffEdit.cursorForPosition(coord)
1125 self.lDiffEdit.setTextCursor(cursor)
1126
1127 self.__lDiffMenu.popup(self.lDiffEdit.mapToGlobal(coord))
1128
1129 def __showRDiffContextMenu(self, coord):
1130 """
1131 Private slot to show the context menu of the status list.
1132
1133 @param coord position of the mouse pointer (QPoint)
1134 """
1135 if bool(self.rDiffEdit.toPlainText()):
1136 cursor = self.rDiffEdit.textCursor()
1137 if cursor.hasSelection():
1138 self.__unstageLinesAct.setEnabled(True)
1139 self.__unstageHunkAct.setEnabled(False)
1140 else:
1141 self.__unstageLinesAct.setEnabled(False)
1142 self.__unstageHunkAct.setEnabled(True)
1143
1144 cursor = self.rDiffEdit.cursorForPosition(coord)
1145 self.rDiffEdit.setTextCursor(cursor)
1146
1147 self.__rDiffMenu.popup(self.rDiffEdit.mapToGlobal(coord))
1148
1149 def __stageHunkOrLines(self):
1150 """
1151 Private method to stage the selected lines or hunk.
1152 """
1153 cursor = self.lDiffEdit.textCursor()
1154 startIndex, endIndex = self.__selectedLinesIndexes(self.lDiffEdit)
1155 if cursor.hasSelection():
1156 patch = self.lDiffParser.createLinesPatch(startIndex, endIndex)
1157 else:
1158 patch = self.lDiffParser.createHunkPatch(startIndex)
1159 if patch:
1160 patchFile = self.__tmpPatchFileName()
1161 try:
1162 f = open(patchFile, "w")
1163 f.write(patch)
1164 f.close()
1165 self.vcs.gitApply(self.dname, patchFile, cached=True,
1166 noDialog=True)
1167 self.on_refreshButton_clicked()
1168 finally:
1169 os.remove(patchFile)
1170
1171 def __unstageHunkOrLines(self):
1172 """
1173 Private method to unstage the selected lines or hunk.
1174 """
1175 cursor = self.rDiffEdit.textCursor()
1176 startIndex, endIndex = self.__selectedLinesIndexes(self.rDiffEdit)
1177 if cursor.hasSelection():
1178 patch = self.rDiffParser.createLinesPatch(startIndex, endIndex,
1179 reverse=True)
1180 else:
1181 patch = self.rDiffParser.createHunkPatch(startIndex)
1182 if patch:
1183 patchFile = self.__tmpPatchFileName()
1184 try:
1185 f = open(patchFile, "w")
1186 f.write(patch)
1187 f.close()
1188 self.vcs.gitApply(self.dname, patchFile, cached=True,
1189 reverse=True, noDialog=True)
1190 self.on_refreshButton_clicked()
1191 finally:
1192 os.remove(patchFile)
1193
1194 def __revertHunkOrLines(self):
1195 """
1196 Private method to revert the selected lines or hunk.
1197 """
1198 cursor = self.lDiffEdit.textCursor()
1199 startIndex, endIndex = self.__selectedLinesIndexes(self.lDiffEdit)
1200 if cursor.hasSelection():
1201 title = self.tr("Revert selected lines")
1202 else:
1203 title = self.tr("Revert hunk")
1204 res = E5MessageBox.yesNo(
1205 self,
1206 title,
1207 self.tr("""Are you sure you want to revert the selected"""
1208 """ changes?"""))
1209 if res:
1210 if cursor.hasSelection():
1211 patch = self.lDiffParser.createLinesPatch(startIndex, endIndex,
1212 reverse=True)
1213 else:
1214 patch = self.lDiffParser.createHunkPatch(startIndex)
1215 if patch:
1216 patchFile = self.__tmpPatchFileName()
1217 try:
1218 f = open(patchFile, "w")
1219 f.write(patch)
1220 f.close()
1221 self.vcs.gitApply(self.dname, patchFile, reverse=True,
1222 noDialog=True)
1223 self.on_refreshButton_clicked()
1224 finally:
1225 os.remove(patchFile)
1226
1227 def __selectedLinesIndexes(self, diffEdit):
1228 """
1229 Private method to extract the indexes of the selected lines.
1230
1231 @param diffEdit reference to the edit widget (QTextEdit)
1232 @return tuple of start and end indexes (integer, integer)
1233 """
1234 cursor = diffEdit.textCursor()
1235 selectionStart = cursor.selectionStart()
1236 selectionEnd = cursor.selectionEnd()
1237
1238 startIndex = -1
1239
1240 lineStart = 0
1241 for lineIdx, line in enumerate(diffEdit.toPlainText().splitlines()):
1242 lineEnd = lineStart + len(line)
1243 if lineStart <= selectionStart <= lineEnd:
1244 startIndex = lineIdx
1245 if lineStart <= selectionEnd <= lineEnd:
1246 endIndex = lineIdx
1247 break
1248 lineStart = lineEnd + 1
1249
1250 return startIndex, endIndex
1251
1252 def __tmpPatchFileName(self):
1253 """
1254 Private method to generate a temporary patch file.
1255
1256 @return name of the temporary file (string)
1257 """
1258 prefix = 'eric-git-{0}-'.format(os.getpid())
1259 suffix = '-patch'
1260 fd, path = tempfile.mkstemp(suffix, prefix)
1261 os.close(fd)
1262 return path
1263
1264 def __refreshDiff(self):
1265 """
1266 Private method to refresh the diff output after a refresh.
1267 """
1268 if self.__selectedName:
1269 for index in range(self.statusList.topLevelItemCount()):
1270 itm = self.statusList.topLevelItem(index)
1271 if itm.text(self.__pathColumn) == self.__selectedName:
1272 itm.setSelected(True)
1273 break
1274
1275 self.__selectedName = ""

eric ide

mercurial