use anyhow::Result;
use super::command::{Command, Outcome};
use crate::git::github::{PreparedPullRequest, PullRequestProgress};
use crate::git::{LocalDiffRequest, PreparedLocalDiff, Repository};
pub(crate) struct Session {
repository: Repository,
local_diffs: LocalDiffWorkspaces<PreparedLocalDiff>,
pull_request_diff: Option<(u64, PreparedPullRequest)>,
}
impl Session {
pub(crate) const fn new(repository: Repository) -> Self {
Self {
repository,
local_diffs: LocalDiffWorkspaces::new(),
pull_request_diff: None,
}
}
pub(crate) fn set_diff_highlighting(&mut self, enabled: bool) {
self.repository.set_diff_highlighting(enabled);
self.local_diffs = LocalDiffWorkspaces::new();
self.pull_request_diff = None;
}
pub(crate) fn execute(&mut self, command: Command) -> Result<Outcome> {
self.execute_with(command, &mut |_| {}, &|| true)
}
pub(crate) fn repository_revision(&self, revision: &str) -> Result<String> {
self.repository.resolve_revision(revision)
}
#[expect(
clippy::too_many_lines,
reason = "the command match is the whole session vocabulary and reads better as one table"
)]
pub(crate) fn execute_with(
&mut self,
command: Command,
progress: &mut dyn FnMut(PullRequestProgress),
wanted: &dyn Fn() -> bool,
) -> Result<Outcome> {
match command {
Command::Status => Ok(Outcome::Status(Box::new(self.repository.status()?))),
Command::History {
revision,
skip,
limit,
} => Ok(Outcome::History(
self.repository.history(&revision, skip, limit)?,
)),
Command::Branches => Ok(Outcome::Branches(self.repository.branches()?)),
Command::HistoryBranches => Ok(Outcome::HistoryBranches(
self.repository.history_branches()?,
)),
Command::Stashes => Ok(Outcome::Stashes(self.repository.stashes()?)),
Command::Worktrees => Ok(Outcome::Worktrees(self.repository.worktrees()?)),
Command::RecentProjects => Ok(Outcome::RecentProjects(
crate::state::load_recent_projects(self.repository.root()),
)),
Command::PrepareLocalDiff { workspace, request } => {
let kind = LocalDiffWorkspaceKind::from_request(&request);
let prepared = self.repository.prepare_local_diff(&request)?;
let index = prepared.index();
self.local_diffs.store(kind, workspace, prepared);
Ok(Outcome::LocalDiffIndex(Box::new(index)))
}
Command::LocalDiffFile { workspace, path } => {
let document = self.local_workspace(workspace)?.diff_file(&path)?;
Ok(Outcome::LocalDiffFile {
path,
document: Box::new(document),
})
}
Command::GitHubRepositories { refresh } => {
let (repositories, warnings) = self.repository.github_repositories(refresh)?;
Ok(Outcome::GitHubRepositories {
repositories,
warnings,
})
}
Command::LocalGitHubRepository => Ok(Outcome::LocalGitHubRepository(
self.repository.local_github_repository()?.map(Box::new),
)),
Command::PullRequestLookup {
repositories,
repository,
number,
refresh,
} => {
progress(PullRequestProgress::LoadingMetadata);
Ok(Outcome::PullRequest(Box::new(
self.repository.pull_request_lookup(
&repositories,
repository.as_deref(),
number,
refresh,
)?,
)))
}
Command::PullRequestStack {
pull_request,
refresh,
} => Ok(Outcome::PullRequestStack(Box::new(
self.repository.pull_request_stack(&pull_request, refresh)?,
))),
Command::PreparePullRequest {
workspace,
pull_request,
} => {
let prepared = self
.repository
.prepare_pull_request_diff(&pull_request, progress)?;
let index = prepared.index();
self.pull_request_diff = Some((workspace, prepared));
Ok(Outcome::PullRequestIndex(Box::new(index)))
}
Command::PreparePullRequestStack {
workspace,
stack,
from,
to,
} => {
let prepared = self
.repository
.prepare_pull_request_stack_diff(&stack, from, to, progress)?;
let index = prepared.index();
self.pull_request_diff = Some((workspace, prepared));
Ok(Outcome::PullRequestIndex(Box::new(index)))
}
Command::PullRequestFile { workspace, path } => Ok(Outcome::PullRequestDiff(Box::new(
self.pull_request_workspace(workspace)?.diff_file(&path)?,
))),
Command::PullRequestFileBatch { workspace, paths } => {
Ok(Outcome::PullRequestDiffBatch(
self.pull_request_workspace(workspace)?.diff_files(&paths)?,
))
}
Command::PullRequestChecks {
pull_request,
refresh,
} => Ok(Outcome::Checks(Box::new(
self.repository
.pull_request_checks(&pull_request, refresh)?,
))),
Command::PullRequestConversation { pull_request } => Ok(Outcome::Conversation(
Box::new(self.repository.pull_request_conversation(&pull_request)?),
)),
Command::PullRequestCommits { pull_request } => Ok(Outcome::Commits(Box::new(
self.repository.pull_request_commits(&pull_request)?,
))),
Command::PullRequestReview { pull_request } => Ok(Outcome::Review(Box::new(
self.repository.pull_request_review(&pull_request)?,
))),
Command::CheckRunLog {
pull_request,
check,
} => Ok(Outcome::CheckLog(Box::new(
self.repository
.pull_request_check_log(&pull_request, &check)?,
))),
Command::WarmCheckRunLogs {
pull_request,
checks,
} => {
let _warmed =
self.repository
.prefetch_check_run_logs(&pull_request, &checks, wanted);
Ok(Outcome::Warmed)
}
Command::WarmPullRequestStackMembers { pull_requests } => {
for pull_request in &pull_requests {
if !wanted() {
return Ok(Outcome::Warmed);
}
drop(self.repository.pull_request_checks(pull_request, false));
}
for pull_request in &pull_requests {
if !wanted() {
return Ok(Outcome::Warmed);
}
drop(self.repository.pull_request_conversation(pull_request));
}
Ok(Outcome::Warmed)
}
Command::Operate(operation) => {
let label = operation.label().to_owned();
let changes_history = operation.changes_history();
let message = self.repository.perform(&operation)?;
Ok(Outcome::Operation {
label,
changes_history,
message,
})
}
Command::OperatePullRequest {
pull_request,
operation,
} => {
let label = operation.label().to_owned();
let message = self
.repository
.perform_pull_request_operation(&pull_request, &operation)?;
Ok(Outcome::Operation {
label,
changes_history: false,
message,
})
}
Command::OperatePullRequestReview {
pull_request,
operation,
} => {
let label = operation.label().to_owned();
let message = self
.repository
.perform_pull_request_review_operation(&pull_request, &operation)?;
Ok(Outcome::Operation {
label,
changes_history: false,
message,
})
}
Command::Search(request) => match &request.target {
crate::search::SearchTarget::PullRequest { workspace, paths } => {
Ok(Outcome::Search(Box::new(
self.pull_request_workspace(*workspace)?.search_paths(
&request.query,
request.mode,
paths,
),
)))
}
_ => Ok(Outcome::Search(Box::new(self.repository.search(&request)?))),
},
}
}
fn local_workspace(&self, workspace: u64) -> Result<&PreparedLocalDiff> {
self.local_diffs
.get(workspace)
.ok_or_else(|| anyhow::anyhow!("Local diff workspace is no longer available"))
}
fn pull_request_workspace(&self, workspace: u64) -> Result<&PreparedPullRequest> {
self.pull_request_diff
.as_ref()
.filter(|(prepared, _)| *prepared == workspace)
.map(|(_, prepared)| prepared)
.ok_or_else(|| anyhow::anyhow!("Pull-request diff workspace is no longer available"))
}
}
#[derive(Clone, Copy)]
enum LocalDiffWorkspaceKind {
Changes,
History,
}
impl LocalDiffWorkspaceKind {
const fn from_request(request: &LocalDiffRequest) -> Self {
match request {
LocalDiffRequest::Commit { .. } => Self::History,
LocalDiffRequest::Changes { .. }
| LocalDiffRequest::Branch { .. }
| LocalDiffRequest::Stash { .. } => Self::Changes,
}
}
}
struct LocalDiffWorkspaces<T> {
changes: Option<(u64, T)>,
history: Option<(u64, T)>,
}
impl<T> LocalDiffWorkspaces<T> {
const fn new() -> Self {
Self {
changes: None,
history: None,
}
}
fn store(&mut self, kind: LocalDiffWorkspaceKind, workspace: u64, prepared: T) {
let slot = match kind {
LocalDiffWorkspaceKind::Changes => &mut self.changes,
LocalDiffWorkspaceKind::History => &mut self.history,
};
*slot = Some((workspace, prepared));
}
fn get(&self, workspace: u64) -> Option<&T> {
[&self.changes, &self.history]
.into_iter()
.flatten()
.find(|(candidate, _)| *candidate == workspace)
.map(|(_, prepared)| prepared)
}
}
#[cfg(test)]
mod tests {
#[cfg(unix)]
use std::cell::Cell;
use std::fs;
#[cfg(unix)]
use super::Outcome;
use super::{Command, LocalDiffRequest, LocalDiffWorkspaceKind, LocalDiffWorkspaces, Session};
#[cfg(unix)]
use crate::git::github::{GitHubRepository, PullRequest};
use crate::git::tests::TestRepository;
#[test]
fn plain_diff_presentation_preserves_text_and_default_sessions_keep_syntax() {
let fixture = TestRepository::with_branch("main");
let repository = fixture.repository();
fs::write(
repository.root().join("main.rs"),
"fn main() {\n\tlet value = \"猫\";\n}\n",
)
.unwrap();
let request = LocalDiffRequest::Changes {
changes: repository.status().unwrap().changes,
version: 0,
expanded: false,
};
let mut session = Session::new(repository);
let mut rendered = Vec::new();
for highlighting in [true, false] {
drop(
session
.execute(Command::PrepareLocalDiff {
workspace: 1,
request: Box::new(request.clone()),
})
.unwrap(),
);
let (_, document) = session
.execute(Command::LocalDiffFile {
workspace: 1,
path: "main.rs".into(),
})
.unwrap()
.local_diff_file()
.unwrap();
assert_eq!(
document
.lines
.iter()
.flat_map(|line| &line.spans)
.any(|span| span.foreground.is_some()),
highlighting
);
rendered.push((
super::super::render::diff(&document),
super::super::render::diff_terminal(&document),
));
session.set_diff_highlighting(false);
}
assert_eq!(rendered[0], rendered[1]);
}
#[test]
fn paused_changes_workspace_survives_history_browsing() {
let mut workspaces = LocalDiffWorkspaces::new();
workspaces.store(LocalDiffWorkspaceKind::Changes, 11, 110);
for generation in 12..100 {
workspaces.store(LocalDiffWorkspaceKind::History, generation, generation * 10);
}
assert_eq!(workspaces.get(11), Some(&110));
assert_eq!(workspaces.get(98), None);
assert_eq!(workspaces.get(99), Some(&990));
}
#[test]
fn each_view_replaces_only_its_own_workspace() {
let mut workspaces = LocalDiffWorkspaces::new();
workspaces.store(LocalDiffWorkspaceKind::Changes, 21, 210);
workspaces.store(LocalDiffWorkspaceKind::History, 22, 220);
workspaces.store(LocalDiffWorkspaceKind::Changes, 23, 230);
assert_eq!(workspaces.get(21), None);
assert_eq!(workspaces.get(22), Some(&220));
assert_eq!(workspaces.get(23), Some(&230));
}
#[cfg(unix)]
#[test]
fn stack_warming_continues_after_a_member_read_fails() {
let fixture = TestRepository::with_branch("main");
let repository = fixture.repository();
let script = repository.root().join("pr");
let calls = repository.root().join("calls");
fs::write(
&script,
format!(
"printf '%s\\n' \"$2\" >> '{}'\nprintf 'failed\\n' >&2\nexit 1\n",
calls.display()
),
)
.unwrap();
let mut session = Session::new(fixture.repository_with_github_cli("/bin/sh".into()));
let pull_requests = [41, 42]
.map(|number| PullRequest {
number,
head_oid: format!("head-{number}"),
base_repository: GitHubRepository {
name_with_owner: "acme/widget".to_owned(),
url: format!("https://example.test/warm/{number}"),
remotes: Vec::new(),
},
..PullRequest::default()
})
.into_iter()
.collect();
let wanted_calls = Cell::new(0);
let outcome = session
.execute_with(
Command::WarmPullRequestStackMembers { pull_requests },
&mut |_| {},
&|| {
let current = wanted_calls.get();
wanted_calls.set(current + 1);
current < 2
},
)
.unwrap();
assert!(matches!(outcome, Outcome::Warmed));
assert_eq!(fs::read_to_string(calls).unwrap(), "41\n42\n");
}
}