Skip to content

Commit 4f76aa4

Browse files
feat(git): wire merge/rebase ops into workspace dock
The GitOperationManager was instantiated in MainWindow but never propagated to the workspace dock or its GitTabWidget, so merge, rebase, and interactive-rebase actions had no way to dispatch work or reflect in-progress state back to the UI. - Inject GitOperationManager into FolderAsWorkspaceDock at wire-up time so both the dock and any lazily-created GitTabWidget share the same manager instance. - Add gitMergeRequested / gitRebaseRequested / gitInteractiveRebaseRequested signals on the dock; MainWindow handles them by showing a BranchPickerPopup and delegating to the manager. - Extend BranchPickerPopup with a select-only mode that emits branchSelected and suppresses the checkout/new-branch context menu — appropriate for merge/rebase target selection. - Add an operation-state banner to GitTabWidget that surfaces Continue / Skip / Abort controls whenever a merge or rebase is suspended, driven by GitOperationManager::operationStateChanged.
1 parent 4a45e2b commit 4f76aa4

7 files changed

Lines changed: 231 additions & 111 deletions

File tree

src/dialogs/MainWindow.cpp

Lines changed: 49 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -2181,6 +2181,8 @@ void MainWindow::registerWorkspaceDock(FolderAsWorkspaceDock *dock)
21812181

21822182
void MainWindow::wireWorkspaceGitSignals(FolderAsWorkspaceDock *dock)
21832183
{
2184+
dock->setGitOperationManager(m_gitOpMgr);
2185+
21842186
connect(dock, &FolderAsWorkspaceDock::gitDiffRequested,
21852187
this, [dock](const GitStatusEntry &entry) {
21862188
dock->showGitDiffPreview(entry);
@@ -2247,6 +2249,52 @@ void MainWindow::wireWorkspaceGitSignals(FolderAsWorkspaceDock *dock)
22472249
});
22482250
menu->addAction(sendToAi);
22492251
});
2252+
2253+
connect(dock, &FolderAsWorkspaceDock::gitMergeRequested, this,
2254+
[this](FolderAsWorkspaceDock *wsDock) {
2255+
auto *ctrl = wsDock->gitTabWidget() ? wsDock->gitTabWidget()->controller() : nullptr;
2256+
if (!ctrl) return;
2257+
auto *picker = new BranchPickerPopup(this);
2258+
picker->setAttribute(Qt::WA_DeleteOnClose);
2259+
picker->setSelectOnly(true, tr("Select branch to merge into %1…").arg(ctrl->currentBranch()));
2260+
picker->setBranches(ctrl->branchesLocal(), ctrl->branchesRemote(),
2261+
ctrl->currentBranch());
2262+
connect(picker, &BranchPickerPopup::branchSelected, this,
2263+
[this, ctrl](const QString &branch) {
2264+
m_gitOpMgr->startMerge(ctrl, branch);
2265+
});
2266+
picker->popupAt(QCursor::pos());
2267+
});
2268+
connect(dock, &FolderAsWorkspaceDock::gitRebaseRequested, this,
2269+
[this](FolderAsWorkspaceDock *wsDock) {
2270+
auto *ctrl = wsDock->gitTabWidget() ? wsDock->gitTabWidget()->controller() : nullptr;
2271+
if (!ctrl) return;
2272+
auto *picker = new BranchPickerPopup(this);
2273+
picker->setAttribute(Qt::WA_DeleteOnClose);
2274+
picker->setSelectOnly(true, tr("Select branch to rebase onto…"));
2275+
picker->setBranches(ctrl->branchesLocal(), ctrl->branchesRemote(),
2276+
ctrl->currentBranch());
2277+
connect(picker, &BranchPickerPopup::branchSelected, this,
2278+
[this, ctrl](const QString &branch) {
2279+
m_gitOpMgr->startRebase(ctrl, branch);
2280+
});
2281+
picker->popupAt(QCursor::pos());
2282+
});
2283+
connect(dock, &FolderAsWorkspaceDock::gitInteractiveRebaseRequested, this,
2284+
[this](FolderAsWorkspaceDock *wsDock) {
2285+
auto *ctrl = wsDock->gitTabWidget() ? wsDock->gitTabWidget()->controller() : nullptr;
2286+
if (!ctrl) return;
2287+
auto *picker = new BranchPickerPopup(this);
2288+
picker->setAttribute(Qt::WA_DeleteOnClose);
2289+
picker->setSelectOnly(true, tr("Select branch for interactive rebase…"));
2290+
picker->setBranches(ctrl->branchesLocal(), ctrl->branchesRemote(),
2291+
ctrl->currentBranch());
2292+
connect(picker, &BranchPickerPopup::branchSelected, this,
2293+
[this, ctrl](const QString &branch) {
2294+
m_gitOpMgr->startInteractiveRebase(ctrl, branch);
2295+
});
2296+
picker->popupAt(QCursor::pos());
2297+
});
22502298
}
22512299

22522300
void MainWindow::restoreOpenWorkspaces()
@@ -3800,116 +3848,10 @@ void MainWindow::saveWorkspaceStatesOnly()
38003848
persistWorkspaceStatesMerged(live);
38013849
}
38023850

3803-
// --- Git Operation Menu & Wiring ---
3851+
// --- Git Operation Wiring ---
38043852

38053853
void MainWindow::setupGitOperationMenu()
38063854
{
3807-
auto *menuGit = menuBar()->addMenu(tr("&Git"));
3808-
3809-
auto *actionMerge = menuGit->addAction(tr("Merge Branch..."));
3810-
auto *actionRebase = menuGit->addAction(tr("Rebase Current Branch..."));
3811-
auto *actionInteractiveRebase = menuGit->addAction(tr("Interactive Rebase..."));
3812-
menuGit->addSeparator();
3813-
auto *actionContinue = menuGit->addAction(tr("Continue"));
3814-
auto *actionSkip = menuGit->addAction(tr("Skip"));
3815-
auto *actionAbort = menuGit->addAction(tr("Abort"));
3816-
3817-
connect(menuGit, &QMenu::aboutToShow, this, [=]() {
3818-
const QString wsRoot = currentWorkspaceRoot();
3819-
bool hasWorkspace = !wsRoot.isEmpty();
3820-
auto state = m_gitOpMgr->state(wsRoot);
3821-
bool idle = (state == GitOperationManager::OperationState::Idle);
3822-
bool suspended = (state == GitOperationManager::OperationState::RebaseSuspended ||
3823-
state == GitOperationManager::OperationState::RebaseSuspendedEdit ||
3824-
state == GitOperationManager::OperationState::MergeConflicted);
3825-
3826-
actionMerge->setEnabled(hasWorkspace && idle);
3827-
actionRebase->setEnabled(hasWorkspace && idle);
3828-
actionInteractiveRebase->setEnabled(hasWorkspace && idle);
3829-
actionContinue->setEnabled(hasWorkspace && suspended);
3830-
actionSkip->setEnabled(hasWorkspace &&
3831-
(state == GitOperationManager::OperationState::RebaseSuspended));
3832-
actionAbort->setEnabled(hasWorkspace && suspended);
3833-
});
3834-
3835-
connect(actionMerge, &QAction::triggered, this, [this]() {
3836-
auto *dock = activeWorkspaceDock();
3837-
if (!dock) return;
3838-
auto *ctrl = dock->gitTabWidget() ? dock->gitTabWidget()->controller() : nullptr;
3839-
if (!ctrl) return;
3840-
// Use BranchPickerPopup for branch selection
3841-
auto *picker = new BranchPickerPopup(this);
3842-
picker->setBranches(ctrl->branchesLocal(), ctrl->branchesRemote(),
3843-
ctrl->currentBranch());
3844-
connect(picker, &BranchPickerPopup::checkoutRequested, this,
3845-
[this, ctrl](const QString &branch) {
3846-
m_gitOpMgr->startMerge(ctrl, branch);
3847-
});
3848-
picker->popupAt(QCursor::pos());
3849-
});
3850-
3851-
connect(actionRebase, &QAction::triggered, this, [this]() {
3852-
auto *dock = activeWorkspaceDock();
3853-
if (!dock) return;
3854-
auto *ctrl = dock->gitTabWidget() ? dock->gitTabWidget()->controller() : nullptr;
3855-
if (!ctrl) return;
3856-
auto *picker = new BranchPickerPopup(this);
3857-
picker->setBranches(ctrl->branchesLocal(), ctrl->branchesRemote(),
3858-
ctrl->currentBranch());
3859-
connect(picker, &BranchPickerPopup::checkoutRequested, this,
3860-
[this, ctrl](const QString &branch) {
3861-
m_gitOpMgr->startRebase(ctrl, branch);
3862-
});
3863-
picker->popupAt(QCursor::pos());
3864-
});
3865-
3866-
connect(actionInteractiveRebase, &QAction::triggered, this, [this]() {
3867-
auto *dock = activeWorkspaceDock();
3868-
if (!dock) return;
3869-
auto *ctrl = dock->gitTabWidget() ? dock->gitTabWidget()->controller() : nullptr;
3870-
if (!ctrl) return;
3871-
auto *picker = new BranchPickerPopup(this);
3872-
picker->setBranches(ctrl->branchesLocal(), ctrl->branchesRemote(),
3873-
ctrl->currentBranch());
3874-
connect(picker, &BranchPickerPopup::checkoutRequested, this,
3875-
[this, ctrl](const QString &branch) {
3876-
m_gitOpMgr->startInteractiveRebase(ctrl, branch);
3877-
});
3878-
picker->popupAt(QCursor::pos());
3879-
});
3880-
3881-
connect(actionContinue, &QAction::triggered, this, [this]() {
3882-
auto *dock = activeWorkspaceDock();
3883-
if (!dock) return;
3884-
auto *ctrl = dock->gitTabWidget() ? dock->gitTabWidget()->controller() : nullptr;
3885-
if (!ctrl) return;
3886-
auto state = m_gitOpMgr->state(ctrl->currentRepo());
3887-
if (state == GitOperationManager::OperationState::MergeConflicted)
3888-
m_gitOpMgr->commitMerge(ctrl);
3889-
else
3890-
m_gitOpMgr->continueRebase(ctrl);
3891-
});
3892-
3893-
connect(actionSkip, &QAction::triggered, this, [this]() {
3894-
auto *dock = activeWorkspaceDock();
3895-
if (!dock) return;
3896-
auto *ctrl = dock->gitTabWidget() ? dock->gitTabWidget()->controller() : nullptr;
3897-
if (!ctrl) return;
3898-
m_gitOpMgr->skipRebase(ctrl);
3899-
});
3900-
3901-
connect(actionAbort, &QAction::triggered, this, [this]() {
3902-
auto *dock = activeWorkspaceDock();
3903-
if (!dock) return;
3904-
auto *ctrl = dock->gitTabWidget() ? dock->gitTabWidget()->controller() : nullptr;
3905-
if (!ctrl) return;
3906-
auto state = m_gitOpMgr->state(ctrl->currentRepo());
3907-
if (state == GitOperationManager::OperationState::MergeConflicted)
3908-
m_gitOpMgr->abortMerge(ctrl);
3909-
else
3910-
m_gitOpMgr->abortRebase(ctrl);
3911-
});
3912-
39133855
// Connect operation manager signals to UI
39143856
connect(m_gitOpMgr, &GitOperationManager::mergeConflicted, this,
39153857
&MainWindow::showConflictListDock);

src/docks/FolderAsWorkspaceDock.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,12 @@ void FolderAsWorkspaceDock::wireTreeContextMenu()
249249
});
250250
}
251251

252+
void FolderAsWorkspaceDock::setGitOperationManager(GitOperationManager *mgr)
253+
{
254+
m_gitOpMgr = mgr;
255+
if (gitTab) gitTab->setOperationManager(mgr);
256+
}
257+
252258
void FolderAsWorkspaceDock::maybeScheduleGitTabForDecoration()
253259
{
254260
if (m_gitTabScheduled || gitTab != nullptr) return;
@@ -365,6 +371,7 @@ void FolderAsWorkspaceDock::ensureGitTab()
365371
return;
366372
}
367373
gitTab = new GitTabWidget(rootPath(), this);
374+
if (m_gitOpMgr) gitTab->setOperationManager(m_gitOpMgr);
368375
auto *layout = qobject_cast<QVBoxLayout *>(ui->gitTab->layout());
369376
if (layout) {
370377
layout->addWidget(gitTab);
@@ -385,6 +392,15 @@ void FolderAsWorkspaceDock::ensureGitTab()
385392
this, &FolderAsWorkspaceDock::gitOpenCommitDetailRequested);
386393
connect(gitTab, &GitTabWidget::changesTreeContextMenuRequested,
387394
this, &FolderAsWorkspaceDock::gitChangesContextMenuRequested);
395+
connect(gitTab, &GitTabWidget::mergeRequested, this, [this]() {
396+
emit gitMergeRequested(this);
397+
});
398+
connect(gitTab, &GitTabWidget::rebaseRequested, this, [this]() {
399+
emit gitRebaseRequested(this);
400+
});
401+
connect(gitTab, &GitTabWidget::interactiveRebaseRequested, this, [this]() {
402+
emit gitInteractiveRebaseRequested(this);
403+
});
388404

389405
// File-tree decoration: subscribe to status changes so the FsModel
390406
// repaints only the rows that changed. The controller is created lazily

src/docks/FolderAsWorkspaceDock.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class QTimer;
4040
class GitTabWidget;
4141
class GitDiffViewController;
4242
class GitCommitView;
43+
class GitOperationManager;
4344
class ScintillaNext;
4445
class SubmoduleStatusFetcher;
4546

@@ -66,6 +67,8 @@ class FolderAsWorkspaceDock : public QDockWidget
6667
void setRootPath(const QString dir);
6768
QString rootPath() const;
6869

70+
void setGitOperationManager(GitOperationManager *mgr);
71+
6972
// Returns the lazily-created Git tab, or nullptr if the user has never
7073
// opened the Git tab in this dock yet.
7174
GitTabWidget *gitTabWidget() const { return gitTab; }
@@ -103,6 +106,9 @@ class FolderAsWorkspaceDock : public QDockWidget
103106
void gitOpenSubmoduleRequested(const QString &absPath);
104107
void gitOpenCommitDetailRequested(const QByteArray &sha);
105108
void gitChangesContextMenuRequested(QMenu *menu, const GitStatusEntry &entry);
109+
void gitMergeRequested(FolderAsWorkspaceDock *dock);
110+
void gitRebaseRequested(FolderAsWorkspaceDock *dock);
111+
void gitInteractiveRebaseRequested(FolderAsWorkspaceDock *dock);
106112
// Forwarded from the per-dock GitDiffViewController — host raises this
107113
// editor as the active tab so the user lands on the rendered diff.
108114
void gitDiffPreviewRendered(ScintillaNext *editor);
@@ -152,6 +158,7 @@ private slots:
152158
GitTabWidget *gitTab = nullptr;
153159
GitDiffViewController *gitDiffViewController = nullptr;
154160
GitCommitView *gitCommitView = nullptr;
161+
GitOperationManager *m_gitOpMgr = nullptr;
155162

156163
QTimer *tooltipTimer;
157164
QPersistentModelIndex pendingTooltipIndex;

src/git/BranchPickerPopup.cpp

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@ void BranchPickerPopup::popupAt(const QPoint &globalPos)
109109
m_filter->setFocus();
110110
}
111111

112+
void BranchPickerPopup::setSelectOnly(bool on, const QString &title)
113+
{
114+
m_selectOnly = on;
115+
if (on && !title.isEmpty())
116+
m_filter->setPlaceholderText(title);
117+
else
118+
m_filter->setPlaceholderText(tr("Search branches…"));
119+
}
120+
112121
void BranchPickerPopup::rebuild()
113122
{
114123
m_model->clear();
@@ -133,9 +142,14 @@ void BranchPickerPopup::rebuild()
133142
if (!locals.isEmpty()) {
134143
addHeader(tr("── Local ──"));
135144
for (const auto &b : locals) {
136-
QString display = b;
137-
if (b == m_current) display = QStringLiteral("") + b + QStringLiteral("");
138-
else display = QStringLiteral(" ") + b + QStringLiteral("");
145+
QString display;
146+
if (m_selectOnly) {
147+
display = (b == m_current) ? QStringLiteral("") + b
148+
: QStringLiteral(" ") + b;
149+
} else {
150+
display = (b == m_current) ? QStringLiteral("") + b + QStringLiteral("")
151+
: QStringLiteral(" ") + b + QStringLiteral("");
152+
}
139153
auto *it = new QStandardItem(display);
140154
it->setData(0, RoleKind);
141155
it->setData(b, RolePayload);
@@ -149,7 +163,9 @@ void BranchPickerPopup::rebuild()
149163
if (!remotes.isEmpty()) {
150164
addHeader(tr("── Remote ──"));
151165
for (const auto &b : remotes) {
152-
auto *it = new QStandardItem(QStringLiteral(" ") + b + QStringLiteral(""));
166+
QString display = m_selectOnly ? QStringLiteral(" ") + b
167+
: QStringLiteral(" ") + b + QStringLiteral("");
168+
auto *it = new QStandardItem(display);
153169
it->setData(1, RoleKind);
154170
it->setData(b, RolePayload);
155171
m_model->appendRow(it);
@@ -176,6 +192,13 @@ void BranchPickerPopup::onActivated(const QModelIndex &index)
176192
if (!index.isValid()) return;
177193
auto *it = m_model->itemFromIndex(index);
178194
if (!it || !it->isEnabled()) return;
195+
196+
if (m_selectOnly) {
197+
const QString payload = it->data(RolePayload).toString();
198+
emit branchSelected(payload);
199+
close();
200+
return;
201+
}
179202
showItemMenu(index);
180203
}
181204

src/git/BranchPickerPopup.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,16 @@ class BranchPickerPopup : public QWidget
3939
bool detachedHead = false);
4040
void popupAt(const QPoint &globalPos);
4141

42+
// In select-only mode, clicking a branch emits branchSelected and closes
43+
// immediately — no context menu with Checkout/New Branch/Set Upstream.
44+
// Use for merge/rebase target selection.
45+
void setSelectOnly(bool on, const QString &title = {});
46+
4247
static QString sanitizeBranchName(const QString &raw);
4348

4449
signals:
4550
void checkoutRequested(const QString &name);
51+
void branchSelected(const QString &name);
4652
void createBranchRequested(const QString &name, const QString &base, bool setUpstream);
4753
void setUpstreamRequested(const QString &remoteBranch);
4854

@@ -65,6 +71,7 @@ private slots:
6571
QStringList m_local;
6672
QStringList m_remote;
6773
bool m_detachedHead = false;
74+
bool m_selectOnly = false;
6875
};
6976

7077
#endif // BRANCH_PICKER_POPUP_H

0 commit comments

Comments
 (0)