feat(blog::components::check+update) update

This commit is contained in:
lumither committed 2025-04-05 18:37:23 -07:00
1 parent 40abec2e90
commit ac8a2359b0
6 files changed
+199 -143

No files matched your search

+3 -3
View File
@@ -1,4 +1,4 @@
mod error; pub mod error;
mod utils; mod utils;
use std::{fs, path::Path, str::FromStr}; use std::{fs, path::Path, str::FromStr};
@@ -8,12 +8,12 @@ use crate::{
utils::front_matter::{parse_front_matter, split_md_front_matter}, utils::front_matter::{parse_front_matter, split_md_front_matter},
}; };
use base64::{Engine, engine::general_purpose}; use base64::{engine::general_purpose, Engine};
use serde::Serialize; use serde::Serialize;
use serde_json::Value; use serde_json::Value;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
#[derive(Default, Debug, Serialize)] #[derive(Default, Debug, Serialize, Clone)]
pub struct MdFile { pub struct MdFile {
pub meta: Value, pub meta: Value,
pub content: String, pub content: String,
@@ -1,30 +1,67 @@
use git2::Delta;
use std::path::PathBuf; use std::path::PathBuf;
use markdown::MdFile;
use uuid::Uuid; use uuid::Uuid;
#[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum Change { pub enum Change {
DELETE { Delete(Delete),
uuid: Uuid, Move(Move),
}, Update(Update),
RENAME { Create(Create),
uuid: Uuid,
from: PathBuf,
to: PathBuf,
},
UPDATE {
uuid: Uuid,
content: String,
},
CREATE {
uuid: Uuid,
path: PathBuf,
content: String,
},
} }
#[allow(dead_code)]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DeltaIdx { pub struct Delete {
pub delta_type: Delta, pub uuid: Uuid,
pub content: String, pub path: PathBuf,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct Move {
pub uuid: Uuid,
pub from: PathBuf,
pub to: PathBuf,
pub payload: MdFile,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct Update {
pub uuid: Uuid,
pub payload: MdFile,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct Create {
pub uuid: Uuid,
pub path: PathBuf,
pub payload: MdFile,
}
#[derive(Debug, Clone, Default)]
pub struct CreateDelete2UpdateSlot {
pub create: Option<Create>,
pub delete: Option<Delete>,
}
impl CreateDelete2UpdateSlot {
pub fn to_change(&self) -> Option<Change> {
match (&self.create, &self.delete) {
(Some(create), Some(delete)) => Some(Change::Move(Move {
uuid: create.uuid,
from: delete.path.clone(),
to: create.path.clone(),
payload: create.payload.clone(),
})),
(Some(create), None) => Some(Change::Create(create.clone())),
(None, Some(delete)) => Some(Change::Delete(delete.clone())),
(None, None) => None,
}
}
} }
@@ -1,7 +1,13 @@
use markdown::error::Error as MarkdownError;
use thiserror::Error; use thiserror::Error;
#[allow(dead_code)]
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
#[error("markdown file error")]
MdFile(#[from] MarkdownError),
#[error("missing essential field `{}` for file `{}`", field, filename)] #[error("missing essential field `{}` for file `{}`", field, filename)]
MissingField { field: String, filename: String }, MissingField { field: String, filename: String },
@@ -1,20 +1,18 @@
use crate::constants::config::general::{default_value, var_name};
use macros::panic_with_log;
use std::sync::Arc;
use std::{ use std::{
env, env,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc,
}; };
use crate::modules::blog::components::check_update::pull::{analyze_deltas, fetch_deltas}; use crate::{
use crate::modules::blog::components::check_update::{ constants::config::general::{default_value, var_name},
// index::drop_index, modules::blog::components::check_update::{pull::fetch_deltas, utils::expand_path},
// utils::{expand_path, index_files, search_md}, scheduler::task_func::AsyncTaskFunc,
utils::expand_path,
}; };
use crate::scheduler::task_func::AsyncTaskFunc; use macros::panic_with_log;
use sqlx::PgPool; use sqlx::PgPool;
use tracing::{info, trace, warn, Level}; use tracing::{trace, warn, Level};
// mod error; // mod error;
// mod index; // mod index;
@@ -95,5 +93,5 @@ async fn workflow(args: Arc<Args>) {
return; return;
} }
let updates = analyze_deltas(deltas, &args.git_work_dir).await; // todo: load delta to database
} }
@@ -1,20 +1,26 @@
use crate::modules::blog::components::check_update::changes::{Change, DeltaIdx};
use crate::modules::blog::components::check_update::error::Error;
use crate::utils::git;
use crate::utils::git::FileDelta;
use git2::{Delta, Repository};
use markdown::MdFile;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::PathBuf;
use crate::{
modules::blog::components::check_update::{
changes::{Change, Create, Delete, Update},
error::Error,
},
utils::{git, git::FileDelta},
};
use markdown::MdFile;
use crate::modules::blog::components::check_update::changes::CreateDelete2UpdateSlot;
use git2::{Delta, Repository};
use tracing::error; use tracing::error;
use uuid::Uuid; use uuid::Uuid;
pub async fn fetch_deltas( pub async fn fetch_deltas(
git_url: &str, git_url: &str,
git_remote_name: &str, git_remote_name: &str,
git_branch: &str, git_remote_branch: &str,
git_work_dir: &PathBuf, git_work_dir: &PathBuf,
) -> Result<Vec<FileDelta>, Error> { ) -> Result<Vec<Change>, Error> {
let repo = match Repository::open(git_work_dir) { let repo = match Repository::open(git_work_dir) {
Ok(repo) => repo, Ok(repo) => repo,
Err(_) => match Repository::clone(git_url, git_work_dir) { Err(_) => match Repository::clone(git_url, git_work_dir) {
@@ -26,114 +32,87 @@ pub async fn fetch_deltas(
}, },
}; };
let updates = match git::sync(git_remote_name, git_branch, &repo) { let mut remote = repo.find_remote(git_remote_name)?;
Ok(updates) => updates, let fetched_commit = git::do_fetch(&repo, &[git_remote_branch], &mut remote)?;
Err(e) => { let commit_deltas = git::calculate_diff(
error!( &repo,
"failed to sync blog content git({}) updates: {}", &repo.reference_to_annotated_commit(&repo.head()?)?,
&git_url, e &fetched_commit,
); )?;
return Err(e.into());
}
};
Ok(updates) // dbg!(&commit_deltas);
}
pub async fn analyze_deltas(file_deltas: Vec<FileDelta>, git_work_dir: &Path) -> Vec<Change> {
dbg!(&file_deltas);
let mut changes: Vec<Change> = vec![]; let mut changes: Vec<Change> = vec![];
let mut merge_deltas: Vec<FileDelta> = vec![]; let mut non_delete_deltas: Vec<FileDelta> = vec![];
for delta in file_deltas.into_iter() { // process deleted deltas before merging commit to prevent "file not found" os error
if delta.status == Delta::Modified { for delta in commit_deltas.into_iter() {
if delta.status == Delta::Deleted {
let file_path = &git_work_dir.join( let file_path = &git_work_dir.join(
delta delta
.new_path .new_path
.expect("cannot parse modified post `new_path`"), .expect("cannot parse modified post `new_path`"),
); );
let post = match MdFile::from_file(file_path) { if let Some(id) = extract_post_id(&MdFile::from_file(file_path)?) {
Ok(post) => post, changes.push(Change::Delete(Delete {
Err(e) => { uuid: id,
error!("md file parsing error: {}", e); path: file_path.clone(),
continue; }))
} } else {
}; error!("failed to parse uuid for file {}", file_path.display());
// todo: add to unresolved error list
let id = match extract_post_id(&post) { }
Some(id) => id,
None => {
// todo: add to unresolved error list
error!(
"failed to parse uuid on post {}",
file_path.to_string_lossy()
);
continue;
}
};
changes.push(Change::UPDATE {
uuid: id,
content: post.content,
})
} else { } else {
merge_deltas.push(delta) non_delete_deltas.push(delta);
}; }
} }
// merge some addition and remove deltas to rename/move changes git::do_merge(&repo, git_remote_branch, fetched_commit)?;
let delta_idx = {
let mut delta_idx: HashMap<Uuid, Vec<DeltaIdx>> = HashMap::new();
for file_delta in merge_deltas.into_iter() {
let file_path = &git_work_dir.join(
file_delta
.new_path
.expect("cannot parse modified post `new_path`"),
);
match file_delta.status {
// todo: split impl
Delta::Added | Delta::Deleted => {
let file_path_str = file_path.to_string_lossy();
let post = match MdFile::from_file(file_path) { // process adds and modifies deltas
Ok(post) => post, for delta in non_delete_deltas.into_iter() {
Err(e) => { let file_path = &git_work_dir.join(
error!("md file parsing error ({}): {}", file_path_str, e); delta
continue; .new_path
} .expect("cannot parse modified post `new_path`"),
}; );
match delta.status {
let id = match extract_post_id(&post) { Delta::Added => {
Some(id) => id, let md_file = MdFile::from_file(file_path)?;
None => { if let Some(id) = extract_post_id(&md_file) {
// todo: add to unresolved error list changes.push(Change::Create(Create {
error!("failed to parse uuid on post {}", file_path_str); uuid: id,
continue; path: file_path.clone(),
} payload: md_file,
}; }))
} else {
let file_delta_idx = DeltaIdx { error!("failed to parse uuid for file {}", file_path.display());
delta_type: file_delta.status, // todo: add to unresolved error list
content: post.content,
};
delta_idx
.entry(id)
// todo: change to shallow copy
.and_modify(|v| v.push(file_delta_idx.clone()))
.or_insert(vec![file_delta_idx]);
} }
_ => { }
error!("unsupported delta on file {}", file_path.to_string_lossy()); Delta::Modified => {
continue; let md_file = MdFile::from_file(file_path)?;
if let Some(id) = extract_post_id(&md_file) {
changes.push(Change::Update(Update {
uuid: id,
payload: md_file,
}))
} else {
error!("failed to parse uuid for file {}", file_path.display());
// todo: add to unresolved error list
} }
}; }
Delta::Deleted => continue,
_ => {
error!("unsupported delta type on file {}", file_path.display());
continue;
}
} }
delta_idx }
};
dbg!(&delta_idx); // merge adds and deletes deltas to moves
let changes = dbg!(merge_changes(changes));
changes Ok(changes)
} }
fn extract_post_id(post: &MdFile) -> Option<Uuid> { fn extract_post_id(post: &MdFile) -> Option<Uuid> {
@@ -145,3 +124,30 @@ fn extract_post_id(post: &MdFile) -> Option<Uuid> {
None => None, None => None,
} }
} }
fn merge_changes(changes: Vec<Change>) -> Vec<Change> {
let mut slots: HashMap<Uuid, CreateDelete2UpdateSlot> = HashMap::new();
let mut merged: Vec<Change> = Vec::new();
for change in changes {
match change {
Change::Create(create) => {
let id = create.uuid;
slots.entry(id).or_default().create = Some(create);
}
Change::Delete(delete) => {
let id = delete.uuid;
slots.entry(id).or_default().delete = Some(delete);
}
_ => merged.push(change),
}
}
for slot in slots.values() {
if let Some(change) = slot.to_change() {
merged.push(change);
}
}
merged
}
+17 -8
View File
@@ -1,13 +1,14 @@
/// git merge /// git operations
/// based on https://github.com/rust-lang/git2-rs/blob/master/examples/pull.rs /// based on https://github.com/rust-lang/git2-rs/blob/master/examples/pull.rs
/// https://github.com/rust-lang/git2-rs/commit/f3b87baed1e33d6c2d94fe1fa6aa6503a071d837 /// https://github.com/rust-lang/git2-rs/commit/f3b87baed1e33d6c2d94fe1fa6aa6503a071d837
/// ///
use git2::{ use git2::{
AnnotatedCommit, AutotagOption, Delta, FetchOptions, Reference, Remote, Repository, build, build, AnnotatedCommit, AutotagOption, Delta, FetchOptions, Reference, Remote, Repository,
}; };
use std::path::PathBuf; use std::path::PathBuf;
use tracing::{error, warn}; use tracing::{error, warn};
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
pub struct FileDelta { pub struct FileDelta {
pub old_path: Option<PathBuf>, pub old_path: Option<PathBuf>,
@@ -15,7 +16,7 @@ pub struct FileDelta {
pub status: Delta, pub status: Delta,
} }
fn do_fetch<'a>( pub fn do_fetch<'a>(
repo: &'a Repository, repo: &'a Repository,
refs: &[&str], refs: &[&str],
remote: &'a mut Remote, remote: &'a mut Remote,
@@ -81,11 +82,11 @@ fn normal_merge(
Ok(()) Ok(())
} }
fn do_merge<'a>( pub fn do_merge<'a>(
repo: &'a Repository, repo: &'a Repository,
remote_branch: &str, remote_branch: &str,
fetch_commit: AnnotatedCommit<'a>, fetch_commit: AnnotatedCommit<'a>,
) -> Result<Vec<FileDelta>, git2::Error> { ) -> Result<(), git2::Error> {
let analysis = repo.merge_analysis(&[&fetch_commit])?; let analysis = repo.merge_analysis(&[&fetch_commit])?;
let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?; let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?;
@@ -114,10 +115,11 @@ fn do_merge<'a>(
} else if analysis.0.is_normal() { } else if analysis.0.is_normal() {
normal_merge(repo, &head_commit, &fetch_commit)?; normal_merge(repo, &head_commit, &fetch_commit)?;
} }
calculate_diff(repo, &head_commit, &fetch_commit)
Ok(())
} }
fn calculate_diff( pub fn calculate_diff(
repo: &Repository, repo: &Repository,
local: &AnnotatedCommit, local: &AnnotatedCommit,
remote: &AnnotatedCommit, remote: &AnnotatedCommit,
@@ -147,6 +149,7 @@ fn calculate_diff(
Ok(updated_files.into_iter().collect()) Ok(updated_files.into_iter().collect())
} }
#[allow(dead_code)]
pub fn sync( pub fn sync(
remote_name: &str, remote_name: &str,
remote_branch: &str, remote_branch: &str,
@@ -154,5 +157,11 @@ pub fn sync(
) -> Result<Vec<FileDelta>, git2::Error> { ) -> Result<Vec<FileDelta>, git2::Error> {
let mut remote = repo.find_remote(remote_name)?; let mut remote = repo.find_remote(remote_name)?;
let fetch_commit = do_fetch(repo, &[remote_branch], &mut remote)?; let fetch_commit = do_fetch(repo, &[remote_branch], &mut remote)?;
do_merge(repo, remote_branch, fetch_commit) let delta = calculate_diff(
repo,
&repo.reference_to_annotated_commit(&repo.head()?)?,
&fetch_commit,
);
do_merge(repo, remote_branch, fetch_commit)?;
delta
} }