Repositories

grarr

(mirrored on github)

Modified Cargo.lock

@@ -8,6 +8,7 @@ dependencies = [
"error-chain 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
"flate2 0.2.19 (registry+https://github.com/rust-lang/crates.io-index)",
"git-appraise 0.3.0 (git+https://github.com/Nemo157/git-appraise-rs?rev=update)",
"git-ship 0.1.0 (git+https://github.com/Nemo157/git-ship-rs)",
"git2 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
"gravatar 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -330,6 +331,15 @@ dependencies = [
]
[[package]]
name = "git-ship"
version = "0.1.0"
source = "git+https://github.com/Nemo157/git-ship-rs#8145d658d4b0e054b605f53da154ac8cd4929656"
dependencies = [
"git2 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "git2"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1161,6 +1171,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0912515a8ff24ba900422ecda800b52f4016a56251922d397c576bf92c690518"
"checksum getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9047cfbd08a437050b363d35ef160452c5fe8ea5187ae0a624708c91581d685"
"checksum git-appraise 0.3.0 (git+https://github.com/Nemo157/git-appraise-rs?rev=update)" = "<none>"
"checksum git-ship 0.1.0 (git+https://github.com/Nemo157/git-ship-rs)" = "<none>"
"checksum git2 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "9de9df4358c17e448a778d90cd0272e1dab5eae30244502333fa2001c4e24357"
"checksum gravatar 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1db2a0cee40395384856d54ada583ca7db8470f9ae216932c5b0cb8bfbc7089c"
"checksum html5ever 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d60508177ec4e5774a112efcf4d4d5f123cb00a43476fa5940b7da568371a165"

Modified Cargo.toml

@@ -34,6 +34,9 @@ walkdir = "1.0"
git = "https://github.com/Nemo157/git-appraise-rs"
rev = "update"
[dependencies.git-ship]
git = "https://github.com/Nemo157/git-ship-rs"
[dependencies.maud]
features = ["iron"]
version = "0.16"

Deleted src/git_ship/capability.rs

@@ -1,78 +0,0 @@
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum Void {
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum Capability {
SideBand,
SideBand64K,
MultiAck,
MultiAckDetailed,
Unknown(String),
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Capabilities(Vec<Capability>);
impl Capabilities {
pub fn new(caps: Vec<Capability>) -> Capabilities {
Capabilities(caps)
}
pub fn empty() -> Capabilities {
Capabilities(Vec::new())
}
pub fn contains(&self, cap: Capability) -> bool {
self.0.contains(&cap)
}
}
impl FromStr for Capability {
type Err = Void;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"side-band" => Capability::SideBand,
"side-band-64k" => Capability::SideBand64K,
"multi_ack" => Capability::MultiAck,
"multi_ack_detailed" => Capability::MultiAckDetailed,
s => Capability::Unknown(s.to_owned()),
})
}
}
impl fmt::Display for Capability {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match *self {
Capability::SideBand => "side-band",
Capability::SideBand64K => "side-band-64k",
Capability::MultiAck => "multi_ack",
Capability::MultiAckDetailed => "multi_ack_detailed",
Capability::Unknown(ref s) => &**s,
})
}
}
impl FromStr for Capabilities {
type Err = Void;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Capabilities(s
.split(' ')
.filter(|s| !s.is_empty())
.map(|s| s.parse().unwrap())
.collect()))
}
}
impl fmt::Display for Capabilities {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for cap in &self.0 {
fmt::Display::fmt(cap, f)?;
f.write_str(" ")?;
}
Ok(())
}
}

Deleted src/git_ship/error.rs

@@ -1,44 +0,0 @@
use std::{io, fmt, error, result};
use git2;
#[derive(Debug)]
pub enum Error {
Git2(git2::Error),
Io(io::Error),
}
pub type Result<T> = result::Result<T, Error>;
impl Error {
pub fn cause(&self) -> &error::Error {
match *self {
Error::Git2(ref err) => err,
Error::Io(ref err) => err,
}
}
}
impl error::Error for Error {
fn description(&self) -> &str {
self.cause().description()
}
fn cause(&self) -> Option<&error::Error> {
Some(self.cause())
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "git_ship error: {}", self.cause())
}
}
impl From<git2::Error> for Error {
fn from(err: git2::Error) -> Error { Error::Git2(err) }
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error { Error::Io(err) }
}

Deleted src/git_ship/mod.rs

@@ -1,16 +0,0 @@
extern crate url;
extern crate git2;
mod error;
pub mod multiplexer;
pub mod pkt_line;
pub mod capability;
pub mod refs;
pub mod upload_pack;
pub use self::error::{Error, Result};
pub use self::multiplexer::Multiplexer;
pub use self::pkt_line::PktLine;
pub use self::capability::{Capability, Capabilities};
// pub use self::upload_pack::UploadPack;

Deleted src/git_ship/multiplexer.rs

@@ -1,77 +0,0 @@
use std::io;
use std::cmp;
use super::pkt_line;
use super::{Capability, Capabilities};
/// A pkt-line multiplexer for the `side-band` and `side-band-64k` capabilities.
/// Also supports being a transparent passthrough when neither capability is
/// specified to make usage easier.
/// https://github.com/git/git/blob/10c78a162fa821ee85203165b805ff46be454091/Documentation/technical/protocol-capabilities.txt#L119
pub struct Multiplexer<'a> {
writer: &'a mut io::Write,
limit: Option<usize>,
buffer: [u8; pkt_line::MAX_PKT_LINE_DATA_LEN],
}
impl<'a> Multiplexer<'a> {
pub fn new(writer: &'a mut io::Write, caps: &Capabilities) -> io::Result<Multiplexer<'a>> {
let limit = match (caps.contains(Capability::SideBand), caps.contains(Capability::SideBand64K)) {
(true, true) => return Err(io::Error::new(io::ErrorKind::InvalidInput, "client cannot send both side-band and side-band-64k")),
(true, false) => Some(1000),
(false, true) => Some(pkt_line::MAX_PKT_LINE_DATA_LEN),
(false, false) => None,
};
Ok(Multiplexer { writer: writer, limit: limit, buffer: [0; pkt_line::MAX_PKT_LINE_DATA_LEN] })
}
pub fn write_packfile<B: AsRef<[u8]>>(&mut self, bytes: B) -> io::Result<()> {
let bytes = bytes.as_ref();
if let Some(limit) = self.limit {
let mut offset = 0;
while offset < bytes.len() {
let len = cmp::min(bytes.len() - offset, limit - 1);
self.buffer[0] = 1;
self.buffer[1..len + 1].copy_from_slice(&bytes[offset..offset + len]);
pkt_line::write(&mut self.writer, &self.buffer[0..len + 1])?;
offset += len;
}
Ok(())
} else {
self.writer.write_all(bytes)
}
}
pub fn write_progress<S: AsRef<str>>(&mut self, msg: S) -> io::Result<()> {
let msg = msg.as_ref();
if let Some(limit) = self.limit {
if msg.len() > limit {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "msg too long"));
}
self.buffer[0] = 2;
self.buffer[1..msg.len() + 1].copy_from_slice(msg.as_bytes());
pkt_line::write(&mut self.writer, &self.buffer[0..msg.len() + 1])?;
self.writer.flush()?;
}
Ok(())
}
#[allow(unused)]
pub fn write_error<S: AsRef<str>>(&mut self, msg: S) -> io::Result<()> {
let msg = msg.as_ref();
if let Some(limit) = self.limit {
if msg.len() > limit {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "msg too long"));
}
self.buffer[0] = 3;
self.buffer[1..msg.len() + 1].copy_from_slice(msg.as_bytes());
pkt_line::write(&mut self.writer, &self.buffer[0..msg.len() + 1])?;
self.writer.flush()?;
}
Ok(())
}
pub fn into_inner(self) -> &'a mut io::Write {
self.writer
}
}

Deleted src/git_ship/pkt_line.rs

@@ -1,106 +0,0 @@
use std::io;
use std::str;
const MAX_PKT_LINE_LEN: usize = 65520;
const PKT_LINE_SIZE_LEN: usize = 4;
pub const MAX_PKT_LINE_DATA_LEN: usize = MAX_PKT_LINE_LEN - PKT_LINE_SIZE_LEN;
#[derive(Eq, PartialEq, Debug, Clone)]
pub enum PktLine<T> {
Flush,
Line(T),
}
pub fn write<W: io::Write, B: AsRef<[u8]>>(mut writer: W, buf: B) -> io::Result<()> {
let buf = buf.as_ref();
if buf.is_empty() {
return Ok(());
}
if buf.len() > MAX_PKT_LINE_DATA_LEN {
return Err(io::Error::new(io::ErrorKind::InvalidData, "The maximum length of a pkt-line's data component is 65516 bytes."));
}
write!(writer, "{:04x}", buf.len() + PKT_LINE_SIZE_LEN)?;
writer.write_all(buf.as_ref())
}
pub fn write_str<W: io::Write, S: AsRef<str>>(mut writer: W, buf: S) -> io::Result<()> {
let buf = buf.as_ref().as_bytes();
if buf.is_empty() {
return Ok(());
}
let len = buf.len();
if buf.len() > MAX_PKT_LINE_DATA_LEN {
return Err(io::Error::new(io::ErrorKind::InvalidData, "The maximum length of a pkt-line's data component is 65516 bytes."));
}
let inject_line_feed = buf[buf.len() - 1] != b'\n' && buf.len() < MAX_PKT_LINE_DATA_LEN;
write!(writer, "{:04x}", len + if inject_line_feed { 5 } else { 4 })?;
writer.write_all(buf)?;
if inject_line_feed {
writer.write_all(b"\n")?;
}
Ok(())
}
pub fn flush<W: io::Write>(mut writer: W) -> io::Result<()> {
writer.write_all(b"0000")
}
fn read_pkt_line_size<R: io::Read>(mut reader: R) -> io::Result<PktLine<usize>> {
let mut size_buf = [0; PKT_LINE_SIZE_LEN];
reader.read_exact(&mut size_buf)?;
let size_str = str::from_utf8(&size_buf)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let size = usize::from_str_radix(size_str, 16)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
if size == 0 {
return Ok(PktLine::Flush);
}
if size < PKT_LINE_SIZE_LEN {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Size less than 4 and not equal to 0"));
}
if size > MAX_PKT_LINE_LEN {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Size greater than 65520"));
}
Ok(PktLine::Line(size - PKT_LINE_SIZE_LEN))
}
pub fn read<'a, R: io::Read>(mut reader: R, buf: &'a mut [u8; MAX_PKT_LINE_DATA_LEN]) -> io::Result<PktLine<&'a [u8]>> {
let size = read_pkt_line_size(&mut reader)?;
let size = match size {
PktLine::Line(size) => size,
PktLine::Flush => return Ok(PktLine::Flush),
};
reader.read_exact(&mut buf[..size])?;
if size > 0 && buf[size - 1] == b'\n' {
Ok(PktLine::Line(&buf[..size - 1]))
} else {
Ok(PktLine::Line(&buf[..size]))
}
}
pub fn each<R: io::Read, F: FnMut(PktLine<&[u8]>) -> io::Result<()>>(mut reader: R, mut callback: F) -> io::Result<()> {
let mut buffer = [0; MAX_PKT_LINE_DATA_LEN];
loop {
match read(&mut reader, &mut buffer) {
Ok(line) => {
callback(line)?;
}
Err(e) => match e.kind() {
io::ErrorKind::UnexpectedEof => return Ok(()),
_ => return Err(e),
}
}
}
}
pub fn each_str<R: io::Read, F: FnMut(PktLine<&str>) -> io::Result<()>>(reader: R, mut callback: F) -> io::Result<()>{
each(reader, |line| {
callback(match line {
PktLine::Flush => PktLine::Flush,
PktLine::Line(buf) => {
let line = str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
PktLine::Line(line)
}
})
})
}

Deleted src/git_ship/refs.rs

@@ -1,95 +0,0 @@
use std::io;
use std::borrow::Borrow;
use super::url::Url;
use super::git2::{self, Oid};
use super::{pkt_line, Capability, Capabilities, Result};
#[derive(Debug)]
pub struct UploadPack {
head: Oid,
refs: Vec<(String, Oid)>,
capabilities: Capabilities,
}
#[derive(Debug)]
pub enum Response {
UploadPack(UploadPack),
Error(&'static str),
}
pub fn prepare(repo: &git2::Repository, url: &Url) -> Result<Response> {
let service = url.query_pairs()
.find(|&(ref key, _)| key == "service")
.map(|(_, id)| id.clone());
let service = service.as_ref().map(Borrow::borrow);
match service {
Some("git-upload-pack") => {
let head = repo.head()?.target().expect("TODO: Better handling of non-HEAD containing repos");
let refs = repo.references()?
.map(|r| {
let r = r?;
let name = r.name()
.expect("TODO: Better handling of non-unicode refs")
.to_owned();
let target = r.resolve()?
.target()
.expect("Resolved references always have a target");
Ok((name, target))
})
.collect::<::std::result::Result<_, git2::Error>>()?;
// TODO: Sort refs by name in C locale
let capabilities = Capabilities::new(vec![
Capability::SideBand,
Capability::SideBand64K,
Capability::MultiAck,
Capability::MultiAckDetailed,
]);
Ok(Response::UploadPack(UploadPack {
head: head,
refs: refs,
capabilities: capabilities,
}))
}
Some(_) => Ok(Response::Error("Unknown git service name")),
None => Ok(Response::Error("Please upgrade your git client.")),
}
}
impl UploadPack {
pub fn write_to(&self, mut writer: &mut io::Write) -> Result<()> {
pkt_line::write_str(&mut writer, "# service=git-upload-pack")?;
pkt_line::flush(&mut writer)?;
pkt_line::write_str(&mut writer, format!("{} HEAD\0{}", self.head, self.capabilities))?;
for &(ref name, ref target) in &self.refs {
pkt_line::write_str(&mut writer, format!("{} {}", target, name))?;
}
pkt_line::flush(&mut writer)?;
Ok(())
}
}
impl Response {
pub fn status_code(&self) -> u16 {
match *self {
Response::UploadPack(_) => 200,
Response::Error(_) => 403,
}
}
pub fn mime_type(&self) -> &'static str {
match *self {
Response::UploadPack(_) => "application/x-git-upload-pack-advertisement",
Response::Error(_) => "text/plain; charset=utf-8",
}
}
pub fn write_to(&self, mut writer: &mut io::Write) -> Result<()> {
match *self {
Response::UploadPack(ref pack) => pack.write_to(writer)?,
Response::Error(ref msg) => writer.write_all(msg.as_bytes())?,
}
Ok(())
}
}

Deleted src/git_ship/upload_pack.rs

@@ -1,274 +0,0 @@
use std::fmt;
use std::rc::Rc;
use std::sync::Mutex;
use std::io;
use std::borrow::Cow;
use super::git2::{self, Oid, PackBuilderStage};
use super::{pkt_line, PktLine, Capability, Capabilities, Multiplexer, Result};
pub struct Pack {
repo: git2::Repository,
commits: Vec<Oid>,
common: Vec<Oid>,
capabilities: Capabilities,
}
#[derive(Debug)]
pub struct Continue {
common: Vec<Oid>,
capabilities: Capabilities,
}
#[derive(Debug)]
pub struct Request {
wants: Vec<Oid>,
haves: Vec<Oid>,
capabilities: Capabilities,
done: bool,
}
#[derive(Debug)]
pub enum Response {
Pack(Pack),
Continue(Continue),
Error(Cow<'static, str>),
}
fn parse(body: &mut io::Read) -> Result<Request> {
let mut request = Request {
wants: Vec::new(),
haves: Vec::new(),
capabilities: Capabilities::empty(),
done: false,
};
pkt_line::each_str(body, |line| {
let line = match line {
PktLine::Flush => return Ok(()),
PktLine::Line(line) => line,
};
if line.len() < 4 {
return Err(io::Error::new(io::ErrorKind::InvalidData, format!("Unexpected pkt-line {}", line)));
}
match &line[0..4] {
"want" => {
let line = line[5..].trim();
let (want, caps) = line.split_at(line.find(' ').unwrap_or(line.len()));
request.wants.push(want.parse().map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?);
if !caps.is_empty() {
request.capabilities = caps.parse().expect("Can't fail");
}
},
"have" => {
request.haves.push(line[5..].trim().parse().map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?);
},
"done" => {
request.done = true;
},
_ => return Err(io::Error::new(io::ErrorKind::InvalidData, format!("Unexpected pkt-line {}", line))),
}
Ok(())
})?;
Ok(request)
}
fn refs(repo: &git2::Repository) -> Result<Vec<Oid>> {
repo.references()?
.map(|r| {
let r = r?;
Ok(r.resolve()?
.target()
.expect("Resolved references always have a target"))
})
.collect()
}
fn graph_ancestor_of_any(repo: &git2::Repository, commit: Oid, descendants: &[Oid]) -> Result<bool> {
for &descendant in descendants {
if repo.graph_descendant_of(descendant, commit)? {
return Ok(true);
}
}
Ok(false)
}
fn graph_descendant_of_any(repo: &git2::Repository, commit: Oid, ancestors: &[Oid]) -> Result<bool> {
for &ancestor in ancestors {
if repo.graph_descendant_of(commit, ancestor)? {
return Ok(true);
}
}
Ok(false)
}
// a commit set is closed if every commit in `descendants` has at least one ancestor in `ancestors`
fn is_closed(repo: &git2::Repository, descendants: &[Oid], ancestors: &[Oid]) -> Result<bool> {
for &descendant in descendants {
if !graph_descendant_of_any(repo, descendant, ancestors)? {
return Ok(false);
}
}
Ok(true)
}
#[allow(collapsible_if)]
fn compute_response(repo: git2::Repository, refs: Vec<Oid>, request: Request) -> Result<Response> {
let mut common = Vec::<Oid>::new();
// for each id given in have
for id in request.haves {
// if it is an ancestor of a ref
if graph_ancestor_of_any(&repo, id, &refs)? {
// and is not an ancestor of a common
if !graph_ancestor_of_any(&repo, id, &common)? {
// add it to common
common.push(id);
}
}
}
if request.done || is_closed(&repo, &request.wants, &common)? {
let commits = {
let mut revwalk = repo.revwalk()?;
for id in request.wants {
revwalk.push(id)?;
}
for &id in &common {
revwalk.hide(id)?;
}
revwalk.collect::<::std::result::Result<_, git2::Error>>()?
};
Ok(Response::Pack(Pack {
repo: repo,
commits: commits,
common: common,
capabilities: request.capabilities
}))
} else {
Ok(Response::Continue(Continue {
common: common,
capabilities: request.capabilities
}))
}
}
pub fn prepare(repo: git2::Repository, body: &mut io::Read) -> Result<Response> {
let request = parse(body)?;
let refs = refs(&repo)?;
if request.wants.is_empty() {
return Ok(Response::Error("need wants".into()));
}
for id in &request.wants {
if !refs.contains(&id) {
return Ok(Response::Error(format!("want missing from refs {}", id).into()));
}
}
Ok(compute_response(repo, refs, request)?)
}
impl Pack {
pub fn write_to(&mut self, mut writer: &mut io::Write) -> Result<()> {
if !self.common.is_empty() && self.capabilities.contains(Capability::MultiAckDetailed) {
for id in &self.common {
pkt_line::write_str(&mut writer, format!("ACK {} common", id))?;
}
pkt_line::write_str(&mut writer, format!("ACK {}", self.common[self.common.len() - 1]))?;
} else {
pkt_line::write_str(&mut writer, "NAK")?;
}
let mut output = Multiplexer::new(writer, &self.capabilities)?;
{
let output = Rc::new(Mutex::new(&mut output));
let mut builder = self.repo.packbuilder()?;
{
let output = output.clone();
let mut first_delta = true;
builder.set_progress_callback(move |stage, current, total| {
let mut output = output.lock().unwrap();
match stage {
PackBuilderStage::AddingObjects => {
let _ = output.write_progress(format!("Counting objects {}\r", current));
}
PackBuilderStage::Deltafication => {
if first_delta {
let _ = output.write_progress("\n");
first_delta = false;
}
let percent = (current as f64 / total as f64) * 100.0;
if current == total {
let _ = output.write_progress(format!(
"Compressing objects: {:.0}% ({}/{}), done\n",
percent, current, total));
} else {
let _ = output.write_progress(format!(
"Compressing objects: {:.0}% ({}/{})\r",
percent, current, total));
}
}
}
true
})?;
}
for &id in &self.commits {
builder.insert_commit(id)?;
}
builder.foreach(|object| output.lock().unwrap().write_packfile(object).is_ok())?;
}
pkt_line::flush(output.into_inner())?;
Ok(())
}
}
impl Continue {
pub fn write_to(&self, mut writer: &mut io::Write) -> Result<()> {
if self.capabilities.contains(Capability::MultiAckDetailed) {
for id in &self.common {
pkt_line::write_str(&mut writer, format!("ACK {} common", id))?;
}
} else {
// TODO
}
pkt_line::write_str(&mut writer, "NAK")?;
Ok(())
}
}
impl Response {
pub fn status_code(&self) -> u16 {
match *self {
Response::Pack(_) => 200,
Response::Continue(_) => 200,
Response::Error(_) => 403,
}
}
pub fn write_to(&mut self, mut writer: &mut io::Write) -> Result<()> {
match *self {
Response::Pack(ref mut pack) => pack.write_to(writer)?,
Response::Continue(ref c) => c.write_to(writer)?,
Response::Error(ref msg) => writer.write_all(msg.as_bytes())?,
}
Ok(())
}
}
impl fmt::Debug for Pack {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
struct R<'a>(&'a git2::Repository);
impl<'a> fmt::Debug for R<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("git2::Repository")
.field("path", &self.0.path())
.finish()
}
}
f.debug_struct("Pack")
.field("repo", &R(&self.repo))
.field("commits", &self.commits)
.field("common", &self.common)
.field("capabilities", &self.capabilities)
.finish()
}
}

Modified src/main.rs

@@ -15,6 +15,7 @@ extern crate error_chain;
extern crate flate2;
extern crate git2;
extern crate git_appraise;
extern crate git_ship;
extern crate gravatar;
#[macro_use]
extern crate iron;
@@ -50,7 +51,6 @@ mod settings;
mod referenced_commit;
mod config;
mod tree_entry;
mod git_ship;
use std::time::Duration;
use std::env;