Plugins/VcsPlugins/vcsGit/GitStatusDialog.py

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

eric ide

mercurial