# # # patch "work.cc" # from [fcc2ee2cea02b5a0c4d94d6a8c4bc52da54d6df4] # to [3d52c005916784461d059e64cf2779d49befdb1e] # # patch "work.hh" # from [227aab1abe271d13771af12c5fb16256dc7cea9e] # to [c23d39b71193d9ddc5f715cf60b000e0dae0982a] # ============================================================ --- work.cc fcc2ee2cea02b5a0c4d94d6a8c4bc52da54d6df4 +++ work.cc 3d52c005916784461d059e64cf2779d49befdb1e @@ -13,7 +13,7 @@ #include #include -#include "app_state.hh" +#include "work.hh" #include "basic_io.hh" #include "cset.hh" #include "localized_file_io.hh" @@ -22,9 +22,11 @@ #include "sanity.hh" #include "safe_map.hh" #include "simplestring_xform.hh" -#include "vocab.hh" -#include "work.hh" #include "revision.hh" +#include "inodeprint.hh" +#include "diff_patch.hh" +#include "ui.hh" +#include "charset.hh" using std::deque; using std::exception; @@ -43,90 +45,444 @@ static string const options_file_name("o static string const inodeprints_file_name("inodeprints"); static string const local_dump_file_name("debug"); static string const options_file_name("options"); -static string const work_file_name("work"); static string const user_log_file_name("log"); +static string const revision_file_name("revision"); +static void +get_revision_path(bookkeeping_path & m_path) +{ + m_path = bookkeeping_root / revision_file_name; + L(FL("revision path is %s") % m_path); +} -// attribute map file +static void +get_options_path(bookkeeping_path & o_path) +{ + o_path = bookkeeping_root / options_file_name; + L(FL("options path is %s") % o_path); +} +static void +get_inodeprints_path(bookkeeping_path & ip_path) +{ + ip_path = bookkeeping_root / inodeprints_file_name; + L(FL("inodeprints path is %s") % ip_path); +} + +// routines for manipulating the bookkeeping directory + +// revision file contains a partial revision describing the workspace +static void +get_work_rev(revision_t & rev) +{ + bookkeeping_path rev_path; + get_revision_path(rev_path); + data rev_data; + MM(rev_data); + try + { + read_data(rev_path, rev_data); + } + catch(exception & e) + { + E(false, F("workspace is corrupt: reading %s: %s") + % rev_path % e.what()); + } + + read_revision(rev_data, rev); + // Currently the revision must have only one ancestor. + I(rev.edges.size() == 1); + + // Mark it so it doesn't creep into the database. + rev.made_for = made_for_workspace; +} + void -file_itemizer::visit_dir(file_path const & path) +workspace::put_work_rev(revision_t const & rev) { - this->visit_file(path); + // Currently the revision must have only one ancestor. + MM(rev); + I(rev.edges.size() == 1); + I(rev.made_for == made_for_workspace); + rev.check_sane(); + + data rev_data; + write_revision(rev, rev_data); + + bookkeeping_path rev_path; + get_revision_path(rev_path); + write_data(rev_path, rev_data); } + +// work file containing rearrangement from uncommitted adds/drops/renames void -file_itemizer::visit_file(file_path const & path) +workspace::get_work_cset(cset & w) { - split_path sp; - path.split(sp); + revision_t rev; + get_work_rev(rev); - if (mask.includes(sp) && known.find(sp) == known.end()) + w = edge_changes(rev.edges.begin()); +} + +// base revision ID +void +workspace::get_revision_id(revision_id & c) +{ + revision_t rev; + get_work_rev(rev); + c = edge_old_revision(rev.edges.begin()); +} + +// structures derived from the work revision, the database, and possibly +// the workspace +void +workspace::get_base_revision(revision_id & rid, + roster_t & ros, + marking_map & mm) +{ + get_revision_id(rid); + + if (!null_id(rid)) { - if (app.lua.hook_ignore_file(path) || app.db.is_dbfile(path)) - ignored.insert(sp); - else - unknown.insert(sp); + + N(db.revision_exists(rid), + F("base revision %s does not exist in database") % rid); + + db.get_roster(rid, ros, mm); } + + L(FL("base roster has %d entries") % ros.all_nodes().size()); } +void +workspace::get_base_revision(revision_id & rid, + roster_t & ros) +{ + marking_map mm; + get_base_revision(rid, ros, mm); +} void -find_missing(roster_t const & new_roster_shape, node_restriction const & mask, - path_set & missing) +workspace::get_base_roster(roster_t & ros) { - node_map const & nodes = new_roster_shape.all_nodes(); - for (node_map::const_iterator i = nodes.begin(); i != nodes.end(); ++i) + revision_id rid; + marking_map mm; + get_base_revision(rid, ros, mm); +} + +void +workspace::get_current_roster_shape(roster_t & ros, node_id_source & nis) +{ + get_base_roster(ros); + cset cs; + get_work_cset(cs); + editable_roster_base er(ros, nis); + cs.apply_to(er); +} + +void +workspace::get_base_and_current_roster_shape(roster_t & base_roster, + roster_t & current_roster, + node_id_source & nis) +{ + get_base_roster(base_roster); + current_roster = base_roster; + cset cs; + get_work_cset(cs); + editable_roster_base er(current_roster, nis); + cs.apply_to(er); +} + +// user log file + +void +workspace::get_user_log_path(bookkeeping_path & ul_path) +{ + ul_path = bookkeeping_root / user_log_file_name; + L(FL("user log path is %s") % ul_path); +} + +void +workspace::read_user_log(utf8 & dat) +{ + bookkeeping_path ul_path; + get_user_log_path(ul_path); + + if (file_exists(ul_path)) { - node_id nid = i->first; + data tmp; + read_data(ul_path, tmp); + system_to_utf8(external(tmp()), dat); + } +} - if (!new_roster_shape.is_root(nid) && mask.includes(new_roster_shape, nid)) +void +workspace::write_user_log(utf8 const & dat) +{ + bookkeeping_path ul_path; + get_user_log_path(ul_path); + + external tmp; + utf8_to_system(dat, tmp); + write_data(ul_path, data(tmp())); +} + +void +workspace::blank_user_log() +{ + data empty; + bookkeeping_path ul_path; + get_user_log_path(ul_path); + write_data(ul_path, empty); +} + +bool +workspace::has_contents_user_log() +{ + utf8 user_log_message; + read_user_log(user_log_message); + return user_log_message().length() > 0; +} + +// _MTN/options handling. + +void +workspace::get_ws_options(utf8 & database_option, + utf8 & branch_option, + utf8 & key_option, + utf8 & keydir_option) +{ + bookkeeping_path o_path; + get_options_path(o_path); + try + { + if (path_exists(o_path)) { - split_path sp; - new_roster_shape.get_name(nid, sp); - file_path fp(sp); + data dat; + read_data(o_path, dat); - if (!path_exists(fp)) - missing.insert(sp); + basic_io::input_source src(dat(), o_path.as_external()); + basic_io::tokenizer tok(src); + basic_io::parser parser(tok); + + while (parser.symp()) + { + string opt, val; + parser.sym(opt); + parser.str(val); + + if (opt == "database") + database_option = val; + else if (opt == "branch") + branch_option = val; + else if (opt == "key") + key_option = val; + else if (opt == "keydir") + keydir_option =val; + else + W(F("unrecognized key '%s' in options file %s - ignored") + % opt % o_path); + } } } + catch(exception & e) + { + W(F("Failed to read options file %s: %s") % o_path % e.what()); + } } void -find_unknown_and_ignored(app_state & app, path_restriction const & mask, - vector const & roots, - path_set & unknown, path_set & ignored) +workspace::set_ws_options(utf8 & database_option, + utf8 & branch_option, + utf8 & key_option, + utf8 & keydir_option) { - revision_t rev; - roster_t new_roster; - path_set known; - temp_node_id_source nis; + // If caller passes an empty string for any of the incoming options, + // we want to leave that option as is in _MTN/options, not write out + // an empty option. + utf8 old_database_option, old_branch_option; + utf8 old_key_option, old_keydir_option; + get_ws_options(old_database_option, old_branch_option, + old_key_option, old_keydir_option); - get_current_roster_shape(new_roster, nis, app); + if (database_option().empty()) + database_option = old_database_option; + if (branch_option().empty()) + branch_option = old_branch_option; + if (key_option().empty()) + key_option = old_key_option; + if (keydir_option().empty()) + keydir_option = old_keydir_option; - new_roster.extract_path_set(known); + basic_io::stanza st; + if (!database_option().empty()) + st.push_str_pair(string("database"), database_option()); + if (!branch_option().empty()) + st.push_str_pair(string("branch"), branch_option()); + if (!key_option().empty()) + st.push_str_pair(string("key"), key_option()); + if (!keydir_option().empty()) + st.push_str_pair(string("keydir"), keydir_option()); - file_itemizer u(app, known, unknown, ignored, mask); - for (vector::const_iterator - i = roots.begin(); i != roots.end(); ++i) + basic_io::printer pr; + pr.print_stanza(st); + + bookkeeping_path o_path; + get_options_path(o_path); + try { - walk_tree(*i, u); + write_data(o_path, pr.buf); } + catch(exception & e) + { + W(F("Failed to write options file %s: %s") % o_path % e.what()); + } } +// local dump file +void +workspace::get_local_dump_path(bookkeeping_path & d_path) +{ + d_path = bookkeeping_root / local_dump_file_name; + L(FL("local dump path is %s") % d_path); +} + +// inodeprint file + +static bool +in_inodeprints_mode() +{ + bookkeeping_path ip_path; + get_inodeprints_path(ip_path); + return file_exists(ip_path); +} + +static void +read_inodeprints(data & dat) +{ + I(in_inodeprints_mode()); + bookkeeping_path ip_path; + get_inodeprints_path(ip_path); + read_data(ip_path, dat); +} + +static void +write_inodeprints(data const & dat) +{ + I(in_inodeprints_mode()); + bookkeeping_path ip_path; + get_inodeprints_path(ip_path); + write_data(ip_path, dat); +} + +void +workspace::enable_inodeprints() +{ + bookkeeping_path ip_path; + get_inodeprints_path(ip_path); + data dat; + write_data(ip_path, dat); +} + +void +workspace::maybe_update_inodeprints() +{ + if (!in_inodeprints_mode()) + return; + + inodeprint_map ipm_new; + temp_node_id_source nis; + roster_t old_roster, new_roster; + + get_base_and_current_roster_shape(old_roster, new_roster, nis); + update_current_roster_from_filesystem(new_roster); + + node_map const & new_nodes = new_roster.all_nodes(); + for (node_map::const_iterator i = new_nodes.begin(); i != new_nodes.end(); ++i) + { + node_id nid = i->first; + if (old_roster.has_node(nid)) + { + node_t old_node = old_roster.get_node(nid); + if (is_file_t(old_node)) + { + node_t new_node = i->second; + I(is_file_t(new_node)); + + file_t old_file = downcast_to_file_t(old_node); + file_t new_file = downcast_to_file_t(new_node); + + if (new_file->content == old_file->content) + { + split_path sp; + new_roster.get_name(nid, sp); + file_path fp(sp); + hexenc ip; + if (inodeprint_file(fp, ip)) + ipm_new.insert(inodeprint_entry(fp, ip)); + } + } + } + } + data dat; + write_inodeprint_map(ipm_new, dat); + write_inodeprints(dat); +} + +// objects and routines for manipulating the workspace itself +namespace { + +struct file_itemizer : public tree_walker +{ + database & db; + lua_hooks & lua; + path_set & known; + path_set & unknown; + path_set & ignored; + path_restriction const & mask; + file_itemizer(database & db, lua_hooks & lua, + path_set & k, path_set & u, path_set & i, + path_restriction const & r) + : db(db), lua(lua), known(k), unknown(u), ignored(i), mask(r) {} + virtual void visit_dir(file_path const & path); + virtual void visit_file(file_path const & path); +}; + +void +file_itemizer::visit_dir(file_path const & path) +{ + this->visit_file(path); +} + +void +file_itemizer::visit_file(file_path const & path) +{ + split_path sp; + path.split(sp); + + if (mask.includes(sp) && known.find(sp) == known.end()) + { + if (lua.hook_ignore_file(path) || db.is_dbfile(path)) + ignored.insert(sp); + else + unknown.insert(sp); + } +} + class addition_builder : public tree_walker { - app_state & app; + database & db; + lua_hooks & lua; roster_t & ros; editable_roster_base & er; public: - addition_builder(app_state & a, - roster_t & r, - editable_roster_base & e) - : app(a), ros(r), er(e) + addition_builder(database & db, lua_hooks & lua, + roster_t & r, editable_roster_base & e) + : db(db), lua(lua), ros(r), er(e) {} virtual void visit_dir(file_path const & path); virtual void visit_file(file_path const & path); @@ -146,7 +502,7 @@ addition_builder::add_node_for(split_pat case path::file: { file_id ident; - I(ident_existing_file(path, ident, app.lua)); + I(ident_existing_file(path, ident, lua)); nid = er.create_file_node(ident); } break; @@ -159,7 +515,7 @@ addition_builder::add_node_for(split_pat er.attach_node(nid, sp); map attrs; - app.lua.hook_init_attributes(path, attrs); + lua.hook_init_attributes(path, attrs); if (attrs.size() > 0) { for (map::const_iterator i = attrs.begin(); @@ -178,7 +534,7 @@ addition_builder::visit_file(file_path c void addition_builder::visit_file(file_path const & path) { - if (app.lua.hook_ignore_file(path) || app.db.is_dbfile(path)) + if (lua.hook_ignore_file(path) || db.is_dbfile(path)) { P(F("skipping ignorable file %s") % path); return; @@ -203,20 +559,491 @@ addition_builder::visit_file(file_path c P(F("adding %s to workspace manifest") % file_path(prefix)); add_node_for(prefix); } + if (!is_dir_t(ros.get_node(prefix))) + { + N(prefix == sp, + F("cannot add %s, because %s is recorded as a file in the workspace manifest") + % file_path(sp) % file_path(sp)); + break; + } } } +struct editable_working_tree : public editable_tree +{ + editable_working_tree(lua_hooks & lua, content_merge_adaptor const & source) + : lua(lua), source(source), next_nid(1), root_dir_attached(true) + {}; + + virtual node_id detach_node(split_path const & src); + virtual void drop_detached_node(node_id nid); + + virtual node_id create_dir_node(); + virtual node_id create_file_node(file_id const & content); + virtual void attach_node(node_id nid, split_path const & dst); + + virtual void apply_delta(split_path const & pth, + file_id const & old_id, + file_id const & new_id); + virtual void clear_attr(split_path const & pth, + attr_key const & name); + virtual void set_attr(split_path const & pth, + attr_key const & name, + attr_value const & val); + + virtual void commit(); + + virtual ~editable_working_tree(); +private: + lua_hooks & lua; + content_merge_adaptor const & source; + node_id next_nid; + std::map written_content; + std::map rename_add_drop_map; + bool root_dir_attached; +}; + + +struct content_merge_empty_adaptor : public content_merge_adaptor +{ + virtual void get_version(file_path const &, + file_id const &, file_data &) const + { I(false); } + virtual void record_merge(file_id const &, file_id const &, + file_id const &, file_data const &, + file_data const &) + { I(false); } + virtual void get_ancestral_roster(node_id, boost::shared_ptr &) + { I(false); } +}; + +// editable_working_tree implementation + +static inline bookkeeping_path +path_for_nid(node_id nid) +{ + return bookkeeping_root / "tmp" / lexical_cast(nid); +} + +// Attaching/detaching the root directory: +// This is tricky, because we don't want to simply move it around, like +// other directories. That would require some very snazzy handling of the +// _MTN directory, and never be possible on windows anyway[1]. So, what we do +// is fake it -- whenever we want to move the root directory into the +// temporary dir, we instead create a new dir in the temporary dir, move +// all of the root's contents into this new dir, and make a note that the root +// directory is logically non-existent. Whenever we want to move some +// directory out of the temporary dir and onto the root directory, we instead +// check that the root is logically nonexistent, move its contents, and note +// that it exists again. +// +// [1] Because the root directory is our working directory, and thus locked in +// place. We _could_ chdir out, then move _MTN out, then move the real root +// directory into our newly-moved _MTN, etc., but aside from being very finicky, +// this would require that we know our root directory's name relative to its +// parent. + +node_id +editable_working_tree::detach_node(split_path const & src) +{ + I(root_dir_attached); + node_id nid = next_nid++; + file_path src_pth(src); + bookkeeping_path dst_pth = path_for_nid(nid); + safe_insert(rename_add_drop_map, make_pair(dst_pth, src_pth)); + make_dir_for(dst_pth); + if (src_pth == file_path()) + { + // root dir detach, so we move contents, rather than the dir itself + mkdir_p(dst_pth); + vector files, dirs; + read_directory(src_pth, files, dirs); + for (vector::const_iterator i = files.begin(); i != files.end(); ++i) + move_file(src_pth / (*i)(), dst_pth / (*i)()); + for (vector::const_iterator i = dirs.begin(); i != dirs.end(); ++i) + if (!bookkeeping_path::is_bookkeeping_path((*i)())) + move_dir(src_pth / (*i)(), dst_pth / (*i)()); + root_dir_attached = false; + } + else + move_path(src_pth, dst_pth); + return nid; +} + void -perform_additions(path_set const & paths, app_state & app, bool recursive) +editable_working_tree::drop_detached_node(node_id nid) { - if (paths.empty()) + bookkeeping_path pth = path_for_nid(nid); + map::const_iterator i + = rename_add_drop_map.find(pth); + I(i != rename_add_drop_map.end()); + P(F("dropping %s") % i->second); + safe_erase(rename_add_drop_map, pth); + delete_file_or_dir_shallow(pth); +} + +node_id +editable_working_tree::create_dir_node() +{ + node_id nid = next_nid++; + bookkeeping_path pth = path_for_nid(nid); + require_path_is_nonexistent(pth, + F("path %s already exists") % pth); + mkdir_p(pth); + return nid; +} + +node_id +editable_working_tree::create_file_node(file_id const & content) +{ + node_id nid = next_nid++; + bookkeeping_path pth = path_for_nid(nid); + require_path_is_nonexistent(pth, + F("path %s already exists") % pth); + safe_insert(written_content, make_pair(pth, content)); + // Defer actual write to moment of attachment, when we know the path + // and can thus determine encoding / linesep convention. + return nid; +} + +void +editable_working_tree::attach_node(node_id nid, split_path const & dst) +{ + bookkeeping_path src_pth = path_for_nid(nid); + file_path dst_pth(dst); + + // Possibly just write data out into the workspace, if we're doing + // a file-create (not a dir-create or file/dir rename). + if (!path_exists(src_pth)) + { + I(root_dir_attached); + map::const_iterator i + = written_content.find(src_pth); + if (i != written_content.end()) + { + P(F("adding %s") % dst_pth); + file_data dat; + source.get_version(dst_pth, i->second, dat); + write_localized_data(dst_pth, dat.inner(), lua); + return; + } + } + + // FIXME: it is weird to do this here, instead of up above, but if we do it + // up above a lot of tests break. those tests are arguably broken -- they + // depend on 'update' clobbering existing, non-versioned files -- but + // putting this up there doesn't actually help, since if we abort in the + // middle of an update to avoid clobbering a file, we just end up leaving + // the working copy in an inconsistent state instead. so for now, we leave + // this check down here. + require_path_is_nonexistent(dst_pth, + F("path '%s' already exists, cannot create") % dst_pth); + + // If we get here, we're doing a file/dir rename, or a dir-create. + map::const_iterator i + = rename_add_drop_map.find(src_pth); + if (i != rename_add_drop_map.end()) + { + P(F("renaming %s to %s") % i->second % dst_pth); + safe_erase(rename_add_drop_map, src_pth); + } + else + P(F("adding %s") % dst_pth); + if (dst_pth == file_path()) + { + // root dir attach, so we move contents, rather than the dir itself + vector files, dirs; + read_directory(src_pth, files, dirs); + for (vector::const_iterator i = files.begin(); i != files.end(); ++i) + { + I(!bookkeeping_path::is_bookkeeping_path((*i)())); + move_file(src_pth / (*i)(), dst_pth / (*i)()); + } + for (vector::const_iterator i = dirs.begin(); i != dirs.end(); ++i) + { + I(!bookkeeping_path::is_bookkeeping_path((*i)())); + move_dir(src_pth / (*i)(), dst_pth / (*i)()); + } + delete_dir_shallow(src_pth); + root_dir_attached = true; + } + else + // This will complain if the move is actually impossible + move_path(src_pth, dst_pth); +} + +void +editable_working_tree::apply_delta(split_path const & pth, + file_id const & old_id, + file_id const & new_id) +{ + file_path pth_unsplit(pth); + require_path_is_file(pth_unsplit, + F("file '%s' does not exist") % pth_unsplit, + F("file '%s' is a directory") % pth_unsplit); + hexenc curr_id_raw; + calculate_ident(pth_unsplit, curr_id_raw, lua); + file_id curr_id(curr_id_raw); + E(curr_id == old_id, + F("content of file '%s' has changed, not overwriting") % pth_unsplit); + P(F("modifying %s") % pth_unsplit); + + file_data dat; + source.get_version(pth_unsplit, new_id, dat); + write_localized_data(pth_unsplit, dat.inner(), lua); +} + +void +editable_working_tree::clear_attr(split_path const & pth, + attr_key const & name) +{ + file_path pth_unsplit(pth); + app.lua.hook_apply_attribute(name(), pth_unsplit, string(""), true); +} + +void +editable_working_tree::set_attr(split_path const & pth, + attr_key const & name, + attr_value const & val) +{ + file_path pth_unsplit(pth); + app.lua.hook_apply_attribute(name(), pth_unsplit, val(), false); +} + +void +editable_working_tree::commit() +{ + I(rename_add_drop_map.empty()); + I(root_dir_attached); +} + +editable_working_tree::~editable_working_tree() +{ +} + +}; // anonymous namespace + +static void +add_parent_dirs(split_path const & dst, roster_t & ros, node_id_source & nis, + database & db, lua_hooks & lua) +{ + editable_roster_base er(ros, nis); + addition_builder build(db, lua, ros, er); + + split_path dirname; + path_component basename; + dirname_basename(dst, dirname, basename); + + // FIXME: this is a somewhat odd way to use the builder + build.visit_dir(dirname); +} + +inline static bool +inodeprint_unchanged(inodeprint_map const & ipm, file_path const & path) +{ + inodeprint_map::const_iterator old_ip = ipm.find(path); + if (old_ip != ipm.end()) + { + hexenc ip; + if (inodeprint_file(path, ip) && ip == old_ip->second) + return true; // unchanged + else + return false; // changed or unavailable + } + else + return false; // unavailable +} + +// updating rosters from the workspace + +// TODO: unchanged, changed, missing might be better as set + +// note that this does not take a restriction because it is used only by +// automate_inventory which operates on the entire, unrestricted, working +// directory. + +void +workspace::classify_roster_paths(roster_t const & ros, + path_set & unchanged, + path_set & changed, + path_set & missing) +{ + temp_node_id_source nis; + inodeprint_map ipm; + + if (in_inodeprints_mode()) + { + data dat; + read_inodeprints(dat); + read_inodeprint_map(dat, ipm); + } + + // this code is speed critical, hence the use of inode fingerprints so be + // careful when making changes in here and preferably do some timing tests + + if (!ros.has_root()) return; - std::vector include_paths; + node_map const & nodes = ros.all_nodes(); + for (node_map::const_iterator i = nodes.begin(); i != nodes.end(); ++i) + { + node_id nid = i->first; + node_t node = i->second; + split_path sp; + ros.get_name(nid, sp); + + file_path fp(sp); + + if (is_dir_t(node) || inodeprint_unchanged(ipm, fp)) + { + // dirs don't have content changes + unchanged.insert(sp); + } + else + { + file_t file = downcast_to_file_t(node); + file_id fid; + if (ident_existing_file(fp, fid, lua)) + { + if (file->content == fid) + unchanged.insert(sp); + else + changed.insert(sp); + } + else + { + missing.insert(sp); + } + } + } +} + +void +workspace::update_current_roster_from_filesystem(roster_t & ros) +{ + update_current_roster_from_filesystem(ros, node_restriction()); +} + +void +workspace::update_current_roster_from_filesystem(roster_t & ros, + node_restriction const & mask) +{ temp_node_id_source nis; + inodeprint_map ipm; + + if (in_inodeprints_mode()) + { + data dat; + read_inodeprints(dat); + read_inodeprint_map(dat, ipm); + } + + size_t missing_files = 0; + + // this code is speed critical, hence the use of inode fingerprints so be + // careful when making changes in here and preferably do some timing tests + + if (!ros.has_root()) + return; + + node_map const & nodes = ros.all_nodes(); + for (node_map::const_iterator i = nodes.begin(); i != nodes.end(); ++i) + { + node_id nid = i->first; + node_t node = i->second; + + // Only analyze files further, not dirs. + if (! is_file_t(node)) + continue; + + // Only analyze restriction-included files. + if (!mask.includes(ros, nid)) + continue; + + split_path sp; + ros.get_name(nid, sp); + file_path fp(sp); + + // Only analyze changed files (or all files if inodeprints mode + // is disabled). + if (inodeprint_unchanged(ipm, fp)) + continue; + + file_t file = downcast_to_file_t(node); + if (!ident_existing_file(fp, file->content, lua)) + { + W(F("missing %s") % (fp)); + missing_files++; + } + } + + N(missing_files == 0, + F("%d missing files; use '%s ls missing' to view\n" + "To restore consistency, on each missing file run either\n" + " '%s drop FILE' to remove it permanently, or\n" + " '%s revert FILE' to restore it.\n" + "To handle all at once, simply use\n" + " '%s drop --missing' or\n" + " '%s revert --missing'") + % missing_files % ui.prog_name % ui.prog_name % ui.prog_name + % ui.prog_name % ui.prog_name); +} + +void +workspace::find_missing(roster_t const & new_roster_shape, + node_restriction const & mask, + path_set & missing) +{ + node_map const & nodes = new_roster_shape.all_nodes(); + for (node_map::const_iterator i = nodes.begin(); i != nodes.end(); ++i) + { + node_id nid = i->first; + + if (!new_roster_shape.is_root(nid) && mask.includes(new_roster_shape, nid)) + { + split_path sp; + new_roster_shape.get_name(nid, sp); + file_path fp(sp); + + if (!path_exists(fp)) + missing.insert(sp); + } + } +} + +void +workspace::find_unknown_and_ignored(path_restriction const & mask, + vector const & roots, + path_set & unknown, path_set & ignored) +{ + path_set known; + roster_t new_roster; + temp_node_id_source nis; + + get_current_roster_shape(new_roster, nis); + + new_roster.extract_path_set(known); + + file_itemizer u(db, lua, known, unknown, ignored, mask); + for (vector::const_iterator + i = roots.begin(); i != roots.end(); ++i) + { + walk_tree(*i, u); + } +} + +void +workspace::perform_additions(path_set const & paths, bool recursive) +{ + if (paths.empty()) + return; + + temp_node_id_source nis; roster_t base_roster, new_roster; - get_base_and_current_roster_shape(base_roster, new_roster, nis, app); + MM(base_roster); + MM(new_roster); + get_base_and_current_roster_shape(base_roster, new_roster, nis); editable_roster_base er(new_roster, nis); @@ -228,7 +1055,7 @@ perform_additions(path_set const & paths } I(new_roster.has_root()); - addition_builder build(app, new_roster, er); + addition_builder build(db, lua, new_roster, er); for (path_set::const_iterator i = paths.begin(); i != paths.end(); ++i) { @@ -236,33 +1063,35 @@ perform_additions(path_set const & paths { // NB.: walk_tree will handle error checking for non-existent paths walk_tree(file_path(*i), build); - include_paths.push_back(file_path(*i)); } else { // in the case where we're just handled a set of paths, we use the builder // in this strange way. build.visit_file(file_path(*i)); - include_paths.push_back(file_path(*i)); } } - cset new_work; - make_cset(base_roster, new_roster, new_work); - put_work_cset(new_work); + revision_id base_rev; + get_revision_id(base_rev); + + revision_t new_work; + make_revision_for_workspace(base_rev, base_roster, new_roster, new_work); + put_work_rev(new_work); } void -perform_deletions(path_set const & paths, app_state & app) +workspace::perform_deletions(path_set const & paths, + bool recursive, bool execute) { if (paths.empty()) return; - std::vector include_paths; - temp_node_id_source nis; roster_t base_roster, new_roster; - get_base_and_current_roster_shape(base_roster, new_roster, nis, app); + MM(base_roster); + MM(new_roster); + get_base_and_current_roster_shape(base_roster, new_roster, nis); // we traverse the the paths backwards, so that we always hit deep paths // before shallow paths (because path_set is lexicographically sorted). @@ -280,7 +1109,6 @@ perform_deletions(path_set const & paths { split_path &p(todo.front()); file_path name(p); - include_paths.push_back(name); if (!new_roster.has_node(p)) P(F("skipping %s, not currently tracked") % name); @@ -292,7 +1120,7 @@ perform_deletions(path_set const & paths dir_t d = downcast_to_dir_t(n); if (!d->children.empty()) { - N(app.recursive, + N(recursive, F("cannot remove %s/, it is not empty") % name); for (dir_map::const_iterator j = d->children.begin(); j != d->children.end(); ++j) @@ -306,7 +1134,7 @@ perform_deletions(path_set const & paths } P(F("dropping %s from workspace manifest") % name); new_roster.drop_detached_node(new_roster.detach_node(p)); - if (app.execute && path_exists(name)) + if (execute && path_exists(name)) delete_file_or_dir_shallow(name); } todo.pop_front(); @@ -317,126 +1145,30 @@ perform_deletions(path_set const & paths } } - cset new_work; - make_cset(base_roster, new_roster, new_work); - put_work_cset(new_work); -} + revision_id base_rev; + get_revision_id(base_rev); -void -perform_attr_scan(std::vector const & paths, app_state & app) -{ - - std::vector init_functions; - bool get_update_func_ok = app.lua.hook_list_init_functions(init_functions); - E(get_update_func_ok, F("Failed to find attribute init functions")); - if (!get_update_func_ok) - { - return; - } - - roster_t old_roster, new_roster; - temp_node_id_source nis; - - app.require_workspace(); - get_base_and_current_roster_shape(old_roster, new_roster, nis, app); - - node_restriction mask(paths, app.get_exclude_paths(), new_roster, app); - editable_roster_base er(new_roster, nis); - - P(F("scanning filesystem for attributes")); - - node_map const & nodes = new_roster.all_nodes(); - for (node_map::const_iterator i = nodes.begin(); i != nodes.end(); ++i) - { - node_id nid = i->first; - node_t node = i->second; - - // Only analyze restriction-included files. - if (!mask.includes(new_roster, nid)) - continue; - - split_path sp; - new_roster.get_name(nid, sp); - file_path name(sp); - - L(FL("%s") % name); - - std::pair curval; - std::pair getval; - bool luaok; - - if (path_exists(name)) - { - for (std::vector::const_iterator i = init_functions.begin(); - i != init_functions.end(); ++i) - { - curval = node->attrs[attr_key(*i)]; - if (curval.first) - { - L(FL("attribute '%s' currently is '%s'") % *i % curval.second); - } - else - { - L(FL("attribute '%s' is currently unset") % *i); - } - - luaok = app.lua.hook_scan_attribute(*i, name, getval); - E(luaok, F("error doing lua hook_scan_attribute for attribute %s") % *i); - if (getval.first) - { - if (curval.first && (curval.second() == getval.second)) - { - L(FL("skipping; filesystem matches recorded workspace.")); - continue; - } - else - { - L(FL("setting attribute to '%s'") % getval.second); - er.set_attr(sp, attr_key(*i), attr_value(getval.second)); - } } - else - { - L(FL("clearing attribute")); - er.clear_attr(sp, attr_key(*i)); - } - } - } - } - cset new_work; - make_cset(old_roster, new_roster, new_work); - put_work_cset(new_work); + revision_t new_work; + make_revision_for_workspace(base_rev, base_roster, new_roster, new_work); + put_work_rev(new_work); } -static void -add_parent_dirs(split_path const & dst, roster_t & ros, node_id_source & nis, - app_state & app) -{ - editable_roster_base er(ros, nis); - addition_builder build(app, ros, er); - - split_path dirname; - path_component basename; - dirname_basename(dst, dirname, basename); - - // FIXME: this is a somewhat odd way to use the builder - build.visit_dir(dirname); -} - void -perform_rename(set const & src_paths, - file_path const & dst_path, - app_state & app) +workspace::perform_rename(set const & src_paths, + file_path const & dst_path, + bool execute) { temp_node_id_source nis; roster_t base_roster, new_roster; + MM(base_roster); + MM(new_roster); split_path dst; set srcs; set< pair > renames; - std::vector fpvect; I(!src_paths.empty()); - get_base_and_current_roster_shape(base_roster, new_roster, nis, app); + get_base_and_current_roster_shape(base_roster, new_roster, nis); dst_path.split(dst); @@ -446,7 +1178,7 @@ perform_rename(set const & sr split_path s; src_paths.begin()->split(s); renames.insert( make_pair(s, dst) ); - add_parent_dirs(dst, new_roster, nis, app); + add_parent_dirs(dst, new_roster, nis, db, lua); } else { @@ -479,10 +1211,18 @@ perform_rename(set const & sr i != renames.end(); i++) { N(new_roster.has_node(i->first), - F("%s does not exist in current revision") % file_path(i->first)); + F("%s does not exist in current manifest") % file_path(i->first)); N(!new_roster.has_node(i->second), - F("destination %s already exists in current revision") % file_path(i->second)); + F("destination %s already exists in current manifest") % file_path(i->second)); + + split_path parent; + path_component basename; + dirname_basename(i->second, parent, basename); + N(new_roster.has_node(parent), + F("destination directory %s does not exist in current manifest") % file_path(parent)); + N(is_dir_t(new_roster.get_node(parent)), + F("destination directory %s is not a directory") % file_path(parent)); } // do the attach/detaching @@ -496,11 +1236,14 @@ perform_rename(set const & sr % file_path(i->second)); } - cset new_work; - make_cset(base_roster, new_roster, new_work); - put_work_cset(new_work); + revision_id base_rev; + get_revision_id(base_rev); - if (app.execute) + revision_t new_work; + make_revision_for_workspace(base_rev, base_roster, new_roster, new_work); + put_work_rev(new_work); + + if (execute) { for (set< pair >::const_iterator i = renames.begin(); i != renames.end(); i++) @@ -513,7 +1256,6 @@ perform_rename(set const & sr if (have_src && !have_dst) { move_path(s, d); - fpvect.push_back(d); } else if (!have_src && !have_dst) { @@ -529,14 +1271,13 @@ perform_rename(set const & sr % s % d); } } - if (fpvect.size() > 0) - update_any_attrs(fpvect, app); } } void -perform_pivot_root(file_path const & new_root, file_path const & put_old, - app_state & app) +workspace::perform_pivot_root(file_path const & new_root, + file_path const & put_old, + bool execute) { split_path new_root_sp, put_old_sp, root_sp; new_root.split(new_root_sp); @@ -545,7 +1286,9 @@ perform_pivot_root(file_path const & new temp_node_id_source nis; roster_t base_roster, new_roster; - get_base_and_current_roster_shape(base_roster, new_roster, nis, app); + MM(base_roster); + MM(new_roster); + get_base_and_current_roster_shape(base_roster, new_roster, nis); I(new_roster.has_root()); N(new_roster.has_node(new_root_sp), @@ -583,343 +1326,33 @@ perform_pivot_root(file_path const & new editable_roster_base e(new_roster, nis); cs.apply_to(e); } - { - cset new_work; - make_cset(base_roster, new_roster, new_work); - put_work_cset(new_work); - } - if (app.execute) - { - empty_file_content_source efcs; - editable_working_tree e(app, efcs); - cs.apply_to(e); - } -} + { + revision_id base_rev; + get_revision_id(base_rev); -// work file containing rearrangement from uncommitted adds/drops/renames - -static void get_work_path(bookkeeping_path & w_path) -{ - w_path = bookkeeping_root / work_file_name; - L(FL("work path is %s") % w_path); -} - -void get_work_cset(cset & w) -{ - bookkeeping_path w_path; - get_work_path(w_path); - if (path_exists(w_path)) + revision_t new_work; + make_revision_for_workspace(base_rev, base_roster, new_roster, new_work); + put_work_rev(new_work); + } + if (execute) { - L(FL("checking for un-committed work file %s") % w_path); - data w_data; - read_data(w_path, w_data); - read_cset(w_data, w); - L(FL("read cset from %s") % w_path); + content_merge_empty_adaptor cmea; + perform_content_update(cs, cmea); } - else - { - L(FL("no un-committed work file %s") % w_path); - } } -void remove_work_cset() -{ - bookkeeping_path w_path; - get_work_path(w_path); - if (file_exists(w_path)) - delete_file(w_path); -} - -void put_work_cset(cset & w) -{ - bookkeeping_path w_path; - get_work_path(w_path); - - if (w.empty()) - { - if (file_exists(w_path)) - delete_file(w_path); - } - else - { - data w_data; - write_cset(w, w_data); - write_data(w_path, w_data); - } -} - -// revision file name - -string revision_file_name("revision"); - -static void get_revision_path(bookkeeping_path & m_path) -{ - m_path = bookkeeping_root / revision_file_name; - L(FL("revision path is %s") % m_path); -} - -void get_revision_id(revision_id & c) -{ - c = revision_id(); - bookkeeping_path c_path; - get_revision_path(c_path); - - require_path_is_file(c_path, - F("workspace is corrupt: %s does not exist") % c_path, - F("workspace is corrupt: %s is a directory") % c_path); - - data c_data; - L(FL("loading revision id from %s") % c_path); - try - { - read_data(c_path, c_data); - } - catch(exception &) - { - N(false, F("Problem with workspace: %s is unreadable") % c_path); - } - c = revision_id(remove_ws(c_data())); -} - -void put_revision_id(revision_id const & rev) -{ - bookkeeping_path c_path; - get_revision_path(c_path); - L(FL("writing revision id to %s") % c_path); - data c_data(rev.inner()() + "\n"); - write_data(c_path, c_data); -} - void -get_base_revision(app_state & app, - revision_id & rid, - roster_t & ros, - marking_map & mm) +workspace::perform_content_update(cset const & update, + content_merge_adaptor const & ca) { - get_revision_id(rid); - - if (!null_id(rid)) - { - - N(app.db.revision_exists(rid), - F("base revision %s does not exist in database") % rid); - - app.db.get_roster(rid, ros, mm); - } - - L(FL("base roster has %d entries") % ros.all_nodes().size()); + editable_working_tree ewt(lua, ca); + update.apply_to(ewt); } -void -get_base_revision(app_state & app, - revision_id & rid, - roster_t & ros) +void +update_any_attrs(std::vector const & include_paths, app_state & app) { - marking_map mm; - get_base_revision(app, rid, ros, mm); -} - -void -get_base_roster(app_state & app, - roster_t & ros) -{ - revision_id rid; - marking_map mm; - get_base_revision(app, rid, ros, mm); -} - -void -get_current_roster_shape(roster_t & ros, node_id_source & nis, app_state & app) -{ - get_base_roster(app, ros); - cset cs; - get_work_cset(cs); - editable_roster_base er(ros, nis); - cs.apply_to(er); -} - -void -get_base_and_current_roster_shape(roster_t & base_roster, - roster_t & current_roster, - node_id_source & nis, - app_state & app) -{ - get_base_roster(app, base_roster); - current_roster = base_roster; - cset cs; - get_work_cset(cs); - editable_roster_base er(current_roster, nis); - cs.apply_to(er); -} - -// user log file - -void -get_user_log_path(bookkeeping_path & ul_path) -{ - ul_path = bookkeeping_root / user_log_file_name; - L(FL("user log path is %s") % ul_path); -} - -void -read_user_log(data & dat) -{ - bookkeeping_path ul_path; - get_user_log_path(ul_path); - - if (file_exists(ul_path)) - { - read_data(ul_path, dat); - } -} - -void -write_user_log(data const & dat) -{ - bookkeeping_path ul_path; - get_user_log_path(ul_path); - - write_data(ul_path, dat); -} - -void -blank_user_log() -{ - data empty; - bookkeeping_path ul_path; - get_user_log_path(ul_path); - write_data(ul_path, empty); -} - -bool -has_contents_user_log() -{ - data user_log_message; - read_user_log(user_log_message); - return user_log_message().length() > 0; -} - -// options map file - -void -get_options_path(bookkeeping_path & o_path) -{ - o_path = bookkeeping_root / options_file_name; - L(FL("options path is %s") % o_path); -} - -void -read_options_map(data const & dat, options_map & options) -{ - basic_io::input_source src(dat(), "_MTN/options"); - basic_io::tokenizer tok(src); - basic_io::parser parser(tok); - - // don't clear the options which will have settings from the command line - // options.clear(); - - string opt, val; - while (parser.symp()) - { - parser.sym(opt); - parser.str(val); - // options[opt] = val; - // use non-replacing insert versus replacing with options[opt] = val; - options.insert(make_pair(opt, val)); - } -} - -void -write_options_map(data & dat, options_map const & options) -{ - basic_io::printer pr; - - basic_io::stanza st; - for (options_map::const_iterator i = options.begin(); - i != options.end(); ++i) - st.push_str_pair(i->first, i->second()); - - pr.print_stanza(st); - dat = pr.buf; -} - -// local dump file - -void get_local_dump_path(bookkeeping_path & d_path) -{ - d_path = bookkeeping_root / local_dump_file_name; - L(FL("local dump path is %s") % d_path); -} - -// inodeprint file - -void -get_inodeprints_path(bookkeeping_path & ip_path) -{ - ip_path = bookkeeping_root / inodeprints_file_name; -} - -bool -in_inodeprints_mode() -{ - bookkeeping_path ip_path; - get_inodeprints_path(ip_path); - return file_exists(ip_path); -} - -void -read_inodeprints(data & dat) -{ - I(in_inodeprints_mode()); - bookkeeping_path ip_path; - get_inodeprints_path(ip_path); - read_data(ip_path, dat); -} - -void -write_inodeprints(data const & dat) -{ - I(in_inodeprints_mode()); - bookkeeping_path ip_path; - get_inodeprints_path(ip_path); - write_data(ip_path, dat); -} - -void -enable_inodeprints() -{ - bookkeeping_path ip_path; - get_inodeprints_path(ip_path); - data dat; - write_data(ip_path, dat); -} - - -bool -get_attribute_from_roster(roster_t const & ros, - file_path const & path, - attr_key const & key, - attr_value & val) -{ - split_path sp; - path.split(sp); - if (ros.has_node(sp)) - { - node_t n = ros.get_node(sp); - full_attr_map_t::const_iterator i = n->attrs.find(key); - if (i != n->attrs.end() && i->second.first) - { - val = i->second.second; - return true; - } - } - return false; -} - - -void update_any_attrs(std::vector const & include_paths, app_state & app) -{ temp_node_id_source nis; roster_t new_roster; get_current_roster_shape(new_roster, nis, app); @@ -964,214 +1397,7 @@ void update_any_attrs(std::vector(nid); -} - -// Attaching/detaching the root directory: -// This is tricky, because we don't want to simply move it around, like -// other directories. That would require some very snazzy handling of the -// _MTN directory, and never be possible on windows anyway[1]. So, what we do -// is fake it -- whenever we want to move the root directory into the -// temporary dir, we instead create a new dir in the temporary dir, move -// all of the root's contents into this new dir, and make a note that the root -// directory is logically non-existent. Whenever we want to move some -// directory out of the temporary dir and onto the root directory, we instead -// check that the root is logically nonexistent, move its contents, and note -// that it exists again. -// -// [1] Because the root directory is our working directory, and thus locked in -// place. We _could_ chdir out, then move _MTN out, then move the real root -// directory into our newly-moved _MTN, etc., but aside from being very finicky, -// this would require that we know our root directory's name relative to its -// parent. - -node_id -editable_working_tree::detach_node(split_path const & src) -{ - I(root_dir_attached); - node_id nid = next_nid++; - file_path src_pth(src); - bookkeeping_path dst_pth = path_for_nid(nid); - safe_insert(rename_add_drop_map, make_pair(dst_pth, src_pth)); - make_dir_for(dst_pth); - if (src_pth == file_path()) - { - // root dir detach, so we move contents, rather than the dir itself - mkdir_p(dst_pth); - vector files, dirs; - read_directory(src_pth, files, dirs); - for (vector::const_iterator i = files.begin(); i != files.end(); ++i) - move_file(src_pth / (*i)(), dst_pth / (*i)()); - for (vector::const_iterator i = dirs.begin(); i != dirs.end(); ++i) - if (!bookkeeping_path::is_bookkeeping_path((*i)())) - move_dir(src_pth / (*i)(), dst_pth / (*i)()); - root_dir_attached = false; - } - else - move_path(src_pth, dst_pth); - return nid; -} - -void -editable_working_tree::drop_detached_node(node_id nid) -{ - bookkeeping_path pth = path_for_nid(nid); - map::const_iterator i - = rename_add_drop_map.find(pth); - I(i != rename_add_drop_map.end()); - P(F("dropping %s") % i->second); - safe_erase(rename_add_drop_map, pth); - delete_file_or_dir_shallow(pth); -} - -node_id -editable_working_tree::create_dir_node() -{ - node_id nid = next_nid++; - bookkeeping_path pth = path_for_nid(nid); - require_path_is_nonexistent(pth, - F("path %s already exists") % pth); - mkdir_p(pth); - return nid; -} - -node_id -editable_working_tree::create_file_node(file_id const & content) -{ - node_id nid = next_nid++; - bookkeeping_path pth = path_for_nid(nid); - require_path_is_nonexistent(pth, - F("path %s already exists") % pth); - safe_insert(written_content, make_pair(pth, content)); - // Defer actual write to moment of attachment, when we know the path - // and can thus determine encoding / linesep convention. - return nid; -} - -void -editable_working_tree::attach_node(node_id nid, split_path const & dst) -{ - bookkeeping_path src_pth = path_for_nid(nid); - file_path dst_pth(dst); - - // Possibly just write data out into the workspace, if we're doing - // a file-create (not a dir-create or file/dir rename). - if (!path_exists(src_pth)) - { - I(root_dir_attached); - map::const_iterator i - = written_content.find(src_pth); - if (i != written_content.end()) - { - P(F("adding %s") % dst_pth); - file_data dat; - source.get_file_content(i->second, dat); - write_localized_data(dst_pth, dat.inner(), app.lua); - return; - } - } - - // FIXME: it is weird to do this here, instead of up above, but if we do it - // up above a lot of tests break. those tests are arguably broken -- they - // depend on 'update' clobbering existing, non-versioned files -- but - // putting this up there doesn't actually help, since if we abort in the - // middle of an update to avoid clobbering a file, we just end up leaving - // the working copy in an inconsistent state instead. so for now, we leave - // this check down here. - require_path_is_nonexistent(dst_pth, - F("path '%s' already exists, cannot create") % dst_pth); - - // If we get here, we're doing a file/dir rename, or a dir-create. - map::const_iterator i - = rename_add_drop_map.find(src_pth); - if (i != rename_add_drop_map.end()) - { - P(F("renaming %s to %s") % i->second % dst_pth); - safe_erase(rename_add_drop_map, src_pth); - } - else - P(F("adding %s") % dst_pth); - if (dst_pth == file_path()) - { - // root dir attach, so we move contents, rather than the dir itself - vector files, dirs; - read_directory(src_pth, files, dirs); - for (vector::const_iterator i = files.begin(); i != files.end(); ++i) - { - I(!bookkeeping_path::is_bookkeeping_path((*i)())); - move_file(src_pth / (*i)(), dst_pth / (*i)()); - } - for (vector::const_iterator i = dirs.begin(); i != dirs.end(); ++i) - { - I(!bookkeeping_path::is_bookkeeping_path((*i)())); - move_dir(src_pth / (*i)(), dst_pth / (*i)()); - } - delete_dir_shallow(src_pth); - root_dir_attached = true; - } - else - // This will complain if the move is actually impossible - move_path(src_pth, dst_pth); -} - -void -editable_working_tree::apply_delta(split_path const & pth, - file_id const & old_id, - file_id const & new_id) -{ - file_path pth_unsplit(pth); - require_path_is_file(pth_unsplit, - F("file '%s' does not exist") % pth_unsplit, - F("file '%s' is a directory") % pth_unsplit); - hexenc curr_id_raw; - calculate_ident(pth_unsplit, curr_id_raw, app.lua); - file_id curr_id(curr_id_raw); - E(curr_id == old_id, - F("content of file '%s' has changed, not overwriting") % pth_unsplit); - P(F("modifying %s") % pth_unsplit); - - file_data dat; - source.get_file_content(new_id, dat); - write_localized_data(pth_unsplit, dat.inner(), app.lua); -} - -void -editable_working_tree::clear_attr(split_path const & pth, - attr_key const & name) -{ - file_path pth_unsplit(pth); - app.lua.hook_apply_attribute(name(), pth_unsplit, string(""), true); -} - -void -editable_working_tree::set_attr(split_path const & pth, - attr_key const & name, - attr_value const & val) -{ - file_path pth_unsplit(pth); - app.lua.hook_apply_attribute(name(), pth_unsplit, val(), false); -} - -void -editable_working_tree::commit() -{ - I(rename_add_drop_map.empty()); - I(root_dir_attached); -} - -editable_working_tree::~editable_working_tree() -{ -} - // Local Variables: // mode: C++ // fill-column: 76 ============================================================ --- work.hh 227aab1abe271d13771af12c5fb16256dc7cea9e +++ work.hh c23d39b71193d9ddc5f715cf60b000e0dae0982a @@ -14,11 +14,9 @@ #include #include -#include "cset.hh" +#include "vocab.hh" #include "paths.hh" #include "roster.hh" -#include "vocab.hh" -#include "file_io.hh" // // this file defines structures to deal with the "workspace" of a tree @@ -30,206 +28,178 @@ // directories). there is no hierarchy of _MTN directories; only one exists, // and it is always at the root. it contains the following files: // -// _MTN/revision -- contains the id of the checked out revision -// _MTN/work -- (optional) a set of added, deleted or moved pathnames -// this file is, syntactically, a cset + +// _MTN/revision -- this file can be thought of as an approximation to the +// revision that would be added to the database if one +// were to execute 'mtn commit' with the current set of +// changes. it records the id of the revision that was +// checked out (the "parent revision") plus a cset +// describing pathname and attribute modifications +// relative to that revision. if the workspace is the +// result of a merge, the revision will have more than +// one parent and thus more than one cset. files +// changed solely in content do not appear in +// _MTN/revision; this is the major difference between +// the revision in this file and the revision that 'mtn +// commit' adds to the database. // _MTN/options -- the database, branch and key options currently in use // _MTN/log -- user edited log file -// _MTN/inodeprints -- file fingerprint cache, presence turns on "reckless" -// mode +// _MTN/inodeprints -- file fingerprint cache, see below // // as work proceeds, the files in the workspace either change their // sha1 fingerprints from those listed in the revision's manifest, or else are // added or deleted or renamed (and the paths of those changes recorded in -// '_MTN/work'). +// '_MTN/revision'). // -// when it comes time to commit, the cset in _MTN/work (which can have no -// deltas) is applied to the base roster, then a new roster is built by -// analyzing the content of every file in the roster, as it appears in the -// workspace. a final cset is calculated which contains the requisite -// deltas, and placed in a rev, which is written to the db. -// -// _MTN/inodeprints, if present, can be used to speed up this last step. +// many operations need to work with a revision that accurately describes +// both pathname and content changes. constructing this revision is the +// function of update_current_roster_from_filesystem(). this operation +// intrinsically requires reading every file in the workspace, which can be +// slow. _MTN/inodeprints, if present, is used to speed up this process; it +// records information accessible via stat() that is expected to change +// whenever a file is modified. this expectation is not true under all +// conditions, but works in practice (it is, for instance, the same +// expectation used by "make"). nonetheless, this mode is off by default. class path_restriction; +class node_restriction; +class content_merge_adaptor; +class database; +class lua_hooks; -struct file_itemizer : public tree_walker +struct workspace { - app_state & app; - path_set & known; - path_set & unknown; - path_set & ignored; - path_restriction const & mask; - file_itemizer(app_state & a, path_set & k, path_set & u, path_set & i, - path_restriction const & r) - : app(a), known(k), unknown(u), ignored(i), mask(r) {} - virtual void visit_dir(file_path const & path); - virtual void visit_file(file_path const & path); -}; + void find_missing(roster_t const & new_roster_shape, + node_restriction const & mask, + path_set & missing); -void -find_missing(roster_t const & new_roster_shape, node_restriction const & mask, - path_set & missing); + void find_unknown_and_ignored(path_restriction const & mask, + std::vector const & roots, + path_set & unknown, path_set & ignored); -void -find_unknown_and_ignored(app_state & app, path_restriction const & mask, - std::vector const & roots, - path_set & unknown, path_set & ignored); + void perform_additions(path_set const & targets, bool recursive = true); -void -perform_additions(path_set const & targets, app_state & app, bool recursive = true); + void perform_deletions(path_set const & targets, bool recursive, + bool execute); -void -perform_attr_scan(std::vector const & paths, app_state &app); + void perform_rename(std::set const & src_paths, + file_path const & dst_dir, + bool execute); -void -perform_deletions(path_set const & targets, app_state & app); + void perform_pivot_root(file_path const & new_root, + file_path const & put_old, + bool execute); -void -perform_rename(std::set const & src_paths, - file_path const & dst_dir, - app_state & app); + void perform_content_update(cset const & cs, + content_merge_adaptor const & ca); -void -perform_pivot_root(file_path const & new_root, file_path const & put_old, - app_state & app); + void update_any_attrs(std::vector const & include_paths, app_state & app); -// the "work" file contains the current cset representing uncommitted -// add/drop/rename operations (not deltas) + // transitional: the write half of this is exposed, the read half isn't. + // write out a new (partial) revision describing the current workspace; + // the important pieces of this are the base revision id and the "shape" + // changeset (representing tree rearrangements). + void put_work_rev(revision_t const & rev); -void get_work_cset(cset & w); -void remove_work_cset(); -void put_work_cset(cset & w); + // the current cset representing uncommitted add/drop/rename operations + // (not deltas) + void get_work_cset(cset & w); -// the "revision" file contains the base revision id that the current working -// copy was checked out from + // the base revision id that the current working copy was checked out from + void get_revision_id(revision_id & c); -void get_revision_id(revision_id & c); -void put_revision_id(revision_id const & rev); -void get_base_revision(app_state & app, - revision_id & rid, - roster_t & ros, - marking_map & mm); -void get_base_revision(app_state & app, - revision_id & rid, - roster_t & ros); -void get_base_roster(app_state & app, roster_t & ros); + // structures derived from the above + void get_base_revision(revision_id & rid, roster_t & ros); + void get_base_revision(revision_id & rid, roster_t & ros, marking_map & mm); + void get_base_roster(roster_t & ros); -// This returns the current roster, except it does not bother updating the -// hashes in that roster -- the "shape" is correct, all files and dirs exist -// and under the correct names -- but do not trust file content hashes. -void get_current_roster_shape(roster_t & ros, node_id_source & nis, app_state & app); + // This returns the current roster, except it does not bother updating the + // hashes in that roster -- the "shape" is correct, all files and dirs exist + // and under the correct names -- but do not trust file content hashes. + // If you need the current roster with correct file content hashes, call + // update_current_roster_from_filesystem on the result of this function. + void get_current_roster_shape(roster_t & ros, node_id_source & nis); -// These returns the current roster, except they do not bother updating the -// hashes in that roster -- the "shape" is correct, all files and dirs exist -// and under the correct names -- but do not trust file content hashes. -void get_base_and_current_roster_shape(roster_t & base_roster, - roster_t & current_roster, - node_id_source & nis, - app_state & app); + // This returns both the base roster (as get_base_roster would) and the + // current roster shape (as get_current_roster_shape would). The caveats + // for get_current_roster_shape also apply to this function. + void get_base_and_current_roster_shape(roster_t & base_roster, + roster_t & current_roster, + node_id_source & nis); -// the "user log" is a file the user can edit as they program to record -// changes they make to their source code. Upon commit the file is read -// and passed to the edit_comment lua hook. If the commit is a success, -// the user log is then blanked. If the commit does not succeed, no -// change is made to the user log file. + void classify_roster_paths(roster_t const & ros, + path_set & unchanged, + path_set & changed, + path_set & missing); -void get_user_log_path(bookkeeping_path & ul_path); + // This updates the file-content hashes in ROSTER, which is assumed to be + // the "current" roster returned by one of the above get_*_roster_shape + // functions. If a node_restriction is provided, only the files matching + // the restriction have their hashes updated. + void update_current_roster_from_filesystem(roster_t & ros); + void update_current_roster_from_filesystem(roster_t & ros, + node_restriction const & mask); -void read_user_log(data & dat); -void write_user_log(data const & dat); + // the "user log" is a file the user can edit as they program to record + // changes they make to their source code. Upon commit the file is read + // and passed to the edit_comment lua hook. If the commit is a success, + // the user log is then blanked. If the commit does not succeed, no + // change is made to the user log file. -void blank_user_log(); + void get_user_log_path(bookkeeping_path & ul_path); + void read_user_log(utf8 & dat); + void write_user_log(utf8 const & dat); + void blank_user_log(); + bool has_contents_user_log(); -bool has_contents_user_log(); + // the "options map" is another administrative file, stored in + // _MTN/options. it keeps a list of name/value pairs which are considered + // "persistent options", associated with a particular the workspace and + // implied unless overridden on the command line. the set of valid keys + // corresponds exactly to the argument list of these functions. -// the "options map" is another administrative file, stored in -// _MTN/options. it keeps a list of name/value pairs which are considered -// "persistent options", associated with a particular the workspace and -// implied unless overridden on the command line. the main ones are -// --branch and --db, although some others may follow in the future. + void get_ws_options(utf8 & database_option, + utf8 & branch_option, + utf8 & key_option, + utf8 & keydir_option); + void set_ws_options(utf8 & database_option, + utf8 & branch_option, + utf8 & key_option, + utf8 & keydir_option); -typedef std::map options_map; + // the "workspace format version" is a nonnegative integer value, stored + // in _MTN/format as an unadorned decimal number. at any given time + // monotone supports actual use of only one workspace format. + // check_ws_format throws an error if the workspace's format number is not + // equal to the currently supported format number. it is automatically + // called for all commands defined with CMD() (not CMD_NO_WORKSPACE()). + // migrate_ws_format is called only on explicit user request (mtn ws + // migrate) and will convert a workspace from any older format to the new + // one. unlike most routines in this class, it is defined in its own + // file, work_migration.cc. finally, write_ws_format is called only when + // a workspace is created, and simply writes the current workspace format + // number to _MTN/format. + void check_ws_format(); + void migrate_ws_format(); + void write_ws_format(); -void get_options_path(bookkeeping_path & o_path); + // the "local dump file' is a debugging file, stored in _MTN/debug. if we + // crash, we save some debugging information here. -void read_options_map(data const & dat, options_map & options); + void get_local_dump_path(bookkeeping_path & d_path); -void write_options_map(data & dat, - options_map const & options); + // the 'inodeprints file' contains inode fingerprints -// the "local dump file' is a debugging file, stored in _MTN/debug. if we -// crash, we save some debugging information here. + void enable_inodeprints(); + void maybe_update_inodeprints(); -void get_local_dump_path(bookkeeping_path & d_path); - -// the 'inodeprints file' contains inode fingerprints - -void get_inodeprints_path(bookkeeping_path & ip_path); - -bool in_inodeprints_mode(); - -void read_inodeprints(data & dat); - -void write_inodeprints(data const & dat); - -void enable_inodeprints(); - -bool get_attribute_from_roster(roster_t const & ros, - file_path const & path, - attr_key const & key, - attr_value & val); - -void update_any_attrs(std::vector const & include_paths, - app_state & app); - -struct file_content_source -{ - virtual void get_file_content(file_id const & fid, - file_data & dat) const = 0; - virtual ~file_content_source() {}; -}; - -struct empty_file_content_source : public file_content_source -{ - virtual void get_file_content(file_id const & fid, - file_data & dat) const - { - I(false); - } -}; - -struct editable_working_tree : public editable_tree -{ - editable_working_tree(app_state & app, file_content_source const & source); - - virtual node_id detach_node(split_path const & src); - virtual void drop_detached_node(node_id nid); - - virtual node_id create_dir_node(); - virtual node_id create_file_node(file_id const & content); - virtual void attach_node(node_id nid, split_path const & dst); - - virtual void apply_delta(split_path const & pth, - file_id const & old_id, - file_id const & new_id); - virtual void clear_attr(split_path const & pth, - attr_key const & name); - virtual void set_attr(split_path const & pth, - attr_key const & name, - attr_value const & val); - - virtual void commit(); - - virtual ~editable_working_tree(); + // constructor and locals. by caching pointers to the database and the + // lua hooks, we don't have to know about app_state. + workspace(database & db, lua_hooks & lua) : db(db), lua(lua) {}; private: - app_state & app; - file_content_source const & source; - node_id next_nid; - std::map written_content; - std::map rename_add_drop_map; - bool root_dir_attached; + database & db; + lua_hooks & lua; }; // Local Variables: