summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2022-08-05 14:01:47 +0000
committerLars Wirzenius <liw@liw.fi>2022-08-05 14:01:47 +0000
commitaacb704f8cc7e227b9a301a74d879957f4af766a (patch)
tree460752d8e864237774947a3515bcb35435b3dc2e
parentb45262174d06b15b7a888e6bcad42f6685ba89a2 (diff)
parentd9f66f00a5898b4a3cf2ea8ada9a2d08f34c670e (diff)
downloadriki-aacb704f8cc7e227b9a301a74d879957f4af766a.tar.gz
Merge branch 'git-ts' into 'main'
add src/git.rs Closes #3 See merge request larswirzenius/riki!35
-rw-r--r--riki.md2
-rw-r--r--src/bin/riki.rs45
-rw-r--r--src/error.rs9
-rw-r--r--src/git.rs89
-rw-r--r--src/lib.rs1
-rw-r--r--src/page.rs8
-rw-r--r--src/site.rs15
-rw-r--r--src/util.rs2
8 files changed, 167 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
new file mode 100644
index 0000000..89e84db
--- /dev/null
+++ 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<String, SiteError> {
+ 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<HashMap<PathBuf, SystemTime>, 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<secs>\d+)$"#).expect("regex compilation");
+ let filepat = Regex::new(r#"^:\S+ \S+ \S+ \S+ (?P<flag>\S)\t(?P<filename>\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::<u64>()
+ .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<Vec<PathBuf>, SiteError> {
+ let mut dirty = vec![];
+ if cwd.join(".git").is_dir() {
+ let output = git(&["status", "--short"], cwd)?;
+
+ let pat = Regex::new(r#"^.(?P<status>.) (?P<filename>\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/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;
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<Name>,
page_queue: PageSet,
+ whatchanged: HashMap<PathBuf, SystemTime>,
}
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<bool, SiteError> {
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<SystemTime> {
+ let relative = name.source_path().strip_prefix(&self.builder.srcdir()).unwrap();
+ self.whatchanged.get(relative).copied()
+ }
+
fn process_wikipage(&mut self) -> Result<bool, SiteError> {
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<SystemTime, SiteError> {
}
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 = &times[0];