From fe85456490c4f297dbd9b773cf5c3b8c6b053c42 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Fri, 5 Aug 2022 15:04:25 +0300 Subject: add src/git.rs Sponsored-by: author --- src/git.rs | 0 src/lib.rs | 1 + 2 files changed, 1 insertion(+) create mode 100644 src/git.rs diff --git a/src/git.rs b/src/git.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/lib.rs b/src/lib.rs index b78fda2..a699e7d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,5 +15,6 @@ pub mod page; pub mod parser; pub mod site; pub mod token; +pub mod git; pub mod util; pub mod wikitext; -- cgit v1.2.1 From d9f66f00a5898b4a3cf2ea8ada9a2d08f34c670e Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Fri, 5 Aug 2022 15:09:09 +0300 Subject: feat: set file commit time to git commit timestamp, unless changed If a file is committed to git and hasn't been changed, use the latest commit time for the file as the timestamp of the output file. This lacks tests, as I find it too much of a bother to set up git histories with timestamps. Sponsored-by: author --- riki.md | 2 +- src/bin/riki.rs | 45 +++++++++++++++++++++++++++-- src/error.rs | 9 ++++++ src/git.rs | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/page.rs | 8 ++++++ src/site.rs | 15 +++++++++- src/util.rs | 2 ++ 7 files changed, 166 insertions(+), 4 deletions(-) diff --git a/riki.md b/riki.md index c5dc4c0..e5ae0f0 100644 --- a/riki.md +++ b/riki.md @@ -519,7 +519,7 @@ the site content should be excluded._ given an installed riki given file site/index.mdwn from empty given file site/img.jpg from empty -given file site/.git/HEAD from empty +given file site/.git from empty given file site/index.mdwn~ from empty given file site/#index.mdwn# from empty when I run riki list site diff --git a/src/bin/riki.rs b/src/bin/riki.rs index 70bf71c..271cbb1 100644 --- a/src/bin/riki.rs +++ b/src/bin/riki.rs @@ -2,11 +2,12 @@ use clap::{CommandFactory, FromArgMatches, Parser}; use git_testament::{git_testament, render_testament, GitModification}; use log::{debug, error, info}; use riki::error::SiteError; +use riki::git::{git_dirty, git_whatchanged}; use riki::name::Name; use riki::site::Site; -use riki::util::{canonicalize, copy_file_from_source, mkdir, set_mtime}; +use riki::util::{canonicalize, copy_file_from_source, get_mtime, mkdir, set_mtime}; use std::error::Error; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; const ENVLOG: &str = "RIKI_LOG"; @@ -46,6 +47,7 @@ fn real_main() -> Result<(), SiteError> { match args.command { Command::Build(cmd) => cmd.run()?, Command::List(cmd) => cmd.run()?, + Command::Timestamps(cmd) => cmd.run()?, } } @@ -108,6 +110,7 @@ struct Args { enum Command { Build(Build), List(List), + Timestamps(Timestamps), } /// Build the site. @@ -180,3 +183,41 @@ impl List { Ok(()) } } + +/// Show the timestamp for each source file. +#[derive(Parser)] +struct Timestamps { + /// Directory where source files are. + srcdir: PathBuf, +} + +impl Timestamps { + fn run(&self) -> Result<(), SiteError> { + let srcdir = canonicalize(&self.srcdir)?; + let mut site = Site::new(&srcdir, &srcdir); + site.scan()?; + let mut names: Vec<&Name> = site.pages_and_files().collect(); + names.sort_by_cached_key(|name| name.page_path()); + + let whatchanged = git_whatchanged(&srcdir)?; + eprintln!("whatchanged: {:#?}", whatchanged); + + let dirty = git_dirty(&srcdir)?; + eprintln!("dirty: {:#?}", dirty); + + println!(); + for name in names { + let relative = name.source_path().strip_prefix(&srcdir).unwrap(); + if Self::is_dirty(relative, &dirty) { + println!("dirty: {} {:?}", name, get_mtime(name.source_path())?); + } else if let Some(timestamp) = whatchanged.get(relative) { + println!("git: {} {:?}", name, timestamp); + } + } + Ok(()) + } + + fn is_dirty(filename: &Path, dirty: &[PathBuf]) -> bool { + dirty.iter().any(|x| *x == filename) + } +} diff --git a/src/error.rs b/src/error.rs index aee8252..4fdcc1f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -64,4 +64,13 @@ pub enum SiteError { #[error("failed to convert time to Unix time")] UnixTime(#[source] std::time::SystemTimeError), + + #[error("failed to parse Unix timetamp: {0}")] + ParseUnixTimestamp(String, #[source] std::num::ParseIntError), + + #[error("faileed to invoked git with subcommand {0} in {1}")] + GitInvoke(String, PathBuf, #[source] std::io::Error), + + #[error("git {0} in in {1}:\n{2}")] + GitError(String, PathBuf, String), } diff --git a/src/git.rs b/src/git.rs index e69de29..89e84db 100644 --- a/src/git.rs +++ b/src/git.rs @@ -0,0 +1,89 @@ +use crate::error::SiteError; +use regex::Regex; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +pub fn git(args: &[&str], cwd: &Path) -> Result { + assert!(!args.is_empty()); + let output = Command::new("git") + .args(args) + .current_dir(cwd) + .output() + .map_err(|e| SiteError::GitInvoke(args[0].into(), cwd.into(), e))?; + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).into()) + } else { + let stderr = String::from_utf8_lossy(&output.stderr).into(); + Err(SiteError::GitError(args[0].into(), cwd.into(), stderr)) + } +} + +pub fn git_whatchanged(cwd: &Path) -> Result, SiteError> { + let mut files = HashMap::new(); + if cwd.join(".git").is_dir() { + let output = git(&["whatchanged", "--pretty=format:%ad", "--date=unix"], cwd)?; + let timepat = Regex::new(r#"^(?P\d+)$"#).expect("regex compilation"); + let filepat = Regex::new(r#"^:\S+ \S+ \S+ \S+ (?P\S)\t(?P\S+)$"#) + .expect("regex compilation"); + let mut mtime = None; + + for line in output.lines() { + if let Some(caps) = timepat.captures(line) { + let secs = caps.name("secs").unwrap().as_str(); + let timestamp = secs + .parse::() + .map_err(|e| SiteError::ParseUnixTimestamp(secs.into(), e))?; + mtime = Some(UNIX_EPOCH + Duration::new(timestamp, 0)); + } else if let Some(caps) = filepat.captures(line) { + let flag = caps.name("flag").unwrap().as_str(); + let filename = PathBuf::from(caps.name("filename").unwrap().as_str()); + if (flag == "M" || flag == "A") && !files.contains_key(&filename) { + assert!(mtime.is_some()); + files.insert(filename, mtime.unwrap()); + } + } else if line.trim().is_empty() { + mtime = None; + } + } + } + Ok(files) +} + +pub fn git_dirty(cwd: &Path) -> Result, SiteError> { + let mut dirty = vec![]; + if cwd.join(".git").is_dir() { + let output = git(&["status", "--short"], cwd)?; + + let pat = Regex::new(r#"^.(?P.) (?P\S.+)$"#).expect("regex compilation"); + for line in output.lines() { + if let Some(caps) = pat.captures(line) { + let status = GitStatus::from(caps.name("status").unwrap().as_str()); + let filename = caps.name("filename").unwrap().as_str(); + if status == GitStatus::Dirty { + dirty.push(filename.into()); + } + } + } + } + + Ok(dirty) +} + +#[derive(Debug, Eq, PartialEq)] +pub enum GitStatus { + Clean, + Dirty, + Unknown, +} + +impl From<&str> for GitStatus { + fn from(status: &str) -> Self { + match status { + "M" => Self::Dirty, + "?" => Self::Unknown, + _ => Self::Clean, + } + } +} diff --git a/src/page.rs b/src/page.rs index d3bc8e7..822aee6 100644 --- a/src/page.rs +++ b/src/page.rs @@ -39,6 +39,10 @@ impl WikitextPage { &self.meta } + pub fn meta_mut(&mut self) -> &mut PageMeta { + &mut self.meta + } + pub fn wikitext(&self) -> &str { &self.wikitext } @@ -161,6 +165,10 @@ impl PageMeta { pub fn mtime(&self) -> SystemTime { self.mtime } + + pub fn set_mtime(&mut self, mtime: SystemTime) { + self.mtime = mtime; + } } #[derive(Debug, Default)] diff --git a/src/site.rs b/src/site.rs index 2d1368d..3200b58 100644 --- a/src/site.rs +++ b/src/site.rs @@ -1,4 +1,5 @@ use crate::error::SiteError; +use crate::git::git_whatchanged; use crate::name::{Name, NameBuilder, Names}; use crate::page::{MarkdownPage, UnprocessedPage, WikitextPage}; use crate::parser::WikitextParser; @@ -7,6 +8,7 @@ use crate::util::make_relative_link; use log::{debug, info, trace}; use std::collections::HashMap; use std::path::{Path, PathBuf}; +use std::time::SystemTime; use walkdir::WalkDir; pub struct Site { @@ -18,6 +20,7 @@ pub struct Site { patterns: TokenPatterns, name_queue: Vec, page_queue: PageSet, + whatchanged: HashMap, } impl Site { @@ -37,10 +40,12 @@ impl Site { patterns: TokenPatterns::default(), name_queue: vec![], page_queue: PageSet::default(), + whatchanged: HashMap::new(), } } pub fn scan(&mut self) -> Result<(), SiteError> { + self.whatchanged = git_whatchanged(self.builder.srcdir())?; for name in self.all_files()? { trace!("scan: name={}", name); if name.is_wikitext_page() { @@ -85,7 +90,10 @@ impl Site { fn process_name(&mut self) -> Result { if let Some(name) = self.name_queue.pop() { debug!("loading wikitext page {}", name.source_path().display()); - let page = WikitextPage::read(&name)?; + let mut page = WikitextPage::read(&name)?; + if let Some(mtime) = self.git_commit_timestamp(&name) { + page.meta_mut().set_mtime(mtime); + } self.files.insert(name); self.add_wikitextpage(page); Ok(true) @@ -95,6 +103,11 @@ impl Site { } } + fn git_commit_timestamp(&self, name: &Name) -> Option { + let relative = name.source_path().strip_prefix(&self.builder.srcdir()).unwrap(); + self.whatchanged.get(relative).copied() + } + fn process_wikipage(&mut self) -> Result { if let Some(page) = self.wikitext_pages.pop() { debug!("processing wikitext page {}", page.meta().path().display()); diff --git a/src/util.rs b/src/util.rs index 1f69523..7909080 100644 --- a/src/util.rs +++ b/src/util.rs @@ -34,6 +34,8 @@ pub fn get_mtime(src: &Path) -> Result { } pub fn set_mtime(filename: &Path, mtime: SystemTime) -> Result<(), SiteError> { + trace!("set_mtime: filename={} mtime={:?}", filename.display(), mtime); + let mtime = timespec(mtime)?; let times = [mtime, mtime]; let times: *const timespec = ×[0]; -- cgit v1.2.1