Skip to content

Commit 9b4f331

Browse files
committed
feat(review-platform): add Gitee support
Add a Gitee API v5 provider for pull requests, issue evidence, check output, comments, approval, and resetting the current user's approval. Integrate provider identity with the existing review tools and Deep Review routing. Filter Gitee pull requests before pagination, distinguish open requests from drafts, and populate statistics for the visible page with bounded concurrency. Keep unknown counts distinct from zero, preserve valid metadata on enrichment failure, and bind list/detail statistics and caches to matching PR revisions. Bind review diffs to immutable revisions, report potentially capped file and commit collections, and preserve section coverage warnings. Keep Gitee credentials in a separate store for downgrade compatibility and redact URLs from transport errors. Advertise state filtering support for older hosts. Document supported operations and add HTTP contract, compatibility, frontend, public read-only smoke, and native desktop workflow coverage.
1 parent 059c666 commit 9b4f331

27 files changed

Lines changed: 3667 additions & 160 deletions

File tree

‎src/apps/desktop/src/api/review_platform_api.rs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::api::app_state::AppState;
44
use log::error;
55
use openbitfun_core::service::review_platform::{
66
untrusted_repository_error_message, ReviewPlatformCiLog, ReviewPlatformDetailSection,
7-
ReviewPlatformError, ReviewPlatformIssueEvidence, ReviewPlatformKind,
7+
ReviewPlatformError, ReviewPlatformIssueEvidence, ReviewPlatformKind, ReviewPlatformListState,
88
ReviewPlatformPullRequestDetail, ReviewPlatformPullRequestDetailPage,
99
ReviewPlatformPullRequestReviewTarget, ReviewPlatformService, ReviewPlatformWorkspaceSnapshot,
1010
};
@@ -18,6 +18,8 @@ pub struct ReviewPlatformWorkspaceSnapshotRequest {
1818
pub remote_id: Option<String>,
1919
pub page: Option<u32>,
2020
pub per_page: Option<u32>,
21+
#[serde(default)]
22+
pub state: ReviewPlatformListState,
2123
}
2224

2325
#[derive(Debug, Deserialize)]
@@ -76,11 +78,12 @@ pub async fn review_platform_get_workspace_snapshot(
7678
_state: State<'_, AppState>,
7779
request: ReviewPlatformWorkspaceSnapshotRequest,
7880
) -> Result<ReviewPlatformWorkspaceSnapshot, String> {
79-
ReviewPlatformService::workspace_snapshot(
81+
ReviewPlatformService::workspace_snapshot_with_state(
8082
&request.repository_path,
8183
request.remote_id.as_deref(),
8284
request.page,
8385
request.per_page,
86+
request.state,
8487
)
8588
.await
8689
.map_err(|error| {

‎src/crates/assembly/core/src/agentic/tools/implementations/get_file_diff_tool.rs‎

Lines changed: 58 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ impl GetFileDiffTool {
201201
let platform = match pull_request.platform() {
202202
"github" => Some(ReviewPlatformKind::Github),
203203
"gitlab" => Some(ReviewPlatformKind::Gitlab),
204+
"gitee" => Some(ReviewPlatformKind::Gitee),
204205
"gitcode" => None,
205206
value => {
206207
return Err(OpenBitFunError::tool(format!(
@@ -2034,60 +2035,65 @@ mod tests {
20342035

20352036
#[test]
20362037
fn pull_request_diff_route_uses_prepared_provider_identity_not_remote_id() {
2037-
let mut context = prepared_context();
2038-
context.custom_data.insert(
2039-
"deep_review_run_manifest".to_string(),
2040-
json!({
2041-
"evidencePack": {
2042-
"reviewTarget": {
2043-
"version": 1,
2044-
"source": "pull_request",
2045-
"fingerprint": "provider-route-fingerprint",
2046-
"baseRevision": "1111111111111111111111111111111111111111",
2047-
"headRevision": "2222222222222222222222222222222222222222",
2048-
"completeness": "complete",
2049-
"workspaceBinding": "unavailable",
2050-
"pullRequest": {
2051-
"remoteId": "fabricated-remote-that-must-not-route",
2052-
"platform": "github",
2053-
"host": "github.com",
2054-
"projectPath": "exact/project",
2055-
"pullRequestId": "42",
2056-
"number": 42,
2057-
"webUrl": "https://github.com/exact/project/pull/42"
2058-
},
2059-
"files": [{
2060-
"path": "src/lib.rs",
2061-
"status": "modified",
2062-
"completeness": "complete"
2063-
}],
2064-
"limitations": []
2038+
for (platform_name, platform, host) in [
2039+
("github", ReviewPlatformKind::Github, "github.com"),
2040+
("gitee", ReviewPlatformKind::Gitee, "gitee.com"),
2041+
] {
2042+
let mut context = prepared_context();
2043+
context.custom_data.insert(
2044+
"deep_review_run_manifest".to_string(),
2045+
json!({
2046+
"evidencePack": {
2047+
"reviewTarget": {
2048+
"version": 1,
2049+
"source": "pull_request",
2050+
"fingerprint": "provider-route-fingerprint",
2051+
"baseRevision": "1111111111111111111111111111111111111111",
2052+
"headRevision": "2222222222222222222222222222222222222222",
2053+
"completeness": "complete",
2054+
"workspaceBinding": "unavailable",
2055+
"pullRequest": {
2056+
"remoteId": "fabricated-remote-that-must-not-route",
2057+
"platform": platform_name,
2058+
"host": host,
2059+
"projectPath": "exact/project",
2060+
"pullRequestId": "42",
2061+
"number": 42,
2062+
"webUrl": format!("https://{host}/exact/project/pulls/42")
2063+
},
2064+
"files": [{
2065+
"path": "src/lib.rs",
2066+
"status": "modified",
2067+
"completeness": "complete"
2068+
}],
2069+
"limitations": []
2070+
}
20652071
}
2072+
}),
2073+
);
2074+
let evidence = GetFileDiffTool::target_evidence(&context)
2075+
.expect("evidence should parse")
2076+
.expect("evidence should exist");
2077+
2078+
let route =
2079+
GetFileDiffTool::pull_request_file_diff_route(&context, &evidence, "src/lib.rs")
2080+
.expect("prepared provider route should be exact");
2081+
2082+
assert_eq!(
2083+
route,
2084+
ProviderFileDiffRoute::Identity {
2085+
platform,
2086+
host: host.to_string(),
2087+
project_path: "exact/project".to_string(),
2088+
pull_request_id: "42".to_string(),
2089+
base_revision: "1111111111111111111111111111111111111111".to_string(),
2090+
head_revision: "2222222222222222222222222222222222222222".to_string(),
2091+
file_path: "src/lib.rs".to_string(),
2092+
file_page_hint: Some(1),
2093+
repository_path: None,
20662094
}
2067-
}),
2068-
);
2069-
let evidence = GetFileDiffTool::target_evidence(&context)
2070-
.expect("evidence should parse")
2071-
.expect("evidence should exist");
2072-
2073-
let route =
2074-
GetFileDiffTool::pull_request_file_diff_route(&context, &evidence, "src/lib.rs")
2075-
.expect("prepared provider route should be exact");
2076-
2077-
assert_eq!(
2078-
route,
2079-
ProviderFileDiffRoute::Identity {
2080-
platform: ReviewPlatformKind::Github,
2081-
host: "github.com".to_string(),
2082-
project_path: "exact/project".to_string(),
2083-
pull_request_id: "42".to_string(),
2084-
base_revision: "1111111111111111111111111111111111111111".to_string(),
2085-
head_revision: "2222222222222222222222222222222222222222".to_string(),
2086-
file_path: "src/lib.rs".to_string(),
2087-
file_page_hint: Some(1),
2088-
repository_path: None,
2089-
}
2090-
);
2095+
);
2096+
}
20912097
}
20922098

20932099
#[tokio::test]

‎src/crates/assembly/core/src/agentic/tools/implementations/review_platform_tool.rs‎

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ use crate::agentic::tools::framework::{
88
};
99
use crate::service::review_platform::{
1010
ReviewPlatformApprovalRequest, ReviewPlatformCreatePullRequestRequest,
11-
ReviewPlatformDetailSection, ReviewPlatformError, ReviewPlatformKind, ReviewPlatformRemote,
12-
ReviewPlatformReplyToThreadRequest, ReviewPlatformRequestChangesRequest,
11+
ReviewPlatformDetailSection, ReviewPlatformError, ReviewPlatformKind, ReviewPlatformListState,
12+
ReviewPlatformRemote, ReviewPlatformReplyToThreadRequest, ReviewPlatformRequestChangesRequest,
1313
ReviewPlatformResolveThreadRequest, ReviewPlatformService, ReviewPlatformSubmitReviewRequest,
1414
ReviewSubmitEvent,
1515
};
@@ -130,6 +130,7 @@ impl ReviewPlatformTool {
130130
"github" => Ok(ReviewPlatformKind::Github),
131131
"gitlab" => Ok(ReviewPlatformKind::Gitlab),
132132
"gitcode" => Ok(ReviewPlatformKind::Gitcode),
133+
"gitee" => Ok(ReviewPlatformKind::Gitee),
133134
"unknown" => Ok(ReviewPlatformKind::Unknown),
134135
other => Err(OpenBitFunError::tool(format!(
135136
"Unsupported review platform kind: {}",
@@ -138,6 +139,15 @@ impl ReviewPlatformTool {
138139
}
139140
}
140141

142+
fn list_state(input: &Value) -> OpenBitFunResult<ReviewPlatformListState> {
143+
match input.get("state") {
144+
None | Some(Value::Null) => Ok(ReviewPlatformListState::All),
145+
Some(state) => serde_json::from_value(state.clone()).map_err(|error| {
146+
OpenBitFunError::tool(format!("Invalid pull request state filter: {error}"))
147+
}),
148+
}
149+
}
150+
141151
async fn resolve_remote_id(
142152
action: &str,
143153
repository_path: &str,
@@ -242,7 +252,7 @@ impl Tool for ReviewPlatformTool {
242252
243253
Use this for remote review-platform operations such as discovering remotes, loading the workspace PR snapshot, counting pull requests, listing pull requests, opening full or paginated pull request detail, loading CI logs, creating a pull request, replying to review threads, submitting a comment review, approving, revoking approval, requesting changes, or resolving a review thread. Use ExecCommand for local repository state and branch/commit/push operations.
244254
245-
GitHub authentication is owned by the local `gh` CLI and must never use token actions. Authentication-token actions are only for GitLab and GitCode when the user explicitly provides a token or asks to clear a stored token. Never guess or expose token values.
255+
GitHub authentication is owned by the local `gh` CLI and must never use token actions. Authentication-token actions are only for GitLab, GitCode, and Gitee when the user explicitly provides a token or asks to clear a stored token. Never guess or expose token values.
246256
247257
When returning pull request results to the user, include the provider web URL so the chat UI can open the pull request detail panel naturally."#.to_string())
248258
}
@@ -301,6 +311,11 @@ When returning pull request results to the user, include the provider web URL so
301311
"type": "integer",
302312
"description": "Page size for list_pull_requests, get_workspace_snapshot, or get_pull_request_detail_page."
303313
},
314+
"state": {
315+
"type": "string",
316+
"enum": ["all", "open", "draft", "merged", "closed"],
317+
"description": "Repository-wide PR state filter for list_pull_requests, count_pull_requests, or get_workspace_snapshot; defaults to all. Only use filters advertised in capabilities.supportedPullRequestStates."
318+
},
304319
"section": {
305320
"type": "string",
306321
"enum": ["overview", "ci", "files", "commits", "reviews"],
@@ -316,16 +331,16 @@ When returning pull request results to the user, include the provider web URL so
316331
},
317332
"platform": {
318333
"type": "string",
319-
"enum": ["github", "gitlab", "gitcode", "unknown"],
320-
"description": "GitLab or GitCode platform kind for update_auth_token or clear_auth_token. GitHub uses local gh authentication."
334+
"enum": ["github", "gitlab", "gitcode", "gitee", "unknown"],
335+
"description": "GitLab, GitCode, or Gitee platform kind for update_auth_token or clear_auth_token. GitHub uses local gh authentication."
321336
},
322337
"host": {
323338
"type": "string",
324339
"description": "Review platform host for update_auth_token or clear_auth_token."
325340
},
326341
"token": {
327342
"type": "string",
328-
"description": "GitLab or GitCode personal access token for update_auth_token. Only provide this when the user explicitly asks to store that token. Never provide a GitHub token."
343+
"description": "GitLab, GitCode, or Gitee personal access token for update_auth_token. Only provide this when the user explicitly asks to store that token. Never provide a GitHub token."
329344
},
330345
"title": {
331346
"type": "string",
@@ -782,11 +797,12 @@ When returning pull request results to the user, include the provider web URL so
782797
.and_then(Value::as_u64)
783798
.map(|value| value as u32);
784799
let remote_id = Self::optional_string_field(input, "remote_id");
785-
let snapshot = ReviewPlatformService::workspace_snapshot(
800+
let snapshot = ReviewPlatformService::workspace_snapshot_with_state(
786801
&repository_path,
787802
remote_id.as_deref(),
788803
page,
789804
per_page,
805+
Self::list_state(input)?,
790806
)
791807
.await
792808
.map_err(|error| OpenBitFunError::tool(error.to_string()))?;
@@ -820,11 +836,12 @@ When returning pull request results to the user, include the provider web URL so
820836
let remote_id = resolved_remote_id
821837
.clone()
822838
.expect("remote-bound action should resolve a remote");
823-
let snapshot = ReviewPlatformService::workspace_snapshot(
839+
let snapshot = ReviewPlatformService::workspace_snapshot_with_state(
824840
&repository_path,
825841
Some(remote_id.as_str()),
826842
Some(1),
827843
Some(1),
844+
Self::list_state(input)?,
828845
)
829846
.await
830847
.map_err(|error| OpenBitFunError::tool(error.to_string()))?;
@@ -864,11 +881,12 @@ When returning pull request results to the user, include the provider web URL so
864881
let remote_id = resolved_remote_id
865882
.clone()
866883
.expect("remote-bound action should resolve a remote");
867-
let snapshot = ReviewPlatformService::workspace_snapshot(
884+
let snapshot = ReviewPlatformService::workspace_snapshot_with_state(
868885
&repository_path,
869886
Some(remote_id.as_str()),
870887
page,
871888
per_page,
889+
Self::list_state(input)?,
872890
)
873891
.await
874892
.map_err(|error| OpenBitFunError::tool(error.to_string()))?;
@@ -1221,7 +1239,8 @@ fn canonical_supported_remotes(remotes: &[ReviewPlatformRemote]) -> Vec<&ReviewP
12211239
ReviewPlatformKind::Github => 0,
12221240
ReviewPlatformKind::Gitlab => 1,
12231241
ReviewPlatformKind::Gitcode => 2,
1224-
ReviewPlatformKind::Unknown => 3,
1242+
ReviewPlatformKind::Gitee => 3,
1243+
ReviewPlatformKind::Unknown => 4,
12251244
};
12261245
let normalized_host = remote.host.trim().to_ascii_lowercase();
12271246
let normalized_project = remote.project_path.trim_matches('/').to_ascii_lowercase();
@@ -1271,6 +1290,26 @@ fn remote_selection_result(
12711290
mod tests {
12721291
use super::*;
12731292

1293+
#[test]
1294+
fn gitee_uses_the_existing_write_permission_and_concurrency_boundary() {
1295+
let tool = ReviewPlatformTool::new();
1296+
assert_eq!(
1297+
ReviewPlatformTool::platform_kind(&json!({"platform":"gitee"})).unwrap(),
1298+
ReviewPlatformKind::Gitee
1299+
);
1300+
assert!(!tool.is_readonly());
1301+
for action in [
1302+
ACTION_CREATE,
1303+
ACTION_SUBMIT_REVIEW,
1304+
ACTION_APPROVE,
1305+
ACTION_REVOKE_APPROVAL,
1306+
ACTION_UPDATE_AUTH_TOKEN,
1307+
] {
1308+
assert!(!tool.is_concurrency_safe(Some(&json!({"action":action,"platform":"gitee"}))));
1309+
}
1310+
assert!(tool.is_concurrency_safe(Some(&json!({"action":ACTION_LIST,"platform":"gitee"}))));
1311+
}
1312+
12741313
fn github_remote(id: &str, name: &str, project_path: &str) -> ReviewPlatformRemote {
12751314
serde_json::from_value(json!({
12761315
"id": id,

‎src/crates/assembly/core/src/service/review_platform/mod.rs‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ pub use openbitfun_services_integrations::review_platform::{
1616
ReviewPlatformCapabilities, ReviewPlatformCiItem, ReviewPlatformCiLog, ReviewPlatformCommit,
1717
ReviewPlatformCreatePullRequestRequest, ReviewPlatformDetailSection, ReviewPlatformError,
1818
ReviewPlatformFile, ReviewPlatformIssueComment, ReviewPlatformIssueEvidence,
19-
ReviewPlatformKind, ReviewPlatformPullRequest, ReviewPlatformPullRequestDetail,
20-
ReviewPlatformPullRequestDetailPage, ReviewPlatformPullRequestFileDiff,
21-
ReviewPlatformPullRequestReviewTarget, ReviewPlatformRemote,
19+
ReviewPlatformKind, ReviewPlatformListState, ReviewPlatformPullRequest,
20+
ReviewPlatformPullRequestDetail, ReviewPlatformPullRequestDetailPage,
21+
ReviewPlatformPullRequestFileDiff, ReviewPlatformPullRequestReviewTarget, ReviewPlatformRemote,
2222
ReviewPlatformReplyToThreadRequest, ReviewPlatformRepositoryRef,
2323
ReviewPlatformRequestChangesRequest, ReviewPlatformResolveThreadRequest,
2424
ReviewPlatformSubmitReviewRequest, ReviewPlatformThread, ReviewPlatformThreadKind,
@@ -159,6 +159,18 @@ impl ReviewPlatformService {
159159
.await
160160
}
161161

162+
pub async fn workspace_snapshot_with_state(
163+
repository_path: &str,
164+
remote_id: Option<&str>,
165+
page: Option<u32>,
166+
per_page: Option<u32>,
167+
state: ReviewPlatformListState,
168+
) -> Result<ReviewPlatformWorkspaceSnapshot, ReviewPlatformError> {
169+
owner_service()?
170+
.workspace_snapshot_with_state(repository_path, remote_id, page, per_page, state)
171+
.await
172+
}
173+
162174
pub async fn pull_request_detail(
163175
repository_path: &str,
164176
remote_id: &str,

‎src/crates/execution/agent-runtime/src/deep_review/target_evidence.rs‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ fn parse_pull_request_identity(
643643
));
644644
}
645645
let platform = required_string(value, &["platform"], "reviewTarget.pullRequest.platform")?;
646-
if !matches!(platform.as_str(), "github" | "gitlab" | "gitcode") {
646+
if !matches!(platform.as_str(), "github" | "gitlab" | "gitcode" | "gitee") {
647647
return Err(ReviewTargetEvidenceValidationError::invalid(
648648
"reviewTarget.pullRequest.platform",
649649
"unknown provider",
@@ -1046,6 +1046,32 @@ mod tests {
10461046
assert_eq!(evidence.diff_revisions_for_path("src/lib.rs"), None);
10471047
}
10481048

1049+
#[test]
1050+
fn gitee_targets_keep_provider_identity_and_disallow_live_repository_fallback() {
1051+
let mut value = manifest();
1052+
let target = &mut value["evidencePack"]["reviewTarget"];
1053+
target["source"] = json!("pull_request");
1054+
target["workspaceBinding"] = json!("unavailable");
1055+
target["pullRequest"] = json!({
1056+
"remoteId": "origin:gitee:example__repo", "platform": "gitee", "host": "gitee.com",
1057+
"projectPath": "example/repo", "pullRequestId": "69", "number": 69,
1058+
"webUrl": "https://gitee.com/example/repo/pulls/69"
1059+
});
1060+
let evidence = ReviewTargetEvidence::from_manifest(&value)
1061+
.unwrap()
1062+
.unwrap();
1063+
assert_eq!(evidence.pull_request().unwrap().platform(), "gitee");
1064+
assert_eq!(evidence.pull_request().unwrap().pull_request_id(), "69");
1065+
assert!(!evidence.allows_live_repository_context());
1066+
assert_eq!(evidence.diff_revisions_for_path("src/lib.rs"), None);
1067+
value["evidencePack"]["reviewTarget"]["pullRequest"]["platform"] = json!("future-provider");
1068+
assert!(ReviewTargetEvidence::from_manifest(&value).is_err());
1069+
assert_eq!(
1070+
value["evidencePack"]["reviewTarget"]["pullRequest"]["platform"],
1071+
"future-provider"
1072+
);
1073+
}
1074+
10491075
#[test]
10501076
fn validates_evidence_and_packet_paths_against_the_target() {
10511077
let value = scoped_manifest();

0 commit comments

Comments
 (0)