Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions gitfourchette/forms/commitinfodialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# -----------------------------------------------------------------------------
# Copyright (C) 2026 Iliyas Jorio.
# This file is part of GitFourchette, distributed under the GNU GPL v3.
# For full terms, see the included LICENSE file.
# -----------------------------------------------------------------------------

from gitfourchette.localization import _
from gitfourchette.qt import *
from gitfourchette.toolbox.iconbank import stockIcon


class CommitInfoDialog(QDialog):
"""
Rich summary (HTML) plus optional read-only body, in a resizable dialog.
Used instead of QMessageBox.setDetailedText because QMessageBox forces a
fixed size whenever its layout updates.
"""

def __init__(
self,
parent: QWidget | None,
title: str,
summaryHtml: str,
body: str = "",
):
super().__init__(parent)

self.setWindowTitle(title)
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
self.setWindowModality(Qt.WindowModality.WindowModal)

pm = stockIcon("SP_MessageBoxInformation").pixmap(48, 48)
iconLabel = QLabel(self)
iconLabel.setPixmap(pm)
iconLabel.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)

self.summaryLabel = QLabel(self)
self.summaryLabel.setObjectName("commit_info_summary")
self.summaryLabel.setTextFormat(Qt.TextFormat.RichText)
self.summaryLabel.setText(summaryHtml)
self.summaryLabel.setWordWrap(True)
self.summaryLabel.setOpenExternalLinks(False)
self.summaryLabel.setTextInteractionFlags(
Qt.TextInteractionFlag.TextBrowserInteraction)
self.summaryLabel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)

self._detailsEdit: QPlainTextEdit | None = None
self._detailsToggleButton: QAbstractButton | None = None

textColumn = QVBoxLayout()
textColumn.setSpacing(12)
textColumn.addWidget(self.summaryLabel)
if body:
self._detailsEdit = QPlainTextEdit(self)
self._detailsEdit.setPlainText(body)
self._detailsEdit.setReadOnly(True)
self._detailsEdit.setMinimumHeight(200)
self._detailsEdit.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
textColumn.addWidget(self._detailsEdit, stretch=1)

row = QHBoxLayout()
row.addWidget(iconLabel, alignment=Qt.AlignmentFlag.AlignTop)
row.addLayout(textColumn, stretch=1)

buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok, parent=self)
buttonBox.accepted.connect(self.accept)
buttonBox.rejected.connect(self.reject)
ok = buttonBox.button(QDialogButtonBox.StandardButton.Ok)
if ok:
ok.setDefault(True)

if body:
self._detailsToggleButton = buttonBox.addButton(
_("Hide Details..."), QDialogButtonBox.ButtonRole.ActionRole)
self._detailsToggleButton.clicked.connect(self._toggleDetailsPane)

self._outerLayout = QVBoxLayout(self)
self._outerLayout.addLayout(row, stretch=1 if body else 0)
self._outerLayout.addWidget(buttonBox)

self.setMinimumWidth(480)
self._resizableMinimumSize = self.minimumSize()
self._resizableMaximumSize = self.maximumSize()
self._detailsVisible = bool(body)
QTimer.singleShot(0, self._applyResizePolicy)

def _toggleDetailsPane(self):
if not self._detailsEdit or not self._detailsToggleButton:
return
show = not self._detailsEdit.isVisible()
self._detailsEdit.setVisible(show)
self._detailsVisible = show
self._detailsToggleButton.setText(
_("Hide Details...") if show else _("Show Details..."))

# No extra vertical slack above the button row when details are hidden.
self._outerLayout.setStretch(0, 1 if show else 0)
self._outerLayout.activate()
self.updateGeometry()

self._applyResizePolicy()

def _applyResizePolicy(self):
minHint = self.minimumSizeHint()
if self._detailsVisible:
minSize = QSize(self._resizableMinimumSize)
minSize.setWidth(max(480, minSize.width()))
minSize.setHeight(max(minHint.height(), minSize.height()))
self.setSizeGripEnabled(True)
self.setMinimumSize(minSize)
self.setMaximumSize(self._resizableMaximumSize)
if self.height() < minSize.height():
self.resize(max(self.width(), minSize.width()), minSize.height())
return

compactSize = QSize(minHint)
compactSize.setWidth(max(480, compactSize.width()))
self.setSizeGripEnabled(False)
self.resize(compactSize)
self.setMinimumSize(compactSize)
self.setMaximumSize(compactSize)
30 changes: 7 additions & 23 deletions gitfourchette/tasks/misctasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from pathlib import Path

from gitfourchette import settings
from gitfourchette.forms.commitinfodialog import CommitInfoDialog
from gitfourchette.forms.ignorepatterndialog import IgnorePatternDialog
from gitfourchette.forms.reposettingsdialog import RepoSettingsDialog
from gitfourchette.gitdriver.parsers import iterateLines
Expand Down Expand Up @@ -169,29 +170,12 @@ def tableRow(th, td):
<table>{table}</table>
"""

messageBox = asyncMessageBox(
self.parentWidget(), "information", title, markup,
buttons=QMessageBox.StandardButton.Ok, macShowTitle=False)

if details:
messageBox.setDetailedText(details)

# Pre-click "Show Details" button
for button in messageBox.buttons():
role = messageBox.buttonRole(button)
if role == QMessageBox.ButtonRole.ActionRole:
button.click()
elif role == QMessageBox.ButtonRole.AcceptRole:
messageBox.setDefaultButton(button)

# Bind links to callbacks
label: QLabel = messageBox.findChild(QLabel, "qt_msgbox_label")
assert label
label.setOpenExternalLinks(False)
label.linkActivated.connect(linkBundle.processLink)
label.linkActivated.connect(messageBox.accept)

yield from self.flowDialog(messageBox)
dialog = CommitInfoDialog(self.parentWidget(), title, markup, details)

dialog.summaryLabel.linkActivated.connect(linkBundle.processLink)
dialog.summaryLabel.linkActivated.connect(dialog.accept)

yield from self.flowDialog(dialog)


class VerifyGpgSignature(RepoTask):
Expand Down
4 changes: 2 additions & 2 deletions test/test_gpgsigning.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def testCommitWithPgpSignature(tempDir, mainWindow, tempGpgHome, amend):
# Look for GPG signing information in GetCommitInfo dialog
triggerMenuAction(mainWindow.menuBar(), "view/go to head")
triggerContextMenuAction(rw.graphView.viewport(), "get info")
findQMessageBox(rw, f"signature:.+good signature; key trusted.+{aliceKeyId}").reject()
rejectCommitInfoDialog(rw, f"signature:.+good signature; key trusted.+{aliceKeyId}")

runGit("verify-commit", "-v", str(commit.id), directory=wd)

Expand Down Expand Up @@ -217,7 +217,7 @@ def testCommitWithSshSignature(tempDir, mainWindow, tempGpgHome, amend, passphra
# Look for signing information in GetCommitInfo dialog
triggerMenuAction(mainWindow.menuBar(), "view/go to head")
triggerContextMenuAction(rw.graphView.viewport(), "get info")
findQMessageBox(rw, "signature:.+good signature; key trusted").reject()
rejectCommitInfoDialog(rw, "signature:.+good signature; key trusted")


@requiresGpg
Expand Down
15 changes: 8 additions & 7 deletions test/test_graphview.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,10 +388,11 @@ def testCommitInfo(tempDir, mainWindow, method):
else:
raise NotImplementedError(f"unknown method {method}")

qmb = findQMessageBox(rw, "Merge branch 'a' into c")
assert str(oid1) in qmb.text()
assert "A U Thor" in qmb.text()
qmb.accept()
dlg = findCommitInfoDialog(rw, "Merge branch 'a' into c")
summary = dlg.summaryLabel.text()
assert str(oid1) in summary
assert "A U Thor" in summary
dlg.accept()


def testCommitInfoJumpToParent(tempDir, mainWindow):
Expand All @@ -401,10 +402,10 @@ def testCommitInfoJumpToParent(tempDir, mainWindow):
rw.jump(NavLocator.inCommit(oid1, "a/a1.txt"), check=True)

triggerContextMenuAction(rw.graphView.viewport(), "get info")
qmb = findQMessageBox(rw, "Merge branch 'a' into c")
dlg = findCommitInfoDialog(rw, "Merge branch 'a' into c")

# Click on a "parent" link; this should close the message box and jump to another commit
label = qmb.findChild(QLabel, "qt_msgbox_label")
# Click on a "parent" link; this should close the dialog and jump to another commit
label = dlg.summaryLabel
parentLink = re.search(r'<a href="(.*\S+)">6462e7d.+</a>', label.text(), re.I).group(1)
label.linkActivated.emit(parentLink)
assert rw.navLocator.commit == Oid(hex="6462e7d8024396b14d7651e2ec11e2bbf07a05c4")
Expand Down
2 changes: 1 addition & 1 deletion test/test_tasks_blame.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ def testBlameContextMenu(blameWindow):
assert NavLocator.inCommit(BlameFixture.revs["initial"], "hello.txt").isSimilarEnoughTo(rw.navLocator)

triggerContextMenuAction(viewport, "commit info")
acceptQMessageBox(blameWindow, r"first commit.+acecd5e.+j\. david")
acceptCommitInfoDialog(blameWindow, r"first commit.+acecd5e.+j\. david")

qcbSetIndex(scrubber, "uncommitted")
triggerContextMenuAction(viewport, "show diff in working directory")
Expand Down
35 changes: 35 additions & 0 deletions test/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,41 @@ def attempt() -> RepoWidget | None:
return rw


def findCommitInfoDialog(parent: QWidget, contentPattern: str):
"""Find a visible :class:`~gitfourchette.forms.commitinfodialog.CommitInfoDialog` by text in summary or body."""
from gitfourchette.forms.commitinfodialog import CommitInfoDialog

numFound = 0
haystack = ""
for dlg in parent.findChildren(CommitInfoDialog):
if not dlg.isVisibleTo(parent):
continue
numFound += 1
parts = [dlg.windowTitle()]
for w in dlg.findChildren(QLabel):
parts.append(w.text())
for w in dlg.findChildren(QPlainTextEdit):
parts.append(w.toPlainText())
haystack = stripHtml("\n".join(parts))
if re.search(contentPattern, haystack, re.IGNORECASE | re.DOTALL):
return dlg

raise KeyError(
f'did not find "{contentPattern}" among {numFound} CommitInfoDialogs. Last haystack: {haystack!r}')


def acceptCommitInfoDialog(parent: QWidget, contentPattern: str):
findCommitInfoDialog(parent, contentPattern).accept()
parent.activateWindow()
QTest.qWait(0)


def rejectCommitInfoDialog(parent: QWidget, contentPattern: str):
findCommitInfoDialog(parent, contentPattern).reject()
parent.activateWindow()
QTest.qWait(0)


def findQMessageBox(parent: QWidget, textPattern: str) -> QMessageBox:
numBoxesFound = 0
haystack = ""
Expand Down