use std::path::Path;
use argonauts_core::TaskId;
use argonauts_core::names::{MessageId, SessionKey};
use argonauts_core::steps::StepName;
use crate::error::EngineResult;
use crate::git::Git;
use crate::state::RunState;
pub const SESSION_TRAILER: &str = "Argonauts-Session";
pub const START_TRAILER: &str = "Argonauts-Start";
pub const TASK_TRAILER: &str = "Argonauts-Task";
pub fn session_line(session: &SessionKey) -> String {
format!("{SESSION_TRAILER}: {session}")
}
pub fn session_lines(session: &SessionKey, start: u32) -> String {
if start > 1 {
format!("{}\n{START_TRAILER}: {start}", session_line(session))
} else {
session_line(session)
}
}
fn start_of(message: &str) -> u32 {
let prefix = format!("{START_TRAILER}: ");
message
.lines()
.find_map(|line| line.trim_end().strip_prefix(&prefix)?.parse().ok())
.unwrap_or(1)
}
pub fn task_line(task: &TaskId) -> String {
format!("{TASK_TRAILER}: {task}")
}
fn has_line(message: &str, line: &str) -> bool {
message.lines().any(|l| l.trim_end() == line)
}
pub fn holds_start(message: &str, session: &SessionKey, start: u32) -> bool {
has_line(message, &session_line(session)) && start_of(message) == start
}
pub async fn commit_for_session(
git: &Git,
dir: &Path,
range: &str,
session: &SessionKey,
start: u32,
) -> EngineResult<Option<String>> {
let log = git.log_messages(dir, range).await?;
Ok(log
.into_iter()
.find(|(_, message)| holds_start(message, session, start))
.map(|(sha, _)| sha))
}
pub async fn merge_for_task(
git: &Git,
dir: &Path,
range: &str,
task: &TaskId,
) -> EngineResult<Option<String>> {
let line = task_line(task);
for (sha, message) in git.log_messages(dir, range).await? {
if has_line(&message, &line) && git.parents(dir, &sha).await?.len() > 1 {
return Ok(Some(sha));
}
}
Ok(None)
}
pub async fn merge_of_head(
git: &Git,
dir: &Path,
range: &str,
task: &TaskId,
task_head: &str,
) -> EngineResult<Option<String>> {
let line = task_line(task);
for (sha, message) in git.log_messages(dir, range).await? {
if !has_line(&message, &line) {
continue;
}
let parents = git.parents(dir, &sha).await?;
if parents.iter().skip(1).any(|parent| parent == task_head) {
return Ok(Some(sha));
}
}
Ok(None)
}
pub fn delivered(state: &RunState, message: &MessageId, session: &SessionKey) -> bool {
state
.blackboard
.delivered
.iter()
.any(|(m, s)| m == message && s == session)
}
pub fn unfinished_session(state: &RunState, task: &TaskId, step: StepName) -> Option<SessionKey> {
state
.sessions
.by_key
.values()
.filter(|s| s.task.as_ref() == Some(task) && s.started.step == Some(step))
.filter(|s| s.end.is_none())
.max_by_key(|s| s.since)
.map(|s| s.key.clone())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::git::MergeOutcome;
use crate::testutil::{TestRepo, git_test_env};
fn git() -> Git {
Git::with_env(git_test_env())
}
#[tokio::test]
async fn a_session_commit_is_found_by_its_key() {
let repo = TestRepo::new();
let git = git();
let base = git.head_commit(&repo.root).await.unwrap().unwrap();
let key = SessionKey::numbered(7);
let range = format!("{base}..HEAD");
assert_eq!(
commit_for_session(&git, &repo.root, &range, &key, 1)
.await
.unwrap(),
None
);
repo.write("a.txt", "a");
let message = format!("task-001: work\n\n{}", session_line(&key));
let sha = git.commit_all(&repo.root, &message).await.unwrap().unwrap();
assert_eq!(
commit_for_session(&git, &repo.root, &range, &key, 1)
.await
.unwrap(),
Some(sha)
);
let other = SessionKey::numbered(70);
assert_eq!(
commit_for_session(&git, &repo.root, &range, &other, 1)
.await
.unwrap(),
None,
"s-070 is not s-007"
);
assert_eq!(
commit_for_session(&git, &repo.root, &range, &key, 2)
.await
.unwrap(),
None,
"a resumed start commits again"
);
repo.write("b.txt", "b");
let message = format!("task-001: work\n\n{}", session_lines(&key, 2));
let resumed = git.commit_all(&repo.root, &message).await.unwrap().unwrap();
assert_eq!(
commit_for_session(&git, &repo.root, &range, &key, 2)
.await
.unwrap(),
Some(resumed)
);
}
#[tokio::test]
async fn a_merge_naming_the_task_is_found_and_plain_commits_are_not() {
let repo = TestRepo::new();
let git = git();
let base = git.head_commit(&repo.root).await.unwrap().unwrap();
let task = TaskId::new("task-001").unwrap();
let range = format!("{base}..HEAD");
repo.write("x.txt", "x");
git.commit_all(&repo.root, &format!("Note\n\n{}", task_line(&task)))
.await
.unwrap();
assert_eq!(
merge_for_task(&git, &repo.root, &range, &task)
.await
.unwrap(),
None
);
let tree = repo.dir.path().join("tree");
git.add_worktree(&repo.root, &tree, "argonauts/r/task-001", &base)
.await
.unwrap();
std::fs::write(tree.join("a.txt"), "a").unwrap();
let head = git.commit_all(&tree, "work").await.unwrap().unwrap();
let message = format!("Merge task-001\n\n{}", task_line(&task));
let MergeOutcome::Merged(merge) = git
.merge_no_ff(&repo.root, "argonauts/r/task-001", &message)
.await
.unwrap()
else {
panic!("expected a merge");
};
assert_eq!(
merge_for_task(&git, &repo.root, &range, &task)
.await
.unwrap(),
Some(merge.clone())
);
assert_eq!(
merge_of_head(&git, &repo.root, &range, &task, &head)
.await
.unwrap(),
Some(merge)
);
std::fs::write(tree.join("b.txt"), "b").unwrap();
let newer = git.commit_all(&tree, "more").await.unwrap().unwrap();
assert_eq!(
merge_of_head(&git, &repo.root, &range, &task, &newer)
.await
.unwrap(),
None
);
}
#[test]
fn delivered_needs_the_same_message_and_session() {
let mut state = RunState::default();
let (m1, s1) = (MessageId::numbered(1), SessionKey::numbered(1));
state.blackboard.delivered.push((m1.clone(), s1.clone()));
assert!(delivered(&state, &m1, &s1));
assert!(!delivered(&state, &m1, &SessionKey::numbered(2)));
assert!(!delivered(&state, &MessageId::numbered(2), &s1));
}
}