Skip to main content

atomcode_core/atomgit/
fixissue.rs

1//! Fixissue workflow: pull an issue, check it's assigned to the current
2//! user, and produce a prompt that the agent can act on.
3
4use anyhow::{anyhow, Result};
5
6use crate::auth;
7
8use super::client::Client;
9use super::models::{Comment, Issue};
10use super::url::{detect_cwd_atomgit_repo, IssueRef, RepoRef};
11
12/// Label applied to the issue after a successful fixissue run.
13pub const FIXED_LABEL: &str = "fixed";
14
15/// Result of prepping a fix-issue run. Either "go, here's the prompt" or
16/// "skip, here's why" — callers typically just print the reason and exit 0.
17pub enum Prepared {
18    Run {
19        prompt: String,
20        issue_title: String,
21        issue_number: u64,
22        /// Preserved so the CLI driver can post back a comment + label
23        /// when the agent finishes successfully — without re-parsing the URL.
24        issue_ref: IssueRef,
25    },
26    Skip {
27        reason: String,
28    },
29}
30
31/// Post-run side effects on the issue: comment with the agent's repair
32/// summary and add the `fixed` label. Both are best-effort from the
33/// caller's perspective — on error we return it so the CLI can print
34/// a clear message, but the local fix has already been applied.
35///
36/// Ordering: comment first (the big artifact the user cares about), label
37/// second. If the comment succeeds but label add fails, the user at least
38/// has the repair record on the issue.
39pub fn post_completion(issue_ref: &IssueRef, summary: &str) -> anyhow::Result<()> {
40    let client = Client::from_stored_auth()?;
41    client.post_issue_comment(issue_ref, summary)?;
42    client.add_issue_label(issue_ref, FIXED_LABEL)?;
43    Ok(())
44}
45
46/// Given an issue URL, resolve it via the AtomGit API and — if it's
47/// assigned to the logged-in user — build a prompt that briefs the agent
48/// on what to fix.
49pub fn prepare(issue_url: &str, working_dir: &std::path::Path) -> Result<Prepared> {
50    let r = IssueRef::parse(issue_url)?;
51
52    // Validate cwd's git origin matches the issue's repo so the agent
53    // doesn't accidentally patch a different codebase. Failure modes:
54    //   * cwd not a git repo / no `origin` / origin on another host:
55    //     treat as "can't validate" and proceed (user may intentionally
56    //     be reading an issue from outside its repo — still useful for
57    //     analysis, plus the prompt carries the repo URL).
58    //   * origin IS on atomgit.com but owner/repo don't match:
59    //     Skip — this is almost certainly a mistake (wrong cwd).
60    let issue_repo = RepoRef::from(&r);
61    let mut cwd_hint: Option<RepoRef> = None;
62    match detect_cwd_atomgit_repo(working_dir) {
63        Ok(Some(cwd_repo)) => {
64            if !cwd_repo.matches(&issue_repo) {
65                return Ok(Prepared::Skip {
66                    reason: format!(
67                        "cwd points to {}/{} but the issue is in {}/{}. Skipping — cd to the matching repo first (or pass the right URL).",
68                        cwd_repo.owner, cwd_repo.repo, issue_repo.owner, issue_repo.repo
69                    ),
70                });
71            }
72            cwd_hint = Some(cwd_repo);
73        }
74        Ok(None) => {
75            // Not a git repo or non-atomgit origin. Fall through.
76        }
77        Err(_) => {
78            // `git` not installed / not on PATH. Same handling as "can't validate".
79        }
80    }
81
82    let me = auth::current_user()
83        .ok_or_else(|| anyhow!("not logged in — run `atomcode login` first"))?;
84
85    let client = Client::from_stored_auth()?;
86    let issue = client.get_issue(&r)?;
87    // Keep the binding above alive only while it's used; suppress unused-var
88    // warning for the cwd-validation-passed case.
89    let _ = cwd_hint;
90
91    if !issue.is_assigned_to(&me.username) {
92        return Ok(Prepared::Skip {
93            reason: format!(
94                "issue #{} is assigned to {}, not you ({}). Skipping — fixissue only runs on issues assigned to the current user.",
95                issue.number,
96                issue.assignee_list(),
97                me.username
98            ),
99        });
100    }
101
102    let comments = client.get_issue_comments(&r);
103    let prompt = build_prompt(&issue, &comments, working_dir, &r);
104    Ok(Prepared::Run {
105        prompt,
106        issue_title: issue.title.clone(),
107        issue_number: issue.number,
108        issue_ref: r,
109    })
110}
111
112fn build_prompt(
113    issue: &Issue,
114    comments: &[Comment],
115    working_dir: &std::path::Path,
116    r: &IssueRef,
117) -> String {
118    let mut out = String::new();
119    out.push_str(&format!(
120        "请分析并修复以下 AtomGit issue,直接在当前本地项目里改代码(不要 commit 或 push,改完即可)。\n\n"
121    ));
122    out.push_str("---\n");
123    out.push_str(&format!("## Issue #{}: {}\n", issue.number, issue.title));
124    if let Some(url) = &issue.html_url {
125        out.push_str(&format!("URL: {}\n", url));
126    } else {
127        out.push_str(&format!(
128            "URL: https://atomgit.com/{}/{}/issues/{}\n",
129            r.owner, r.repo, r.number
130        ));
131    }
132    out.push_str(&format!("状态: {}\n", issue.state));
133    if !issue.labels.is_empty() {
134        let labels: Vec<&str> = issue.labels.iter().map(|l| l.name.as_str()).collect();
135        out.push_str(&format!("标签: {}\n", labels.join(", ")));
136    }
137    if let Some(reporter) = &issue.user {
138        out.push_str(&format!("报告人: {}\n", reporter.login));
139    }
140    out.push_str(&format!("工作目录: {}\n", working_dir.display()));
141    out.push_str("\n### 正文\n\n");
142    out.push_str(issue.body.as_deref().unwrap_or("(空)"));
143    out.push('\n');
144
145    let with_body: Vec<&Comment> = comments
146        .iter()
147        .filter(|c| {
148            c.body
149                .as_deref()
150                .map(|b| !b.trim().is_empty())
151                .unwrap_or(false)
152        })
153        .collect();
154    if !with_body.is_empty() {
155        out.push_str(&format!("\n### 评论 ({})\n", with_body.len()));
156        for (i, c) in with_body.iter().enumerate() {
157            let author = c
158                .user
159                .as_ref()
160                .map(|u| u.login.as_str())
161                .unwrap_or("unknown");
162            let body = c.body.as_deref().unwrap_or("");
163            out.push_str(&format!("\n**#{} — @{}:**\n{}\n", i + 1, author, body));
164        }
165    }
166
167    out.push_str("\n---\n\n");
168    out.push_str(
169        "请按以下步骤处理:\n\
170         1. 先总结 issue 的核心问题(一段话);\n\
171         2. 定位代码中相关文件、给出修复方案;\n\
172         3. 直接在本地项目里实施修复(可调用工具编辑文件、跑测试);\n\
173         4. 修复完成后用 `## 修复摘要` 为标题,写一段简洁摘要,包含:\n\
174            - 改动的文件清单\n\
175            - 核心修复逻辑说明\n\
176            - 是否通过编译 / 测试\n\
177         \n⚠️ 不要执行 git commit / push,只改本地文件。\n\
178         \n📝 重要:你在这轮对话里的所有文本回复会被自动发布到上述 issue 的评论区作为修复记录,\
179         修复完成后,issue 会被自动打上 `fixed` 标签。请确保输出的内容适合作为正式评论。\n",
180    );
181    out
182}