mirror of https://github.com/djcb/mu.git
Merge branch 'wip/djcb/move'
This commit is contained in:
commit
942b822d4a
|
@ -138,7 +138,6 @@ test_flags_from_delta_expr()
|
|||
"-S", Flags::Seen).value() ==
|
||||
Flags::Unread);
|
||||
|
||||
|
||||
static_assert(flags_from_delta_expr("+R+P-F", Flags::Seen).value() ==
|
||||
(Flags::Seen|Flags::Passed|Flags::Replied));
|
||||
/* '-B' is invalid */
|
||||
|
|
|
@ -144,7 +144,6 @@ constexpr std::array<MessageFlagInfo, 14> AllMessageFlagInfos = {{
|
|||
MessageFlagInfo{Flags::HasAttachment,'a', "attach", MessageFlagCategory::Content,
|
||||
"Has at least one attachment"
|
||||
},
|
||||
|
||||
MessageFlagInfo{Flags::Unread, 'u', "unread", MessageFlagCategory::Pseudo,
|
||||
"New or not seen message"
|
||||
},
|
||||
|
@ -289,9 +288,9 @@ flags_from_absolute_expr(std::string_view expr, bool ignore_invalid = false)
|
|||
* @param expr delta expression
|
||||
* @param flags existing flags
|
||||
* @param ignore_invalid if @true, ignore invalid flags, otherwise return
|
||||
* nullopt if an invalid flag is encountered
|
||||
* Nothing if an invalid flag is encountered
|
||||
*
|
||||
* @return new flags, or nullopt in case of error
|
||||
* @return new flags, or Nothing in case of error
|
||||
*/
|
||||
constexpr Option<Flags>
|
||||
flags_from_delta_expr(std::string_view expr, Flags flags,
|
||||
|
@ -317,7 +316,6 @@ flags_from_delta_expr(std::string_view expr, Flags flags,
|
|||
}
|
||||
|
||||
return imply_unread(flags);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -329,8 +327,7 @@ flags_from_delta_expr(std::string_view expr, Flags flags,
|
|||
* @return either messages flags or Nothing in case of error.
|
||||
*/
|
||||
constexpr Option<Flags>
|
||||
flags_from_expr(std::string_view expr,
|
||||
Option<Flags> flags = Nothing)
|
||||
flags_from_expr(std::string_view expr, Option<Flags> flags = Nothing)
|
||||
{
|
||||
if (expr.empty())
|
||||
return Nothing;
|
||||
|
@ -367,7 +364,7 @@ flags_filter(Flags flags, MessageFlagCategory cat)
|
|||
* @return filtered flags
|
||||
*/
|
||||
constexpr Flags
|
||||
flags_mail_dir_file(Flags flags)
|
||||
flags_maildir_file(Flags flags)
|
||||
{
|
||||
for (auto&& info : AllMessageFlagInfos)
|
||||
if (info.category != MessageFlagCategory::Maildir &&
|
||||
|
|
|
@ -421,7 +421,7 @@ Mu::maildir_determine_target(const std::string& old_path,
|
|||
Flags newflags,
|
||||
bool new_name)
|
||||
{
|
||||
newflags = flags_mail_dir_file(newflags); // filter out irrelevant flags.
|
||||
newflags = flags_maildir_file(newflags); // filter out irrelevant flags.
|
||||
|
||||
/* sanity checks */
|
||||
if (const auto checked{check_determine_target_params(
|
||||
|
|
|
@ -64,7 +64,6 @@ Result<void> maildir_mkdir(const std::string& path, mode_t mode=0700,
|
|||
*/
|
||||
Result<void> maildir_link(const std::string& src, const std::string& targetpath,
|
||||
bool unique_names=true);
|
||||
|
||||
/**
|
||||
* Recursively delete all the symbolic links in a directory tree
|
||||
*
|
||||
|
@ -95,7 +94,7 @@ Result<void> maildir_move_message(const std::string& oldpath,
|
|||
*
|
||||
* @param old_path an absolute file system path to an existing message in an
|
||||
* actual maildir
|
||||
* @param root_maildir_path the absolete file system path under which
|
||||
* @param root_maildir_path the absolute file system path under which
|
||||
* all maildirs live.
|
||||
* @param target_maildir the target maildir; note that this the base-level
|
||||
* Maildir, ie. /home/user/Maildir/archive, and must _not_ include the
|
||||
|
@ -103,18 +102,18 @@ Result<void> maildir_move_message(const std::string& oldpath,
|
|||
* same filesystem. Can be empty if the message should not be moved to
|
||||
* a different maildir; note that this may still involve a
|
||||
* move to another directory (say, from new/ to cur/)
|
||||
* @param flags to set for the target (influences the filename, path). Any none-Maildir/File
|
||||
* flags are ignored.
|
||||
* @param flags to set for the target (influences the filename, path).
|
||||
* Any non-Maildir/File flags are ignored.
|
||||
* @param new_name whether to change the basename of the file
|
||||
*
|
||||
* @return Full path name of the target file or an Error
|
||||
*/
|
||||
Result<std::string>
|
||||
maildir_determine_target(const std::string& old_path,
|
||||
const std::string& root_maildir_path,
|
||||
const std::string& target_maildir,
|
||||
Flags newflags,
|
||||
bool new_name);
|
||||
const std::string& root_maildir_path,
|
||||
const std::string& target_maildir,
|
||||
Flags newflags,
|
||||
bool new_name);
|
||||
|
||||
} // namespace Mu
|
||||
|
||||
|
|
|
@ -925,9 +925,8 @@ Server::Private::perform_move(Store::Id docid,
|
|||
|
||||
/* note: we get back _all_ the messages that changed; the first is the
|
||||
* primary mover; the rest (if present) are any dups affected */
|
||||
const auto ids{unwrap(store().move_message(docid, maildir, flags, move_opts))};
|
||||
|
||||
for (auto&& id: ids) {
|
||||
const auto id_paths{unwrap(store().move_message(docid, maildir, flags, move_opts))};
|
||||
for (auto& [id,path]: id_paths) {
|
||||
auto idmsg{store().find_message(id)};
|
||||
if (!idmsg)
|
||||
mu_warning("failed to find message for id {}", id);
|
||||
|
@ -1113,15 +1112,14 @@ Server::Private::view_mark_as_read(Store::Id docid, Message&& msg, bool rename)
|
|||
}
|
||||
|
||||
// move message + dups, present results.
|
||||
|
||||
Store::MoveOptions move_opts{Store::MoveOptions::DupFlags};
|
||||
if (rename)
|
||||
move_opts |= Store::MoveOptions::ChangeName;
|
||||
auto&& ids = unwrap(store().move_message(docid, {}, nflags, move_opts));
|
||||
for (auto&& [id, moved_msg]: store().find_messages(ids)) {
|
||||
|
||||
const auto ids{Store::id_vec(unwrap(store().move_message(docid, {}, nflags, move_opts)))};
|
||||
for (auto&& [id, moved_msg]: store().find_messages(ids))
|
||||
output(mu_format("({} {})", id == docid ? ":view" : ":update",
|
||||
msg_sexp_str(moved_msg, id, {})));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
|
106
lib/mu-store.cc
106
lib/mu-store.cc
|
@ -145,10 +145,12 @@ struct Store::Private {
|
|||
Result<Store::Id> update_message_unlocked(Message& msg, Store::Id docid);
|
||||
Result<Store::Id> update_message_unlocked(Message& msg, const std::string& old_path);
|
||||
|
||||
Result<Message> move_message_unlocked(Message&& msg,
|
||||
Option<const std::string&> target_mdir,
|
||||
Option<Flags> new_flags,
|
||||
MoveOptions opts);
|
||||
|
||||
using PathMessage = std::pair<std::string, Message>;
|
||||
Result<PathMessage> move_message_unlocked(Message&& msg,
|
||||
Option<const std::string&> target_mdir,
|
||||
Option<Flags> new_flags,
|
||||
MoveOptions opts);
|
||||
XapianDb xapian_db_;
|
||||
Config config_;
|
||||
ContactsCache contacts_cache_;
|
||||
|
@ -341,8 +343,7 @@ Store::indexer()
|
|||
Result<Store::Id>
|
||||
Store::add_message(Message& msg, bool use_transaction, bool is_new)
|
||||
{
|
||||
const auto mdir{maildir_from_path(msg.path(),
|
||||
root_maildir())};
|
||||
const auto mdir{maildir_from_path(msg.path(), root_maildir())};
|
||||
if (!mdir)
|
||||
return Err(mdir.error());
|
||||
if (auto&& res = msg.set_maildir(mdir.value()); !res)
|
||||
|
@ -428,6 +429,23 @@ Store::find_message(Store::Id docid) const
|
|||
return priv_->find_message_unlocked(docid);
|
||||
}
|
||||
|
||||
Option<Store::Id>
|
||||
Store::find_message_id(const std::string& path) const
|
||||
{
|
||||
constexpr auto path_field{field_from_id(Field::Id::Path)};
|
||||
|
||||
std::lock_guard guard{priv_->lock_};
|
||||
|
||||
auto enq{xapian_db().enquire()};
|
||||
enq.set_query(Xapian::Query{path_field.xapian_term(path)});
|
||||
|
||||
if (auto mset{enq.get_mset(0, 1)}; mset.empty())
|
||||
return Nothing; // message not found
|
||||
else
|
||||
return Some(*mset.begin());
|
||||
}
|
||||
|
||||
|
||||
Store::IdMessageVec
|
||||
Store::find_messages(IdVec ids) const
|
||||
{
|
||||
|
@ -443,7 +461,7 @@ Store::find_messages(IdVec ids) const
|
|||
}
|
||||
|
||||
/**
|
||||
* Move a message in store and filesystem.
|
||||
* Move a message in store and filesystem; with DryRun, only calculate the target name.
|
||||
*
|
||||
* Lock is assumed taken already
|
||||
*
|
||||
|
@ -454,7 +472,7 @@ Store::find_messages(IdVec ids) const
|
|||
*
|
||||
* @return the Message after the moving, or an Error
|
||||
*/
|
||||
Result<Message>
|
||||
Result<Store::Private::PathMessage>
|
||||
Store::Private::move_message_unlocked(Message&& msg,
|
||||
Option<const std::string&> target_mdir,
|
||||
Option<Flags> new_flags,
|
||||
|
@ -472,21 +490,25 @@ Store::Private::move_message_unlocked(Message&& msg,
|
|||
if (!target_path)
|
||||
return Err(target_path.error());
|
||||
|
||||
/* 2. let's move it */
|
||||
if (const auto res = maildir_move_message(msg.path(), target_path.value()); !res)
|
||||
return Err(res.error());
|
||||
// in dry-run mode, we only determine the target-path
|
||||
if (none_of(opts & MoveOptions::DryRun)) {
|
||||
|
||||
/* 3. file move worked, now update the message with the new info.*/
|
||||
if (auto&& res = msg.update_after_move(
|
||||
target_path.value(), target_maildir, target_flags); !res)
|
||||
return Err(res.error());
|
||||
/* 2. let's move it */
|
||||
if (const auto res = maildir_move_message(msg.path(), target_path.value()); !res)
|
||||
return Err(res.error());
|
||||
|
||||
/* 4. update message worked; re-store it */
|
||||
if (auto&& res = update_message_unlocked(msg, old_path); !res)
|
||||
return Err(res.error());
|
||||
/* 3. file move worked, now update the message with the new info.*/
|
||||
if (auto&& res = msg.update_after_move(
|
||||
target_path.value(), target_maildir, target_flags); !res)
|
||||
return Err(res.error());
|
||||
|
||||
/* 4. update message worked; re-store it */
|
||||
if (auto&& res = update_message_unlocked(msg, old_path); !res)
|
||||
return Err(res.error());
|
||||
}
|
||||
|
||||
/* 6. Profit! */
|
||||
return Ok(std::move(msg));
|
||||
return Ok(PathMessage{std::move(*target_path), std::move(msg)});
|
||||
}
|
||||
|
||||
Store::IdVec
|
||||
|
@ -498,8 +520,7 @@ Store::find_duplicates(const std::string& message_id) const
|
|||
}
|
||||
|
||||
|
||||
|
||||
Result<Store::IdVec>
|
||||
Result<Store::IdPathVec>
|
||||
Store::move_message(Store::Id id,
|
||||
Option<const std::string&> target_mdir,
|
||||
Option<Flags> new_flags,
|
||||
|
@ -519,16 +540,15 @@ Store::move_message(Store::Id id,
|
|||
return Err(Error::Code::Store, "cannot find message <{}>", id);
|
||||
|
||||
const auto message_id{msg->message_id()};
|
||||
auto res{priv_->move_message_unlocked(std::move(*msg),
|
||||
target_mdir, new_flags, opts)};
|
||||
auto res{priv_->move_message_unlocked(std::move(*msg), target_mdir, new_flags, opts)};
|
||||
if (!res)
|
||||
return Err(res.error());
|
||||
|
||||
IdVec ids{id};
|
||||
IdPathVec id_paths{{id, res->first}};
|
||||
if (none_of(opts & Store::MoveOptions::DupFlags) || message_id.empty() || !new_flags)
|
||||
return Ok(std::move(ids));
|
||||
return Ok(std::move(id_paths));
|
||||
|
||||
/* handle the dupflags case; i.e. apply (a subset of) the flags to
|
||||
/* handle the dup-flags case; i.e. apply (a subset of) the flags to
|
||||
* all messages with the same message-id as well */
|
||||
auto dups{priv_->find_duplicates_unlocked(*this, message_id)};
|
||||
for (auto&& dupid: dups) {
|
||||
|
@ -542,20 +562,34 @@ Store::move_message(Store::Id id,
|
|||
|
||||
/* For now, don't change Draft/Flagged/Trashed */
|
||||
const auto dup_flags{filter_dup_flags(dup_msg->flags(), *new_flags)};
|
||||
/* use the updated new_flags and default MoveOptions (so we don't recurse, nor do we
|
||||
* change the base-name of moved messages) */
|
||||
/* use the updated new_flags and MoveOptions without DupFlags (so we don't
|
||||
* recurse) */
|
||||
opts = opts & ~MoveOptions::DupFlags;
|
||||
if (auto dup_res = priv_->move_message_unlocked(
|
||||
std::move(*dup_msg), Nothing,
|
||||
dup_flags,
|
||||
Store::MoveOptions::None); !dup_res)
|
||||
std::move(*dup_msg), Nothing, dup_flags, opts); !dup_res)
|
||||
mu_warning("failed to move dup: {}", dup_res.error().what());
|
||||
else
|
||||
ids.emplace_back(dupid);
|
||||
id_paths.emplace_back(dupid, dup_res->first);
|
||||
}
|
||||
|
||||
return Ok(std::move(ids));
|
||||
// sort the dup paths by name;
|
||||
std::sort(id_paths.begin() + 1, id_paths.end(),
|
||||
[](const auto& idp1, const auto& idp2) { return idp1.second < idp2.second; });
|
||||
|
||||
return Ok(std::move(id_paths));
|
||||
}
|
||||
|
||||
Store::IdVec
|
||||
Store::id_vec(const IdPathVec& ips)
|
||||
{
|
||||
IdVec idv;
|
||||
for (auto&& ip: ips)
|
||||
idv.emplace_back(ip.first);
|
||||
|
||||
return idv;
|
||||
}
|
||||
|
||||
|
||||
time_t
|
||||
Store::dirstamp(const std::string& path) const
|
||||
{
|
||||
|
@ -660,9 +694,11 @@ std::vector<std::string>
|
|||
Store::maildirs() const
|
||||
{
|
||||
std::vector<std::string> mdirs;
|
||||
const auto prefix_size = root_maildir().size();
|
||||
const auto prefix_size{root_maildir().size()};
|
||||
|
||||
Scanner::Handler handler = [&](const std::string& path, auto&& _1, auto&& _2) {
|
||||
mdirs.emplace_back(path.substr(prefix_size));
|
||||
auto md{path.substr(prefix_size)};
|
||||
mdirs.emplace_back(std::move(md.empty() ? "/" : md));
|
||||
return true;
|
||||
};
|
||||
|
||||
|
|
|
@ -47,6 +47,8 @@ public:
|
|||
using Id = Xapian::docid; /**< Id for a message in the store */
|
||||
static constexpr Id InvalidId = 0; /**< Invalid store id */
|
||||
using IdVec = std::vector<Id>; /**< Vector of document ids */
|
||||
using IdPathVec = std::vector<std::pair<Id, std::string>>;
|
||||
/**< vector of id, path pairs */
|
||||
|
||||
/**
|
||||
* Configuration options.
|
||||
|
@ -246,6 +248,15 @@ public:
|
|||
*/
|
||||
Option<Message> find_message(Id id) const;
|
||||
|
||||
/**
|
||||
* Find a message's docid based on its path
|
||||
*
|
||||
* @param path path to the message
|
||||
*
|
||||
* @return the docid or Nothing if not found
|
||||
*/
|
||||
Option<Id> find_message_id(const std::string& path) const;
|
||||
|
||||
/**
|
||||
* Find the messages for the given ids
|
||||
*
|
||||
|
@ -282,27 +293,38 @@ public:
|
|||
enum struct MoveOptions {
|
||||
None = 0, /**< Defaults */
|
||||
ChangeName = 1 << 0, /**< Change the name when moving */
|
||||
DupFlags = 1 << 1, /**< Update flags for duplicate messages too*/
|
||||
DupFlags = 1 << 1, /**< Update flags for duplicate messages too */
|
||||
DryRun = 1 << 2, /**< Don't really move, just determine target paths */
|
||||
};
|
||||
|
||||
/**
|
||||
* Move a message both in the filesystem and in the store. After a
|
||||
* successful move, the message is updated.
|
||||
* Move a message both in the filesystem and in the store. After a successful move, the
|
||||
* message is updated.
|
||||
*
|
||||
* @param id the id for some message
|
||||
* @param target_mdir the target maildir (if any)
|
||||
* @param new_flags new flags (if any)
|
||||
* @param change_name whether to change the name
|
||||
* @param opts move options
|
||||
*
|
||||
* @return Result, either an IdVec with ids for the moved
|
||||
* message(s) or some error. Note that in case of success at least one
|
||||
* message is returned, and only with MoveOptions::DupFlags can it be
|
||||
* more than one.
|
||||
* @return Result, either an IdPathVec with ids and paths for the moved message(s) or some
|
||||
* error. Note that in case of success at least one message is returned, and only with
|
||||
* MoveOptions::DupFlags can it be more than one.
|
||||
*
|
||||
* The first element of the IdPathVec, is the main message that got move; any subsequent
|
||||
* (if any) are the duplicate paths, sorted by path-name.
|
||||
*/
|
||||
Result<IdVec> move_message(Store::Id id,
|
||||
Option<const std::string&> target_mdir = Nothing,
|
||||
Option<Flags> new_flags = Nothing,
|
||||
MoveOptions opts = MoveOptions::None);
|
||||
Result<IdPathVec> move_message(Store::Id id,
|
||||
Option<const std::string&> target_mdir = Nothing,
|
||||
Option<Flags> new_flags = Nothing,
|
||||
MoveOptions opts = MoveOptions::None);
|
||||
/**
|
||||
* Convert IdPathVec -> IdVec
|
||||
*
|
||||
* @param ips idpath vector
|
||||
*
|
||||
* @return vector of ids
|
||||
*/
|
||||
static IdVec id_vec(const IdPathVec& ips);
|
||||
|
||||
/**
|
||||
* Prototype for the ForEachMessageFunc
|
||||
|
|
|
@ -71,13 +71,7 @@ make_test_store(const std::string& test_path, const TestMap& test_map,
|
|||
assert_valid_result(store);
|
||||
|
||||
/* index the messages */
|
||||
auto res = store->indexer().start({});
|
||||
g_assert_true(res);
|
||||
while(store->indexer().is_running()) {
|
||||
using namespace std::chrono_literals;
|
||||
std::this_thread::sleep_for(100ms);
|
||||
}
|
||||
|
||||
g_assert_true(store->indexer().start({},true/*block*/));
|
||||
if (test_map.size() > 0)
|
||||
g_assert_false(store->empty());
|
||||
|
||||
|
@ -575,7 +569,7 @@ Boo!
|
|||
assert_valid_result(moved_msgs);
|
||||
|
||||
g_assert_true(moved_msgs->size() == 1);
|
||||
auto&& moved_msg_opt = store.find_message(moved_msgs->at(0));
|
||||
auto&& moved_msg_opt = store.find_message(moved_msgs->at(0).first);
|
||||
g_assert_true(!!moved_msg_opt);
|
||||
const auto&moved_msg = std::move(*moved_msg_opt);
|
||||
const auto new_path = moved_msg.path();
|
||||
|
|
|
@ -381,7 +381,7 @@ Yes, that would be excellent.
|
|||
const auto msgs3 = store->move_message(msg->docid(), {}, Flags::Seen);
|
||||
assert_valid_result(msgs3);
|
||||
g_assert_true(msgs3->size() == 1);
|
||||
auto&& msg3_opt{store->find_message(msgs3->at(0))};
|
||||
auto&& msg3_opt{store->find_message(msgs3->at(0).first/*id*/)};
|
||||
g_assert_true(!!msg3_opt);
|
||||
auto&& msg3{std::move(*msg3_opt)};
|
||||
|
||||
|
@ -442,11 +442,11 @@ Yes, that would be excellent.
|
|||
assert_valid_result(mres);
|
||||
mu_info("found {} matches", mres->size());
|
||||
for (auto&& m: *mres)
|
||||
mu_info("id: {}", m);
|
||||
mu_info("id: {}: {}", m.first, m.second);
|
||||
|
||||
// al three dups should have been updated
|
||||
g_assert_cmpuint(mres->size(), ==, 3);
|
||||
auto&& id_msgs{store->find_messages(*mres)};
|
||||
auto&& id_msgs{store->find_messages(Store::id_vec(*mres))};
|
||||
|
||||
// first should be the original
|
||||
g_assert_cmpuint(id_msgs.at(0).first, ==, ids.at(0));
|
||||
|
|
|
@ -105,5 +105,11 @@ test('test-html-to-text',
|
|||
cpp_args: ['-DBUILD_TESTS'],
|
||||
dependencies: [glib_dep, lib_mu_utils_dep]))
|
||||
|
||||
test('test-error',
|
||||
executable('test-error', 'mu-error.cc',
|
||||
install: false,
|
||||
cpp_args: ['-DBUILD_TESTS'],
|
||||
dependencies: [glib_dep, lib_mu_utils_dep]))
|
||||
|
||||
|
||||
subdir('tests')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||
** Copyright (C) 2020-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||
**
|
||||
** This program is free software; you can redistribute it and/or modify it
|
||||
** under the terms of the GNU General Public License as published by the
|
||||
|
@ -148,20 +148,16 @@ using ArgMap = CommandHandler::ArgMap;
|
|||
using ArgInfo = CommandHandler::ArgInfo;
|
||||
using CommandInfo = CommandHandler::CommandInfo;
|
||||
|
||||
|
||||
static bool
|
||||
static Result<void>
|
||||
call(const CommandInfoMap& cmap, const std::string& str) try {
|
||||
|
||||
const auto cmd{Command::make_parse(str)};
|
||||
if (!cmd)
|
||||
throw Error(Error::Code::Internal, "invalid sexp str");
|
||||
|
||||
const auto res{CommandHandler(cmap).invoke(*cmd)};
|
||||
return !!res;
|
||||
if (const auto cmd{Command::make_parse(str)}; !cmd)
|
||||
return Err(Error::Code::Internal, "invalid s-expression '{}'", str);
|
||||
else
|
||||
return CommandHandler(cmap).invoke(*cmd);
|
||||
|
||||
} catch (const Error& err) {
|
||||
mu_warning("{}", err.what());
|
||||
return false;
|
||||
return Err(Error{err});
|
||||
}
|
||||
|
||||
static void
|
||||
|
@ -169,18 +165,49 @@ test_command()
|
|||
{
|
||||
allow_warnings();
|
||||
|
||||
CommandInfoMap cmap;
|
||||
cmap.emplace(
|
||||
CommandInfoMap ci_map;
|
||||
ci_map.emplace(
|
||||
"my-command",
|
||||
CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}},
|
||||
{":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}},
|
||||
"My command,",
|
||||
{}});
|
||||
ci_map.emplace(
|
||||
"another-command",
|
||||
CommandInfo{
|
||||
ArgMap{
|
||||
{":queries", ArgInfo{Sexp::Type::List, false,
|
||||
"queries for which to get read/unread numbers"}},
|
||||
{":symbol", ArgInfo{Sexp::Type::Symbol, true,
|
||||
"some boring symbol"}},
|
||||
{":bool", ArgInfo{Sexp::Type::Symbol, true,
|
||||
"some even more boring boolean symbol"}},
|
||||
{":symbol2", ArgInfo{Sexp::Type::Symbol, false,
|
||||
"some even more boring symbol"}},
|
||||
{":bool2", ArgInfo{Sexp::Type::Symbol, false,
|
||||
"some boring boolean symbol"}},
|
||||
},
|
||||
"get unread/totals information for a list of queries",
|
||||
[&](const auto& params) {
|
||||
const auto queries{params.string_vec_arg(":queries")
|
||||
.value_or(std::vector<std::string>{})};
|
||||
g_assert_cmpuint(queries.size(),==,3);
|
||||
g_assert_true(params.bool_arg(":bool").value_or(false) == true);
|
||||
assert_equal(params.symbol_arg(":symbol").value_or("boo"), "sym");
|
||||
|
||||
g_assert_true(call(cmap, "(my-command :param1 \"hello\")"));
|
||||
g_assert_true(call(cmap, "(my-command :param1 \"hello\" :param2 123)"));
|
||||
g_assert_false(!!params.bool_arg(":bool2"));
|
||||
g_assert_false(!!params.bool_arg(":symbol2"));
|
||||
|
||||
g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 123 :param3 xxx)"));
|
||||
}});
|
||||
|
||||
CommandHandler handler(std::move(ci_map));
|
||||
const auto cmap{handler.info_map()};
|
||||
|
||||
assert_valid_result(call(cmap, "(my-command :param1 \"hello\")"));
|
||||
assert_valid_result(call(cmap, "(my-command :param1 \"hello\" :param2 123)"));
|
||||
g_assert_false(!!call(cmap, "(my-command :param1 \"hello\" :param2 123 :param3 xxx)"));
|
||||
assert_valid_result(call(cmap, "(another-command :queries (\"foo\" \"bar\" \"cuux\") "
|
||||
":symbol sym :bool true)"));
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||
** Copyright (C) 2020-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||
**
|
||||
** This program is free software; you can redistribute it and/or modify it
|
||||
** under the terms of the GNU General Public License as published by the
|
||||
|
@ -218,7 +218,7 @@ struct CommandHandler {
|
|||
* first, then alphabetical.
|
||||
*
|
||||
* @return vec with the sorted names.
|
||||
*/
|
||||
*/ /* LCOV_EXCL_START */
|
||||
std::vector<std::string> sorted_argnames() const {
|
||||
// sort args -- by required, then alphabetical.
|
||||
std::vector<std::string> names;
|
||||
|
@ -234,6 +234,7 @@ struct CommandHandler {
|
|||
});
|
||||
return names;
|
||||
}
|
||||
/* LCOV_EXCL_STOP */
|
||||
|
||||
};
|
||||
|
||||
|
@ -263,6 +264,7 @@ private:
|
|||
const CommandInfoMap cmap_;
|
||||
};
|
||||
|
||||
/* LCOV_EXCL_START */
|
||||
static inline std::ostream&
|
||||
operator<<(std::ostream& os, const CommandHandler::ArgInfo& info)
|
||||
{
|
||||
|
@ -270,6 +272,7 @@ operator<<(std::ostream& os, const CommandHandler::ArgInfo& info)
|
|||
|
||||
return os;
|
||||
}
|
||||
/* LCOV_EXCL_STOP */
|
||||
|
||||
static inline std::ostream&
|
||||
operator<<(std::ostream& os, const CommandHandler::CommandInfo& info)
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||
**
|
||||
** This program is free software; you can redistribute it and/or modify it
|
||||
** under the terms of the GNU General Public License as published by the
|
||||
** Free Software Foundation; either version 3, or (at your option) any
|
||||
** later version.
|
||||
**
|
||||
** This program 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 General Public License for more details.
|
||||
**
|
||||
** You should have received a copy of the GNU General Public License
|
||||
** along with this program; if not, write to the Free Software Foundation,
|
||||
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
**
|
||||
*/
|
||||
|
||||
|
||||
#if BUILD_TESTS
|
||||
|
||||
#include "mu-error.hh"
|
||||
#include "mu-test-utils.hh"
|
||||
|
||||
using namespace Mu;
|
||||
|
||||
static void
|
||||
test_fill_error()
|
||||
{
|
||||
const Error err{Error::Code::Internal, "boo!"};
|
||||
GError *gerr{};
|
||||
|
||||
err.fill_g_error(&gerr);
|
||||
|
||||
assert_equal(gerr->message, "boo!");
|
||||
g_assert_cmpint(gerr->code, ==, static_cast<int>(err.code()));
|
||||
|
||||
g_clear_error(&gerr);
|
||||
}
|
||||
|
||||
static void
|
||||
test_add_hint()
|
||||
{
|
||||
Error err(Error::Code::Internal, "baa!");
|
||||
err.add_hint("hello");
|
||||
|
||||
assert_equal(err.hint(), "hello");
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
main(int argc, char* argv[])
|
||||
{
|
||||
mu_test_init(&argc, &argv);
|
||||
|
||||
g_test_add_func("/error/fill-error", test_fill_error);
|
||||
g_test_add_func("/error/add-hint", test_add_hint);
|
||||
|
||||
return g_test_run();
|
||||
|
||||
}
|
||||
|
||||
#endif /*BUILD_TESTS*/
|
|
@ -44,8 +44,8 @@ constexpr uint32_t err_enum(uint8_t code, uint8_t rv, uint8_t cat) {
|
|||
|
||||
struct Error final : public std::exception {
|
||||
|
||||
// 16 lower bits are for the error code the next 8 bits is for the return code
|
||||
// upper byte is for flags
|
||||
// 16 lower bits are for the error code;the next 8 bits are for the return code; the upper
|
||||
// byte is for flags
|
||||
static constexpr uint8_t SoftError = 1;
|
||||
|
||||
enum struct Code: uint32_t {
|
||||
|
|
|
@ -36,11 +36,13 @@
|
|||
|
||||
using namespace Mu;
|
||||
|
||||
/* LCOV_EXCL_START*/
|
||||
bool
|
||||
Mu::mu_test_mu_hacker()
|
||||
{
|
||||
return !!g_getenv("MU_HACKER");
|
||||
}
|
||||
/* LCOV_EXCL_STOP*/
|
||||
|
||||
|
||||
const char*
|
||||
|
@ -65,8 +67,10 @@ Mu::set_en_us_utf8_locale()
|
|||
setlocale(LC_ALL, "en_US.UTF-8");
|
||||
|
||||
if (strcmp(nl_langinfo(CODESET), "UTF-8") != 0) {
|
||||
/* LCOV_EXCL_START*/
|
||||
mu_println("Note: Unit tests require the en_US.utf8 locale. "
|
||||
"Ignoring test cases.");
|
||||
/* LCOV_EXCL_STOP*/
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -130,13 +134,10 @@ Mu::TempDir::~TempDir()
|
|||
return;
|
||||
}
|
||||
|
||||
/* ugly */
|
||||
GError *err{};
|
||||
const auto cmd{mu_format("/bin/rm -rf '{}'", path_)};
|
||||
if (!g_spawn_command_line_sync(cmd.c_str(), NULL,
|
||||
NULL, NULL, &err)) {
|
||||
mu_warning("error: {}", err ? err->message : "?");
|
||||
g_clear_error(&err);
|
||||
if (auto&& res{run_command0({RM_PROGRAM, "-fr", path_})}; !res) {
|
||||
/* LCOV_EXCL_START*/
|
||||
mu_warning("error removing {}: {}", path_, format_as(res.error()));
|
||||
/* LCOV_EXCL_STOP*/
|
||||
} else
|
||||
mu_debug("removed '{}'", path_);
|
||||
}
|
||||
|
|
|
@ -158,10 +158,13 @@ Mu::runtime_path(Mu::RuntimePath path, const std::string& muhome)
|
|||
return mu_config;
|
||||
case Mu::RuntimePath::Scripts:
|
||||
return join_paths(mu_config, "scripts");
|
||||
/*LCOV_EXCL_START*/
|
||||
default:
|
||||
throw std::logic_error("unknown path");
|
||||
/*LCOV_EXCL_STOP*/
|
||||
}
|
||||
}
|
||||
|
||||
/* LCOV_EXCL_START*/
|
||||
static gpointer
|
||||
cancel_wait(gpointer data)
|
||||
|
@ -212,6 +215,7 @@ Mu::g_cancellable_new_with_timeout(guint timeout)
|
|||
}
|
||||
/* LCOV_EXCL_STOP*/
|
||||
|
||||
/* LCOV_EXCL_START*/
|
||||
Result<std::string>
|
||||
Mu::read_from_stdin()
|
||||
{
|
||||
|
@ -234,13 +238,14 @@ Mu::read_from_stdin()
|
|||
G_MEMORY_OUTPUT_STREAM(outmem))),
|
||||
g_memory_output_stream_get_size(G_MEMORY_OUTPUT_STREAM(outmem))});
|
||||
}
|
||||
|
||||
/* LCOV_EXCL_STOP*/
|
||||
|
||||
|
||||
/*
|
||||
* Set the child to a group leader to avoid being killed when the
|
||||
* parent group is killed.
|
||||
*/
|
||||
/*LCOV_EXCL_START*/
|
||||
static void
|
||||
maybe_setsid (G_GNUC_UNUSED gpointer user_data)
|
||||
{
|
||||
|
@ -248,6 +253,7 @@ maybe_setsid (G_GNUC_UNUSED gpointer user_data)
|
|||
setsid();
|
||||
#endif /*HAVE_SETSID*/
|
||||
}
|
||||
/*LCOV_EXCL_STOP*/
|
||||
|
||||
Result<Mu::CommandOutput>
|
||||
Mu::run_command(std::initializer_list<std::string> args, bool try_setsid)
|
||||
|
@ -451,7 +457,20 @@ test_join_paths()
|
|||
assert_equal(join_paths("/a/b///c/d//", "e"), "/a/b/c/d/e");
|
||||
}
|
||||
|
||||
static void
|
||||
test_runtime_paths()
|
||||
{
|
||||
TempDir tdir;
|
||||
|
||||
assert_equal(runtime_path(RuntimePath::Cache, tdir.path()), tdir.path());
|
||||
assert_equal(runtime_path(RuntimePath::XapianDb, tdir.path()),
|
||||
join_paths(tdir.path(), "xapian"));
|
||||
assert_equal(runtime_path(RuntimePath::Bookmarks, tdir.path()),
|
||||
join_paths(tdir.path(), "bookmarks"));
|
||||
assert_equal(runtime_path(RuntimePath::Config, tdir.path()), tdir.path());
|
||||
assert_equal(runtime_path(RuntimePath::Scripts, tdir.path()),
|
||||
join_paths(tdir.path(), "scripts"));
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char* argv[])
|
||||
|
@ -473,6 +492,8 @@ main(int argc, char* argv[])
|
|||
test_program_in_path);
|
||||
g_test_add_func("/utils/join-paths",
|
||||
test_join_paths);
|
||||
g_test_add_func("/utils/runtime-paths",
|
||||
test_runtime_paths);
|
||||
|
||||
return g_test_run();
|
||||
}
|
||||
|
|
|
@ -47,7 +47,6 @@
|
|||
|
||||
namespace Mu {
|
||||
|
||||
|
||||
/*
|
||||
* Separator characters used in various places; importantly,
|
||||
* they are not used in UTF-8
|
||||
|
@ -55,7 +54,6 @@ namespace Mu {
|
|||
constexpr const auto SepaChar1 = '\xfe';
|
||||
constexpr const auto SepaChar2 = '\xff';
|
||||
|
||||
|
||||
/*
|
||||
* Logging/printing/formatting functions connect libfmt with the Glib logging
|
||||
* system. We wrap so perhaps at some point (C++23?) we can use std:: instead.
|
||||
|
@ -88,6 +86,7 @@ void mu_warning(fmt::format_string<T...> frm, T&&... args) noexcept {
|
|||
g_log("mu", G_LOG_LEVEL_WARNING, "%s",
|
||||
fmt::format(frm, std::forward<T>(args)...).c_str());
|
||||
}
|
||||
/* LCOV_EXCL_START*/
|
||||
template<typename...T>
|
||||
void mu_critical(fmt::format_string<T...> frm, T&&... args) noexcept {
|
||||
g_log("mu", G_LOG_LEVEL_CRITICAL, "%s",
|
||||
|
@ -98,6 +97,7 @@ void mu_error(fmt::format_string<T...> frm, T&&... args) noexcept {
|
|||
g_log("mu", G_LOG_LEVEL_ERROR, "%s",
|
||||
fmt::format(frm, std::forward<T>(args)...).c_str());
|
||||
}
|
||||
/* LCOV_EXCL_STOP*/
|
||||
|
||||
/*
|
||||
* Printing; add our wrapper functions, one day we might be able to use std::
|
||||
|
@ -355,8 +355,10 @@ struct StopWatch {
|
|||
StopWatch(const std::string name) : start_{Clock::now()}, name_{name} {}
|
||||
~StopWatch() {
|
||||
const auto us{static_cast<double>(to_us(Clock::now() - start_))};
|
||||
/* LCOV_EXCL_START*/
|
||||
if (us > 2000000)
|
||||
mu_debug("sw: {}: finished after {:.1f} s", name_, us / 1000000);
|
||||
/* LCOV_EXCL_STOP*/
|
||||
else if (us > 2000)
|
||||
mu_debug("sw: {}: finished after {:.1f} ms", name_, us / 1000);
|
||||
else
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
#+TITLE: MU MOVE
|
||||
#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
|
||||
|
||||
* NAME
|
||||
|
||||
*mu move* - move a message file or change its flags
|
||||
|
||||
* SYNOPSIS
|
||||
|
||||
*mu [common-options] move [options] <src> [--flags=<flags>] [<target>]*
|
||||
|
||||
* DESCRIPTION
|
||||
|
||||
*mu move* is the command for moving messages in a Maildir or changing their flags.
|
||||
|
||||
For any change, both the message file in the file system as well as its
|
||||
representation in the database are updated accordingly.
|
||||
|
||||
The source message file and target-maildir must reside under the root-maildir
|
||||
for mu's database (see *mu info store*).
|
||||
|
||||
* MOVE OPTIONS
|
||||
|
||||
** --flags=<flags>
|
||||
|
||||
specify the new message flags. See *FLAGS* for details.
|
||||
|
||||
** --change-name
|
||||
|
||||
change the basename of the message file when moving; this can be useful when
|
||||
using some external tools such as *mbsync(1)* which otherwise get confused
|
||||
|
||||
** --update-dups
|
||||
|
||||
update the flags of duplicate messages too, where "duplicate messages" are
|
||||
defined as all message that share the same message-id. Note that the
|
||||
Draft/Flagged/Trashed flags are deliberately _not_ changed if you change those on
|
||||
the source message.
|
||||
|
||||
** --dry-run,-n
|
||||
|
||||
print the target filename(s), but don't change anything.
|
||||
|
||||
Note that with the ~--change-name~, the target name is not constant, so you cannot
|
||||
use a dry-run to predict the exact name when doing a 'real' run.
|
||||
|
||||
#+include: "common-options.inc" :minlevel 1
|
||||
|
||||
* FLAGS
|
||||
|
||||
(Note: if you are not familiar with Maildirs, please refer to the *maildir(5)*
|
||||
man-page, or see http://cr.yp.to/proto/maildir.html)
|
||||
|
||||
The message flags specify the Maildir-metadata for a message and are represented
|
||||
by uppercase letters at the end of the message file name for all 'non-new'
|
||||
messages, i.e. messages that live in the ~cur/~ sub-directory of a Maildir.
|
||||
|
||||
| Flag | Meaning |
|
||||
|------+------------------------------------|
|
||||
| D | Draft message |
|
||||
| F | Flagged message |
|
||||
| P | Passed message (i.e., 'forwarded') |
|
||||
| R | Replied message |
|
||||
| S | Seen message |
|
||||
| T | Trashed; to be deleted later |
|
||||
|
||||
New messages (in the ~new/~ sub-directory) do not have flags encoded in their
|
||||
file-name; but we *mu* uses 'N' in the ~--flags~ to represent that:
|
||||
|
||||
| Flag | Meaning |
|
||||
|------+---------|
|
||||
| N | New |
|
||||
|
||||
Thus, changing flags means changing the letters at the end of the message
|
||||
file-name, except when setting or removing the 'N' (new) flag. Setting or
|
||||
un-setting the New flag causes the message is to be moved from ~cur/~ to ~new/~ or
|
||||
vice-versa, respectively. When marking a message as New, it looses the other
|
||||
flags.
|
||||
|
||||
* ABSOLUTE AND RELATIVE FLAGS
|
||||
|
||||
You can specify the flags with the ~--flags~ parameter, and do either with either
|
||||
*absolute* or *relative* flags.
|
||||
|
||||
Absolute flags just specify the new flags by their letters; e.g. to specify a
|
||||
/Trashed/, /Seen/, /Replied/ message, you'd use ~--flags STR~.
|
||||
#+end_example
|
||||
|
||||
Relative flags are relative to the current flags for some message, and each of
|
||||
the flags is prefixed with either ~+~ ("add this flag") or ~-~ ("remove this flag").
|
||||
|
||||
So to add the /Seen/ flag and remove the /Draft/ flag from whatever the message
|
||||
already has, ~--flags +S-D~.
|
||||
|
||||
You cannot combine relative and relative flags.
|
||||
|
||||
* EXAMPLES
|
||||
|
||||
** change some flags
|
||||
#+begin_example
|
||||
$ mu move /home/user/Maildir/inbox/cur/1695559560.a73985881f4611ac2.hostname!2,S --flags +F
|
||||
/home/user/Maildir/inbox/cur/1695559560.a73985881f4611ac2.hostname!2,FS
|
||||
#+end_example
|
||||
|
||||
** move to a different maildir
|
||||
#+begin_example
|
||||
$ mu move /home/user/Maildir/project1/cur/1695559560.a73985881f4611ac2.hostname!2,S /project2
|
||||
/home/user/Maildir/project2/cur/1695559560.a73985881f4611ac2.hostname!2,S
|
||||
#+end_example
|
||||
|
||||
#+include: "prefooter.inc" :minlevel 1
|
||||
|
||||
* SEE ALSO
|
||||
|
||||
*maildir(5)*
|
|
@ -26,6 +26,7 @@ mu = executable(
|
|||
'mu-cmd-init.cc',
|
||||
'mu-cmd-index.cc',
|
||||
'mu-cmd-mkdir.cc',
|
||||
'mu-cmd-move.cc',
|
||||
'mu-cmd-remove.cc',
|
||||
'mu-cmd-script.cc',
|
||||
'mu-cmd-server.cc',
|
||||
|
@ -76,6 +77,13 @@ test('test-cmd-mkdir',
|
|||
cpp_args: ['-DBUILD_TESTS'],
|
||||
dependencies: [glib_dep, lib_mu_dep]))
|
||||
|
||||
test('test-cmd-move',
|
||||
executable('test-cmd-move',
|
||||
'mu-cmd-move.cc',
|
||||
install: false,
|
||||
cpp_args: ['-DBUILD_TESTS'],
|
||||
dependencies: [glib_dep, lib_mu_dep]))
|
||||
|
||||
test('test-cmd-remove',
|
||||
executable('test-cmd-remove',
|
||||
'mu-cmd-remove.cc',
|
||||
|
|
|
@ -113,20 +113,13 @@ test_add_fail()
|
|||
|
||||
|
||||
int
|
||||
main(int argc, char* argv[]) try {
|
||||
|
||||
main(int argc, char* argv[])
|
||||
{
|
||||
mu_test_init(&argc, &argv);
|
||||
|
||||
g_test_add_func("/cmd/add/ok", test_add_ok);
|
||||
g_test_add_func("/cmd/add/fail", test_add_fail);
|
||||
|
||||
return g_test_run();
|
||||
|
||||
} catch (const Error& e) {
|
||||
mu_printerrln("{}", e.what());
|
||||
return 1;
|
||||
} catch (...) {
|
||||
mu_printerrln("caught exception");
|
||||
return 1;
|
||||
}
|
||||
#endif /*BUILD_TESTS*/
|
||||
|
|
|
@ -0,0 +1,276 @@
|
|||
/*
|
||||
** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||
**
|
||||
** This program is free software; you can redistribute it and/or modify it
|
||||
** under the terms of the GNU General Public License as published by the
|
||||
** Free Software Foundation; either version 3, or (at your option) any
|
||||
** later version.
|
||||
**
|
||||
** This program 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 General Public License for more details.
|
||||
**
|
||||
** You should have received a copy of the GNU General Public License
|
||||
** along with this program; if not, write to the Free Software Foundation,
|
||||
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
**
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "mu-cmd.hh"
|
||||
|
||||
#include "mu-store.hh"
|
||||
#include "mu-maildir.hh"
|
||||
#include "message/mu-message-file.hh"
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
using namespace Mu;
|
||||
|
||||
|
||||
Result<void>
|
||||
Mu::mu_cmd_move(Mu::Store& store, const Options& opts)
|
||||
{
|
||||
const auto& src{opts.move.src};
|
||||
if (::access(src.c_str(), R_OK) != 0 || determine_dtype(src) != DT_REG)
|
||||
return Err(Error::Code::InvalidArgument,
|
||||
"Source is not a readable file");
|
||||
|
||||
auto id{store.find_message_id(src)};
|
||||
if (!id)
|
||||
return Err(Error{Error::Code::InvalidArgument,
|
||||
"Source file is not present in database"}
|
||||
.add_hint("Perhaps run mu index?"));
|
||||
|
||||
std::string dest{opts.move.dest};
|
||||
Option<const std::string&> dest_path;
|
||||
if (dest.empty() && opts.move.flags.empty())
|
||||
return Err(Error::Code::InvalidArgument,
|
||||
"Must have at least one of destination and flags");
|
||||
else if (!dest.empty()) {
|
||||
const auto mdirs{store.maildirs()};
|
||||
mu_printerrln("XXXX");
|
||||
for (auto&& m:mdirs)
|
||||
mu_printerrln("m:'{}'", m);
|
||||
|
||||
if (!seq_some(mdirs, [&](auto &&d){ return d == dest;}))
|
||||
return Err(Error{Error::Code::InvalidArgument,
|
||||
"No maildir '{}' in store", dest}
|
||||
.add_hint("Try 'mu mkdir'"));
|
||||
else
|
||||
dest_path = dest;
|
||||
}
|
||||
|
||||
auto old_flags{flags_from_path(src)};
|
||||
if (!old_flags)
|
||||
return Err(Error::Code::InvalidArgument, "failed to determine old flags");
|
||||
|
||||
Flags new_flags;
|
||||
if (!opts.move.flags.empty()) {
|
||||
if (auto&& nflags{flags_from_expr(to_string_view(opts.move.flags),
|
||||
*old_flags)}; !nflags)
|
||||
return Err(Error::Code::InvalidArgument, "Invalid flags");
|
||||
else
|
||||
new_flags = flags_maildir_file(*nflags);
|
||||
|
||||
if (any_of(new_flags & Flags::New) && new_flags != Flags::New)
|
||||
return Err(Error{Error::Code::File,
|
||||
"the New flag cannot be combined with others"}
|
||||
.add_hint("See the mu-move manpage"));
|
||||
}
|
||||
|
||||
Store::MoveOptions move_opts{};
|
||||
if (opts.move.change_name)
|
||||
move_opts |= Store::MoveOptions::ChangeName;
|
||||
if (opts.move.update_dups)
|
||||
move_opts |= Store::MoveOptions::DupFlags;
|
||||
if (opts.move.dry_run)
|
||||
move_opts |= Store::MoveOptions::DryRun;
|
||||
|
||||
auto id_paths = store.move_message(*id, dest_path, new_flags, move_opts);
|
||||
if (!id_paths)
|
||||
return Err(std::move(id_paths.error()));
|
||||
|
||||
for (const auto&[_id, path]: *id_paths)
|
||||
mu_println("{}", path);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
|
||||
#ifdef BUILD_TESTS
|
||||
/*
|
||||
* Tests.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "utils/mu-test-utils.hh"
|
||||
|
||||
static void
|
||||
test_move_dry_run()
|
||||
{
|
||||
allow_warnings();
|
||||
|
||||
TempDir tdir;
|
||||
const auto dbpath{runtime_path(RuntimePath::XapianDb, tdir.path())};
|
||||
|
||||
auto res = run_command0({CP_PROGRAM, "-r", MU_TESTMAILDIR, tdir.path()});
|
||||
assert_valid_command(res);
|
||||
|
||||
const auto testpath{join_paths(tdir.path(), "testdir")};
|
||||
const auto src{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,S")};
|
||||
{
|
||||
auto store = Store::make_new(dbpath, testpath, {});
|
||||
assert_valid_result(store);
|
||||
g_assert_true(store->indexer().start({}, true/*block*/));
|
||||
}
|
||||
|
||||
// make a message 'New'
|
||||
{
|
||||
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
|
||||
"--flags", "N", "--dry-run"});
|
||||
assert_valid_command(res);
|
||||
|
||||
auto dst{join_paths(testpath, "new", "1220863042.12663_1.mindcrime")};
|
||||
assert_equal(res->standard_out, dst + '\n');
|
||||
|
||||
g_assert_true(::access(dst.c_str(), F_OK) != 0);
|
||||
g_assert_true(::access(src.c_str(), F_OK) == 0);
|
||||
}
|
||||
|
||||
// change some flags
|
||||
{
|
||||
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
|
||||
"--flags", "FP", "--dry-run"});
|
||||
assert_valid_command(res);
|
||||
|
||||
auto dst{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,FP")};
|
||||
assert_equal(res->standard_out, dst + '\n');
|
||||
}
|
||||
|
||||
// change some relative flag
|
||||
{
|
||||
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
|
||||
"--flags", "+F", "--dry-run"});
|
||||
assert_valid_command(res);
|
||||
|
||||
auto dst{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,FS")};
|
||||
assert_equal(res->standard_out, dst + '\n');
|
||||
}
|
||||
|
||||
{
|
||||
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
|
||||
"--flags", "-S+P+T", "--dry-run"});
|
||||
assert_valid_command(res);
|
||||
|
||||
auto dst{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,PT")};
|
||||
assert_equal(res->standard_out, dst + '\n');
|
||||
}
|
||||
|
||||
// change maildir
|
||||
for (auto& o : {"o1", "o2"})
|
||||
assert_valid_result(maildir_mkdir(join_paths(tdir.path(), "testdir", o)));
|
||||
|
||||
{
|
||||
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
|
||||
"/o1", "--flags", "-S+F", "--dry-run"});
|
||||
assert_valid_command(res);
|
||||
assert_equal(res->standard_out,
|
||||
join_paths(testpath,
|
||||
"o1/cur", "1220863042.12663_1.mindcrime!2,F") + "\n");
|
||||
}
|
||||
|
||||
// change-dups; first create some dups and index them.
|
||||
assert_valid_result(run_command0({CP_PROGRAM, src, join_paths(testpath, "o1/cur")}));
|
||||
assert_valid_result(run_command0({CP_PROGRAM, src, join_paths(testpath, "o2/cur")}));
|
||||
{
|
||||
auto store = Store::make(dbpath, Store::Options::Writable);
|
||||
assert_valid_result(store);
|
||||
g_assert_true(store->indexer().start({}, true/*block*/));
|
||||
}
|
||||
|
||||
// change some flags + update dups
|
||||
{
|
||||
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
|
||||
"--flags", "-S+S+T+R", "--update-dups", "--dry-run"});
|
||||
assert_valid_command(res);
|
||||
|
||||
auto p{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,RST")};
|
||||
auto p1{join_paths(testpath, "o1", "cur", "1220863042.12663_1.mindcrime!2,RS")};
|
||||
auto p2{join_paths(testpath, "o2", "cur", "1220863042.12663_1.mindcrime!2,RS")};
|
||||
|
||||
assert_equal(res->standard_out, mu_format("{}\n{}\n{}\n", p, p1, p2));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
test_move_real()
|
||||
{
|
||||
allow_warnings();
|
||||
|
||||
TempDir tdir;
|
||||
const auto dbpath{runtime_path(RuntimePath::XapianDb, tdir.path())};
|
||||
|
||||
auto res = run_command0({CP_PROGRAM, "-r", MU_TESTMAILDIR, tdir.path()});
|
||||
assert_valid_command(res);
|
||||
|
||||
const auto testpath{join_paths(tdir.path(), "testdir")};
|
||||
const auto src{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,S")};
|
||||
{
|
||||
auto store = Store::make_new(dbpath, testpath, {});
|
||||
assert_valid_result(res);
|
||||
g_assert_true(store->indexer().start({}, true/*block*/));
|
||||
}
|
||||
|
||||
{
|
||||
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
|
||||
"--flags", "N"});
|
||||
assert_valid_command(res);
|
||||
auto dst{join_paths(testpath, "new", "1220863042.12663_1.mindcrime")};
|
||||
g_assert_true(::access(dst.c_str(), F_OK) == 0);
|
||||
g_assert_true(::access(src.c_str(), F_OK) != 0);
|
||||
}
|
||||
|
||||
// change flags, maildir, update-dups
|
||||
// change-dups; first create some dups and index them.
|
||||
const auto src2{join_paths(testpath, "cur", "1305664394.2171_402.cthulhu!2,")};
|
||||
for (auto& o : {"o1", "o2", "o3"})
|
||||
assert_valid_result(maildir_mkdir(join_paths(tdir.path(), "testdir", o)));
|
||||
assert_valid_result(run_command0({CP_PROGRAM, src2, join_paths(testpath, "o1/cur")}));
|
||||
assert_valid_result(run_command0({CP_PROGRAM, src2, join_paths(testpath, "o2/new")}));
|
||||
{
|
||||
auto store = Store::make(dbpath, Store::Options::Writable);
|
||||
assert_valid_result(store);
|
||||
g_assert_true(store->indexer().start({}, true/*block*/));
|
||||
}
|
||||
|
||||
auto res2 = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src2, "/o3",
|
||||
"--flags", "-S+S+T+R", "--update-dups", "--change-name"});
|
||||
assert_valid_command(res2);
|
||||
|
||||
auto store = Store::make(dbpath, Store::Options::Writable);
|
||||
assert_valid_result(store);
|
||||
g_assert_true(store->indexer().start({}, true/*block*/));
|
||||
|
||||
for (auto&& f: split(res2->standard_out, "\n")) {
|
||||
//mu_println(">> {}", f);
|
||||
if (f.length() > 2)
|
||||
g_assert_true(::access(f.c_str(), F_OK) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char* argv[])
|
||||
{
|
||||
mu_test_init(&argc, &argv);
|
||||
|
||||
g_test_add_func("/cmd/move/dry-run", test_move_dry_run);
|
||||
g_test_add_func("/cmd/move/real", test_move_real);
|
||||
|
||||
return g_test_run();
|
||||
|
||||
}
|
||||
#endif /*BUILD_TESTS*/
|
|
@ -140,6 +140,8 @@ Mu::mu_cmd_execute(const Options& opts) try {
|
|||
return with_writable_store(mu_cmd_add, opts);
|
||||
case Options::SubCommand::Remove:
|
||||
return with_writable_store(mu_cmd_remove, opts);
|
||||
case Options::SubCommand::Move:
|
||||
return with_writable_store(mu_cmd_move, opts);
|
||||
case Options::SubCommand::Index:
|
||||
return with_writable_store(mu_cmd_index, opts);
|
||||
|
||||
|
|
|
@ -127,6 +127,15 @@ Result<void> mu_cmd_init(const Options& opts);
|
|||
*/
|
||||
Result<void> mu_cmd_mkdir(const Options& opts);
|
||||
|
||||
/**
|
||||
* execute the 'move' command
|
||||
*
|
||||
* @param opts configuration options
|
||||
*
|
||||
* @return Ok() or some error
|
||||
*/
|
||||
Result<void> mu_cmd_move(Store& store, const Options& opts);
|
||||
|
||||
/**
|
||||
* execute the 'remove' command
|
||||
*
|
||||
|
|
|
@ -197,6 +197,12 @@ static const std::function ExpandPath = [](std::string filepath)->std::string {
|
|||
return filepath = std::move(res.value());
|
||||
};
|
||||
|
||||
|
||||
// Canonicalize path
|
||||
static const std::function CanonicalizePath = [](std::string filepath)->std::string {
|
||||
return filepath = canonicalize_filename(filepath);
|
||||
};
|
||||
|
||||
/*
|
||||
* common
|
||||
*/
|
||||
|
@ -481,6 +487,31 @@ sub_mkdir(CLI::App& sub, Options& opts)
|
|||
->required();
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
sub_move(CLI::App& sub, Options& opts)
|
||||
{
|
||||
sub.add_flag("--change-name", opts.move.change_name,
|
||||
"Change name of target file");
|
||||
sub.add_flag("--update-dups", opts.move.update_dups,
|
||||
"Update duplicate messages too");
|
||||
sub.add_flag("--dry-run,-n", opts.move.dry_run,
|
||||
"Print target name, but do not change anything");
|
||||
|
||||
sub.add_option("--flags", opts.move.flags, "Target flags")
|
||||
->type_name("<flags>");
|
||||
|
||||
sub.add_option("source", opts.move.src, "Message file to move")
|
||||
->type_name("<message-path>")
|
||||
->transform(ExpandPath, "expand path")
|
||||
->transform(CanonicalizePath, "canonicalize path")
|
||||
->required();
|
||||
sub.add_option("destination", opts.move.dest,
|
||||
"Destination maildir")
|
||||
->type_name("<maildir>");
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
sub_remove(CLI::App& sub, Options& opts)
|
||||
{
|
||||
|
@ -602,7 +633,7 @@ AssocPairs<SubCommand, CommandInfo, Options::SubCommandNum> SubCommandInfos= {{
|
|||
},
|
||||
{ SubCommand::Info,
|
||||
{Category::NeedsReadOnlyStore,
|
||||
"info", "Show information about the message store database", sub_info }
|
||||
"info", "Show information", sub_info }
|
||||
},
|
||||
{ SubCommand::Init,
|
||||
{Category::NeedsWritableStore,
|
||||
|
@ -612,6 +643,10 @@ AssocPairs<SubCommand, CommandInfo, Options::SubCommandNum> SubCommandInfos= {{
|
|||
{Category::None,
|
||||
"mkdir", "Create a new Maildir", sub_mkdir }
|
||||
},
|
||||
{ SubCommand::Move,
|
||||
{Category::NeedsWritableStore,
|
||||
"move", "Move a message or change flags", sub_move }
|
||||
},
|
||||
{ SubCommand::Remove,
|
||||
{Category::NeedsWritableStore,
|
||||
"remove", "Remove message from file-system and database", sub_remove }
|
||||
|
@ -718,7 +753,8 @@ add_global_options(CLI::App& cli, Options& opts)
|
|||
|
||||
cli.add_flag("-q,--quiet", opts.quiet, "Hide non-essential output");
|
||||
cli.add_flag("-v,--verbose", opts.verbose, "Show verbose output");
|
||||
cli.add_flag("--log-stderr", opts.log_stderr, "Log to stderr");
|
||||
cli.add_flag("--log-stderr", opts.log_stderr, "Log to stderr")
|
||||
->group(""/*always hide*/);
|
||||
cli.add_flag("--nocolor", opts.nocolor, "Don't show ANSI colors")
|
||||
->default_val(Options::default_no_color())
|
||||
->default_str(Options::default_no_color() ? "<true>" : "<false>");
|
||||
|
@ -780,7 +816,7 @@ There is NO WARRANTY, to the extent permitted by law.
|
|||
->transform(ExpandPath, "expand path");
|
||||
}
|
||||
|
||||
/* add scripts (if supported) as semi-subscommands as well */
|
||||
/* add scripts (if supported) as semi-subcommands as well */
|
||||
const auto scripts = add_scripts(app, opts);
|
||||
|
||||
try {
|
||||
|
@ -842,6 +878,11 @@ Options::category(Options::SubCommand sub)
|
|||
static constexpr bool
|
||||
validate_subcommand_ids()
|
||||
{
|
||||
size_t val{};
|
||||
for (auto& cmd: Options::SubCommands)
|
||||
if (static_cast<size_t>(cmd) != val++)
|
||||
return false;
|
||||
|
||||
for (auto u = 0U; u != SubCommandInfos.size(); ++u)
|
||||
if (static_cast<size_t>(SubCommandInfos.at(u).first) != u)
|
||||
return false;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
**
|
||||
** This program 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
|
||||
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
** GNU General Public License for more details.
|
||||
**
|
||||
** You should have received a copy of the GNU General Public License
|
||||
|
@ -36,10 +36,10 @@
|
|||
/* command-line options for Mu */
|
||||
namespace Mu {
|
||||
struct Options {
|
||||
using OptSize = Option<std::size_t>;
|
||||
using SizeVec = std::vector<std::size_t>;
|
||||
using OptTStamp = Option<std::time_t>;
|
||||
using OptFieldId = Option<Field::Id>;
|
||||
using OptSize = Option<std::size_t>;
|
||||
using SizeVec = std::vector<std::size_t>;
|
||||
using OptTStamp = Option<std::time_t>;
|
||||
using OptFieldId = Option<Field::Id>;
|
||||
using StringVec = std::vector<std::string>;
|
||||
|
||||
/*
|
||||
|
@ -62,10 +62,11 @@ struct Options {
|
|||
|
||||
enum struct SubCommand {
|
||||
Add, Cfind, Extract, Fields, Find, Help, Index,Info, Init, Mkdir,
|
||||
Remove, Script, Server, Verify, View/*must be last*/
|
||||
Move, Remove, Script, Server, Verify, View,
|
||||
// <private>
|
||||
__count__
|
||||
};
|
||||
static constexpr std::size_t SubCommandNum =
|
||||
1 + static_cast<std::size_t>(SubCommand::View);
|
||||
static constexpr auto SubCommandNum = static_cast<size_t>(SubCommand::__count__);
|
||||
static constexpr std::array<SubCommand, SubCommandNum> SubCommands = {{
|
||||
SubCommand::Add,
|
||||
SubCommand::Cfind,
|
||||
|
@ -77,6 +78,7 @@ struct Options {
|
|||
SubCommand::Info,
|
||||
SubCommand::Init,
|
||||
SubCommand::Mkdir,
|
||||
SubCommand::Move,
|
||||
SubCommand::Remove,
|
||||
SubCommand::Script,
|
||||
SubCommand::Server,
|
||||
|
@ -84,7 +86,6 @@ struct Options {
|
|||
SubCommand::View
|
||||
}};
|
||||
|
||||
|
||||
Option<SubCommand> sub_command; /**< The chosen sub-command, if any. */
|
||||
|
||||
/*
|
||||
|
@ -117,16 +118,16 @@ struct Options {
|
|||
* Extract
|
||||
*/
|
||||
struct Extract: public Crypto {
|
||||
std::string message; /**< path to message file */
|
||||
std::string message; /**< path to message file */
|
||||
bool save_all; /**< extract all parts */
|
||||
bool save_attachments; /**< extract all attachment parts */
|
||||
SizeVec parts; /**< parts to save / open */
|
||||
SizeVec parts; /**< parts to save / open */
|
||||
std::string targetdir{}; /**< where to save attachments */
|
||||
bool overwrite; /**< overwrite same-named files */
|
||||
bool play; /**< try to 'play' attachment */
|
||||
std::string filename_rx; /**< Filename rx to save */
|
||||
bool uncooked{}; /**< Whether to avoid massaging
|
||||
* output filename */
|
||||
std::string filename_rx; /**< Filename rx to save */
|
||||
bool uncooked{}; /**< Whether to avoid massaging
|
||||
* the output filename */
|
||||
} extract;
|
||||
|
||||
/*
|
||||
|
@ -138,7 +139,7 @@ struct Options {
|
|||
*/
|
||||
struct Find {
|
||||
std::string fields; /**< fields to show in output */
|
||||
Field::Id sortfield; /**< field to sort by */
|
||||
Field::Id sortfield; /**< field to sort by */
|
||||
OptSize maxnum; /**< max # of entries to print */
|
||||
bool reverse; /**< sort in revers order (z->a) */
|
||||
bool threads; /**< show message threads */
|
||||
|
@ -146,7 +147,7 @@ struct Options {
|
|||
std::string linksdir; /**< directory for links */
|
||||
OptSize summary_len; /**< max # of lines for summary */
|
||||
std::string bookmark; /**< use bookmark */
|
||||
bool analyze; /**< analyze query */
|
||||
bool analyze; /**< analyze query */
|
||||
|
||||
enum struct Format { Plain, Links, Xml, Json, Sexp, Exec };
|
||||
Format format; /**< Output format */
|
||||
|
@ -158,7 +159,7 @@ struct Options {
|
|||
bool auto_retrieve; /**< assume we're online */
|
||||
bool decrypt; /**< try to decrypt the body */
|
||||
|
||||
StringVec query; /**< search query */
|
||||
StringVec query; /**< search query */
|
||||
} find;
|
||||
|
||||
struct Help {
|
||||
|
@ -189,10 +190,10 @@ struct Options {
|
|||
StringVec my_addresses; /**< personal e-mail addresses */
|
||||
StringVec ignored_addresses; /**< addresses to be ignored for
|
||||
* the contacts-cache */
|
||||
OptSize max_msg_size; /**< max size for message files */
|
||||
OptSize max_msg_size; /**< max size for message files */
|
||||
OptSize batch_size; /**< db transaction batch size */
|
||||
bool reinit; /**< re-initialize */
|
||||
bool support_ngrams; /**< support CJK etc. ngrams */
|
||||
bool support_ngrams; /**< support CJK etc. ngrams */
|
||||
|
||||
} init;
|
||||
|
||||
|
@ -204,6 +205,19 @@ struct Options {
|
|||
mode_t mode; /**< Mode for the maildir */
|
||||
} mkdir;
|
||||
|
||||
/*
|
||||
* Move
|
||||
*/
|
||||
struct Move {
|
||||
std::string src; /**< Source file */
|
||||
std::string dest; /**< Destination dir */
|
||||
std::string flags; /**< Flags for destination */
|
||||
bool change_name; /**< Change basename for destination */
|
||||
bool update_dups; /**< Update duplicate messages too */
|
||||
bool dry_run; /**< Just print the result path,
|
||||
but do not change anything */
|
||||
} move;
|
||||
|
||||
/*
|
||||
* Remove
|
||||
*/
|
||||
|
@ -215,7 +229,7 @@ struct Options {
|
|||
* Scripts (i.e., finding scriot)
|
||||
*/
|
||||
struct Script {
|
||||
std::string name; /**< name of script */
|
||||
std::string name; /**< name of script */
|
||||
StringVec params; /**< script params */
|
||||
} script;
|
||||
|
||||
|
@ -225,7 +239,7 @@ struct Options {
|
|||
struct Server {
|
||||
bool commands; /**< dump docs for commands */
|
||||
std::string eval; /**< command to evaluate */
|
||||
bool allow_temp_file; /**< temp-file optimization allowed? */
|
||||
bool allow_temp_file; /**< temp-file optimization allowed? */
|
||||
} server;
|
||||
|
||||
/*
|
||||
|
|
Loading…
Reference in New Issue