Implement the Git workflow
This commit is contained in:
1707
crates/bds-core/src/engine/git.rs
Normal file
1707
crates/bds-core/src/engine/git.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ pub mod calendar;
|
||||
pub mod error;
|
||||
pub mod gallery_import;
|
||||
pub mod generation;
|
||||
pub mod git;
|
||||
pub mod media;
|
||||
pub mod menu;
|
||||
pub mod meta;
|
||||
|
||||
@@ -929,8 +929,6 @@ pub(crate) fn rebuild_canonical_post(
|
||||
.to_string();
|
||||
|
||||
let hash = content_hash(content.as_bytes());
|
||||
let now = now_unix_ms();
|
||||
|
||||
let status = match fm.status.as_str() {
|
||||
"published" => PostStatus::Published,
|
||||
"archived" => PostStatus::Archived,
|
||||
@@ -960,7 +958,7 @@ pub(crate) fn rebuild_canonical_post(
|
||||
post.tags = fm.tags;
|
||||
post.categories = fm.categories;
|
||||
post.created_at = fm.created_at;
|
||||
post.updated_at = now;
|
||||
post.updated_at = fm.updated_at;
|
||||
post.published_at = fm.published_at;
|
||||
qp::update_post(conn, &post)?;
|
||||
Ok(false)
|
||||
@@ -993,7 +991,7 @@ pub(crate) fn rebuild_canonical_post(
|
||||
published_categories: None,
|
||||
published_excerpt: None,
|
||||
created_at: fm.created_at,
|
||||
updated_at: now,
|
||||
updated_at: fm.updated_at,
|
||||
published_at: fm.published_at,
|
||||
};
|
||||
qp::insert_post(conn, &post)?;
|
||||
@@ -1747,6 +1745,7 @@ mod tests {
|
||||
assert_eq!(post.title, "Rebuilt Post");
|
||||
assert_eq!(post.slug, "rebuilt-post");
|
||||
assert_eq!(post.tags, vec!["test"]);
|
||||
assert_eq!(post.updated_at, 1_705_320_000_000);
|
||||
|
||||
// Verify translation in DB
|
||||
let trans =
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::db::queries::script as qs;
|
||||
use crate::engine::{EngineError, EngineResult};
|
||||
use crate::model::{Script, ScriptStatus};
|
||||
use crate::util::frontmatter::read_script_file;
|
||||
use crate::util::now_unix_ms;
|
||||
|
||||
/// Report returned by `rebuild_scripts_from_filesystem`.
|
||||
#[derive(Debug, Default)]
|
||||
@@ -83,8 +82,6 @@ pub(crate) fn rebuild_single_script(
|
||||
.to_string();
|
||||
|
||||
let kind = fm.kind.parse().map_err(EngineError::Parse)?;
|
||||
let now = now_unix_ms();
|
||||
|
||||
// File exists on disk -> Published; content is None in DB
|
||||
let status = ScriptStatus::Published;
|
||||
|
||||
@@ -101,7 +98,7 @@ pub(crate) fn rebuild_single_script(
|
||||
script.status = status;
|
||||
script.content = None;
|
||||
script.created_at = fm.created_at;
|
||||
script.updated_at = now;
|
||||
script.updated_at = fm.updated_at;
|
||||
qs::update_script(conn, &script)?;
|
||||
Ok(false)
|
||||
}
|
||||
@@ -119,7 +116,7 @@ pub(crate) fn rebuild_single_script(
|
||||
status,
|
||||
content: None,
|
||||
created_at: fm.created_at,
|
||||
updated_at: now,
|
||||
updated_at: fm.updated_at,
|
||||
};
|
||||
qs::insert_script(conn, &script)?;
|
||||
Ok(true)
|
||||
@@ -246,6 +243,7 @@ end
|
||||
assert!(script.enabled);
|
||||
assert_eq!(script.version, 5);
|
||||
assert_eq!(script.status, ScriptStatus::Published);
|
||||
assert_eq!(script.updated_at, 1_704_067_200_000);
|
||||
assert!(script.content.is_none());
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::db::queries::template as qt;
|
||||
use crate::engine::{EngineError, EngineResult};
|
||||
use crate::model::{Template, TemplateStatus};
|
||||
use crate::util::frontmatter::read_template_file;
|
||||
use crate::util::now_unix_ms;
|
||||
|
||||
/// Report returned by `rebuild_templates_from_filesystem`.
|
||||
#[derive(Debug, Default)]
|
||||
@@ -83,8 +82,6 @@ pub(crate) fn rebuild_single_template(
|
||||
.to_string();
|
||||
|
||||
let kind = fm.kind.parse().map_err(EngineError::Parse)?;
|
||||
let now = now_unix_ms();
|
||||
|
||||
// File exists on disk -> Published; content is None in DB
|
||||
let status = TemplateStatus::Published;
|
||||
|
||||
@@ -100,7 +97,7 @@ pub(crate) fn rebuild_single_template(
|
||||
tpl.status = status;
|
||||
tpl.content = None;
|
||||
tpl.created_at = fm.created_at;
|
||||
tpl.updated_at = now;
|
||||
tpl.updated_at = fm.updated_at;
|
||||
qt::update_template(conn, &tpl)?;
|
||||
Ok(false)
|
||||
}
|
||||
@@ -117,7 +114,7 @@ pub(crate) fn rebuild_single_template(
|
||||
status,
|
||||
content: None,
|
||||
created_at: fm.created_at,
|
||||
updated_at: now,
|
||||
updated_at: fm.updated_at,
|
||||
};
|
||||
qt::insert_template(conn, &tpl)?;
|
||||
Ok(true)
|
||||
@@ -236,6 +233,7 @@ updatedAt: \"2024-01-01T00:00:00.000Z\"
|
||||
assert_eq!(tpl.version, 3);
|
||||
assert_eq!(tpl.status, TemplateStatus::Published);
|
||||
assert!(tpl.content.is_none());
|
||||
assert_eq!(tpl.updated_at, 1_704_067_200_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
199
crates/bds-core/tests/git_engine.rs
Normal file
199
crates/bds-core/tests/git_engine.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use std::fs;
|
||||
|
||||
use bds_core::engine::git::{FileStatusKind, GitEngine};
|
||||
|
||||
fn git(dir: &std::path::Path, args: &[&str]) {
|
||||
let output = std::process::Command::new("git")
|
||||
.args(args)
|
||||
.current_dir(dir)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn git_text(dir: &std::path::Path, args: &[&str]) -> String {
|
||||
let output = std::process::Command::new("git")
|
||||
.args(args)
|
||||
.current_dir(dir)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
String::from_utf8(output.stdout).unwrap().trim().to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_diff_history_and_missing_diff_sides_follow_the_spec() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
git(dir.path(), &["init", "-b", "master"]);
|
||||
git(dir.path(), &["config", "user.name", "RuDS Test"]);
|
||||
git(dir.path(), &["config", "user.email", "test@example.com"]);
|
||||
fs::write(dir.path().join("kept.txt"), "before\n").unwrap();
|
||||
fs::write(dir.path().join("deleted.txt"), "gone\n").unwrap();
|
||||
git(dir.path(), &["add", "-A"]);
|
||||
git(dir.path(), &["commit", "-m", "initial"]);
|
||||
|
||||
fs::write(dir.path().join("kept.txt"), "after\n").unwrap();
|
||||
fs::remove_file(dir.path().join("deleted.txt")).unwrap();
|
||||
fs::write(dir.path().join("added.txt"), "new\n").unwrap();
|
||||
|
||||
let engine = GitEngine::new(dir.path());
|
||||
let status = engine.status().unwrap();
|
||||
assert!(
|
||||
status
|
||||
.iter()
|
||||
.any(|file| { file.path == "kept.txt" && file.kind == FileStatusKind::Modified })
|
||||
);
|
||||
assert!(
|
||||
status
|
||||
.iter()
|
||||
.any(|file| { file.path == "deleted.txt" && file.kind == FileStatusKind::Deleted })
|
||||
);
|
||||
assert!(
|
||||
status
|
||||
.iter()
|
||||
.any(|file| { file.path == "added.txt" && file.kind == FileStatusKind::Untracked })
|
||||
);
|
||||
|
||||
let diff = engine.diff().unwrap();
|
||||
assert!(diff.unstaged.contains("-before"));
|
||||
assert!(diff.unstaged.contains("+after"));
|
||||
|
||||
let added = engine.file_diff("added.txt").unwrap();
|
||||
assert_eq!(added.original, "");
|
||||
assert_eq!(added.modified, "new\n");
|
||||
let deleted = engine.file_diff("deleted.txt").unwrap();
|
||||
assert_eq!(deleted.original, "gone\n");
|
||||
assert_eq!(deleted.modified, "");
|
||||
|
||||
assert_eq!(engine.file_history("kept.txt").unwrap().len(), 1);
|
||||
|
||||
git(dir.path(), &["add", "-A"]);
|
||||
git(dir.path(), &["commit", "-m", "working tree changes"]);
|
||||
let hash = git_text(dir.path(), &["rev-parse", "HEAD"]);
|
||||
let changes = engine.commit_files(&hash).unwrap();
|
||||
let added_change = changes
|
||||
.iter()
|
||||
.find(|change| change.path == "added.txt")
|
||||
.unwrap();
|
||||
let deleted_change = changes
|
||||
.iter()
|
||||
.find(|change| change.path == "deleted.txt")
|
||||
.unwrap();
|
||||
let added = engine.commit_file_diff(&hash, added_change).unwrap();
|
||||
let deleted = engine.commit_file_diff(&hash, deleted_change).unwrap();
|
||||
assert_eq!(
|
||||
(added.original.as_str(), added.modified.as_str()),
|
||||
("", "new\n")
|
||||
);
|
||||
assert_eq!(
|
||||
(deleted.original.as_str(), deleted.modified.as_str()),
|
||||
("gone\n", "")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commit_rejects_blank_messages_before_staging() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
git(dir.path(), &["init", "-b", "master"]);
|
||||
fs::write(dir.path().join("untracked.txt"), "content\n").unwrap();
|
||||
|
||||
let error = GitEngine::new(dir.path()).commit_all(" \n\t ").unwrap_err();
|
||||
assert!(error.to_string().contains("commit message"));
|
||||
|
||||
let output = std::process::Command::new("git")
|
||||
.args(["diff", "--cached", "--name-only"])
|
||||
.current_dir(dir.path())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.stdout.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_state_history_fetch_and_fast_forward_pull_use_real_refs() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let remote = root.path().join("remote.git");
|
||||
let local = root.path().join("local");
|
||||
let peer = root.path().join("peer");
|
||||
fs::create_dir(&local).unwrap();
|
||||
git(
|
||||
root.path(),
|
||||
&["init", "--bare", "-b", "master", remote.to_str().unwrap()],
|
||||
);
|
||||
git(&local, &["init", "-b", "master"]);
|
||||
git(&local, &["config", "user.name", "Local"]);
|
||||
git(&local, &["config", "user.email", "local@example.com"]);
|
||||
fs::write(local.join("post.md"), "one\n").unwrap();
|
||||
git(&local, &["add", "-A"]);
|
||||
git(&local, &["commit", "-m", "local one"]);
|
||||
let engine = GitEngine::new(&local);
|
||||
engine.set_remote(remote.to_str().unwrap()).unwrap();
|
||||
git(&local, &["push", "-u", "origin", "master"]);
|
||||
|
||||
git(
|
||||
root.path(),
|
||||
&["clone", remote.to_str().unwrap(), peer.to_str().unwrap()],
|
||||
);
|
||||
git(&peer, &["config", "user.name", "Peer"]);
|
||||
git(&peer, &["config", "user.email", "peer@example.com"]);
|
||||
fs::write(peer.join("post.md"), "two\n").unwrap();
|
||||
git(&peer, &["add", "-A"]);
|
||||
git(&peer, &["commit", "-m", "remote two"]);
|
||||
git(&peer, &["push"]);
|
||||
|
||||
let mut streamed = String::new();
|
||||
engine
|
||||
.fetch(|| false, |chunk| streamed.push_str(&chunk.text))
|
||||
.unwrap();
|
||||
let state = engine.remote_state().unwrap();
|
||||
assert_eq!(state.behind, 1);
|
||||
assert_eq!(state.ahead, 0);
|
||||
assert!(
|
||||
engine
|
||||
.history("master")
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|commit| commit.subject.as_deref() == Some("remote two")
|
||||
&& commit.sync_status == bds_core::engine::git::SyncStatus::RemoteOnly)
|
||||
);
|
||||
|
||||
engine.pull(|| false, |_| {}).unwrap();
|
||||
let state = engine.remote_state().unwrap();
|
||||
assert_eq!((state.ahead, state.behind), (0, 0));
|
||||
assert_eq!(fs::read_to_string(local.join("post.md")).unwrap(), "two\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_history_follows_renames_and_is_limited_to_fifty_commits() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
git(dir.path(), &["init", "-b", "master"]);
|
||||
git(dir.path(), &["config", "user.name", "RuDS Test"]);
|
||||
git(dir.path(), &["config", "user.email", "test@example.com"]);
|
||||
fs::write(dir.path().join("old.txt"), "0\n").unwrap();
|
||||
git(dir.path(), &["add", "-A"]);
|
||||
git(dir.path(), &["commit", "-m", "old name"]);
|
||||
git(dir.path(), &["mv", "old.txt", "new.txt"]);
|
||||
git(dir.path(), &["commit", "-m", "renamed"]);
|
||||
for index in 1..=49 {
|
||||
fs::write(dir.path().join("new.txt"), format!("{index}\n")).unwrap();
|
||||
git(dir.path(), &["add", "new.txt"]);
|
||||
git(dir.path(), &["commit", "-m", &format!("change {index}")]);
|
||||
}
|
||||
|
||||
let history = GitEngine::new(dir.path()).file_history("new.txt").unwrap();
|
||||
|
||||
assert_eq!(history.len(), 50);
|
||||
assert_eq!(history[0].subject.as_deref(), Some("change 49"));
|
||||
assert!(
|
||||
history
|
||||
.iter()
|
||||
.any(|commit| commit.subject.as_deref() == Some("renamed"))
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user