use std::path::Path;
use std::time::Duration;
use anyhow::Result;
use crate::{
git::{self, RepositoryState},
Store,
};
pub const STORE_GIT_DIR: &str = ".git/";
pub const GIT_PULL_OUTDATED: Duration = Duration::from_secs(30);
pub struct Sync<'a> {
store: &'a Store,
}
impl<'a> Sync<'a> {
pub fn new(store: &'a Store) -> Sync<'a> {
Self { store }
}
fn path(&self) -> &Path {
&self.store.root
}
pub fn readyness(&self) -> Result<Readyness> {
let path = self.path();
if !self.is_init() {
return Ok(Readyness::NoSync);
}
match git::git_state(path).unwrap() {
RepositoryState::Clean => {
if is_dirty(path)? {
Ok(Readyness::Dirty)
} else {
Ok(Readyness::Ready)
}
}
state => Ok(Readyness::RepoState(state)),
}
}
pub fn prepare(&self) -> Result<()> {
if !self.is_init() {
return Ok(());
}
if !self.has_remote()? {
return Ok(());
}
let repo = self.path();
if git::git_branch_upstream(repo, "HEAD")?.is_none() {
let remotes = git::git_remote(repo)?;
if remotes.len() != 1 {
return Ok(());
}
let remote = &remotes[0];
git::git_fetch(repo, Some(remote))?;
let remote_branches = git::git_branch_remote(repo)?;
if remote_branches.is_empty() {
return Ok(());
}
let branch = git::git_current_branch(repo)?;
let upstream_ref = format!("{}/{}", remote, branch);
if !remote_branches.contains(&upstream_ref) {
return Ok(());
}
git::git_branch_set_upstream(repo, None, &upstream_ref)?;
}
self.pull()?;
Ok(())
}
pub fn finalize<M: AsRef<str>>(&self, msg: M) -> Result<()> {
if !self.is_init() {
return Ok(());
}
if is_dirty(self.path())? {
self.commit_all(msg, false)?;
}
if !self.has_remote()? || !safe_need_to_push(self.path()) {
return Ok(());
}
let mut set_branch = None;
let mut set_upstream = None;
let repo = self.path();
if git::git_branch_upstream(repo, "HEAD")?.is_none() {
let remotes = git::git_remote(repo)?;
if remotes.len() == 1 {
let remote = &remotes[0];
git::git_fetch(repo, Some(remote))?;
let remote_branches = git::git_branch_remote(repo)?;
let branch = git::git_current_branch(repo)?;
let upstream_ref = format!("{}/{}", remote, branch);
if !remote_branches.contains(&upstream_ref) {
set_branch = Some(branch);
set_upstream = Some(remote.to_string());
}
}
}
self.push(set_branch.as_deref(), set_upstream.as_deref())?;
Ok(())
}
pub fn init(&self) -> Result<()> {
git::git_init(self.path())?;
self.commit_all("Initialize sync with git", true)?;
Ok(())
}
pub fn clone(&self, url: &str, quiet: bool) -> Result<()> {
let path = self
.path()
.to_str()
.expect("failed to determine clone path");
git::git_clone(self.path(), url, path, quiet)?;
Ok(())
}
pub fn is_init(&self) -> bool {
self.path().join(STORE_GIT_DIR).is_dir()
}
pub fn remotes(&self) -> Result<Vec<String>> {
git::git_remote(self.path())
}
pub fn remote_url(&self, remote: &str) -> Result<String> {
git::git_remote_get_url(self.path(), remote)
}
pub fn add_remote_url(&self, remote: &str, url: &str) -> Result<()> {
git::git_remote_add(self.path(), remote, url)
}
pub fn set_remote_url(&self, remote: &str, url: &str) -> Result<()> {
git::git_remote_remove(self.path(), remote)?;
self.add_remote_url(remote, url)
}
pub fn has_remote(&self) -> Result<bool> {
if !self.is_init() {
return Ok(false);
}
git::git_has_remote(self.path())
}
fn pull(&self) -> Result<()> {
git::git_pull(self.path())
}
fn push(&self, set_branch: Option<&str>, set_upstream: Option<&str>) -> Result<()> {
git::git_push(self.path(), set_branch, set_upstream)
}
fn commit_all<M: AsRef<str>>(&self, msg: M, commit_empty: bool) -> Result<()> {
let path = self.path();
git::git_add_all(path)?;
git::git_commit(path, msg.as_ref(), commit_empty)
}
}
#[derive(Debug)]
pub enum Readyness {
NoSync,
RepoState(git::RepositoryState),
Dirty,
Ready,
}
impl Readyness {
pub fn is_ready(&self) -> bool {
match self {
Self::Ready => true,
_ => false,
}
}
}
fn is_dirty(repo: &Path) -> Result<bool> {
git::git_has_changes(repo)
}
fn safe_need_to_push(repo: &Path) -> bool {
match need_to_push(repo) {
Ok(push) => push,
Err(err) => {
eprintln!(
"failed to test if local branch is different than remote, ignoring: {}",
err,
);
true
}
}
}
fn need_to_push(repo: &Path) -> Result<bool> {
let last_pulled = git::git_last_pull_time(repo)?;
if last_pulled.elapsed()? > GIT_PULL_OUTDATED {
return Ok(true);
}
let branch = git::git_current_branch(repo)?;
let upstream = match git::git_branch_upstream(repo, &branch)? {
Some(upstream) => upstream,
None => return Ok(true),
};
Ok(git::git_ref_hash(repo, branch)? != git::git_ref_hash(repo, upstream)?)
}