Skip to content
Merged
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
3 changes: 0 additions & 3 deletions pnpm-lock.yaml

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

104 changes: 78 additions & 26 deletions src/gh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,26 @@ pub struct RemoteRepo {
#[serde(rename_all = "camelCase")]
pub struct PullRequestInfo {
pub title: String,
pub state: String,
pub draft: bool,
pub merged: bool,
pub status: PullStatus,
pub author: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub additions: i64,
pub deletions: i64,
pub changed_files: i64,
pub commits: i64,
pub head_ref: String,
pub head_label: String,
pub head_repo: String,
pub base_ref: String,
pub base_label: String,
pub base_repo: String,
pub base_branch: String,
pub head_branch: String,
}

/// Display status of a pull request; serialized as the capitalized label the
/// web UI shows (mirrored by `PullRequestInfo["status"]` in web/src).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum PullStatus {
Open,
Draft,
Merged,
Closed,
}

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -359,33 +363,48 @@ pub async fn pull_request_info(
let response = fetch_pull(github_host, org, repo, number).await?;
Ok(PullRequestInfo {
title: response.title,
state: response.state,
draft: response.draft,
merged: response.merged,
status: pull_status(&response.state, response.draft, response.merged),
author: response.user.map(|user| user.login).unwrap_or_default(),
created_at: response.created_at,
updated_at: response.updated_at,
additions: response.additions,
deletions: response.deletions,
changed_files: response.changed_files,
commits: response.commits,
head_ref: response.head.ref_name,
head_label: response.head.label,
head_repo: response
.head
.repo
.map(|repo| repo.full_name)
.unwrap_or_default(),
base_ref: response.base.ref_name,
base_label: response.base.label,
base_repo: response
.base
.repo
.map(|repo| repo.full_name)
.unwrap_or_default(),
base_branch: branch_label(&response.base),
head_branch: branch_label(&response.head),
})
}

fn pull_status(state: &str, draft: bool, merged: bool) -> PullStatus {
if merged {
PullStatus::Merged
} else if state.eq_ignore_ascii_case("closed") {
PullStatus::Closed
} else if draft {
PullStatus::Draft
} else {
PullStatus::Open
}
}

/// GitHub's `owner:branch` label, else `repo:ref`, else the bare ref.
fn branch_label(branch: &PullRef) -> String {
let label = branch.label.trim();
let ref_name = branch.ref_name.trim();
let repo = branch
.repo
.as_ref()
.map_or("", |repo| repo.full_name.trim());
if !label.is_empty() {
label.to_string()
} else if !repo.is_empty() && !ref_name.is_empty() {
format!("{repo}:{ref_name}")
} else {
ref_name.to_string()
}
}

fn github_side(side: &str) -> &str {
match side {
"deletions" => "LEFT",
Expand Down Expand Up @@ -1038,6 +1057,39 @@ mod tests {
}
}

#[test]
fn pull_status_prefers_merged_then_closed_then_draft() {
assert_eq!(pull_status("closed", false, true), PullStatus::Merged);
assert_eq!(pull_status("closed", true, false), PullStatus::Closed);
assert_eq!(pull_status("open", true, false), PullStatus::Draft);
assert_eq!(pull_status("open", false, false), PullStatus::Open);
assert_eq!(
serde_json::to_string(&PullStatus::Draft).unwrap(),
"\"Draft\""
);
}

#[test]
fn branch_label_falls_back_to_repo_and_ref() {
let branch = |label: &str, repo: Option<&str>, ref_name: &str| PullRef {
ref_name: ref_name.into(),
label: label.into(),
repo: repo.map(|name| RepoName {
full_name: name.into(),
}),
sha: String::new(),
};
assert_eq!(
branch_label(&branch("me:feat", Some("me/x"), "feat")),
"me:feat"
);
assert_eq!(
branch_label(&branch(" ", Some("me/x"), "feat")),
"me/x:feat"
);
assert_eq!(branch_label(&branch("", None, "feat")), "feat");
}

#[test]
fn encode_path_segments_percent_encodes_each_segment() {
assert_eq!(encode_path_segments("src/main.rs"), "src/main.rs");
Expand Down
1 change: 0 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^6.0.5",
"enhanced-resolve": "^5.24.5",
"oxfmt": "^0.62.0",
"oxlint": "^1.77.0",
"typescript": "~7.0.2",
Expand Down
3 changes: 1 addition & 2 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { BrowserRouter, Routes, Route } from "react-router";
import { Home } from "./components/Home";
import { Home, NotFound } from "./components/Home";
import { DiffView } from "./components/DiffView";
import { NotFound } from "./components/NotFound";

export default function App() {
return (
Expand Down
Loading
Loading