Skip to content

Commit f049e96

Browse files
feat: PR-aware worktree merge (push to PR branch), conflict handling, worktree path in context messages
1 parent 66eaf1b commit f049e96

3 files changed

Lines changed: 122 additions & 38 deletions

File tree

crates/tauri-app/frontend/src/components/chat/ChatArea.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,17 @@ export function ChatArea() {
182182
<span class="wt-branch">{worktree()!.branch}</span>
183183
<span class="wt-path">{worktree()!.path}</span>
184184
</div>
185-
<button class="wt-merge-btn" onClick={handleMergeWorktree}>Merge back to main</button>
185+
<button class="wt-merge-btn" onClick={handleMergeWorktree}>
186+
{(() => {
187+
const proj = activeProject();
188+
const tab = store.activeTab;
189+
if (proj && tab) {
190+
const prMap = store.projectPrMap[proj.id];
191+
if (prMap?.[tab]) return `Push to PR #${prMap[tab]}`;
192+
}
193+
return "Merge back to main";
194+
})()}
195+
</button>
186196
</div>
187197
</Show>
188198

crates/tauri-app/frontend/src/components/chat/ThreadSetup.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,11 @@ export function ThreadSetup(props: Props) {
5555
}))
5656
);
5757
}
58-
// Add a context message
59-
const context = createWorktree
60-
? `Working on branch \`${branchName}\` in a worktree. What would you like to do?`
61-
: `Working on the main branch. What would you like to do?`;
58+
// Add a context message — include worktree path
59+
const wt = store.worktrees[props.threadId];
60+
const context = createWorktree && wt
61+
? `Working on branch \`${branchName}\` in a worktree at ${wt.path}. What would you like to do?`
62+
: `Working on the main branch at ${props.repoPath}. What would you like to do?`;
6263
const msgId = await ipc.persistUserMessage(props.threadId, context);
6364
setStore("threadMessages", props.threadId, (msgs) => [
6465
...(msgs || []),
@@ -102,8 +103,10 @@ export function ThreadSetup(props: Props) {
102103
console.error("Worktree creation failed (continuing without):", e);
103104
}
104105

105-
// Inject context
106-
const context = `I'm working on PR #${pr.number}: "${pr.title}" by ${pr.author}\n\nBranch: ${pr.branch}${pr.base}\n+${pr.additions} -${pr.deletions} across ${pr.changed_files} files\n${pr.labels.length > 0 ? `Labels: ${pr.labels.join(", ")}\n` : ""}Help me review or continue work on this PR.`;
106+
// Inject context — include worktree path so the agent knows where to work
107+
const wt = store.worktrees[props.threadId];
108+
const wtInfo = wt ? `\n\nWorktree: ${wt.path} (branch: ${wt.branch})` : "";
109+
const context = `I'm working on PR #${pr.number}: "${pr.title}" by ${pr.author}\n\nBranch: ${pr.branch}${pr.base}\n+${pr.additions} -${pr.deletions} across ${pr.changed_files} files\n${pr.labels.length > 0 ? `Labels: ${pr.labels.join(", ")}\n` : ""}${wtInfo}\n\nHelp me review or continue work on this PR.`;
107110
const msgId = await ipc.persistUserMessage(props.threadId, context);
108111
setStore("threadMessages", props.threadId, (msgs) => [
109112
...(msgs || []),

crates/tauri-app/src/commands/worktree.rs

Lines changed: 102 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -141,40 +141,111 @@ pub fn merge_worktree(
141141
main_branch
142142
};
143143

144-
// Merge the worktree branch into main
145-
let merge = Command::new("git")
146-
.args(["merge", &branch, "--no-edit"])
147-
.current_dir(&project_path)
148-
.output()
149-
.map_err(|e| format!("Failed to merge: {e}"))?;
144+
// Check if this thread is linked to a PR
145+
let pr_number = {
146+
let db = state.db.lock().map_err(|e| format!("{e}"))?;
147+
codeforge_persistence::queries::get_setting(
148+
db.conn(),
149+
&format!("pr:{thread_id}"),
150+
)
151+
.ok()
152+
.flatten()
153+
};
150154

151-
if !merge.status.success() {
152-
return Err(format!(
153-
"Merge failed (may have conflicts): {}",
154-
String::from_utf8_lossy(&merge.stderr)
155-
));
156-
}
155+
if let Some(_pr_num) = &pr_number {
156+
// PR mode: commit and push the worktree branch (don't merge to main)
157+
// First, commit any uncommitted changes in the worktree
158+
let status = Command::new("git")
159+
.args(["status", "--porcelain"])
160+
.current_dir(&worktree_path)
161+
.output()
162+
.map_err(|e| format!("Failed to check status: {e}"))?;
157163

158-
// Remove worktree
159-
let _ = Command::new("git")
160-
.args(["worktree", "remove", &worktree_path, "--force"])
161-
.current_dir(&project_path)
162-
.output();
164+
let has_changes = !String::from_utf8_lossy(&status.stdout).trim().is_empty();
163165

164-
// Delete the branch
165-
let _ = Command::new("git")
166-
.args(["branch", "-d", &branch])
167-
.current_dir(&project_path)
168-
.output();
166+
if has_changes {
167+
let _ = Command::new("git")
168+
.args(["add", "-A"])
169+
.current_dir(&worktree_path)
170+
.output();
169171

170-
// Remove setting
171-
{
172-
let db = state.db.lock().map_err(|e| format!("{e}"))?;
173-
let _ = codeforge_persistence::queries::delete_setting(
174-
db.conn(),
175-
&format!("worktree:{thread_id}"),
176-
);
177-
}
172+
let _ = Command::new("git")
173+
.args(["commit", "-m", "Changes from CodeForge"])
174+
.current_dir(&worktree_path)
175+
.output();
176+
}
177+
178+
// Push the branch
179+
let push = Command::new("git")
180+
.args(["push", "origin", &branch])
181+
.current_dir(&worktree_path)
182+
.output()
183+
.map_err(|e| format!("Failed to push: {e}"))?;
184+
185+
if !push.status.success() {
186+
let stderr = String::from_utf8_lossy(&push.stderr);
187+
return Err(format!("Push failed: {stderr}"));
188+
}
189+
190+
// Remove worktree but keep branch (PR is still open)
191+
let _ = Command::new("git")
192+
.args(["worktree", "remove", &worktree_path, "--force"])
193+
.current_dir(&project_path)
194+
.output();
195+
196+
// Remove worktree setting
197+
{
198+
let db = state.db.lock().map_err(|e| format!("{e}"))?;
199+
let _ = codeforge_persistence::queries::delete_setting(
200+
db.conn(),
201+
&format!("worktree:{thread_id}"),
202+
);
203+
}
204+
205+
Ok(format!("Pushed {branch} to origin"))
206+
} else {
207+
// Normal mode: merge worktree branch into main
208+
let merge = Command::new("git")
209+
.args(["merge", &branch, "--no-edit"])
210+
.current_dir(&project_path)
211+
.output()
212+
.map_err(|e| format!("Failed to merge: {e}"))?;
213+
214+
if !merge.status.success() {
215+
let stderr = String::from_utf8_lossy(&merge.stderr).to_string();
216+
217+
// Abort the failed merge to leave repo in clean state
218+
let _ = Command::new("git")
219+
.args(["merge", "--abort"])
220+
.current_dir(&project_path)
221+
.output();
178222

179-
Ok(format!("Merged {branch} into {main_branch}"))
223+
return Err(format!(
224+
"Merge has conflicts. Resolve them manually in the worktree at {worktree_path} then try again.\n\nConflict details: {stderr}"
225+
));
226+
}
227+
228+
// Remove worktree
229+
let _ = Command::new("git")
230+
.args(["worktree", "remove", &worktree_path, "--force"])
231+
.current_dir(&project_path)
232+
.output();
233+
234+
// Delete the branch
235+
let _ = Command::new("git")
236+
.args(["branch", "-d", &branch])
237+
.current_dir(&project_path)
238+
.output();
239+
240+
// Remove setting
241+
{
242+
let db = state.db.lock().map_err(|e| format!("{e}"))?;
243+
let _ = codeforge_persistence::queries::delete_setting(
244+
db.conn(),
245+
&format!("worktree:{thread_id}"),
246+
);
247+
}
248+
249+
Ok(format!("Merged {branch} into {main_branch}"))
250+
}
180251
}

0 commit comments

Comments
 (0)