Merge pull request #759 from matthiasbeyer/libimagstorestdhook/git-commit-on-drop

libimagstorestdhook/git: commit on drop
This commit is contained in:
Matthias Beyer 2016-10-11 18:14:28 +02:00 committed by GitHub
commit 906e26df6f
9 changed files with 372 additions and 15 deletions

View file

@ -10,7 +10,7 @@ implicit-create = false
# Hooks which get executed right before the Store is closed.
# They get the store path as StoreId passed, so they can alter the complete
# store, so these hooks should be chosen carefully.
store-unload-hook-aspects = [ "debug" ]
store-unload-hook-aspects = [ "debug", "vcs" ]
pre-create-hook-aspects = [ "debug", "vcs" ]
post-create-hook-aspects = [ "debug", "vcs" ]
@ -58,6 +58,10 @@ try_checkout_ensure_branch = true
# Commit configuration
[store.hooks.stdhook_git_update.commit]
# Enable committing here. If not enabled, the "stdhook_git_storeunload" hook
# will commit all changes in one commit when the store is closed.
enabled = false
# Whether to do the commit interactively
interactive = false
@ -89,6 +93,10 @@ try_checkout_ensure_branch = true
# Commit configuration
[store.hooks.stdhook_git_delete.commit]
# Enable committing here. If not enabled, the "stdhook_git_storeunload" hook
# will commit all changes in one commit when the store is closed.
enabled = false
# Whether to do the commit interactively
interactive = false
@ -100,3 +108,47 @@ interactive_editor = false
# Commit message if the commit is not interactive
message = "Deleted"
[store.hooks.stdhook_git_storeunload]
aspect = "vcs"
# set to false to disable
enabled = true
# 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_storeunload.commit]
# Enable on-unload-committing, causing the store-unload hook to commit the
# changes to the store. This has no effect if the changes were already committed
# by the other git hooks.
enabled = true
# Do a git-add on all files that are not in the index yet, before committing.
# This must be turned on, as we do not support adding with "Update" hooks and
# only committing with the "Drop" hook, yet.
# So, effectively, disabling this will disable committing.
#
# If not set: false
add_wt_changes = true
# 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 = "Commit on drop"

View file

@ -64,6 +64,7 @@ impl<'a> Runtime<'a> {
use libimagstorestdhook::debug::DebugHook;
use libimagstorestdhook::vcs::git::delete::DeleteHook as GitDeleteHook;
use libimagstorestdhook::vcs::git::update::UpdateHook as GitUpdateHook;
use libimagstorestdhook::vcs::git::store_unload::StoreUnloadHook as GitStoreUnloadHook;
use libimagerror::trace::trace_error;
use libimagerror::trace::trace_error_dbg;
use libimagerror::into::IntoError;
@ -158,8 +159,9 @@ 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),
(Box::new(GitDeleteHook::new(sp.clone(), HP::PostDelete)), "vcs", HP::PostDelete),
(Box::new(GitUpdateHook::new(sp.clone(), HP::PostUpdate)), "vcs", HP::PostUpdate),
(Box::new(GitStoreUnloadHook::new(sp)), "vcs", HP::StoreUnload),
];
for (hook, aspectname, position) in hooks {

View file

@ -26,25 +26,28 @@ pub enum StoreAction {
Retrieve,
Update,
Delete,
StoreUnload,
}
impl StoreAction {
pub fn uppercase(&self) -> &str {
match *self {
StoreAction::Create => "CREATE",
StoreAction::Retrieve => "RETRIEVE",
StoreAction::Update => "UPDATE",
StoreAction::Delete => "DELETE",
StoreAction::Create => "CREATE",
StoreAction::Retrieve => "RETRIEVE",
StoreAction::Update => "UPDATE",
StoreAction::Delete => "DELETE",
StoreAction::StoreUnload => "STORE UNLOAD",
}
}
pub fn as_commit_message(&self) -> &str {
match *self {
StoreAction::Create => "Create",
StoreAction::Retrieve => "Retrieve",
StoreAction::Update => "Update",
StoreAction::Delete => "Delete",
StoreAction::Create => "Create",
StoreAction::Retrieve => "Retrieve",
StoreAction::Update => "Update",
StoreAction::Delete => "Delete",
StoreAction::StoreUnload => "Store Unload",
}
}
}
@ -54,10 +57,11 @@ 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",
StoreAction::Create => "create",
StoreAction::Retrieve => "retrieve",
StoreAction::Update => "update",
StoreAction::Delete => "delete",
StoreAction::StoreUnload => "store unload",
})
}

View file

@ -195,3 +195,24 @@ pub fn is_enabled(cfg: &Value) -> bool {
get_bool_cfg(Some(cfg), "enabled", true, true)
}
/// Check whether committing is enabled for a hook.
pub fn committing_is_enabled(cfg: &Value) -> Result<bool> {
match cfg.lookup("commit.enabled") {
Some(&Value::Boolean(b)) => Ok(b),
Some(_) => {
warn!("Config setting whether committing is enabled or not has wrong type.");
warn!("Expected Boolean");
Err(GHEK::ConfigTypeError.into_error())
},
None => {
warn!("No config setting whether committing is enabled or not.");
Err(GHEK::NoConfigError.into_error())
},
}
.map_err_into(GHEK::ConfigError)
}
pub fn add_wt_changes_before_committing(cfg: &Value) -> bool {
get_bool_cfg(Some(cfg), "commit.add_wt_changes", true, true)
}

View file

@ -105,6 +105,7 @@ impl StoreIdAccessor for DeleteHook {
use vcs::git::util::fetch_index;
use vcs::git::config::abort_on_repo_init_err;
use vcs::git::config::is_enabled;
use vcs::git::config::committing_is_enabled;
use git2::{ADD_DEFAULT, STATUS_WT_DELETED, IndexMatchedPath};
debug!("[GIT DELETE HOOK]: {:?}", id);
@ -189,6 +190,11 @@ impl StoreIdAccessor for DeleteHook {
.map_into_hook_error()
);
if !try!(committing_is_enabled(cfg)) {
debug!("Committing not enabled. This is fine, returning now...");
return Ok(())
}
let mut parents = Vec::new();
{
let commit = try!(

View file

@ -40,6 +40,7 @@ generate_error_module!(
RepositoryPathAddingError => "Error while adding Path to Index",
RepositoryCommittingError => "Error while committing",
RepositoryParentFetchingError => "Error while fetching parent of commit",
RepositoryStatusFetchError => "Error while fetching repository status",
HeadFetchError => "Error while getting HEAD",
NotOnBranch => "No Branch is checked out",

View file

@ -23,6 +23,7 @@ pub mod delete;
mod error;
mod result;
mod runtime;
pub mod store_unload;
pub mod update;
pub mod util;

View file

@ -0,0 +1,264 @@
//
// imag - the personal information management suite for the commandline
// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; version
// 2.1 of the License.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
use std::path::PathBuf;
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::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 StoreUnloadHook {
storepath: PathBuf,
runtime: GRuntime,
}
impl StoreUnloadHook {
pub fn new(storepath: PathBuf) -> StoreUnloadHook {
StoreUnloadHook {
runtime: GRuntime::new(&storepath),
storepath: storepath,
}
}
}
impl Debug for StoreUnloadHook {
fn fmt(&self, fmt: &mut Formatter) -> RResult<(), FmtError> {
write!(fmt, "StoreUnloadHook(storepath={:?}, repository={}, cfg={:?})",
self.storepath,
(if self.runtime.has_repository() { "Some(_)" } else { "None" }),
self.runtime.has_config())
}
}
impl Hook for StoreUnloadHook {
fn name(&self) -> &'static str {
"stdhook_git_storeunload"
}
/// 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 StoreUnloadHook {
fn accessor(&self) -> HookDataAccessor {
HookDataAccessor::StoreIdAccess(self)
}
}
impl StoreIdAccessor for StoreUnloadHook {
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 vcs::git::config::is_enabled;
use vcs::git::config::committing_is_enabled;
use vcs::git::config::add_wt_changes_before_committing;
use git2::{ADD_DEFAULT,
StatusOptions,
Status,
StatusShow as STShow,
STATUS_INDEX_NEW as I_NEW,
STATUS_INDEX_DELETED as I_DEL,
STATUS_INDEX_RENAMED as I_REN,
STATUS_INDEX_MODIFIED as I_MOD,
STATUS_WT_NEW as WT_NEW,
STATUS_WT_DELETED as WT_DEL,
STATUS_WT_RENAMED as WT_REN,
STATUS_WT_MODIFIED as WT_MOD};
let action = StoreAction::StoreUnload;
let cfg = try!(self.runtime.config_value_or_err(&action));
if !is_enabled(cfg) {
return Ok(())
}
if !self.runtime.has_repository() {
debug!("[GIT STORE UNLOAD 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 STORE UNLOAD HOOK]: Config says we should abort if we have no repository");
debug!("[GIT STORE UNLOAD HOOK]: Returing Err(_)");
return Err(GHEK::RepositoryInitError.into_error())
.map_err_into(GHEK::RepositoryError)
.map_into_hook_error()
} else {
debug!("[GIT STORE UNLOAD HOOK]: Config says it is okay to not have a repository");
debug!("[GIT STORE UNLOAD HOOK]: Returing Ok(())");
return Ok(())
}
}
let _ = try!(self.runtime.ensure_cfg_branch_is_checked_out(&action));
let repo = try!(self.runtime.repository(&action));
let mut index = try!(fetch_index(repo, &action));
let check_dirty = |show: STShow, new: Status, modif: Status, del: Status, ren: Status| {
let mut status_options = StatusOptions::new();
status_options.show(show);
status_options.include_untracked(true);
repo.statuses(Some(&mut status_options))
.map(|statuses| {
statuses.iter()
.map(|s| s.status())
.map(|s| {
debug!("STATUS_WT_NEW = {}", s == new);
debug!("STATUS_WT_MODIFIED = {}", s == modif);
debug!("STATUS_WT_DELETED = {}", s == del);
debug!("STATUS_WT_RENAMED = {}", s == ren);
s
})
.any(|s| s == new || s == modif || s == del || s == ren)
})
.map_err_into(GHEK::RepositoryStatusFetchError)
.map_err_into(GHEK::RepositoryError)
.map_into_hook_error()
};
if try!(check_dirty(STShow::Workdir, WT_NEW, WT_MOD, WT_DEL, WT_REN)) {
if add_wt_changes_before_committing(cfg) {
debug!("Adding WT changes before committing.");
try!(index.add_all(&["*"], ADD_DEFAULT, None)
.map_err_into(GHEK::RepositoryPathAddingError)
.map_err_into(GHEK::RepositoryError)
.map_into_hook_error());
} else {
warn!("WT dirty, but adding files before committing on Drop disabled.");
warn!("Continuing without adding changes to the index.");
}
} else {
debug!("WT not dirty.");
}
if try!(check_dirty(STShow::Index, I_NEW, I_MOD, I_DEL, I_REN)) {
debug!("INDEX DIRTY!");
} else {
debug!("INDEX CLEAN... not continuing!");
return Ok(());
}
let signature = try!(
repo.signature()
.map_err_into(GHEK::MkSignature)
.map_dbg_err_str("Failed to fetch signature")
.map_dbg_str("[GIT STORE UNLOAD 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 STORE UNLOAD HOOK]: Fetched HEAD")
.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 STORE UNLOAD HOOK]: Wrote index tree")
.map_into_hook_error()
);
if !try!(committing_is_enabled(cfg)) {
debug!("Committing not enabled. This is fine, returning now...");
return Ok(())
}
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 STORE UNLOAD 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 STORE UNLOAD 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 STORE UNLOAD 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 STORE UNLOAD 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 STORE UNLOAD HOOK]: Wrote index")
.map_into_hook_error()
.map(|_| ())
}
}

View file

@ -119,6 +119,7 @@ impl StoreIdAccessor for UpdateHook {
use vcs::git::util::fetch_index;
use vcs::git::config::abort_on_repo_init_err;
use vcs::git::config::is_enabled;
use vcs::git::config::committing_is_enabled;
use git2::{ADD_DEFAULT, STATUS_WT_NEW, STATUS_WT_MODIFIED, IndexMatchedPath};
debug!("[GIT UPDATE HOOK]: {:?}", id);
@ -212,6 +213,11 @@ impl StoreIdAccessor for UpdateHook {
.map_into_hook_error()
);
if !try!(committing_is_enabled(cfg)) {
debug!("Committing not enabled. This is fine, returning now...");
return Ok(())
}
let mut parents = Vec::new();
{
let commit = try!(