Merge pull request #759 from matthiasbeyer/libimagstorestdhook/git-commit-on-drop
libimagstorestdhook/git: commit on drop
This commit is contained in:
commit
906e26df6f
9 changed files with 372 additions and 15 deletions
54
imagrc.toml
54
imagrc.toml
|
@ -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"
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
@ -159,7 +160,8 @@ impl<'a> Runtime<'a> {
|
|||
|
||||
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(GitUpdateHook::new(sp.clone(), HP::PostUpdate)), "vcs", HP::PostUpdate),
|
||||
(Box::new(GitStoreUnloadHook::new(sp)), "vcs", HP::StoreUnload),
|
||||
];
|
||||
|
||||
for (hook, aspectname, position) in hooks {
|
||||
|
|
|
@ -26,6 +26,7 @@ pub enum StoreAction {
|
|||
Retrieve,
|
||||
Update,
|
||||
Delete,
|
||||
StoreUnload,
|
||||
}
|
||||
|
||||
impl StoreAction {
|
||||
|
@ -36,6 +37,7 @@ impl StoreAction {
|
|||
StoreAction::Retrieve => "RETRIEVE",
|
||||
StoreAction::Update => "UPDATE",
|
||||
StoreAction::Delete => "DELETE",
|
||||
StoreAction::StoreUnload => "STORE UNLOAD",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,6 +47,7 @@ impl StoreAction {
|
|||
StoreAction::Retrieve => "Retrieve",
|
||||
StoreAction::Update => "Update",
|
||||
StoreAction::Delete => "Delete",
|
||||
StoreAction::StoreUnload => "Store Unload",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,6 +61,7 @@ impl Display for StoreAction {
|
|||
StoreAction::Retrieve => "retrieve",
|
||||
StoreAction::Update => "update",
|
||||
StoreAction::Delete => "delete",
|
||||
StoreAction::StoreUnload => "store unload",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -23,6 +23,7 @@ pub mod delete;
|
|||
mod error;
|
||||
mod result;
|
||||
mod runtime;
|
||||
pub mod store_unload;
|
||||
pub mod update;
|
||||
pub mod util;
|
||||
|
||||
|
|
264
libimagstorestdhook/src/vcs/git/store_unload.rs
Normal file
264
libimagstorestdhook/src/vcs/git/store_unload.rs
Normal 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(|_| ())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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!(
|
||||
|
|
Loading…
Reference in a new issue