use chrono::{DateTime, Utc};
use color_eyre::eyre::{eyre, Result};
use mktemp::Temp;
use std::thread::sleep;
use std::time::Duration;
use std::{collections::HashMap, path::PathBuf};
use tracing::{debug, error, info};
use git_meta::{GitCommitMeta, GitCredentials, GitRepo};
type BranchHeads = HashMap<String, GitCommitMeta>;
type PathAlert = HashMap<String, Vec<PathBuf>>;
#[derive(Clone, Debug)]
pub struct GitRepoWatchHandler {
pub repo: GitRepo,
pub state: Option<GitRepoState>,
pub branch_filter: Option<Vec<String>>,
pub path_filter: Option<Vec<PathBuf>>,
pub use_shallow: bool,
pub poll_freq: Duration,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct GitRepoState {
pub last_updated: Option<DateTime<Utc>>,
pub branch_heads: BranchHeads,
pub path_alert: PathAlert,
}
impl GitRepoWatchHandler {
pub fn new<U: AsRef<str>>(url: U) -> Result<Self> {
Ok(GitRepoWatchHandler {
repo: GitRepo::new(url)?,
state: None,
branch_filter: None,
path_filter: None,
use_shallow: false,
poll_freq: Duration::from_secs(5),
})
}
pub fn with_branch(mut self, branch: Option<String>) -> Self {
self.repo = self.repo.with_branch(branch);
self
}
pub fn with_commit(mut self, id: Option<String>) -> Result<Self> {
let tempdir = Temp::new_dir()?;
let _clone = self.repo.to_clone().git_clone(tempdir.to_path_buf())?;
let repo = GitRepo::open(tempdir.to_path_buf(), self.repo.branch.clone(), id.clone())?;
let repo_head = self
.repo
.head
.clone()
.ok_or_else(|| eyre!("Repo HEAD commit is not set"))?;
let repo_branch = self
.repo
.branch
.clone()
.ok_or_else(|| eyre!("Repo branch is not set"))?;
let mut path_alert: HashMap<String, Vec<PathBuf>> = HashMap::new();
let changed_paths = repo.to_info().list_files_changed_at(repo_head.id.clone())?;
if let Some(paths) = changed_paths {
path_alert.insert(repo_branch.clone(), paths);
}
self.repo = repo;
let mut head = HashMap::new();
head.insert(
repo_branch,
GitCommitMeta {
id: repo_head.id.clone(),
message: repo_head.message,
timestamp: repo_head.timestamp,
},
);
let repo_report = GitRepoState {
branch_heads: head,
last_updated: Some(Utc::now()),
path_alert,
};
self.state = Some(repo_report);
self.repo = self.repo.with_commit(id)?;
Ok(self)
}
pub fn with_credentials(mut self, creds: Option<GitCredentials>) -> Self {
self.repo.credentials = creds;
self
}
pub fn with_branch_filter(mut self, branch_list: Option<Vec<String>>) -> Self {
self.branch_filter = branch_list;
self
}
pub fn with_path_filter(mut self, path_list: Option<Vec<PathBuf>>) -> Self {
self.path_filter = path_list;
self
}
#[cfg(feature = "shallow_clone")]
pub fn with_shallow_clone(mut self, shallow_choice: bool) -> Self {
self.use_shallow = shallow_choice;
self
}
pub fn with_poll_freq(mut self, frequency: Duration) -> Self {
self.poll_freq = frequency;
self
}
pub fn state(&self) -> Option<GitRepoState> {
self.state.clone()
}
fn _update_state(&mut self) -> Result<GitRepoState> {
let prev_state = self.clone();
let temp_path = Temp::new_dir()?;
self.repo = if cfg!(feature = "shallow_clone") {
match &self.use_shallow {
true => {
debug!("Shallow clone");
self.repo
.to_clone()
.git_clone_shallow(&temp_path.as_path())?
}
false => {
debug!("Deep clone");
self.repo.to_clone().git_clone(&temp_path.as_path())?
}
}
} else {
debug!("Deep clone");
self.repo.to_clone().git_clone(&temp_path.as_path())?
};
let mut repo_report = GitRepoState::default();
let branch_heads = self
.repo
.clone()
.to_info()
.get_remote_branch_head_refs(self.branch_filter.clone())?;
repo_report.branch_heads = branch_heads.clone();
let mut path_alert = HashMap::new();
for (branch, commit) in branch_heads {
if let Some(c) = prev_state
.state()
.unwrap_or_default()
.branch_heads
.get(&branch)
{
let paths = self
.repo
.to_info()
.list_files_changed_between(commit.id, c.clone().id)?;
if let Some(p) = paths {
path_alert.insert(branch, p);
} else {
error!("There are no ")
}
} else {
let paths = self.repo.to_info().list_files_changed_at(commit.id)?;
if let Some(p) = paths {
path_alert.insert(branch, p);
}
};
}
repo_report.path_alert = path_alert;
repo_report.last_updated = Some(Utc::now());
self.state = Some(repo_report.clone());
Ok(repo_report)
}
pub async fn update_state(&mut self) -> Result<GitRepoState> {
self._update_state()
}
pub fn update_state_sync(&mut self) -> Result<GitRepoState> {
self._update_state()
}
pub async fn watch_new_commits(
&mut self,
pre_run: bool,
closure: impl Fn(GitRepoState),
) -> Result<()> {
let mut branch_heads_state = self.update_state().await?;
if pre_run {
closure(branch_heads_state.clone());
}
loop {
sleep(self.poll_freq);
let snapshot = self.get_branches_snapshot()?;
branch_heads_state = self.update_state().await?;
self.run_code_if_new_commit_in_branch(branch_heads_state, snapshot.clone(), &closure)?;
}
}
fn run_code_if_new_commit_in_branch(
&self,
current_state: GitRepoState,
current_commits: HashMap<String, GitCommitMeta>,
closure: impl Fn(GitRepoState) + Copy,
) -> Result<bool> {
for (branch, commit) in current_commits {
match current_state.branch_heads.get(&branch) {
Some(c) => {
if &commit == c {
info!("No new commits in branch {} found", branch);
} else {
info!("New commit in branch {} found", branch);
if let Some(state) = self.state() {
closure(state);
} else {
return Err(eyre!("No state found"));
}
}
}
None => {
info!("New branch '{}' found", branch);
if let Some(state) = self.state() {
closure(state);
} else {
return Err(eyre!("No state found"));
}
}
}
}
Ok(true)
}
pub fn watch_new_commits_sync(
&mut self,
pre_run: bool,
closure: impl Fn(GitRepoState),
) -> Result<()> {
if pre_run {
if let Some(state) = self.state() {
closure(state);
} else {
return Err(eyre!("No state found"));
}
}
loop {
sleep(self.poll_freq);
let snapshot = self.get_branches_snapshot()?;
let branch_heads_state = self.update_state_sync()?;
self.run_code_if_new_commit_in_branch(branch_heads_state, snapshot.clone(), &closure)?;
}
}
fn get_branches_snapshot(&self) -> Result<HashMap<String, GitCommitMeta>> {
if let Some(state) = self.state.clone() {
Ok(state.branch_heads)
} else {
Err(eyre!("Unable to get snapshot of branch HEAD refs"))
}
}
}