use git2::{
Cred,
FetchOptions,
PushOptions,
RemoteCallbacks,
Status,
};
use anyhow::bail;
use std::path::Path;
pub struct Repository {
path: String,
repo: Option< git2::Repository >,
}
impl Repository {
pub fn new( path: &str ) -> Self {
Self {
path: path.to_owned(),
repo: None,
}
}
pub fn open( &mut self ) -> anyhow::Result<()> {
let repo = match git2::Repository::discover( &self.path ) {
Ok(repo) => repo,
Err(e) => bail!("failed to open: {}", e),
};
self.repo = Some( repo );
Ok(())
}
pub fn get_dirty( &mut self ) -> Vec<String> {
match &self.repo {
Some( repo ) => {
let mut dirty = Vec::new();
let mut check_s = Status::empty();
check_s.insert(Status::INDEX_NEW);
check_s.insert(Status::INDEX_MODIFIED);
check_s.insert(Status::WT_NEW);
check_s.insert(Status::WT_MODIFIED);
let mut skip_s = Status::empty();
skip_s.insert(Status::IGNORED);
skip_s.insert(Status::WT_NEW);
for se in repo.statuses( None ).unwrap().iter() {
let s = se.status();
if s.intersects( check_s ) {
dirty.push( se.path().unwrap_or( "" ).to_owned());
} else {
if !s.intersects( skip_s ) {
println!("Not dirty {:?} {}", s, se.path().unwrap_or( "" ) );
}
}
}
dirty
},
None => {
Vec::new()
},
}
}
pub fn commit( &mut self, files: &Vec<String>, message: &str ) -> anyhow::Result<()> {
match &self.repo {
Some( repo ) => {
let sig = repo.signature()?;
let mut index = match repo.index() {
Err(e) => bail!("No index for repository {}", &e),
Ok(index) => index,
};
let cwd = match std::env::current_dir() {
Ok( wd ) => wd,
Err( e ) => bail!("No current working directory found: {}", &e),
};
let rwd = match repo.workdir() {
Some( wd ) => wd,
None => bail!("No workdir for repository"),
};
for f in files.iter() {
let p = Path::new( &cwd ).join( &f );
let p = match p.strip_prefix( &rwd ) {
Ok( p ) => p,
Err( e ) => bail!("Error stripping {:?} from {:?}: {}", &rwd, &p, &e),
};
index.add_path( &p )?;
};
index.write()?;
let mut index = match repo.index() {
Err(e) => bail!("No index for repository {}", &e),
Ok(index) => index,
};
let oid = match index.write_tree() {
Ok( oid ) => oid,
Err( e ) => bail!("Error writing tree for repository: {}", &e ),
};
index.write()?;
let tree = match repo.find_tree(oid) {
Ok( tree ) => tree,
Err( e ) => bail!("Error findind tree for OID {}: {}", &oid, &e),
};
let parent = match repo.revparse_ext( "HEAD" ) {
Ok( ( object, _ ) ) => object,
Err( e ) => bail!( "Error finding HEAD {}", &e ),
};
let parent = match parent.as_commit() {
Some( commit ) => commit,
None => bail!( "Parent is not a commit" ),
};
repo.commit(
Some("HEAD"),
&sig,
&sig,
message,
&tree,
&[ parent ],
)?;
Ok(())
},
None => bail!( "No repo open for commit" ),
}
}
pub fn tag( &mut self, tag: &str, msg: &str ) -> anyhow::Result<()> {
match &self.repo {
Some( repo ) => {
let rv = repo.revparse( "HEAD" )?;
let ho = match rv.from() {
Some( ho ) => ho,
None => bail!( "No HEAD found!" ),
};
println!("Tagging {} with {}", &ho.id(), &tag);
let sig = repo.signature()?;
let _tag_oid = repo.tag( tag, &ho, &sig, msg, true )?;
Ok(())
},
None => bail!( "No repo open for tag" ),
}
}
fn credentials_cb( _url: &str, username_from_url: Option<&str>, _allowed_types: git2::CredentialType ) -> Result<Cred, git2::Error> {
Cred::ssh_key(
username_from_url.unwrap(),
None,
std::path::Path::new(&format!("{}/.ssh/id_ed25519", std::env::var("HOME").unwrap())),
None,
)
}
pub fn fetch( &mut self ) -> anyhow::Result<usize> {
match &self.repo {
Some( repo ) => {
let remote_name = "origin";
let mut remote = match repo.find_remote( &remote_name ) {
Ok( remote ) => remote,
Err( e ) => bail!( "Couldn't find remote({}): {}", &remote_name, &e ),
};
let mut cbs = RemoteCallbacks::new();
cbs.credentials(|url, username_from_url, allowed_types| { Repository::credentials_cb( url, username_from_url, allowed_types ) });
cbs.transfer_progress(|progress| {
println!("Transfer progress: {}", progress.received_bytes());
println!("{}/{} objects", progress.received_objects(), progress.total_objects());
true
});
let mut opts = FetchOptions::new();
opts.remote_callbacks( cbs );
remote.fetch(&["main"], Some( &mut opts ), None)?;
let stats = remote.stats();
println!("Fetched {} bytes.", stats.received_bytes());
println!("Fetched {} objects.", stats.received_objects());
Ok(stats.total_objects())
},
None => bail!( "No repo open for fetch" ),
}
}
pub fn rebase( &mut self ) -> anyhow::Result<()> {
match &self.repo {
Some( repo ) => {
let rv = repo.revparse( "origin/HEAD" )?;
let oho = match rv.from() {
Some( oho ) => oho,
None => bail!( "No origin/HEAD found!" ),
};
let upstream = match repo.find_annotated_commit( oho.id() ) {
Ok( commit ) => commit,
Err( e ) => bail!( "No commit for origin/HEAD! {}", &e ),
};
println!("Rebasing on upstream {} {}", upstream.id(), "" ); let mut rebase = repo.rebase( None, Some( &upstream ), None, None )?;
println!("{}", rebase.len());
while let Some( ro ) = rebase.next() {
match ro {
Ok( _ro ) => {
let sig = repo.signature()?;
match rebase.commit(
None,
&sig,
None,
) {
Ok( _r ) => {
},
Err( e ) => {
dbg!( &e );
},
}
},
Err( e ) => {
dbg!( &e );
},
}
}
rebase.finish(None)?;
},
None => bail!( "No repo open for rebase" ),
}
Ok(())
}
pub fn push( &mut self ) -> anyhow::Result<usize> {
match &self.repo {
Some( repo ) => {
let remote_name = "origin";
let mut remote = match repo.find_remote( &remote_name ) {
Ok( remote ) => remote,
Err( e ) => bail!( "Couldn't find remote({}): {}", &remote_name, &e ),
};
let mut cbs = RemoteCallbacks::new();
cbs.credentials(|url, username_from_url, allowed_types| { Repository::credentials_cb( url, username_from_url, allowed_types ) });
cbs.transfer_progress(|progress| {
println!("Transfer progress: {}", progress.received_bytes());
println!("{}/{} objects", progress.received_objects(), progress.total_objects());
true
});
let mut opts = PushOptions::new();
opts.remote_callbacks( cbs );
remote.push(
&["refs/heads/main"],
Some( &mut opts )
)?;
Ok( 0 )
},
None => bail!( "No repo open for push" ),
}
}
pub fn push_tag( &mut self, tag: &str ) -> anyhow::Result<usize> {
match &self.repo {
Some( repo ) => {
let remote_name = "origin";
let mut remote = match repo.find_remote( &remote_name ) {
Ok( remote ) => remote,
Err( e ) => bail!( "Couldn't find remote({}): {}", &remote_name, &e ),
};
let mut cbs = RemoteCallbacks::new();
cbs.credentials(|_url, username_from_url, _allowed_types| {
Cred::ssh_key(
username_from_url.unwrap(),
None,
std::path::Path::new(&format!("{}/.ssh/id_ed25519", std::env::var("HOME").unwrap())),
None,
)
});
cbs.transfer_progress(|progress| {
println!("Transfer progress: {}", progress.received_bytes());
println!("{}/{} objects", progress.received_objects(), progress.total_objects());
true
});
cbs.push_update_reference(|name, status|{
println!("Push Update Reference: {} -> {:?}", name, status);
Ok(())
});
let mut opts = PushOptions::new();
opts.remote_callbacks( cbs );
let tag_ref = format!("refs/tags/{}", &tag);
println!("Pushing ref {}", &tag_ref);
remote.push(
&[ &tag_ref ],
Some( &mut opts )
)?;
Ok( 0 )
},
None => bail!( "No repo open for push" ),
}
}
}