Merge pull request #486 from matthiasbeyer/libimagstorestdhook/git
libimagstorestdhook/git
This commit is contained in:
commit
e1177717ee
19 changed files with 1140 additions and 33 deletions
|
@ -38,10 +38,15 @@ script:
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
packages:
|
packages:
|
||||||
|
- cmake
|
||||||
- libcurl4-openssl-dev
|
- libcurl4-openssl-dev
|
||||||
- libdw-dev
|
- libdw-dev
|
||||||
- libelf-dev
|
- libelf-dev
|
||||||
|
- libzzip-dev
|
||||||
- make
|
- make
|
||||||
|
- tree
|
||||||
|
sources:
|
||||||
|
- kalakris-cmake
|
||||||
after_success:
|
after_success:
|
||||||
- |
|
- |
|
||||||
pushd .imag-documentation &&
|
pushd .imag-documentation &&
|
||||||
|
|
|
@ -5,6 +5,12 @@ let
|
||||||
rustc
|
rustc
|
||||||
cargo
|
cargo
|
||||||
];
|
];
|
||||||
|
|
||||||
|
dependencies = with pkgs; [
|
||||||
|
openssl
|
||||||
|
zlib
|
||||||
|
cmake
|
||||||
|
];
|
||||||
in
|
in
|
||||||
|
|
||||||
pkgs.stdenv.mkDerivation rec {
|
pkgs.stdenv.mkDerivation rec {
|
||||||
|
@ -12,7 +18,7 @@ pkgs.stdenv.mkDerivation rec {
|
||||||
src = ./.;
|
src = ./.;
|
||||||
version = "0.0.0";
|
version = "0.0.0";
|
||||||
|
|
||||||
buildInputs = [ env ];
|
buildInputs = [ env dependencies ];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
104
imagrc.toml
104
imagrc.toml
|
@ -12,25 +12,113 @@ implicit-create = false
|
||||||
# store, so these hooks should be chosen carefully.
|
# store, so these hooks should be chosen carefully.
|
||||||
store-unload-hook-aspects = [ "debug" ]
|
store-unload-hook-aspects = [ "debug" ]
|
||||||
|
|
||||||
pre-create-hook-aspects = [ "debug" ]
|
pre-create-hook-aspects = [ "debug", "vcs" ]
|
||||||
post-create-hook-aspects = [ "debug" ]
|
post-create-hook-aspects = [ "debug", "vcs" ]
|
||||||
|
|
||||||
pre-move-hook-aspects = [ "debug" ]
|
pre-move-hook-aspects = [ "debug" ]
|
||||||
post-move-hook-aspects = [ "debug" ]
|
post-move-hook-aspects = [ "debug" ]
|
||||||
|
|
||||||
pre-retrieve-hook-aspects = [ "debug" ]
|
pre-retrieve-hook-aspects = [ "debug", "vcs" ]
|
||||||
post-retrieve-hook-aspects = [ "debug" ]
|
post-retrieve-hook-aspects = [ "debug", "vcs" ]
|
||||||
|
|
||||||
pre-update-hook-aspects = [ "debug" ]
|
pre-update-hook-aspects = [ "debug", "vcs" ]
|
||||||
post-update-hook-aspects = [ "debug" ]
|
post-update-hook-aspects = [ "debug", "vcs" ]
|
||||||
|
|
||||||
pre-delete-hook-aspects = [ "debug" ]
|
pre-delete-hook-aspects = [ "debug", "vcs" ]
|
||||||
post-delete-hook-aspects = [ "debug" ]
|
post-delete-hook-aspects = [ "debug", "vcs" ]
|
||||||
|
|
||||||
[store.aspects.debug]
|
[store.aspects.debug]
|
||||||
parallel = false
|
parallel = false
|
||||||
mutable_hooks = true
|
mutable_hooks = true
|
||||||
|
|
||||||
|
[store.aspects.vcs]
|
||||||
|
parallel = false
|
||||||
|
mutable_hooks = false
|
||||||
|
|
||||||
[store.hooks.stdhook_debug]
|
[store.hooks.stdhook_debug]
|
||||||
aspect = "debug"
|
aspect = "debug"
|
||||||
|
|
||||||
|
[store.hooks.stdhook_git_create]
|
||||||
|
aspect = "vcs"
|
||||||
|
|
||||||
|
# Fail if the repository cannot be opened. If this is set to `false`, the error
|
||||||
|
# will be printed, but will not abort the store operation. `true` will print the
|
||||||
|
# error and abort the store action.
|
||||||
|
abort_on_repo_init_failure = true
|
||||||
|
|
||||||
|
# Ensure to be on this branche before doing anything.
|
||||||
|
ensure_branch = "refs/heads/master"
|
||||||
|
|
||||||
|
# Try to checkout the ensure_branch if it isn't checked out
|
||||||
|
try_checkout_ensure_branch = true
|
||||||
|
|
||||||
|
[store.hooks.stdhook_git_retrieve]
|
||||||
|
aspect = "vcs"
|
||||||
|
|
||||||
|
# Fail if the repository cannot be opened. If this is set to `false`, the error
|
||||||
|
# will be printed, but will not abort the store operation. `true` will print the
|
||||||
|
# error and abort the store action.
|
||||||
|
abort_on_repo_init_failure = true
|
||||||
|
|
||||||
|
# Ensure to be on this branche before doing anything.
|
||||||
|
ensure_branch = "refs/heads/master"
|
||||||
|
|
||||||
|
# Try to checkout the ensure_branch if it isn't checked out
|
||||||
|
try_checkout_ensure_branch = true
|
||||||
|
|
||||||
|
[store.hooks.stdhook_git_update]
|
||||||
|
aspect = "vcs"
|
||||||
|
|
||||||
|
# Fail if the repository cannot be opened. If this is set to `false`, the error
|
||||||
|
# will be printed, but will not abort the store operation. `true` will print the
|
||||||
|
# error and abort the store action.
|
||||||
|
abort_on_repo_init_failure = true
|
||||||
|
|
||||||
|
# Ensure to be on this branche before doing anything.
|
||||||
|
ensure_branch = "refs/heads/master"
|
||||||
|
|
||||||
|
# Try to checkout the ensure_branch if it isn't checked out
|
||||||
|
try_checkout_ensure_branch = true
|
||||||
|
|
||||||
|
# Commit configuration
|
||||||
|
[store.hooks.stdhook_git_update.commit]
|
||||||
|
|
||||||
|
# Whether to do the commit interactively
|
||||||
|
interactive = false
|
||||||
|
|
||||||
|
# Set to true to use the $EDITOR for the commit, to false to do on commandline
|
||||||
|
# When committing without editor, only a single line is allowed as commit
|
||||||
|
# message
|
||||||
|
interactive_editor = false
|
||||||
|
|
||||||
|
# Commit message if the commit is not interactive
|
||||||
|
message = "Update"
|
||||||
|
|
||||||
|
[store.hooks.stdhook_git_delete]
|
||||||
|
aspect = "vcs"
|
||||||
|
|
||||||
|
# Fail if the repository cannot be opened. If this is set to `false`, the error
|
||||||
|
# will be printed, but will not abort the store operation. `true` will print the
|
||||||
|
# error and abort the store action.
|
||||||
|
abort_on_repo_init_failure = true
|
||||||
|
|
||||||
|
# Ensure to be on this branche before doing anything.
|
||||||
|
ensure_branch = "refs/heads/master"
|
||||||
|
|
||||||
|
# Try to checkout the ensure_branch if it isn't checked out
|
||||||
|
try_checkout_ensure_branch = true
|
||||||
|
|
||||||
|
# Commit configuration
|
||||||
|
[store.hooks.stdhook_git_delete.commit]
|
||||||
|
|
||||||
|
# Whether to do the commit interactively
|
||||||
|
interactive = false
|
||||||
|
|
||||||
|
# Set to true to use the $EDITOR for the commit, to false to do on commandline
|
||||||
|
# When committing without editor, only a single line is allowed as commit
|
||||||
|
# message
|
||||||
|
interactive_editor = false
|
||||||
|
|
||||||
|
# Commit message if the commit is not interactive
|
||||||
|
message = "Deleted"
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,8 @@ impl<'a> Runtime<'a> {
|
||||||
use libimagstore::hook::Hook;
|
use libimagstore::hook::Hook;
|
||||||
use libimagstore::error::StoreErrorKind;
|
use libimagstore::error::StoreErrorKind;
|
||||||
use libimagstorestdhook::debug::DebugHook;
|
use libimagstorestdhook::debug::DebugHook;
|
||||||
|
use libimagstorestdhook::vcs::git::delete::DeleteHook as GitDeleteHook;
|
||||||
|
use libimagstorestdhook::vcs::git::update::UpdateHook as GitUpdateHook;
|
||||||
use libimagerror::trace::trace_error;
|
use libimagerror::trace::trace_error;
|
||||||
use libimagerror::trace::trace_error_dbg;
|
use libimagerror::trace::trace_error_dbg;
|
||||||
use libimagerror::into::IntoError;
|
use libimagerror::into::IntoError;
|
||||||
|
@ -107,7 +109,7 @@ impl<'a> Runtime<'a> {
|
||||||
write!(stderr(), "Store-config: {:?}\n", store_config).ok();
|
write!(stderr(), "Store-config: {:?}\n", store_config).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
Store::new(storepath, store_config).map(|mut store| {
|
Store::new(storepath.clone(), store_config).map(|mut store| {
|
||||||
// If we are debugging, generate hooks for all positions
|
// If we are debugging, generate hooks for all positions
|
||||||
if is_debugging {
|
if is_debugging {
|
||||||
let hooks : Vec<(Box<Hook>, &str, HP)> = vec![
|
let hooks : Vec<(Box<Hook>, &str, HP)> = vec![
|
||||||
|
@ -134,6 +136,24 @@ impl<'a> Runtime<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sp = storepath;
|
||||||
|
|
||||||
|
let hooks : Vec<(Box<Hook>, &str, HP)> = vec![
|
||||||
|
(Box::new(GitDeleteHook::new(sp.clone(), HP::PostDelete)) , "vcs", HP::PostDelete),
|
||||||
|
(Box::new(GitUpdateHook::new(sp, HP::PostUpdate)) , "vcs", HP::PostUpdate),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (hook, aspectname, position) in hooks {
|
||||||
|
if let Err(e) = store.register_hook(position, &String::from(aspectname), hook) {
|
||||||
|
if e.err_type() == StoreErrorKind::HookRegisterError {
|
||||||
|
trace_error_dbg(&e);
|
||||||
|
warn!("Registering git hook with store failed");
|
||||||
|
} else {
|
||||||
|
trace_error(&e);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Runtime {
|
Runtime {
|
||||||
cli_matches: matches,
|
cli_matches: matches,
|
||||||
configuration: cfg,
|
configuration: cfg,
|
||||||
|
|
|
@ -68,10 +68,6 @@ impl MutableHookDataAccessor for Aspect {
|
||||||
fn access_mut(&self, fle: &mut FileLockEntry) -> HookResult<()> {
|
fn access_mut(&self, fle: &mut FileLockEntry) -> HookResult<()> {
|
||||||
debug!("Checking whether mutable hooks are allowed");
|
debug!("Checking whether mutable hooks are allowed");
|
||||||
debug!("-> config = {:?}", self.cfg);
|
debug!("-> config = {:?}", self.cfg);
|
||||||
if !self.cfg.as_ref().map(|c| c.allow_mutable_hooks()).unwrap_or(false) {
|
|
||||||
debug!("Apparently mutable hooks are not allowed... failing now.");
|
|
||||||
return Err(HE::new(HEK::MutableHooksNotAllowed, None));
|
|
||||||
}
|
|
||||||
|
|
||||||
let accessors : Vec<HDA> = self.hooks.iter().map(|h| h.accessor()).collect();
|
let accessors : Vec<HDA> = self.hooks.iter().map(|h| h.accessor()).collect();
|
||||||
|
|
||||||
|
@ -82,8 +78,15 @@ impl MutableHookDataAccessor for Aspect {
|
||||||
accessors.iter().fold_defresult(|accessor| {
|
accessors.iter().fold_defresult(|accessor| {
|
||||||
let res = match accessor {
|
let res = match accessor {
|
||||||
&HDA::StoreIdAccess(ref accessor) => accessor.access(fle.get_location()),
|
&HDA::StoreIdAccess(ref accessor) => accessor.access(fle.get_location()),
|
||||||
&HDA::MutableAccess(ref accessor) => accessor.access_mut(fle),
|
|
||||||
&HDA::NonMutableAccess(ref accessor) => accessor.access(fle),
|
&HDA::NonMutableAccess(ref accessor) => accessor.access(fle),
|
||||||
|
&HDA::MutableAccess(ref accessor) => {
|
||||||
|
if !self.cfg.as_ref().map(|c| c.allow_mutable_hooks()).unwrap_or(false) {
|
||||||
|
debug!("Apparently mutable hooks are not allowed... failing now.");
|
||||||
|
return Err(HE::new(HEK::MutableHooksNotAllowed, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
accessor.access_mut(fle)
|
||||||
|
},
|
||||||
};
|
};
|
||||||
trace_hook_errors(res)
|
trace_hook_errors(res)
|
||||||
})
|
})
|
||||||
|
|
|
@ -493,21 +493,7 @@ impl Store {
|
||||||
|
|
||||||
/// Return the `FileLockEntry` and write to disk
|
/// Return the `FileLockEntry` and write to disk
|
||||||
pub fn update<'a>(&'a self, mut entry: FileLockEntry<'a>) -> Result<()> {
|
pub fn update<'a>(&'a self, mut entry: FileLockEntry<'a>) -> Result<()> {
|
||||||
if let Err(e) = self.execute_hooks_for_mut_file(self.pre_update_aspects.clone(), &mut entry) {
|
self._update(&mut entry).map_err_into(SEK::UpdateCallError)
|
||||||
return Err(e)
|
|
||||||
.map_err_into(SEK::PreHookExecuteError)
|
|
||||||
.map_err_into(SEK::HookExecutionError)
|
|
||||||
.map_err_into(SEK::UpdateCallError);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = self._update(&entry) {
|
|
||||||
return Err(e).map_err_into(SEK::UpdateCallError);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.execute_hooks_for_mut_file(self.post_update_aspects.clone(), &mut entry)
|
|
||||||
.map_err_into(SEK::PostHookExecuteError)
|
|
||||||
.map_err_into(SEK::HookExecutionError)
|
|
||||||
.map_err_into(SEK::UpdateCallError)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Internal method to write to the filesystem store.
|
/// Internal method to write to the filesystem store.
|
||||||
|
@ -515,7 +501,13 @@ impl Store {
|
||||||
/// # Assumptions
|
/// # Assumptions
|
||||||
/// This method assumes that entry is dropped _right after_ the call, hence
|
/// This method assumes that entry is dropped _right after_ the call, hence
|
||||||
/// it is not public.
|
/// it is not public.
|
||||||
fn _update<'a>(&'a self, entry: &FileLockEntry<'a>) -> Result<()> {
|
fn _update<'a>(&'a self, entry: &mut FileLockEntry<'a>) -> Result<()> {
|
||||||
|
let _ = try!(self.execute_hooks_for_mut_file(self.pre_update_aspects.clone(), entry)
|
||||||
|
.map_err_into(SEK::PreHookExecuteError)
|
||||||
|
.map_err_into(SEK::HookExecutionError)
|
||||||
|
.map_err_into(SEK::UpdateCallError)
|
||||||
|
);
|
||||||
|
|
||||||
let mut hsmap = match self.entries.write() {
|
let mut hsmap = match self.entries.write() {
|
||||||
Err(_) => return Err(SE::new(SEK::LockPoisoned, None)),
|
Err(_) => return Err(SE::new(SEK::LockPoisoned, None)),
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
|
@ -532,7 +524,11 @@ impl Store {
|
||||||
try!(se.write_entry(&entry.entry));
|
try!(se.write_entry(&entry.entry));
|
||||||
se.status = StoreEntryStatus::Present;
|
se.status = StoreEntryStatus::Present;
|
||||||
|
|
||||||
Ok(())
|
|
||||||
|
self.execute_hooks_for_mut_file(self.post_update_aspects.clone(), entry)
|
||||||
|
.map_err_into(SEK::PostHookExecuteError)
|
||||||
|
.map_err_into(SEK::HookExecutionError)
|
||||||
|
.map_err_into(SEK::UpdateCallError)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve a copy of a given entry, this cannot be used to mutate
|
/// Retrieve a copy of a given entry, this cannot be used to mutate
|
||||||
|
@ -865,7 +861,7 @@ impl<'a> DerefMut for FileLockEntry<'a> {
|
||||||
impl<'a> Drop for FileLockEntry<'a> {
|
impl<'a> Drop for FileLockEntry<'a> {
|
||||||
/// This will silently ignore errors, use `Store::update` if you want to catch the errors
|
/// This will silently ignore errors, use `Store::update` if you want to catch the errors
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let _ = self.store._update(self);
|
let _ = self.store._update(self).map_err(|e| trace_error(&e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ authors = ["Matthias Beyer <mail@beyermatthias.de>"]
|
||||||
toml = "0.2.*"
|
toml = "0.2.*"
|
||||||
log = "0.3"
|
log = "0.3"
|
||||||
fs2 = "0.2"
|
fs2 = "0.2"
|
||||||
|
git2 = "0.4"
|
||||||
|
|
||||||
[dependencies.libimagstore]
|
[dependencies.libimagstore]
|
||||||
path = "../libimagstore"
|
path = "../libimagstore"
|
||||||
|
@ -14,6 +15,12 @@ path = "../libimagstore"
|
||||||
[dependencies.libimagentrylink]
|
[dependencies.libimagentrylink]
|
||||||
path = "../libimagentrylink"
|
path = "../libimagentrylink"
|
||||||
|
|
||||||
|
[dependencies.libimaginteraction]
|
||||||
|
path = "../libimaginteraction"
|
||||||
|
|
||||||
[dependencies.libimagerror]
|
[dependencies.libimagerror]
|
||||||
path = "../libimagerror"
|
path = "../libimagerror"
|
||||||
|
|
||||||
|
[dependencies.libimagutil]
|
||||||
|
path = "../libimagutil"
|
||||||
|
|
||||||
|
|
|
@ -17,12 +17,16 @@
|
||||||
#[macro_use] extern crate log;
|
#[macro_use] extern crate log;
|
||||||
extern crate toml;
|
extern crate toml;
|
||||||
extern crate fs2;
|
extern crate fs2;
|
||||||
|
extern crate git2;
|
||||||
|
|
||||||
extern crate libimagstore;
|
extern crate libimagstore;
|
||||||
extern crate libimagentrylink;
|
extern crate libimagentrylink;
|
||||||
|
extern crate libimaginteraction;
|
||||||
#[macro_use] extern crate libimagerror;
|
#[macro_use] extern crate libimagerror;
|
||||||
|
extern crate libimagutil;
|
||||||
|
|
||||||
pub mod debug;
|
pub mod debug;
|
||||||
pub mod flock;
|
pub mod flock;
|
||||||
pub mod linkverify;
|
pub mod linkverify;
|
||||||
|
pub mod vcs;
|
||||||
|
|
||||||
|
|
46
libimagstorestdhook/src/vcs/git/action.rs
Normal file
46
libimagstorestdhook/src/vcs/git/action.rs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
use std::fmt::{Display, Formatter, Error};
|
||||||
|
|
||||||
|
/// Utility type to specify which kind of store action is running
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum StoreAction {
|
||||||
|
Create,
|
||||||
|
Retrieve,
|
||||||
|
Update,
|
||||||
|
Delete,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StoreAction {
|
||||||
|
|
||||||
|
pub fn uppercase(&self) -> &str {
|
||||||
|
match *self {
|
||||||
|
StoreAction::Create => "CREATE",
|
||||||
|
StoreAction::Retrieve => "RETRIEVE",
|
||||||
|
StoreAction::Update => "UPDATE",
|
||||||
|
StoreAction::Delete => "DELETE",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_commit_message(&self) -> &str {
|
||||||
|
match *self {
|
||||||
|
StoreAction::Create => "Create",
|
||||||
|
StoreAction::Retrieve => "Retrieve",
|
||||||
|
StoreAction::Update => "Update",
|
||||||
|
StoreAction::Delete => "Delete",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for StoreAction {
|
||||||
|
|
||||||
|
fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error> {
|
||||||
|
write!(fmt, "StoreAction: {}",
|
||||||
|
match *self {
|
||||||
|
StoreAction::Create => "create",
|
||||||
|
StoreAction::Retrieve => "retrieve",
|
||||||
|
StoreAction::Update => "update",
|
||||||
|
StoreAction::Delete => "delete",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
169
libimagstorestdhook/src/vcs/git/config.rs
Normal file
169
libimagstorestdhook/src/vcs/git/config.rs
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
use toml::Value;
|
||||||
|
|
||||||
|
use libimagerror::into::IntoError;
|
||||||
|
use libimagstore::storeid::StoreId;
|
||||||
|
|
||||||
|
use vcs::git::error::GitHookErrorKind as GHEK;
|
||||||
|
use vcs::git::error::MapErrInto;
|
||||||
|
use vcs::git::result::Result;
|
||||||
|
|
||||||
|
use vcs::git::action::StoreAction;
|
||||||
|
|
||||||
|
use git2::Repository;
|
||||||
|
|
||||||
|
/// Check the configuration whether we should commit interactively
|
||||||
|
pub fn commit_interactive(config: &Value, action: &StoreAction) -> bool {
|
||||||
|
match config.lookup("commit.interactive") {
|
||||||
|
Some(&Value::Boolean(b)) => b,
|
||||||
|
Some(_) => {
|
||||||
|
warn!("Configuration error, 'store.hooks.stdhook_git_{}.commit.interactive' must be a Boolean.",
|
||||||
|
action);
|
||||||
|
warn!("Defaulting to commit.interactive = false");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
warn!("Unavailable configuration for");
|
||||||
|
warn!("\t'store.hooks.stdhook_git_{}.commit.interactive'", action);
|
||||||
|
warn!("Defaulting to false");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check the configuration whether we should commit with the editor
|
||||||
|
fn commit_with_editor(config: &Value, action: &StoreAction) -> bool {
|
||||||
|
match config.lookup("commit.interactive_editor") {
|
||||||
|
Some(&Value::Boolean(b)) => b,
|
||||||
|
Some(_) => {
|
||||||
|
warn!("Configuration error, 'store.hooks.stdhook_git_{}.commit.interactive_editor' must be a Boolean.",
|
||||||
|
action);
|
||||||
|
warn!("Defaulting to commit.interactive_editor = false");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
warn!("Unavailable configuration for");
|
||||||
|
warn!("\t'store.hooks.stdhook_git_{}.commit.interactive_editor'", action);
|
||||||
|
warn!("Defaulting to false");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the commit default message
|
||||||
|
fn commit_default_msg<'a>(config: &'a Value, action: &'a StoreAction) -> &'a str {
|
||||||
|
match config.lookup("commit.message") {
|
||||||
|
Some(&Value::String(ref b)) => b,
|
||||||
|
Some(_) => {
|
||||||
|
warn!("Configuration error, 'store.hooks.stdhook_git_{}.commit.message' must be a String.",
|
||||||
|
action);
|
||||||
|
warn!("Defaulting to commit.message = '{}'", action.as_commit_message());
|
||||||
|
action.as_commit_message()
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
warn!("Unavailable configuration for");
|
||||||
|
warn!("\t'store.hooks.stdhook_git_{}.commit.message'", action);
|
||||||
|
warn!("Defaulting to commit.message = '{}'", action.as_commit_message());
|
||||||
|
action.as_commit_message()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the commit template
|
||||||
|
///
|
||||||
|
/// TODO: Implement good template string
|
||||||
|
fn commit_template(action: &StoreAction, id: &StoreId) -> String {
|
||||||
|
format!(r#"
|
||||||
|
# Please commit your changes and remove these lines.
|
||||||
|
#
|
||||||
|
# You're about to commit changes via the {action} Hook
|
||||||
|
#
|
||||||
|
# Altered file: {id}
|
||||||
|
#
|
||||||
|
"#,
|
||||||
|
action = action,
|
||||||
|
id = id.local().display())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a commit message
|
||||||
|
///
|
||||||
|
/// Uses the functions `commit_interactive()` and `commit_with_editor()`
|
||||||
|
/// or reads one from the commandline or uses the `commit_default_msg()` string to create a commit
|
||||||
|
/// message.
|
||||||
|
pub fn commit_message(repo: &Repository, config: &Value, action: StoreAction, id: &StoreId) -> Result<String> {
|
||||||
|
use libimaginteraction::ask::ask_string;
|
||||||
|
use libimagutil::edit::edit_in_tmpfile_with_command;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
if commit_interactive(config, &action) {
|
||||||
|
if commit_with_editor(config, &action) {
|
||||||
|
repo.config()
|
||||||
|
.map_err_into(GHEK::GitConfigFetchError)
|
||||||
|
.and_then(|c| c.get_string("core.editor").map_err_into(GHEK::GitConfigEditorFetchError))
|
||||||
|
.map_err_into(GHEK::ConfigError)
|
||||||
|
.map(Command::new)
|
||||||
|
.and_then(|cmd| {
|
||||||
|
let mut s = commit_template(&action, id);
|
||||||
|
edit_in_tmpfile_with_command(cmd, &mut s).map(|_| s)
|
||||||
|
.map_err_into(GHEK::EditorError)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(ask_string("Commit Message", None, false, false, None, "> "))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(String::from(commit_default_msg(config, &action)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether the hook should abort if the repository cannot be initialized
|
||||||
|
pub fn abort_on_repo_init_err(cfg: &Value) -> bool {
|
||||||
|
get_bool_cfg(Some(cfg), "abort_on_repo_init_failure", true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the branch which must be checked out before running the hook (if any).
|
||||||
|
///
|
||||||
|
/// If there is no configuration for this, this is `Ok(None)`, otherwise we try to find the
|
||||||
|
/// configuration `String`.
|
||||||
|
pub fn ensure_branch(cfg: Option<&Value>) -> Result<Option<String>> {
|
||||||
|
match cfg {
|
||||||
|
Some(cfg) => {
|
||||||
|
match cfg.lookup("ensure_branch") {
|
||||||
|
Some(&Value::String(ref s)) => Ok(Some(s.clone())),
|
||||||
|
Some(_) => {
|
||||||
|
warn!("Configuration error, 'ensure_branch' must be a String.");
|
||||||
|
Err(GHEK::ConfigTypeError.into_error())
|
||||||
|
.map_err_into(GHEK::ConfigTypeError)
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
debug!("No key `ensure_branch'");
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether we should check out a branch before committing.
|
||||||
|
pub fn do_checkout_ensure_branch(cfg: Option<&Value>) -> bool {
|
||||||
|
get_bool_cfg(cfg, "try_checkout_ensure_branch", true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to get a boolean value from the configuration.
|
||||||
|
fn get_bool_cfg(cfg: Option<&Value>, name: &str, on_fail: bool, on_unavail: bool) -> bool {
|
||||||
|
cfg.map(|cfg| {
|
||||||
|
match cfg.lookup(name) {
|
||||||
|
Some(&Value::Boolean(b)) => b,
|
||||||
|
Some(_) => {
|
||||||
|
warn!("Configuration error, '{}' must be a Boolean (true|false).", name);
|
||||||
|
warn!("Assuming '{}' now.", on_fail);
|
||||||
|
on_fail
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
debug!("No key '{}' - Assuming '{}'", name, on_unavail);
|
||||||
|
on_unavail
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(on_unavail)
|
||||||
|
}
|
||||||
|
|
212
libimagstorestdhook/src/vcs/git/delete.rs
Normal file
212
libimagstorestdhook/src/vcs/git/delete.rs
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::fmt::{Debug, Formatter, Error as FmtError};
|
||||||
|
use std::result::Result as RResult;
|
||||||
|
|
||||||
|
use toml::Value;
|
||||||
|
|
||||||
|
use libimagerror::trace::trace_error;
|
||||||
|
use libimagstore::storeid::StoreId;
|
||||||
|
use libimagstore::hook::Hook;
|
||||||
|
use libimagstore::hook::result::HookResult;
|
||||||
|
use libimagstore::hook::position::HookPosition;
|
||||||
|
use libimagstore::hook::accessor::{HookDataAccessor, HookDataAccessorProvider};
|
||||||
|
use libimagstore::hook::accessor::StoreIdAccessor;
|
||||||
|
use libimagutil::debug_result::*;
|
||||||
|
|
||||||
|
use vcs::git::error::GitHookErrorKind as GHEK;
|
||||||
|
use vcs::git::error::MapErrInto;
|
||||||
|
use vcs::git::runtime::Runtime as GRuntime;
|
||||||
|
|
||||||
|
pub struct DeleteHook {
|
||||||
|
storepath: PathBuf,
|
||||||
|
|
||||||
|
runtime: GRuntime,
|
||||||
|
|
||||||
|
position: HookPosition,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeleteHook {
|
||||||
|
|
||||||
|
pub fn new(storepath: PathBuf, p: HookPosition) -> DeleteHook {
|
||||||
|
DeleteHook {
|
||||||
|
runtime: GRuntime::new(&storepath),
|
||||||
|
storepath: storepath,
|
||||||
|
position: p,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for DeleteHook {
|
||||||
|
fn fmt(&self, fmt: &mut Formatter) -> RResult<(), FmtError> {
|
||||||
|
write!(fmt, "DeleteHook(storepath={:?}, repository={}, pos={:?}, cfg={:?})",
|
||||||
|
self.storepath,
|
||||||
|
(if self.runtime.has_repository() { "Some(_)" } else { "None" }),
|
||||||
|
self.position,
|
||||||
|
self.runtime.has_config())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Hook for DeleteHook {
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"stdhook_git_delete"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the configuration of the hook. See
|
||||||
|
/// `libimagstorestdhook::vcs::git::runtime::Runtime::set_config()`.
|
||||||
|
///
|
||||||
|
/// This function traces the error (using `trace_error()`) that
|
||||||
|
/// `libimagstorestdhook::vcs::git::runtime::Runtime::set_config()`
|
||||||
|
/// returns, if any.
|
||||||
|
fn set_config(&mut self, config: &Value) {
|
||||||
|
if let Err(e) = self.runtime.set_config(config) {
|
||||||
|
trace_error(&e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HookDataAccessorProvider for DeleteHook {
|
||||||
|
|
||||||
|
fn accessor(&self) -> HookDataAccessor {
|
||||||
|
HookDataAccessor::StoreIdAccess(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StoreIdAccessor for DeleteHook {
|
||||||
|
|
||||||
|
fn access(&self, id: &StoreId) -> HookResult<()> {
|
||||||
|
use libimagerror::into::IntoError;
|
||||||
|
use vcs::git::action::StoreAction;
|
||||||
|
use vcs::git::config::commit_message;
|
||||||
|
use vcs::git::error::MapIntoHookError;
|
||||||
|
use vcs::git::util::fetch_index;
|
||||||
|
use vcs::git::config::abort_on_repo_init_err;
|
||||||
|
use git2::{ADD_DEFAULT, STATUS_WT_DELETED, IndexMatchedPath};
|
||||||
|
|
||||||
|
debug!("[GIT DELETE HOOK]: {:?}", id);
|
||||||
|
|
||||||
|
let action = StoreAction::Delete;
|
||||||
|
|
||||||
|
if !self.runtime.has_repository() {
|
||||||
|
debug!("[GIT DELETE HOOK]: Runtime has no repository...");
|
||||||
|
if try!(self.runtime.config_value_or_err(&action).map(|c| abort_on_repo_init_err(c))) {
|
||||||
|
// Abort on repo init failure
|
||||||
|
debug!("[GIT DELETE HOOK]: Config says we should abort if we have no repository");
|
||||||
|
debug!("[GIT DELETE HOOK]: Returing Err(_)");
|
||||||
|
return Err(GHEK::RepositoryInitError.into_error())
|
||||||
|
.map_err_into(GHEK::RepositoryError)
|
||||||
|
.map_into_hook_error()
|
||||||
|
} else {
|
||||||
|
debug!("[GIT DELETE HOOK]: Config says it is okay to not have a repository");
|
||||||
|
debug!("[GIT DELETE HOOK]: Returing Ok(())");
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = try!(self.runtime.ensure_cfg_branch_is_checked_out(&action));
|
||||||
|
let cfg = try!(self.runtime.config_value_or_err(&action));
|
||||||
|
let repo = try!(self.runtime.repository(&action));
|
||||||
|
let mut index = try!(fetch_index(repo, &action));
|
||||||
|
|
||||||
|
let signature = try!(
|
||||||
|
repo.signature()
|
||||||
|
.map_err_into(GHEK::MkSignature)
|
||||||
|
.map_dbg_err_str("Failed to fetch signature")
|
||||||
|
.map_dbg_str("[GIT DELETE HOOK]: Fetched signature object")
|
||||||
|
.map_into_hook_error()
|
||||||
|
);
|
||||||
|
|
||||||
|
let head = try!(
|
||||||
|
repo.head()
|
||||||
|
.map_err_into(GHEK::HeadFetchError)
|
||||||
|
.map_dbg_err_str("Failed to fetch HEAD")
|
||||||
|
.map_dbg_str("[GIT DELETE HOOK]: Fetched HEAD")
|
||||||
|
.map_into_hook_error()
|
||||||
|
);
|
||||||
|
|
||||||
|
let file_status = try!(
|
||||||
|
repo
|
||||||
|
.status_file(id.local())
|
||||||
|
.map_dbg_err_str("Failed to fetch file status")
|
||||||
|
.map_dbg_err(|e| format!("\t-> {:?}", e))
|
||||||
|
.map_dbg_str("[GIT DELETE HOOK]: Fetched file status")
|
||||||
|
.map_err_into(GHEK::RepositoryFileStatusError)
|
||||||
|
.map_into_hook_error()
|
||||||
|
);
|
||||||
|
|
||||||
|
let cb = &mut |path: &Path, _matched_spec: &[u8]| -> i32 {
|
||||||
|
debug!("[GIT DELETE HOOK]: Checking file status for: {}", path.display());
|
||||||
|
if file_status.contains(STATUS_WT_DELETED) {
|
||||||
|
debug!("[GIT DELETE HOOK]: File is deleted: {}", path.display());
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
debug!("[GIT DELETE HOOK]: Ignoring file: {}", path.display());
|
||||||
|
1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try!(
|
||||||
|
index.add_all(&[id.local()], ADD_DEFAULT, Some(cb as &mut IndexMatchedPath))
|
||||||
|
.map_err_into(GHEK::RepositoryPathAddingError)
|
||||||
|
.map_dbg_err_str("Failed to add to index")
|
||||||
|
.map_dbg_str("[GIT DELETE HOOK]: Fetched index")
|
||||||
|
.map_into_hook_error()
|
||||||
|
);
|
||||||
|
|
||||||
|
let tree_id = try!(
|
||||||
|
index.write_tree()
|
||||||
|
.map_err_into(GHEK::RepositoryIndexWritingError)
|
||||||
|
.map_dbg_err_str("Failed to write tree")
|
||||||
|
.map_dbg_str("[GIT DELETE HOOK]: Wrote index tree")
|
||||||
|
.map_into_hook_error()
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut parents = Vec::new();
|
||||||
|
{
|
||||||
|
let commit = try!(
|
||||||
|
repo.find_commit(head.target().unwrap())
|
||||||
|
.map_err_into(GHEK::RepositoryParentFetchingError)
|
||||||
|
.map_dbg_err_str("Failed to find commit HEAD")
|
||||||
|
.map_dbg_str("[GIT DELETE HOOK]: Found commit HEAD")
|
||||||
|
.map_into_hook_error()
|
||||||
|
);
|
||||||
|
parents.push(commit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// for converting from Vec<Commit> to Vec<&Commit>
|
||||||
|
let parents = parents.iter().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let tree = try!(
|
||||||
|
repo.find_tree(tree_id)
|
||||||
|
.map_err_into(GHEK::RepositoryParentFetchingError)
|
||||||
|
.map_dbg_err_str("Failed to find tree")
|
||||||
|
.map_dbg_str("[GIT DELETE HOOK]: Found tree for index")
|
||||||
|
.map_into_hook_error()
|
||||||
|
);
|
||||||
|
|
||||||
|
let message = try!(commit_message(&repo, cfg, action, &id)
|
||||||
|
.map_dbg_err_str("Failed to get commit message")
|
||||||
|
.map_dbg_str("[GIT DELETE HOOK]: Got commit message"));
|
||||||
|
|
||||||
|
try!(repo.commit(Some("HEAD"), &signature, &signature, &message, &tree, &parents)
|
||||||
|
.map_dbg_str("Committed")
|
||||||
|
.map_dbg_err_str("Failed to commit")
|
||||||
|
.map_dbg_str("[GIT DELETE HOOK]: Committed")
|
||||||
|
.map_err_into(GHEK::RepositoryCommittingError)
|
||||||
|
.map_into_hook_error()
|
||||||
|
);
|
||||||
|
|
||||||
|
index.write()
|
||||||
|
.map_err_into(GHEK::RepositoryIndexWritingError)
|
||||||
|
.map_dbg_err_str("Failed to write tree")
|
||||||
|
.map_dbg_str("[GIT DELETE HOOK]: Wrote index")
|
||||||
|
.map_into_hook_error()
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
77
libimagstorestdhook/src/vcs/git/error.rs
Normal file
77
libimagstorestdhook/src/vcs/git/error.rs
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
use git2::Error as Git2Error;
|
||||||
|
|
||||||
|
use libimagstore::hook::error::HookError as HE;
|
||||||
|
use libimagstore::hook::error::HookErrorKind as HEK;
|
||||||
|
use libimagstore::hook::result::HookResult;
|
||||||
|
|
||||||
|
generate_error_module!(
|
||||||
|
generate_error_types!(GitHookError, GitHookErrorKind,
|
||||||
|
ConfigError => "Configuration Error",
|
||||||
|
NoConfigError => "No Configuration",
|
||||||
|
ConfigTypeError => "Configuration value type wrong",
|
||||||
|
|
||||||
|
RepositoryError => "Error while interacting with git repository",
|
||||||
|
RepositoryInitError => "Error while loading the git repository",
|
||||||
|
RepositoryBackendError => "Error in the git library",
|
||||||
|
RepositoryBranchError => "Error while interacting with git branch(es)",
|
||||||
|
RepositoryBranchNameFetchingError => "Error while fetching branch name",
|
||||||
|
RepositoryWrongBranchError => "Error because repository is on wrong branch",
|
||||||
|
RepositoryIndexFetchingError => "Error while fetching Repository Index",
|
||||||
|
RepositoryIndexWritingError => "Error while writing Repository Index",
|
||||||
|
RepositoryPathAddingError => "Error while adding Path to Index",
|
||||||
|
RepositoryCommittingError => "Error while committing",
|
||||||
|
RepositoryParentFetchingError => "Error while fetching parent of commit",
|
||||||
|
|
||||||
|
HeadFetchError => "Error while getting HEAD",
|
||||||
|
NotOnBranch => "No Branch is checked out",
|
||||||
|
MkRepo => "Repository creation error",
|
||||||
|
MkSignature => "Error while building Signature object",
|
||||||
|
|
||||||
|
RepositoryFileStatusError => "Error while getting file status",
|
||||||
|
|
||||||
|
GitConfigFetchError => "Error fetching git config",
|
||||||
|
GitConfigEditorFetchError => "Error fetching 'editor' from git config",
|
||||||
|
EditorError => "Error while calling editor"
|
||||||
|
);
|
||||||
|
);
|
||||||
|
|
||||||
|
impl GitHookError {
|
||||||
|
|
||||||
|
pub fn inside_of<T>(self, h: HEK) -> HookResult<T> {
|
||||||
|
Err(HE::new(h, Some(Box::new(self))))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GitHookError> for HE {
|
||||||
|
|
||||||
|
fn from(he: GitHookError) -> HE {
|
||||||
|
HE::new(HEK::HookExecutionError, Some(Box::new(he)))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Git2Error> for GitHookError {
|
||||||
|
|
||||||
|
fn from(ge: Git2Error) -> GitHookError {
|
||||||
|
GitHookError::new(GitHookErrorKind::RepositoryBackendError, Some(Box::new(ge)))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MapIntoHookError<T> {
|
||||||
|
fn map_into_hook_error(self) -> Result<T, HE>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> MapIntoHookError<T> for Result<T, GitHookError> {
|
||||||
|
|
||||||
|
fn map_into_hook_error(self) -> Result<T, HE> {
|
||||||
|
self.map_err(|e| HE::new(HEK::HookExecutionError, Some(Box::new(e))))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use self::error::GitHookError;
|
||||||
|
pub use self::error::GitHookErrorKind;
|
||||||
|
pub use self::error::MapErrInto;
|
||||||
|
|
9
libimagstorestdhook/src/vcs/git/mod.rs
Normal file
9
libimagstorestdhook/src/vcs/git/mod.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
mod action;
|
||||||
|
mod config;
|
||||||
|
pub mod delete;
|
||||||
|
mod error;
|
||||||
|
mod result;
|
||||||
|
mod runtime;
|
||||||
|
pub mod update;
|
||||||
|
pub mod util;
|
||||||
|
|
5
libimagstorestdhook/src/vcs/git/result.rs
Normal file
5
libimagstorestdhook/src/vcs/git/result.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
use std::result::Result as RResult;
|
||||||
|
|
||||||
|
use vcs::git::error::GitHookError;
|
||||||
|
|
||||||
|
pub type Result<T> = RResult<T, GitHookError>;
|
179
libimagstorestdhook/src/vcs/git/runtime.rs
Normal file
179
libimagstorestdhook/src/vcs/git/runtime.rs
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use git2::{Index, Repository};
|
||||||
|
use toml::Value;
|
||||||
|
|
||||||
|
use libimagerror::into::IntoError;
|
||||||
|
use libimagerror::trace::MapErrTrace;
|
||||||
|
use libimagstore::hook::error::CustomData;
|
||||||
|
use libimagstore::hook::error::HookErrorKind as HEK;
|
||||||
|
use libimagstore::hook::result::HookResult;
|
||||||
|
use libimagutil::debug_result::*;
|
||||||
|
|
||||||
|
use vcs::git::action::StoreAction;
|
||||||
|
use vcs::git::result::Result;
|
||||||
|
use vcs::git::error::{MapErrInto, GitHookErrorKind as GHEK};
|
||||||
|
|
||||||
|
/// Runtime object for git hook implementations.
|
||||||
|
///
|
||||||
|
/// Contains some utility functionality to hold the repository and the configuration for the hooks.
|
||||||
|
pub struct Runtime {
|
||||||
|
repository: Option<Repository>,
|
||||||
|
config: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Runtime {
|
||||||
|
|
||||||
|
/// Build a `Runtime` object, pass the store path to build the `Repository` instance the
|
||||||
|
/// `Runtime` has to contain.
|
||||||
|
///
|
||||||
|
/// If the building of the `Repository` fails, this function `trace_error()`s the error and
|
||||||
|
/// returns a `Runtime` object that does _not_ contain a `Repository`.
|
||||||
|
pub fn new(storepath: &PathBuf) -> Runtime {
|
||||||
|
Runtime {
|
||||||
|
repository: Repository::open(storepath).map_err_trace().ok(),
|
||||||
|
config: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the configuration for the `Runtime`. Always returns `Ok(())`.
|
||||||
|
pub fn set_config(&mut self, cfg: &Value) -> Result<()> {
|
||||||
|
self.config = Some(cfg.clone());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether the `Runtime` has a `Repository`
|
||||||
|
pub fn has_repository(&self) -> bool {
|
||||||
|
self.repository.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether the `Runtime` has a configuration
|
||||||
|
pub fn has_config(&self) -> bool {
|
||||||
|
self.config.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the the config value by reference or get an `Err()` which can be returned to the callee
|
||||||
|
/// of the Hook.
|
||||||
|
///
|
||||||
|
/// The `action` Argument is required in case of `Err()` so the error message can be build
|
||||||
|
/// correctly.
|
||||||
|
pub fn config_value_or_err(&self, action: &StoreAction) -> HookResult<&Value> {
|
||||||
|
self.config
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(GHEK::NoConfigError.into_error())
|
||||||
|
.map_err_into(GHEK::ConfigError)
|
||||||
|
.map_err(Box::new)
|
||||||
|
.map_err(|e| HEK::HookExecutionError.into_error_with_cause(e))
|
||||||
|
.map_err(|e| e.with_custom_data(CustomData::default().aborting(false)))
|
||||||
|
.map_dbg_err(|_| {
|
||||||
|
format!("[GIT {} HOOK]: Couldn't get Value object from config", action.uppercase())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the `Repository` object from the `Runtime` or an `Err()` that can be returned to the
|
||||||
|
/// callee of the Hook.
|
||||||
|
///
|
||||||
|
/// The `action` Argument is required in case of `Err()` so the error message can be build
|
||||||
|
/// correctly.
|
||||||
|
pub fn repository(&self, action: &StoreAction) -> HookResult<&Repository> {
|
||||||
|
use vcs::git::error::MapIntoHookError;
|
||||||
|
|
||||||
|
debug!("[GIT {} HOOK]: Getting repository", action.uppercase());
|
||||||
|
self.repository
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(GHEK::MkRepo.into_error())
|
||||||
|
.map_err_into(GHEK::RepositoryError)
|
||||||
|
.map_into_hook_error()
|
||||||
|
.map_dbg_err(|_| format!("[GIT {} HOOK]: Couldn't fetch Repository", action.uppercase()))
|
||||||
|
.map_dbg(|_| format!("[GIT {} HOOK]: Repository object fetched", action.uppercase()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that the branch that is put in the configuration file is checked out, if any.
|
||||||
|
pub fn ensure_cfg_branch_is_checked_out(&self, action: &StoreAction) -> HookResult<()> {
|
||||||
|
use vcs::git::config::ensure_branch;
|
||||||
|
use vcs::git::config::do_checkout_ensure_branch;
|
||||||
|
|
||||||
|
debug!("[GIT {} HOOK]: Ensuring branch checkout", action.uppercase());
|
||||||
|
let head = try!(self
|
||||||
|
.repository(action)
|
||||||
|
.and_then(|r| {
|
||||||
|
debug!("[GIT {} HOOK]: Repository fetched, getting head", action.uppercase());
|
||||||
|
r.head()
|
||||||
|
.map_dbg_err_str("Couldn't fetch HEAD")
|
||||||
|
.map_dbg_err(|e| format!("\tbecause = {:?}", e))
|
||||||
|
.map_err_into(GHEK::HeadFetchError)
|
||||||
|
.map_err(|e| e.into())
|
||||||
|
}));
|
||||||
|
debug!("[GIT {} HOOK]: HEAD fetched", action.uppercase());
|
||||||
|
|
||||||
|
// TODO: Fail if not on branch? hmmh... I'm not sure
|
||||||
|
if !head.is_branch() {
|
||||||
|
debug!("[GIT {} HOOK]: HEAD is not a branch", action.uppercase());
|
||||||
|
return Err(GHEK::NotOnBranch.into_error().into());
|
||||||
|
}
|
||||||
|
debug!("[GIT {} HOOK]: HEAD is a branch", action.uppercase());
|
||||||
|
|
||||||
|
// Check out appropriate branch ... or fail
|
||||||
|
match ensure_branch(self.config.as_ref()) {
|
||||||
|
Ok(Some(s)) => {
|
||||||
|
debug!("[GIT {} HOOK]: We have to ensure branch: {}", action.uppercase(), s);
|
||||||
|
match head.name().map(|name| {
|
||||||
|
debug!("[GIT {} HOOK]: {} == {}", action.uppercase(), name, s);
|
||||||
|
name == s
|
||||||
|
}) {
|
||||||
|
Some(b) => {
|
||||||
|
if b {
|
||||||
|
debug!("[GIT {} HOOK]: Branch already checked out.", action.uppercase());
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
debug!("[GIT {} HOOK]: Branch not checked out.", action.uppercase());
|
||||||
|
|
||||||
|
if !do_checkout_ensure_branch(self.config.as_ref()) {
|
||||||
|
Err(GHEK::RepositoryWrongBranchError.into_error())
|
||||||
|
.map_err_into(GHEK::RepositoryError)
|
||||||
|
} else {
|
||||||
|
// Else try to check out the branch...
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
None => Err(GHEK::RepositoryBranchNameFetchingError.into_error())
|
||||||
|
.map_err_into(GHEK::RepositoryBranchError)
|
||||||
|
.map_err_into(GHEK::RepositoryError),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(None) => {
|
||||||
|
debug!("[GIT {} HOOK]: No branch to checkout", action.uppercase());
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
|
||||||
|
Err(e) => Err(e).map_err_into(GHEK::RepositoryError),
|
||||||
|
}
|
||||||
|
.map_err(Box::new)
|
||||||
|
.map_err(|e| HEK::HookExecutionError.into_error_with_cause(e))
|
||||||
|
.map_dbg(|_| format!("[GIT {} HOOK]: Branch checked out", action.uppercase()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether the WD is "dirty" - whether there is a diff to the repository
|
||||||
|
/// This function returns false if there is no `Repository` object in the `Runtime`
|
||||||
|
pub fn repo_is_dirty(&self, index: &Index) -> bool {
|
||||||
|
match self.repository.as_ref() {
|
||||||
|
Some(repo) => {
|
||||||
|
repo.diff_index_to_workdir(Some(index), None)
|
||||||
|
.map_dbg_str("Fetched diff: Index <-> WD")
|
||||||
|
.map_dbg_err_str("Failed to fetch diff: Index <-> WD")
|
||||||
|
.map(|diff| diff.deltas().count() != 0)
|
||||||
|
.unwrap_or(false)
|
||||||
|
},
|
||||||
|
|
||||||
|
None => {
|
||||||
|
debug!("No repository: Cannot fetch diff: Index <-> WD");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
230
libimagstorestdhook/src/vcs/git/update.rs
Normal file
230
libimagstorestdhook/src/vcs/git/update.rs
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::fmt::{Debug, Formatter, Error as FmtError};
|
||||||
|
use std::result::Result as RResult;
|
||||||
|
|
||||||
|
use toml::Value;
|
||||||
|
|
||||||
|
use libimagerror::trace::trace_error;
|
||||||
|
use libimagstore::hook::Hook;
|
||||||
|
use libimagstore::hook::accessor::StoreIdAccessor;
|
||||||
|
use libimagstore::hook::accessor::{HookDataAccessor, HookDataAccessorProvider};
|
||||||
|
use libimagstore::hook::position::HookPosition;
|
||||||
|
use libimagstore::hook::result::HookResult;
|
||||||
|
use libimagstore::storeid::StoreId;
|
||||||
|
use libimagutil::debug_result::*;
|
||||||
|
|
||||||
|
use vcs::git::error::GitHookErrorKind as GHEK;
|
||||||
|
use vcs::git::error::MapErrInto;
|
||||||
|
use vcs::git::runtime::Runtime as GRuntime;
|
||||||
|
|
||||||
|
/// The `UpdateHook` type
|
||||||
|
///
|
||||||
|
/// Represents a hook which is executed whenever a entry in the store is updated (written to disk).
|
||||||
|
///
|
||||||
|
/// # Time of execution
|
||||||
|
///
|
||||||
|
/// This hook is executed _after_ the store operation succeeded, so _after_ the file is written to
|
||||||
|
/// disk.
|
||||||
|
pub struct UpdateHook {
|
||||||
|
storepath: PathBuf,
|
||||||
|
|
||||||
|
runtime: GRuntime,
|
||||||
|
|
||||||
|
position: HookPosition,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateHook {
|
||||||
|
|
||||||
|
pub fn new(storepath: PathBuf, p: HookPosition) -> UpdateHook {
|
||||||
|
UpdateHook {
|
||||||
|
runtime: GRuntime::new(&storepath),
|
||||||
|
storepath: storepath,
|
||||||
|
position: p,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for UpdateHook {
|
||||||
|
fn fmt(&self, fmt: &mut Formatter) -> RResult<(), FmtError> {
|
||||||
|
write!(fmt, "UpdateHook(storepath={:?}, repository={}, pos={:?}, cfg={:?})",
|
||||||
|
self.storepath,
|
||||||
|
(if self.runtime.has_repository() { "Some(_)" } else { "None" }),
|
||||||
|
self.position,
|
||||||
|
self.runtime.has_config())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hook for UpdateHook {
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"stdhook_git_update"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the configuration of the hook. See
|
||||||
|
/// `libimagstorestdhook::vcs::git::runtime::Runtime::set_config()`.
|
||||||
|
///
|
||||||
|
/// This function traces the error (using `trace_error()`) that
|
||||||
|
/// `libimagstorestdhook::vcs::git::runtime::Runtime::set_config()`
|
||||||
|
/// returns, if any.
|
||||||
|
fn set_config(&mut self, config: &Value) {
|
||||||
|
if let Err(e) = self.runtime.set_config(config) {
|
||||||
|
trace_error(&e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HookDataAccessorProvider for UpdateHook {
|
||||||
|
|
||||||
|
fn accessor(&self) -> HookDataAccessor {
|
||||||
|
HookDataAccessor::StoreIdAccess(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StoreIdAccessor for UpdateHook {
|
||||||
|
|
||||||
|
/// The implementation of the UpdateHook
|
||||||
|
///
|
||||||
|
/// # Scope
|
||||||
|
///
|
||||||
|
/// This hook takes the git index and commits it either interactively or with a default message,
|
||||||
|
/// if there is no configuration for an interactive commit.
|
||||||
|
///
|
||||||
|
fn access(&self, id: &StoreId) -> HookResult<()> {
|
||||||
|
use libimagerror::into::IntoError;
|
||||||
|
use vcs::git::action::StoreAction;
|
||||||
|
use vcs::git::config::commit_message;
|
||||||
|
use vcs::git::error::MapIntoHookError;
|
||||||
|
use vcs::git::util::fetch_index;
|
||||||
|
use vcs::git::config::abort_on_repo_init_err;
|
||||||
|
use git2::{ADD_DEFAULT, STATUS_WT_NEW, STATUS_WT_MODIFIED, IndexMatchedPath};
|
||||||
|
|
||||||
|
debug!("[GIT UPDATE HOOK]: {:?}", id);
|
||||||
|
|
||||||
|
let action = StoreAction::Update;
|
||||||
|
|
||||||
|
if !self.runtime.has_repository() {
|
||||||
|
debug!("[GIT UPDATE HOOK]: Runtime has no repository...");
|
||||||
|
if try!(self.runtime.config_value_or_err(&action).map(|c| abort_on_repo_init_err(c))) {
|
||||||
|
// Abort on repo init failure
|
||||||
|
debug!("[GIT UPDATE HOOK]: Config says we should abort if we have no repository");
|
||||||
|
debug!("[GIT UPDATE HOOK]: Returing Err(_)");
|
||||||
|
return Err(GHEK::RepositoryInitError.into_error())
|
||||||
|
.map_err_into(GHEK::RepositoryError)
|
||||||
|
.map_into_hook_error()
|
||||||
|
} else {
|
||||||
|
debug!("[GIT UPDATE HOOK]: Config says it is okay to not have a repository");
|
||||||
|
debug!("[GIT UPDATE HOOK]: Returing Ok(())");
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = try!(self.runtime.ensure_cfg_branch_is_checked_out(&action));
|
||||||
|
let cfg = try!(self.runtime.config_value_or_err(&action));
|
||||||
|
let repo = try!(self.runtime.repository(&action));
|
||||||
|
let mut index = try!(fetch_index(repo, &action));
|
||||||
|
|
||||||
|
if !self.runtime.repo_is_dirty(&index) {
|
||||||
|
debug!("[GIT UPDATE HOOK]: Repository seems to be clean. I'm done.");
|
||||||
|
return Ok(())
|
||||||
|
} else {
|
||||||
|
debug!("[GIT UPDATE HOOK]: Repository seems to be dirty. Continuing.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let signature = try!(
|
||||||
|
repo.signature()
|
||||||
|
.map_err_into(GHEK::MkSignature)
|
||||||
|
.map_dbg_err_str("Failed to fetch signature")
|
||||||
|
.map_dbg_str("[GIT UPDATE HOOK]: Fetched signature object")
|
||||||
|
.map_into_hook_error()
|
||||||
|
);
|
||||||
|
|
||||||
|
let head = try!(
|
||||||
|
repo.head()
|
||||||
|
.map_err_into(GHEK::HeadFetchError)
|
||||||
|
.map_dbg_err_str("Failed to fetch HEAD")
|
||||||
|
.map_dbg_str("[GIT UPDATE HOOK]: Fetched HEAD")
|
||||||
|
.map_into_hook_error()
|
||||||
|
);
|
||||||
|
|
||||||
|
let file_status = try!(
|
||||||
|
repo
|
||||||
|
.status_file(id.local())
|
||||||
|
.map_dbg_err_str("Failed to fetch file status")
|
||||||
|
.map_dbg_err(|e| format!("\t-> {:?}", e))
|
||||||
|
.map_dbg_str("[GIT UPDATE HOOK]: Fetched file status")
|
||||||
|
.map_err_into(GHEK::RepositoryFileStatusError)
|
||||||
|
.map_into_hook_error()
|
||||||
|
);
|
||||||
|
|
||||||
|
let cb = &mut |path: &Path, _matched_spec: &[u8]| -> i32 {
|
||||||
|
if file_status.contains(STATUS_WT_NEW) || file_status.contains(STATUS_WT_MODIFIED) {
|
||||||
|
debug!("[GIT CREATE HOOK]: File is modified/new: {}", path.display());
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
debug!("[GIT CREATE HOOK]: Ignoring file: {}", path.display());
|
||||||
|
1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try!(
|
||||||
|
index.add_all(&[id.local()], ADD_DEFAULT, Some(cb as &mut IndexMatchedPath))
|
||||||
|
.map_err_into(GHEK::RepositoryPathAddingError)
|
||||||
|
.map_dbg_err_str("Failed to add to index")
|
||||||
|
.map_dbg(|_| format!("[GIT UPDATE HOOK]: Added id ({:?}) to index", id))
|
||||||
|
.map_into_hook_error()
|
||||||
|
);
|
||||||
|
|
||||||
|
let tree_id = try!(
|
||||||
|
index.write_tree()
|
||||||
|
.map_err_into(GHEK::RepositoryIndexWritingError)
|
||||||
|
.map_dbg_err_str("Failed to write tree")
|
||||||
|
.map_dbg_str("[GIT UPDATE HOOK]: Wrote tree")
|
||||||
|
.map_into_hook_error()
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut parents = Vec::new();
|
||||||
|
{
|
||||||
|
let commit = try!(
|
||||||
|
repo.find_commit(head.target().unwrap())
|
||||||
|
.map_err_into(GHEK::RepositoryParentFetchingError)
|
||||||
|
.map_dbg_err_str("Failed to find commit HEAD")
|
||||||
|
.map_dbg_str("[GIT UPDATE HOOK]: Fetched commit parents of HEAD")
|
||||||
|
.map_into_hook_error()
|
||||||
|
);
|
||||||
|
parents.push(commit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// for converting from Vec<Commit> to Vec<&Commit>
|
||||||
|
let parents = parents.iter().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let tree = try!(
|
||||||
|
repo.find_tree(tree_id)
|
||||||
|
.map_err_into(GHEK::RepositoryParentFetchingError)
|
||||||
|
.map_dbg_err_str("Failed to find tree")
|
||||||
|
.map_dbg_str("[GIT UPDATE HOOK]: Found Tree for parents")
|
||||||
|
.map_into_hook_error()
|
||||||
|
);
|
||||||
|
|
||||||
|
let message = try!(commit_message(&repo, cfg, StoreAction::Update, &id)
|
||||||
|
.map_dbg_err_str("Failed to get commit message")
|
||||||
|
.map_dbg_str("[GIT UPDATE HOOK]: Fetched commit message"));
|
||||||
|
|
||||||
|
try!(repo.commit(Some("HEAD"), &signature, &signature, &message, &tree, &parents)
|
||||||
|
.map_dbg_str("Committed")
|
||||||
|
.map_dbg_err_str("Failed to commit")
|
||||||
|
.map_err_into(GHEK::RepositoryCommittingError)
|
||||||
|
.map_into_hook_error()
|
||||||
|
);
|
||||||
|
|
||||||
|
index.write()
|
||||||
|
.map_err_into(GHEK::RepositoryIndexWritingError)
|
||||||
|
.map_dbg_err_str("Failed to write tree")
|
||||||
|
.map_into_hook_error()
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
23
libimagstorestdhook/src/vcs/git/util.rs
Normal file
23
libimagstorestdhook/src/vcs/git/util.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
//! Utility functionality for integrating git hooks in the store
|
||||||
|
//!
|
||||||
|
//! Contains primitives to create a repository within the store path
|
||||||
|
|
||||||
|
use git2::{Repository, Index};
|
||||||
|
|
||||||
|
use vcs::git::error::GitHookErrorKind as GHEK;
|
||||||
|
use vcs::git::error::MapErrInto;
|
||||||
|
use vcs::git::action::StoreAction;
|
||||||
|
use vcs::git::error::MapIntoHookError;
|
||||||
|
|
||||||
|
use libimagutil::debug_result::*;
|
||||||
|
use libimagstore::hook::error::HookError;
|
||||||
|
|
||||||
|
pub fn fetch_index(repo: &Repository, action: &StoreAction) -> Result<Index, HookError> {
|
||||||
|
debug!("[GIT {} HOOK]: Getting Index", action.uppercase());
|
||||||
|
repo.index()
|
||||||
|
.map_dbg_err(|_| format!("[GIT {} HOOK]: Couldn't fetch Index", action.uppercase()))
|
||||||
|
.map_dbg(|_| format!("[GIT {} HOOK]: Index object fetched", action.uppercase()))
|
||||||
|
.map_err_into(GHEK::RepositoryIndexFetchingError)
|
||||||
|
.map_into_hook_error()
|
||||||
|
}
|
||||||
|
|
1
libimagstorestdhook/src/vcs/mod.rs
Normal file
1
libimagstorestdhook/src/vcs/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod git;
|
|
@ -47,10 +47,16 @@ cat_entry() {
|
||||||
}
|
}
|
||||||
|
|
||||||
reset_store() {
|
reset_store() {
|
||||||
|
rm -rf "${STORE}"/.git
|
||||||
rm -r "${STORE}"
|
rm -r "${STORE}"
|
||||||
}
|
}
|
||||||
|
|
||||||
call_test() {
|
call_test() {
|
||||||
|
prepare_store_directory || {
|
||||||
|
err "Preparing store directory failed"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
out "-- TESTING: '$1' --"
|
out "-- TESTING: '$1' --"
|
||||||
$1
|
$1
|
||||||
result=$?
|
result=$?
|
||||||
|
@ -63,6 +69,27 @@ call_test() {
|
||||||
success "-- SUCCESS: '$1' --"
|
success "-- SUCCESS: '$1' --"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
__git() {
|
||||||
|
out "Calling git: $*"
|
||||||
|
git --work-tree=/tmp/store/ --git-dir=/tmp/store/.git $*
|
||||||
|
}
|
||||||
|
|
||||||
|
__git_commit() {
|
||||||
|
out "Calling git-commit: $*"
|
||||||
|
git --work-tree=/tmp/store/ --git-dir=/tmp/store/.git commit -m "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_store_directory() {
|
||||||
|
out "Preparing /tmp/store"
|
||||||
|
mkdir -p /tmp/store/ &&\
|
||||||
|
touch /tmp/store/.gitkeep &&\
|
||||||
|
__git init &&\
|
||||||
|
__git config --local user.email "imag@imag-pim.org" &&\
|
||||||
|
__git config --local user.name "Imag CI" &&\
|
||||||
|
__git add .gitkeep &&\
|
||||||
|
__git_commit 'Initial commit: .gitkeep'
|
||||||
|
}
|
||||||
|
|
||||||
invoke_tests() {
|
invoke_tests() {
|
||||||
out "Invoking tests."
|
out "Invoking tests."
|
||||||
if [[ ! -z "$INVOKE_TEST" ]]; then
|
if [[ ! -z "$INVOKE_TEST" ]]; then
|
||||||
|
|
Loading…
Reference in a new issue