# # # patch "cmd_ws_commit.cc" # from [f19eb71e76506b5ac8a706f610e6a34b595266a1] # to [b3bd3a32ee59af9edb595e9c999e937390b9965b] # # patch "options_list.hh" # from [05705dd75c2e3556f4bac136b13c802d85827e0d] # to [54a563f0f12bd6db64856d316d0c7a13cd0125e4] # # patch "work.cc" # from [b005a944d5e5ebcbfed43ac742871eba7d5e7ad0] # to [4b549d6bcb26d3e0632b08d16871195016a66e17] # # patch "work.hh" # from [cd117c8295b2bdf48db35e92aaf29842c27a1b7f] # to [066d38ca737363f32bfe4b4811689d09ee5066d8] # ============================================================ --- cmd_ws_commit.cc f19eb71e76506b5ac8a706f610e6a34b595266a1 +++ cmd_ws_commit.cc b3bd3a32ee59af9edb595e9c999e937390b9965b @@ -511,36 +511,165 @@ CMD(rename, "rename", "mv", CMD_REF(work CMD(rename, "rename", "mv", CMD_REF(workspace), N_("SRC DEST\n" - "SRC1 [SRC2 [...]] DEST_DIR"), + "SRC1 [SRC2 [...]] DEST_DIR\n" + "--guess"), N_("Renames entries in the workspace"), "", - options::opts::bookkeep_only) + options::opts::bookkeep_only | options::opts::guess) { - if (args.size() < 2) + if (!app.opts.guess && (args.size() < 2)) throw usage(execid); database db(app); workspace work(app); - utf8 dstr = args.back(); - file_path dst_path = file_path_external(dstr); + if (app.opts.guess) + { + // this algorithm compares the list of missing files with the list of + // unknown files and renames any files that exist exactly once in both + // the missing and unknown lists - set src_paths; - for (size_t i = 0; i < args.size()-1; i++) - { - file_path s = file_path_external(idx(args, i)); - src_paths.insert(s); + set missing, unknown, ignored; + vector roots = args_to_paths(args); + temp_node_id_source nis; + roster_t roster; + + if (roots.empty()) + roots.push_back(file_path()); + + work.get_current_roster_shape(db, nis, roster); + node_restriction node_mask(work, args_to_paths(args), + args_to_paths(app.opts.exclude_patterns), + app.opts.depth, + roster); + path_restriction path_mask(work, roots, + args_to_paths(app.opts.exclude_patterns), + app.opts.depth); + + work.find_missing(roster, node_mask, missing); + work.find_unknown_and_ignored(db, path_mask, roots, true, unknown, ignored); + + N(!missing.empty() && !unknown.empty(), F("there are no missing or unknown to guess about")); + N(!missing.empty(), F("there are no missing files to guess about")); + N(!unknown.empty(), F("there are no unknown files to guess about")); + + // calculate file_ids for all of the unknown files and store them + typedef map file_id_map; + file_id_map unknown_ids; + + for (set::iterator i = unknown.begin(); i != unknown.end(); ++i) + { + if (get_path_status(*i) == path::file) + { + file_id id; + calculate_ident(*i, id); + unknown_ids.insert(make_pair(id, *i)); + } + } + + typedef std::list > rename_map; + rename_map renames; + set dirs; + for (set::iterator i = missing.begin(); i != missing.end(); ++i) + { + node_t missing_node = roster.get_node(*i); + if (is_dir_t(missing_node)) + { + // for each directory, find each missing file in the unknown list + // and store the names of the directories the missing files are + // located in. If we found every single missing file, and we + // found a single directory containing all missing files, rename + // the directory we just searched. + bool found_all = true; + set potential_matches; + dir_t missing_dir = downcast_to_dir_t(missing_node); + for (dir_map::iterator k = missing_dir->children.begin(); + k != missing_dir->children.end(); ++k) + { + if (is_file_t(k->second)) + { + file_t missing_file = downcast_to_file_t(k->second); + file_id_map::iterator j = unknown_ids.find(missing_file->content); + if (j != unknown_ids.end()) + potential_matches.insert(j->second.dirname()); + else + { + found_all = false; + break; + } + } + } + + if (found_all and potential_matches.size() == 1) + { + renames.push_back(make_pair(*i, *potential_matches.begin())); + dirs.insert(missing_dir); + } + } + else if (is_file_t(missing_node)) + { + // skip any paths in dirs we have already handled + bool already_renamed = false; + for (set::iterator k = dirs.begin(); + k != dirs.end(); ++k) + { + if ((*k)->has_child(missing_node->name)) + { + already_renamed = true; + break; + } + } + + if (already_renamed) + continue; + + // attempt to rename this file + file_t missing_file = downcast_to_file_t(missing_node); + file_id_map::iterator j = unknown_ids.find(missing_file->content); + + if (j != unknown_ids.end()) + renames.push_back(make_pair(*i, j->second)); + } + + } + + // perform our renames + for (rename_map::iterator i = renames.begin(); i != renames.end(); ++i) + { + set src_path; + src_path.insert(i->first); + + bool ignore_dpath = false; + if (get_path_status(i->second) == path::directory) + ignore_dpath = true; + + work.perform_rename(db, src_path, i->second, app.opts.bookkeep_only, ignore_dpath); + } } + else + { + // perform a standard rename operation - //this catches the case where the user specifies a directory 'by convention' - //that doesn't exist. the code in perform_rename already handles the proper - //cases for more than one source item. - if (src_paths.size() == 1 && dstr()[dstr().size() -1] == '/') - if (get_path_status(*src_paths.begin()) != path::directory) - N(get_path_status(dst_path) == path::directory, - F(_("The specified target directory %s/ doesn't exist.")) % dst_path); + utf8 dstr = args.back(); + file_path dst_path = file_path_external(dstr); - work.perform_rename(db, src_paths, dst_path, app.opts.bookkeep_only); + set src_paths; + for (size_t i = 0; i < args.size()-1; i++) + { + file_path s = file_path_external(idx(args, i)); + src_paths.insert(s); + } + + //this catches the case where the user specifies a directory 'by convention' + //that doesn't exist. the code in perform_rename already handles the proper + //cases for more than one source item. + if (src_paths.size() == 1 && dstr()[dstr().size() -1] == '/') + if (get_path_status(*src_paths.begin()) != path::directory) + N(get_path_status(dst_path) == path::directory, + F(_("The specified target directory %s/ doesn't exist.")) % dst_path); + + work.perform_rename(db, src_paths, dst_path, app.opts.bookkeep_only); + } } ============================================================ --- options_list.hh 05705dd75c2e3556f4bac136b13c802d85827e0d +++ options_list.hh 54a563f0f12bd6db64856d316d0c7a13cd0125e4 @@ -610,6 +610,14 @@ OPT(unknown, "unknown", bool, false, #endif +OPT(guess, "guess", bool, false, + gettext_noop("detect renamed files and directories by comparing missing and unknown files")) +#ifdef option_bodies +{ + guess = true; +} +#endif + OPT(verbose, "verbose", bool, false, gettext_noop("verbose completion output")) #ifdef option_bodies ============================================================ --- work.cc b005a944d5e5ebcbfed43ac742871eba7d5e7ad0 +++ work.cc 4b549d6bcb26d3e0632b08d16871195016a66e17 @@ -687,16 +687,18 @@ struct file_itemizer : public tree_walke { database & db; workspace & work; + bool recursive; set & known; set & unknown; set & ignored; path_restriction const & mask; file_itemizer(database & db, workspace & work, + bool rec, set & k, set & u, set & i, path_restriction const & r) - : db(db), work(work), known(k), unknown(u), ignored(i), mask(r) {} + : db(db), work(work), recursive(rec), known(k), unknown(u), ignored(i), mask(r) {} virtual bool visit_dir(file_path const & path); virtual void visit_file(file_path const & path); }; @@ -706,7 +708,10 @@ file_itemizer::visit_dir(file_path const file_itemizer::visit_dir(file_path const & path) { this->visit_file(path); - return known.find(path) != known.end(); + if (recursive) + return true; + else + return known.find(path) != known.end(); } void @@ -1403,6 +1408,17 @@ workspace::find_unknown_and_ignored(data set & unknown, set & ignored) { + find_unknown_and_ignored(db, mask, roots, false, unknown, ignored); +} + +void +workspace::find_unknown_and_ignored(database & db, + path_restriction const & mask, + vector const & roots, + bool recursive, + set & unknown, + set & ignored) +{ set known; roster_t new_roster; temp_node_id_source nis; @@ -1410,7 +1426,7 @@ workspace::find_unknown_and_ignored(data get_current_roster_shape(db, nis, new_roster); new_roster.extract_path_set(known); - file_itemizer u(db, *this, known, unknown, ignored, mask); + file_itemizer u(db, *this, recursive, known, unknown, ignored, mask); for (vector::const_iterator i = roots.begin(); i != roots.end(); ++i) { @@ -1586,7 +1602,8 @@ workspace::perform_rename(database & db, workspace::perform_rename(database & db, set const & srcs, file_path const & dst, - bool bookkeep_only) + bool bookkeep_only, + bool ignore_dpath) { temp_node_id_source nis; roster_t new_roster; @@ -1616,7 +1633,7 @@ workspace::perform_rename(database & db, //all cases. previously, mtn mv fileA dir/ woudl fail if dir/ wasn't //versioned whereas mtn mv fileA dir/fileA would add dir/ if necessary //and then reparent fileA. - if (get_path_status(dst) == path::directory) + if (get_path_status(dst) == path::directory and not ignore_dpath) dpath = dst / src.basename(); else { ============================================================ --- work.hh cd117c8295b2bdf48db35e92aaf29842c27a1b7f +++ work.hh 066d38ca737363f32bfe4b4811689d09ee5066d8 @@ -124,6 +124,13 @@ public: std::set & unknown, std::set & ignored); + void find_unknown_and_ignored(database & db, + path_restriction const & mask, + std::vector const & roots, + bool recursive, + std::set & unknown, + std::set & ignored); + void perform_additions(database & db, std::set const & targets, bool recursive = false, @@ -137,7 +144,8 @@ public: void perform_rename(database & db, std::set const & src_paths, file_path const & dst_dir, - bool bookkeep_only); + bool bookkeep_only, + bool ignore_dpath = false); void perform_pivot_root(database & db, file_path const & new_root,