feat: improve repository history navigation

This commit is contained in:
Georg Bauer
2026-07-30 17:40:34 +02:00
parent c7de716b8e
commit 8c383e3672
6 changed files with 767 additions and 59 deletions

View File

@@ -390,6 +390,34 @@ fn wire_callbacks(ui: &AppWindow, state: Arc<Mutex<State>>) {
}
});
ui.on_select_branch({
let weak = ui.as_weak();
let state = state.clone();
move |selection| {
let target = {
let mut state = state.lock().unwrap();
let branch = (selection.as_str() != "All").then(|| selection.to_string());
if branch
.as_ref()
.is_some_and(|branch| !state.branches.contains(branch))
{
return;
}
state.active_branch = branch;
state.commits.clear();
state
.active_server
.and_then(|index| state.preferences.servers.get(index))
.cloned()
.zip(state.active_repository.clone())
};
if let (Some(ui), Some((server, (owner, repository)))) = (weak.upgrade(), target) {
ui.set_commits(empty_model());
fetch_commits(&ui, state.clone(), server, owner, repository, false);
}
}
});
ui.on_select_file({
let weak = ui.as_weak();
let state = state.clone();
@@ -410,6 +438,26 @@ fn wire_callbacks(ui: &AppWindow, state: Arc<Mutex<State>>) {
}
});
ui.on_select_pull_file({
let weak = ui.as_weak();
let state = state.clone();
move |path| {
let target = {
let state = state.lock().unwrap();
state
.active_server
.and_then(|index| state.preferences.servers.get(index))
.cloned()
.zip(state.active_pull.clone())
};
if let (Some(ui), Some((server, (owner, repository, number)))) =
(weak.upgrade(), target)
{
fetch_pull_file_diff(&ui, server, owner, repository, number, path.to_string());
}
}
});
ui.on_select_activity({
let weak = ui.as_weak();
let state = state.clone();
@@ -427,6 +475,15 @@ fn wire_callbacks(ui: &AppWindow, state: Arc<Mutex<State>>) {
return;
};
match target.as_str() {
"repository" => {
ui.set_tab("repositories".into());
open_commit_history(
&ui,
state.clone(),
owner.to_string(),
repository.to_string(),
);
}
"issue" => {
state.lock().unwrap().active_issue = Some(number as i64);
ui.set_tab("issues".into());
@@ -525,6 +582,7 @@ fn wire_callbacks(ui: &AppWindow, state: Arc<Mutex<State>>) {
}
ui.set_page(match (ui.get_tab().as_str(), ui.get_page().as_str()) {
("pulls", "pull") => "pulls".into(),
("pulls", "diff") => "pull".into(),
("repositories", "diff") => "files".into(),
("repositories", "files") => "commits".into(),
("repositories", "commits") => "repositories".into(),
@@ -802,7 +860,7 @@ fn fetch_pull(
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration();
let (pull, comments) = tokio::join!(
let (pull, comments, files) = tokio::join!(
apis::repository_api::repo_get_pull_request(
&configuration,
&owner,
@@ -817,10 +875,12 @@ fn fetch_pull(
None,
None,
),
load_pull_files(&configuration, &owner, &repository, number,),
);
Ok::<_, String>(PullDetails {
pull: pull.map_err(|error| error.to_string())?,
comments: comments.map_err(|error| error.to_string())?,
files: files.map_err(|error| error.to_string())?,
})
}
.await;
@@ -847,8 +907,18 @@ fn open_commit_history(
) {
let server = {
let mut state = state.lock().unwrap();
let default_branch = state
.repositories
.iter()
.find(|candidate| candidate.owner == owner && candidate.name == repository)
.map(|candidate| candidate.default_branch.clone())
.unwrap_or_else(|| "main".into());
state.active_repository = Some((owner.clone(), repository.clone()));
state.active_branch = Some(default_branch.clone());
state.branches.clear();
state.commits.clear();
ui.set_branch_choices(model(vec!["All".into(), default_branch.into()]));
ui.set_branch_index(1);
state
.active_server
.and_then(|index| state.preferences.servers.get(index))
@@ -876,8 +946,29 @@ fn fetch_commits(
ui.set_loading(true);
}
let weak = ui.as_weak();
let (active_branch, branches) = {
let state = state.lock().unwrap();
(state.active_branch.clone(), state.branches.clone())
};
spawn(async move {
let result = load_commits(&server, &owner, &repository).await;
let requested_branch = active_branch.clone();
let result = async {
let default_branch = active_branch
.as_deref()
.or(branches.first().map(String::as_str))
.unwrap_or("main");
let branches = if branches.is_empty() {
load_branches(&server, &owner, &repository, default_branch).await?
} else {
branches
};
let commits = match active_branch.as_deref() {
Some(branch) => load_branch_commits(&server, &owner, &repository, branch).await?,
None => load_all_commits(&server, &owner, &repository, &branches).await?,
};
Ok::<_, String>((requested_branch, branches, commits))
}
.await;
post(move || {
let Some(ui) = weak.upgrade() else { return };
if refreshing {
@@ -886,9 +977,43 @@ fn fetch_commits(
ui.set_loading(false);
}
match result {
Ok(commits) => {
state.lock().unwrap().commits = commits;
refresh_commits(&ui, &state.lock().unwrap().commits);
Ok((requested_branch, branches, commits)) => {
let mut state = state.lock().unwrap();
if state.active_branch != requested_branch {
return;
}
state.branches = branches;
state.commits = commits;
let mut choices: Vec<slint::SharedString> = vec!["All".into()];
choices.extend(state.branches.iter().map(Into::into));
ui.set_branch_choices(model(choices));
ui.set_branch_index(
state
.active_branch
.as_ref()
.and_then(|active| {
state.branches.iter().position(|branch| branch == active)
})
.map_or(0, |index| index as i32 + 1),
);
let lane_count = if state.active_branch.is_none() {
state
.commits
.iter()
.flat_map(|commit| {
commit
.top_lanes
.iter()
.chain(commit.bottom_lanes.iter())
.chain(commit.connections.iter())
.chain(commit.node_lane.iter())
})
.max()
.map_or(0, |lane| lane + 1)
} else {
0
};
refresh_commits(&ui, &state.commits, lane_count);
}
Err(error) => ui.set_error(error.into()),
}
@@ -972,28 +1097,68 @@ fn fetch_file_diff(
let Some(ui) = weak.upgrade() else { return };
ui.set_loading(false);
match result {
Ok(diff) => {
ui.set_detail_title(path.rsplit('/').next().unwrap_or(&path).into());
ui.set_diff_columns(diff.columns.min(i32::MAX as usize) as i32);
ui.set_diff_lines(model(
diff.lines
.into_iter()
.map(|line| DiffLine {
old_number: line.old_number.into(),
new_number: line.new_number.into(),
text: line.text.into(),
kind: line.kind.into(),
})
.collect(),
));
ui.set_page("diff".into());
}
Ok(diff) => show_file_diff(&ui, &path, diff),
Err(error) => ui.set_error(error.into()),
}
});
});
}
fn fetch_pull_file_diff(
ui: &AppWindow,
server: Server,
owner: String,
repository: String,
number: i64,
path: String,
) {
ui.set_loading(true);
let weak = ui.as_weak();
spawn(async move {
let result = async {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
apis::repository_api::repo_download_pull_diff_or_patch(
&client.configuration(),
&owner,
&repository,
number,
"diff",
Some(false),
)
.await
.map(|text| diff::parse_file(&text, &path))
.map_err(|error| error.to_string())
}
.await;
post(move || {
let Some(ui) = weak.upgrade() else { return };
ui.set_loading(false);
match result {
Ok(diff) => show_file_diff(&ui, &path, diff),
Err(error) => ui.set_error(error.into()),
}
});
});
}
fn show_file_diff(ui: &AppWindow, path: &str, diff: diff::Parsed) {
ui.set_detail_title(path.rsplit('/').next().unwrap_or(path).into());
ui.set_diff_columns(diff.columns.min(i32::MAX as usize) as i32);
ui.set_diff_lines(model(
diff.lines
.into_iter()
.map(|line| DiffLine {
old_number: line.old_number.into(),
new_number: line.new_number.into(),
text: line.text.into(),
kind: line.kind.into(),
})
.collect(),
));
ui.set_page("diff".into());
}
fn runtime() -> &'static tokio::runtime::Runtime {
RUNTIME.get_or_init(|| tokio::runtime::Runtime::new().expect("Tokio runtime"))
}