Implement the Git workflow

This commit is contained in:
2026-07-19 11:53:02 +02:00
parent 90a9002124
commit 422f71c8ad
20 changed files with 3441 additions and 45 deletions

View File

@@ -31,6 +31,7 @@ use crate::views::{
DashboardCategory, DashboardRecentPost, DashboardState, DashboardStats, DashboardTag,
DashboardTimelineMonth,
},
git::{GitDiffLoad, GitDiffState, GitNetworkCompletion, GitSnapshot, GitUiState},
media_editor::{LinkedPostItem, MediaEditorMsg, MediaEditorState},
metadata_diff::MetadataDiffState,
modal,
@@ -47,6 +48,7 @@ use crate::views::{
mod editor_handlers;
mod engine_handlers;
mod git_handlers;
mod preview_handlers;
mod search;
mod tasks;
@@ -213,6 +215,52 @@ pub enum Message {
},
SiteValidationLoaded(Result<engine::validate_site::SiteValidationReport, String>),
// Git
GitRefresh,
GitLoaded {
repository_dir: PathBuf,
result: Result<GitSnapshot, String>,
},
GitRemoteInputChanged(String),
GitCommitMessageChanged(String),
GitInitialize,
GitSetRemote,
GitCommit,
GitFetch,
GitPull,
GitPush,
GitPruneLfs,
GitLocalFinished {
repository_dir: PathBuf,
operation: engine::git::GitOperation,
result: Result<GitSnapshot, String>,
},
GitNetworkFinished {
repository_dir: PathBuf,
task_id: TaskId,
operation: engine::git::GitOperation,
result: Result<GitNetworkCompletion, String>,
},
OpenGitFileDiff(String),
OpenGitCommitDiff {
hash: String,
subject: String,
},
SelectGitCommitFile {
hash: String,
change: engine::git::ChangedFile,
},
GitDiffLoaded {
repository_dir: PathBuf,
tab_id: String,
result: Result<GitDiffLoad, String>,
},
GitFileHistoryLoaded {
repository_dir: PathBuf,
path: String,
result: Result<Vec<engine::git::GitCommit>, String>,
},
// Editor views
PostEditor(PostEditorMsg),
MediaEditor(MediaEditorMsg),
@@ -823,6 +871,10 @@ pub struct BdsApp {
site_validation_state: SiteValidationState,
metadata_diff_state: MetadataDiffState,
translation_validation_state: crate::views::translation_validation::TranslationValidationState,
git_state: GitUiState,
git_diffs: HashMap<String, GitDiffState>,
git_file_history: Vec<engine::git::GitCommit>,
git_file_history_target: Option<String>,
}
// ───────────────────────────────────────────────────────────
@@ -962,6 +1014,10 @@ impl BdsApp {
site_validation_state: SiteValidationState::default(),
metadata_diff_state: MetadataDiffState::default(),
translation_validation_state: Default::default(),
git_state: GitUiState::default(),
git_diffs: HashMap::new(),
git_file_history: Vec::new(),
git_file_history_target: None,
},
init_task,
)
@@ -1028,6 +1084,10 @@ impl BdsApp {
site_validation_state: SiteValidationState::default(),
metadata_diff_state: MetadataDiffState::default(),
translation_validation_state: Default::default(),
git_state: GitUiState::default(),
git_diffs: HashMap::new(),
git_file_history: Vec::new(),
git_file_history_target: None,
}
}
@@ -1053,6 +1113,8 @@ impl BdsApp {
&& matches!(new_view, SidebarView::Posts | SidebarView::Pages);
if needs_post_refresh {
self.refresh_sidebar_posts()
} else if new_view == SidebarView::Git && new_visible {
self.refresh_git()
} else {
Task::none()
}
@@ -1125,6 +1187,7 @@ impl BdsApp {
} else {
self.active_tab = None;
}
self.git_diffs.remove(&id);
self.enforce_panel_tab_fallback();
self.sync_menu_state();
self.sync_embedded_preview_for_active_post()
@@ -1147,6 +1210,7 @@ impl BdsApp {
self.flush_active_post_editor();
self.tabs.clear();
self.active_tab = None;
self.git_diffs.clear();
self.hide_embedded_preview();
Task::none()
}
@@ -1190,6 +1254,7 @@ impl BdsApp {
if let Some(ref db) = self.db {
match engine::project::set_active_project(db.conn(), &project_id) {
Ok(()) => {
self.reset_git_for_project_change();
self.active_project =
self.projects.iter().find(|p| p.id == project_id).cloned();
self.preview_session = None;
@@ -1235,7 +1300,7 @@ impl BdsApp {
}
}
self.sync_menu_state();
Task::none()
self.refresh_git_if_visible()
}
Message::ProjectSwitched(result) => {
match result {
@@ -1266,6 +1331,7 @@ impl BdsApp {
let _ = engine::project::set_active_project(db.conn(), &project.id);
self.projects =
engine::project::list_projects(db.conn()).unwrap_or_default();
self.reset_git_for_project_change();
self.active_project = Some(project.clone());
self.preview_session = None;
self.data_dir = project.data_path.as_ref().map(PathBuf::from);
@@ -1284,7 +1350,7 @@ impl BdsApp {
}
}
}
Task::none()
self.refresh_git_if_visible()
}
Message::ProjectCreated(result) => {
match result {
@@ -1354,6 +1420,7 @@ impl BdsApp {
let _ = engine::project::set_active_project(db.conn(), &project.id);
self.projects =
engine::project::list_projects(db.conn()).unwrap_or_default();
self.reset_git_for_project_change();
self.active_project = Some(project.clone());
self.data_dir = project.data_path.as_ref().map(PathBuf::from);
self.preview_session = None;
@@ -1367,7 +1434,10 @@ impl BdsApp {
),
);
self.sync_menu_state();
return self.refresh_counts();
return Task::batch([
self.refresh_counts(),
self.refresh_git_if_visible(),
]);
}
Err(error) => self.notify(
ToastLevel::Error,
@@ -1817,7 +1887,11 @@ impl BdsApp {
// ── Panel ──
Message::SetPanelTab(tab) => {
self.panel_tab = tab;
Task::none()
if tab == PanelTab::GitLog {
self.refresh_git_file_history()
} else {
Task::none()
}
}
// ── Settings ──
@@ -1868,6 +1942,26 @@ impl BdsApp {
| Message::SiteGenerationIndexDone { .. }
| Message::SiteValidationLoaded(_)) => self.handle_engine_message(message),
// ── Git ──
message @ (Message::GitRefresh
| Message::GitLoaded { .. }
| Message::GitRemoteInputChanged(_)
| Message::GitCommitMessageChanged(_)
| Message::GitInitialize
| Message::GitSetRemote
| Message::GitCommit
| Message::GitFetch
| Message::GitPull
| Message::GitPush
| Message::GitPruneLfs
| Message::GitLocalFinished { .. }
| Message::GitNetworkFinished { .. }
| Message::OpenGitFileDiff(_)
| Message::OpenGitCommitDiff { .. }
| Message::SelectGitCommitFile { .. }
| Message::GitDiffLoaded { .. }
| Message::GitFileHistoryLoaded { .. }) => self.handle_git_message(message),
// ── Toasts ──
Message::ShowToast(level, msg) => {
self.toasts.push(Toast::new(level, msg));
@@ -2172,6 +2266,9 @@ impl BdsApp {
&self.site_validation_state,
&self.metadata_diff_state,
&self.translation_validation_state,
&self.git_state,
&self.git_diffs,
&self.git_file_history,
)
}
@@ -6201,6 +6298,7 @@ mod tests {
save_editor_settings_state_impl, save_script_editor_state_impl,
save_template_editor_state_impl,
};
use crate::i18n::t;
use crate::state::sidebar_filter::{MediaFilter, PostFilter};
use crate::views::media_editor::{MediaEditorMsg, MediaEditorState};
use crate::views::modal;
@@ -6214,6 +6312,7 @@ mod tests {
use bds_core::engine::generation::GenerationReport;
use bds_core::engine::task::{TaskStatus, TaskStatus::*};
use bds_core::engine::{ai, media, post, script, template};
use bds_core::i18n::UiLocale;
use bds_core::model::{Project, ScriptKind, TemplateKind};
use chrono::{Datelike, TimeZone};
use std::io::{Read, Write};
@@ -6346,6 +6445,22 @@ mod tests {
);
}
#[test]
fn git_network_actions_are_gated_in_airplane_mode() {
let (db, project, tempdir) = setup();
let mut app = BdsApp::new_for_tests(db, project, tempdir.path().to_path_buf());
app.offline_mode = true;
let _ = app.update(Message::GitFetch);
assert!(app.git_state.network_run.is_none());
assert!(
app.toasts
.iter()
.any(|toast| toast.message == t(UiLocale::En, "git.airplaneBlocked"))
);
}
#[test]
fn gallery_completion_reports_every_path_inserts_macro_and_refreshes_editor() {
let (db, project, tmp) = setup();