use iced::Task;
use crate::message::{Message, RepoPayload};
use crate::state::GitKraft;
use super::commands;
pub(crate) fn update(state: &mut GitKraft, message: Message) -> Task<Message> {
match message {
Message::OpenRepo => {
if state.active_tab().has_repo() {
state.tabs.push(crate::state::RepoTab::new_empty());
state.active_tab = state.tabs.len() - 1;
}
let tab = state.active_tab_mut();
tab.is_loading = true;
tab.status_message = Some("Opening folder picker…".into());
commands::pick_folder_open()
}
Message::InitRepo => {
let tab = state.active_tab_mut();
tab.is_loading = true;
tab.status_message = Some("Opening folder picker for init…".into());
commands::pick_folder_init()
}
Message::RepoSelected(maybe_path) => {
if let Some(path) = maybe_path {
let path = path.canonicalize().unwrap_or(path);
if state.switch_to_existing_tab(path.as_path()) {
let tab = state.active_tab_mut();
tab.is_loading = false;
tab.status_message = None;
return Task::none();
}
let tab = state.active_tab_mut();
tab.repo_path = Some(path.clone());
tab.status_message = Some(format!("Opening {}…", path.display()));
commands::load_repo(path)
} else {
if !state.active_tab().has_repo() && state.tabs.len() > 1 {
state.tabs.remove(state.active_tab);
if state.active_tab >= state.tabs.len() {
state.active_tab = state.tabs.len() - 1;
}
} else {
let tab = state.active_tab_mut();
tab.is_loading = false;
tab.status_message = None;
}
Task::none()
}
}
Message::RepoInitSelected(maybe_path) => {
if let Some(path) = maybe_path {
let tab = state.active_tab_mut();
tab.status_message = Some(format!("Initializing {}…", path.display()));
commands::init_repo(path)
} else {
let tab = state.active_tab_mut();
tab.is_loading = false;
tab.status_message = None;
Task::none()
}
}
Message::RepoOpened(result) => handle_repo_loaded(state, result),
Message::RefreshRepo => with_repo!(state, loading, "Refreshing…".into(), |path| {
let depth = state.active_tab().commits.len();
commands::refresh_repo(path, depth)
}),
Message::RepoRefreshed(result) => handle_repo_loaded(state, result),
Message::OpenRecentRepo(path) => {
let path = path.canonicalize().unwrap_or(path);
if state.switch_to_existing_tab(path.as_path()) {
return Task::none();
}
if state.active_tab().has_repo() {
state.tabs.push(crate::state::RepoTab::new_empty());
state.active_tab = state.tabs.len() - 1;
}
let tab = state.active_tab_mut();
tab.repo_path = Some(path.clone());
tab.is_loading = true;
tab.status_message = Some(format!("Opening {}…", path.display()));
commands::load_repo(path)
}
Message::CloseRepo => {
if state.tabs.len() > 1 {
state.tabs.remove(state.active_tab);
if state.active_tab >= state.tabs.len() {
state.active_tab = state.tabs.len() - 1;
}
} else {
state.tabs[state.active_tab] = crate::state::RepoTab::new_empty();
}
state.dragging = None;
state.dragging_h = None;
state.drag_initialized = false;
state.drag_initialized_h = false;
let (open_tabs, active) = state.session_state();
Task::batch([
commands::load_recent_repos_async(),
commands::save_session_async(open_tabs, active),
commands::save_layout_async(state.current_layout()),
])
}
Message::RepoRestoredAt(tab_index, result) => {
handle_repo_loaded_at(state, tab_index, result)
}
Message::RepoRecorded(result) => {
if let Ok(recent) = result {
state.recent_repos = recent;
}
Task::none()
}
Message::SettingsLoaded(result) => {
if let Ok(recent) = result {
state.recent_repos = recent;
}
Task::none()
}
Message::MoreCommitsLoaded(result) => handle_more_commits_loaded(state, result),
Message::GitOperationResult(result) => handle_repo_loaded(state, result),
_ => Task::none(),
}
}
fn handle_repo_loaded(state: &mut GitKraft, result: Result<RepoPayload, String>) -> Task<Message> {
match result {
Ok(payload) => {
let path = payload
.info
.workdir
.clone()
.unwrap_or_else(|| payload.info.path.clone());
let path = path.canonicalize().unwrap_or(path);
let target_idx = state
.tabs
.iter()
.position(|t| t.repo_path.as_deref() == Some(path.as_path()))
.unwrap_or(state.active_tab);
let tab = &mut state.tabs[target_idx];
let is_new_open = tab.repo_info.is_none();
tab.is_loading = false;
tab.apply_payload(payload, path.clone());
tab.commit_display = compute_commit_display(&tab.commits);
tab.branch_context = compute_branch_context(&tab.commits, &tab.graph_rows);
let (open_tabs, active) = state.session_state();
let mut tasks = vec![commands::record_repo_and_save_session_async(
path, open_tabs, active,
)];
if is_new_open {
tasks.push(iced::widget::operation::scroll_to(
crate::features::commits::view::commit_log_scroll_id(target_idx),
iced::widget::operation::AbsoluteOffset { x: 0.0, y: 0.0 },
));
}
Task::batch(tasks)
}
Err(e) => {
let tab = state.active_tab_mut();
tab.is_loading = false;
tab.error_message = Some(e);
tab.status_message = None;
Task::none()
}
}
}
fn handle_repo_loaded_at(
state: &mut GitKraft,
tab_index: usize,
result: Result<RepoPayload, String>,
) -> Task<Message> {
if tab_index >= state.tabs.len() {
return Task::none(); }
state.tabs[tab_index].is_loading = false;
match result {
Ok(payload) => {
let path = payload
.info
.workdir
.clone()
.unwrap_or_else(|| payload.info.path.clone());
let tab = &mut state.tabs[tab_index];
tab.apply_payload(payload, path);
tab.commit_display = compute_commit_display(&tab.commits);
tab.branch_context = compute_branch_context(&tab.commits, &tab.graph_rows);
Task::none()
}
Err(e) => {
let tab = &mut state.tabs[tab_index];
tab.error_message = Some(e);
tab.status_message = None;
Task::none()
}
}
}
fn handle_more_commits_loaded(
state: &mut GitKraft,
result: Result<crate::message::CommitPage, String>,
) -> Task<Message> {
let tab = state.active_tab_mut();
tab.is_loading_more_commits = false;
match result {
Ok(page) => {
let prev_count = tab.commits.len();
let new_total = page.commits.len();
tab.has_more_commits = new_total > prev_count;
if new_total > prev_count {
let new_display = compute_commit_display(&page.commits[prev_count..]);
tab.commit_display.extend(new_display);
}
tab.commits = page.commits;
tab.graph_rows = page.graph_rows;
tab.branch_context = compute_branch_context(&tab.commits, &tab.graph_rows);
}
Err(e) => {
tab.status_message = Some(format!("Failed to load more commits: {e}"));
}
}
Task::none()
}
fn compute_commit_display(commits: &[gitkraft_core::CommitInfo]) -> Vec<(String, String)> {
commits
.iter()
.map(|c| {
let time = gitkraft_core::utils::relative_time(c.time);
let author = gitkraft_core::truncate_str(&c.author_name, 20);
(time, author)
})
.collect()
}
fn compute_branch_context(
commits: &[gitkraft_core::CommitInfo],
graph_rows: &[gitkraft_core::GraphRow],
) -> Vec<Option<String>> {
if commits.len() != graph_rows.len() {
return vec![None; commits.len()];
}
let max_lanes = graph_rows.iter().map(|r| r.width).max().unwrap_or(1);
let mut lane_labels: Vec<Option<String>> = vec![None; max_lanes];
let mut result = Vec::with_capacity(commits.len());
for (i, commit) in commits.iter().enumerate() {
let col = graph_rows[i].node_column;
if let Some(first_ref) = commit.refs.first() {
if col < lane_labels.len() {
lane_labels[col] = Some(first_ref.name.clone());
}
result.push(None);
} else {
let label = if col < lane_labels.len() {
lane_labels[col].clone()
} else {
None
};
result.push(label);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
fn make_commit(oid: &str, refs: Vec<gitkraft_core::RefLabel>) -> gitkraft_core::CommitInfo {
gitkraft_core::CommitInfo {
oid: oid.to_string(),
summary: String::new(),
message: String::new(),
author_name: String::new(),
author_email: String::new(),
time: Default::default(),
parent_ids: Vec::new(),
refs,
}
}
fn make_graph_row(node_column: usize) -> gitkraft_core::GraphRow {
gitkraft_core::GraphRow {
width: node_column + 1,
node_column,
node_color: 0,
edges: Vec::new(),
}
}
#[test]
fn branch_context_empty_when_no_commits() {
let result = compute_branch_context(&[], &[]);
assert!(result.is_empty());
}
#[test]
fn branch_context_none_for_commits_with_refs() {
let refs = vec![gitkraft_core::RefLabel {
name: "main".to_string(),
kind: gitkraft_core::RefKind::Head,
}];
let commits = vec![make_commit("aaa", refs)];
let graph = vec![make_graph_row(0)];
let result = compute_branch_context(&commits, &graph);
assert_eq!(result, vec![None]);
}
#[test]
fn branch_context_inherits_from_ancestor_in_same_lane() {
let refs = vec![gitkraft_core::RefLabel {
name: "main".to_string(),
kind: gitkraft_core::RefKind::Head,
}];
let commits = vec![
make_commit("tip", refs), make_commit("mid", Vec::new()), make_commit("old", Vec::new()), ];
let graph = vec![make_graph_row(0), make_graph_row(0), make_graph_row(0)];
let result = compute_branch_context(&commits, &graph);
assert_eq!(result[0], None); assert_eq!(result[1], Some("main".to_string())); assert_eq!(result[2], Some("main".to_string())); }
#[test]
fn branch_context_separate_lanes_get_separate_labels() {
let main_ref = vec![gitkraft_core::RefLabel {
name: "main".to_string(),
kind: gitkraft_core::RefKind::Head,
}];
let feat_ref = vec![gitkraft_core::RefLabel {
name: "feature".to_string(),
kind: gitkraft_core::RefKind::LocalBranch,
}];
let commits = vec![
make_commit("a", main_ref), make_commit("b", feat_ref), make_commit("c", Vec::new()), make_commit("d", Vec::new()), ];
let graph = vec![
make_graph_row(0),
gitkraft_core::GraphRow {
width: 2,
node_column: 1,
node_color: 1,
edges: Vec::new(),
},
make_graph_row(0),
gitkraft_core::GraphRow {
width: 2,
node_column: 1,
node_color: 1,
edges: Vec::new(),
},
];
let result = compute_branch_context(&commits, &graph);
assert_eq!(result[0], None);
assert_eq!(result[1], None);
assert_eq!(result[2], Some("main".to_string()));
assert_eq!(result[3], Some("feature".to_string()));
}
#[test]
fn branch_context_mismatched_lengths_returns_all_none() {
let commits = vec![make_commit("a", Vec::new())];
let graph = vec![]; let result = compute_branch_context(&commits, &graph);
assert_eq!(result, vec![None]);
}
}