Skip to content
Open
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "codeforge",
"private": true,
"version": "26.4.0",
"version": "26.5.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "CodeForge"
version = "26.4.0"
version = "26.5.0"
description = "CodeForge 是一款轻量级、高性能的桌面代码执行器,专为开发者、学生和编程爱好者设计。"
authors = ["devlive-community"]
edition = "2024"
Expand Down
12 changes: 12 additions & 0 deletions src-tauri/src/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1328,6 +1328,18 @@ pub async fn git_merge(root: String, branch: String) -> Result<String, String> {
.map_err(|e| format!("git 任务失败: {}", e))?
}

/// 冲突文件整文件取一侧:side = "ours" | "theirs",取该侧内容并暂存(标记已解决)。
#[tauri::command]
pub async fn git_resolve_side(root: String, path: String, side: String) -> Result<String, String> {
let flag = if side == "theirs" { "--theirs" } else { "--ours" };
tokio::task::spawn_blocking(move || {
run_git(&root, &["checkout", flag, "--", &path])?;
run_git(&root, &["add", "--", &path])
})
.await
.map_err(|e| format!("git 任务失败: {}", e))?
}

#[derive(Serialize)]
pub struct GitCommit {
hash: String,
Expand Down
241 changes: 241 additions & 0 deletions src-tauri/src/github.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
//! GitHub REST API 集成:用用户配置的 token 直接在应用内创建/查看 PR 与 Issue。
use serde::Serialize;
use serde_json::{Value, json};

const API: &str = "https://api.github.com";

#[derive(Serialize)]
pub struct PrItem {
number: u64,
title: String,
url: String,
state: String,
head: String,
base: String,
author: String,
draft: bool,
}

#[derive(Serialize)]
pub struct IssueItem {
number: u64,
title: String,
url: String,
state: String,
author: String,
comments: u64,
}

fn client() -> reqwest::Client {
reqwest::Client::new()
}

// 统一的鉴权 GET
async fn api_get(token: &str, path: &str) -> Result<Value, String> {
let resp = client()
.get(format!("{}{}", API, path))
.header("Authorization", format!("Bearer {}", token))
.header("Accept", "application/vnd.github+json")
.header("User-Agent", "CodeForge")
.header("X-GitHub-Api-Version", "2022-11-28")
.send()
.await
.map_err(|e| format!("请求失败: {}", e))?;
let status = resp.status();
let data: Value = resp
.json()
.await
.map_err(|e| format!("解析响应失败: {}", e))?;
if !status.is_success() {
return Err(extract_error(&data, status.as_u16()));
}
Ok(data)
}

// 统一的鉴权 POST
async fn api_post(token: &str, path: &str, body: Value) -> Result<Value, String> {
let resp = client()
.post(format!("{}{}", API, path))
.header("Authorization", format!("Bearer {}", token))
.header("Accept", "application/vnd.github+json")
.header("User-Agent", "CodeForge")
.header("X-GitHub-Api-Version", "2022-11-28")
.json(&body)
.send()
.await
.map_err(|e| format!("请求失败: {}", e))?;
let status = resp.status();
let data: Value = resp
.json()
.await
.map_err(|e| format!("解析响应失败: {}", e))?;
if !status.is_success() {
return Err(extract_error(&data, status.as_u16()));
}
Ok(data)
}

fn extract_error(data: &Value, status: u16) -> String {
let msg = data["message"].as_str().unwrap_or("未知错误");
// 附带首个字段级错误,便于定位
let detail = data["errors"][0]["message"]
.as_str()
.map(|s| format!("({})", s))
.unwrap_or_default();
format!("GitHub API 错误 ({}): {}{}", status, msg, detail)
}

fn pr_from(v: &Value) -> PrItem {
PrItem {
number: v["number"].as_u64().unwrap_or(0),
title: v["title"].as_str().unwrap_or("").to_string(),
url: v["html_url"].as_str().unwrap_or("").to_string(),
state: v["state"].as_str().unwrap_or("").to_string(),
head: v["head"]["ref"].as_str().unwrap_or("").to_string(),
base: v["base"]["ref"].as_str().unwrap_or("").to_string(),
author: v["user"]["login"].as_str().unwrap_or("").to_string(),
draft: v["draft"].as_bool().unwrap_or(false),
}
}

fn issue_from(v: &Value) -> IssueItem {
IssueItem {
number: v["number"].as_u64().unwrap_or(0),
title: v["title"].as_str().unwrap_or("").to_string(),
url: v["html_url"].as_str().unwrap_or("").to_string(),
state: v["state"].as_str().unwrap_or("").to_string(),
author: v["user"]["login"].as_str().unwrap_or("").to_string(),
comments: v["comments"].as_u64().unwrap_or(0),
}
}

fn require_token(token: &str) -> Result<(), String> {
if token.trim().is_empty() {
return Err("未配置 GitHub Token(设置 → 通用 → GitHub)".to_string());
}
Ok(())
}

/// 列出仓库的 Pull Request(默认 open)。
#[tauri::command]
pub async fn github_list_prs(
token: String,
owner: String,
repo: String,
state: Option<String>,
) -> Result<Vec<PrItem>, String> {
require_token(&token)?;
let st = state.unwrap_or_else(|| "open".to_string());
let data = api_get(
&token,
&format!("/repos/{}/{}/pulls?state={}&per_page=50", owner, repo, st),
)
.await?;
Ok(data
.as_array()
.map(|a| a.iter().map(pr_from).collect())
.unwrap_or_default())
}

/// 创建 Pull Request。
#[tauri::command]
pub async fn github_create_pr(
token: String,
owner: String,
repo: String,
title: String,
head: String,
base: String,
body: Option<String>,
draft: Option<bool>,
) -> Result<PrItem, String> {
require_token(&token)?;
let payload = json!({
"title": title,
"head": head,
"base": base,
"body": body.unwrap_or_default(),
"draft": draft.unwrap_or(false),
});
let data = api_post(&token, &format!("/repos/{}/{}/pulls", owner, repo), payload).await?;
Ok(pr_from(&data))
}

/// 列出仓库的 Issue(默认 open;GitHub 会把 PR 也算作 issue,这里过滤掉)。
#[tauri::command]
pub async fn github_list_issues(
token: String,
owner: String,
repo: String,
state: Option<String>,
) -> Result<Vec<IssueItem>, String> {
require_token(&token)?;
let st = state.unwrap_or_else(|| "open".to_string());
let data = api_get(
&token,
&format!("/repos/{}/{}/issues?state={}&per_page=50", owner, repo, st),
)
.await?;
Ok(data
.as_array()
.map(|a| {
a.iter()
.filter(|v| v.get("pull_request").is_none())
.map(issue_from)
.collect()
})
.unwrap_or_default())
}

#[derive(Serialize)]
pub struct RepoBranches {
default_branch: String,
branches: Vec<String>,
}

/// 取仓库的默认分支与全部分支(供新建 PR 选择 base)。
#[tauri::command]
pub async fn github_repo_branches(
token: String,
owner: String,
repo: String,
) -> Result<RepoBranches, String> {
require_token(&token)?;
let info = api_get(&token, &format!("/repos/{}/{}", owner, repo)).await?;
let default_branch = info["default_branch"]
.as_str()
.unwrap_or("main")
.to_string();
let brs = api_get(
&token,
&format!("/repos/{}/{}/branches?per_page=100", owner, repo),
)
.await?;
let branches = brs
.as_array()
.map(|a| {
a.iter()
.filter_map(|b| b["name"].as_str().map(String::from))
.collect()
})
.unwrap_or_default();
Ok(RepoBranches {
default_branch,
branches,
})
}

/// 创建 Issue。
#[tauri::command]
pub async fn github_create_issue(
token: String,
owner: String,
repo: String,
title: String,
body: Option<String>,
) -> Result<IssueItem, String> {
require_token(&token)?;
let payload = json!({ "title": title, "body": body.unwrap_or_default() });
let data = api_post(&token, &format!("/repos/{}/{}/issues", owner, repo), payload).await?;
Ok(issue_from(&data))
}
9 changes: 8 additions & 1 deletion src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mod env_manager;
mod env_providers;
mod example;
mod execution;
mod github;
mod filesystem;
mod font;
mod geo;
Expand Down Expand Up @@ -71,7 +72,7 @@ use crate::filesystem::{
git_file_diff, git_file_head, git_get_identity, git_get_signing, git_graph, git_hook_delete,
git_hook_read, git_hook_save, git_hooks, git_ignore_add, git_ignore_append_block, git_init,
git_log, git_log_file, git_merge, git_op_abort, git_op_continue, git_op_skip, git_op_state,
git_permalink, git_pull, git_pull_rebase, git_push, git_push_force, git_push_tags,
git_permalink, git_pull, git_pull_rebase, git_push, git_push_force, git_push_tags, git_resolve_side,
git_rebase_interactive, git_reflog, git_remote_add, git_remote_branches, git_remote_remove,
git_remotes, git_reset, git_restore_file, git_revert, git_set_identity, git_set_signing,
git_set_upstream, git_show, git_stage, git_staged_diff, git_stash_apply, git_stash_drop,
Expand Down Expand Up @@ -256,6 +257,12 @@ fn main() {
git_remote_branches,
git_checkout_track,
git_merge,
git_resolve_side,
github::github_list_prs,
github::github_create_pr,
github::github_repo_branches,
github::github_list_issues,
github::github_create_issue,
git_blame,
git_file_head,
git_log,
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "CodeForge",
"version": "26.4.0",
"version": "26.5.0",
"identifier": "org.devlive.codeforge",
"build": {
"beforeDevCommand": "pnpm dev",
Expand Down
Loading
Loading