# # # patch "selectors.cc" # from [3dc0d2b2b0d271f2e96a1afacb43d7565b03f4c2] # to [1bc409d04ba133812a530ec4aa12bc8d0761cd54] # ============================================================ --- selectors.cc 3dc0d2b2b0d271f2e96a1afacb43d7565b03f4c2 +++ selectors.cc 1bc409d04ba133812a530ec4aa12bc8d0761cd54 @@ -9,11 +9,13 @@ #include "base.hh" #include "selectors.hh" + #include "sanity.hh" #include "constants.hh" #include "database.hh" #include "app_state.hh" #include "project.hh" +#include "revision.hh" #include "globish.hh" #include "cmd.hh" #include "work.hh" @@ -21,6 +23,7 @@ #include "roster.hh" #include +#include #include using std::make_pair; @@ -31,6 +34,776 @@ using std::inserter; using std::set_intersection; using std::inserter; +using boost::shared_ptr; + +class selector +{ +public: + static shared_ptr create(options const & opts, + lua_hooks & lua, + project_t & project, + string const & orig); + static shared_ptr create_simple_selector(options const & opts, + lua_hooks & lua, + project_t & project, + string const & orig); + virtual set complete(project_t & project) = 0; +}; + +class author_selector : public selector +{ + string value; +public: + author_selector(string const & arg) : value(arg) {} + virtual set complete(project_t & project) + { + set ret; + project.db.select_cert(author_cert_name(), value, ret); + return ret; + } +}; +class branch_selector : public selector +{ + string value; +public: + branch_selector(string const & arg, options const & opts) : value(arg) + { + if (value.empty()) + { + workspace::require_workspace(F("the empty branch selector b: refers to the current branch")); + value = opts.branch(); + } + } + virtual set complete(project_t & project) + { + set ret; + project.db.select_cert(branch_cert_name(), value, ret); + return ret; + } +}; +class cert_selector : public selector +{ + string value; +public: + cert_selector(string const & arg) : value(arg) + { + E(!value.empty(), origin::user, + F("the cert selector c: may not be empty")); + } + virtual set complete(project_t & project) + { + set ret; + size_t equals = value.find("="); + if (equals == string::npos) + project.db.select_cert(value, ret); + else + project.db.select_cert(value.substr(0, equals), value.substr(equals + 1), ret); + return ret; + } +}; +string preprocess_date_for_selector(string sel, lua_hooks & lua, bool equals) +{ + string tmp; + if (lua.hook_exists("expand_date")) + { + E(lua.hook_expand_date(sel, tmp), origin::user, + F("selector '%s' is not a valid date\n") % sel); + } + else + { + tmp = sel; + } + // if we still have a too short datetime string, expand it with + // default values, but only if the type is earlier or later; + // for searching a specific date cert this makes no sense + // FIXME: this is highly speculative if expand_date wasn't called + // beforehand - tmp could be _anything_ but a partial date string + if (tmp.size()<8 && !equals) + tmp += "-01T00:00:00"; + else if (tmp.size()<11 && !equals) + tmp += "T00:00:00"; + E(tmp.size()==19 || equals, origin::user, + F("selector '%s' is not a valid date (%s)") % sel % tmp); + + if (sel != tmp) + { + P (F ("expanded date '%s' -> '%s'\n") % sel % tmp); + sel = tmp; + } + if (equals && sel.size() < 19) + sel = string("*") + sel + "*"; // to be GLOBbed later + return sel; +} +class date_selector : public selector +{ + string value; +public: + date_selector(string const & arg, lua_hooks & lua) + : value(preprocess_date_for_selector(arg, lua, true)) {} + virtual set complete(project_t & project) + { + set ret; + project.db.select_date(value, "GLOB", ret); + return ret; + } +}; +class earlier_than_selector : public selector +{ + string value; +public: + earlier_than_selector(string const & arg, lua_hooks & lua) + : value(preprocess_date_for_selector(arg, lua, false)) {} + virtual set complete(project_t & project) + { + set ret; + project.db.select_date(value, "<=", ret); + return ret; + } +}; +class head_selector : public selector +{ + string value; + bool ignore_suspend; +public: + head_selector(string const & arg, options const & opts) + : value(arg) + { + if (value.empty()) + { + workspace::require_workspace(F("the empty head selector h: refers to " + "the head of the current branch")); + value = opts.branch(); + } + ignore_suspend = opts.ignore_suspend_certs; + } + virtual set complete(project_t & project) + { + set ret; + + set branch_names; + project.get_branch_list(globish(value, origin::user), branch_names); + + L(FL("found %d matching branches") % branch_names.size()); + + // for each branch name, get the branch heads + for (set::const_iterator bn = branch_names.begin(); + bn != branch_names.end(); bn++) + { + set branch_heads; + project.get_branch_heads(*bn, branch_heads, ignore_suspend); + ret.insert(branch_heads.begin(), branch_heads.end()); + L(FL("after get_branch_heads for %s, heads has %d entries") + % (*bn) % ret.size()); + } + + return ret; + } +}; +class ident_selector : public selector +{ + string value; +public: + ident_selector(string const & arg) : value(arg) {} + virtual set complete(project_t & project) + { + set ret; + project.db.complete(value, ret); + return ret; + } + bool is_full_length() const + { + return value.size() == constants::idlen; + } + revision_id get_assuming_full_length() const + { + return decode_hexenc_as(value, origin::user); + } +}; +class later_than_selector : public selector +{ + string value; +public: + later_than_selector(string const & arg, lua_hooks & lua) + : value(preprocess_date_for_selector(arg, lua, false)) {} + virtual set complete(project_t & project) + { + set ret; + project.db.select_date(value, ">", ret); + return ret; + } +}; +class message_selector : public selector +{ + string value; +public: + message_selector(string const & arg) : value(arg) {} + virtual set complete(project_t & project) + { + set changelogs, comments; + project.db.select_cert(changelog_cert_name(), value, changelogs); + project.db.select_cert(comment_cert_name(), value, comments); + + changelogs.insert(comments.begin(), comments.end()); + return changelogs; + } +}; +class parent_selector : public selector +{ + string value; +public: + parent_selector(string const & arg, + options const & opts, + lua_hooks & lua, + project_t & project) + : value(arg) + { + if (value.empty()) + { + workspace work(lua, F("the empty parent selector p: refers to " + "the base revision of the workspace")); + + parent_map parents; + set parent_ids; + + work.get_parent_rosters(project.db, parents); + + for (parent_map::const_iterator i = parents.begin(); + i != parents.end(); ++i) + { + parent_ids.insert(i->first); + } + + diagnose_ambiguous_expansion(opts, lua, project, "p:", parent_ids); + value = encode_hexenc((* parent_ids.begin()).inner()(), + origin::internal); + + } + } + virtual set complete(project_t & project) + { + set ret; + project.db.select_parent(value, ret); + return ret; + } +}; +class tag_selector : public selector +{ + string value; +public: + tag_selector(string const & arg) : value(arg) {} + virtual set complete(project_t & project) + { + set ret; + project.db.select_cert(tag_cert_name(), value, ret); + return ret; + } +}; +class update_selector : public selector +{ + string value; +public: + update_selector(string const & arg, lua_hooks & lua) + { + E(arg.empty(), origin::user, + F("no value is allowed with the update selector u:")); + + workspace work(lua, F("the update selector u: refers to the " + "revision before the last update in the " + "workspace")); + revision_id update_id; + work.get_update_id(update_id); + value = encode_hexenc(update_id.inner()(), origin::internal); + } + virtual set complete(project_t & project) + { + set ret; + project.db.complete(value, ret); + return ret; + } +}; +class working_base_selector : public selector +{ + set ret; +public: + working_base_selector(string const & arg, project_t & project, lua_hooks & lua) + { + E(arg.empty(), origin::user, + F("no value is allowed with the base revision selector w:")); + + workspace work(lua, F("the selector w: returns the " + "base revision(s) of the workspace")); + parent_map parents; + work.get_parent_rosters(project.db, parents); + + for (parent_map::const_iterator i = parents.begin(); + i != parents.end(); ++i) + { + ret.insert(i->first); + } + } + virtual set complete(project_t & project) + { + return ret; + } +}; + +class unknown_selector : public selector +{ + string value; +public: + unknown_selector(string const & arg) : value(arg) {} + virtual set complete(project_t & project) + { + set ret; + project.db.select_author_tag_or_branch(value, ret); + return ret; + } +}; + +class or_selector : public selector +{ + vector > members; +public: + void add(shared_ptr s) + { + members.push_back(s); + } + virtual set complete(project_t & project) + { + set ret; + for (vector >::const_iterator i = members.begin(); + i != members.end(); ++i) + { + set current = (*i)->complete(project); + ret.insert(current.begin(), current.end()); + } + return ret; + } +}; +class and_selector : public selector +{ + vector > members; +public: + void add(shared_ptr s) + { + members.push_back(s); + } + virtual set complete(project_t & project) + { + set ret; + bool first = true; + for (vector >::const_iterator i = members.begin(); + i != members.end(); ++i) + { + set current = (*i)->complete(project); + if (first) + { + first = false; + ret = current; + } + else + { + set intersection; + set_intersection(ret.begin(), ret.end(), + current.begin(), current.end(), + inserter(intersection, intersection.end())); + ret = intersection; + } + } + return ret; + } +}; +class nested_selector : public selector +{ + shared_ptr s; +public: + nested_selector(shared_ptr s) : s(s) {} + virtual set complete(project_t & project) + { + return s->complete(project); + } +}; + +set get_ancestors(project_t const & project, + set frontier) +{ + set ret; + while (!frontier.empty()) + { + revision_id revid = *frontier.begin(); + frontier.erase(frontier.begin()); + set p; + project.db.get_revision_parents(revid, p); + frontier.insert(p.begin(), p.end()); + ret.insert(p.begin(), p.end()); + } + return ret; +} + +class fn_selector : public selector +{ + string name; + vector > args; +public: + fn_selector(string const & fn_name) : name(fn_name) {} + void add(shared_ptr s) + { + args.push_back(s); + } + virtual set complete(project_t & project) + { + if (name == "difference") + { + E(args.size() == 2, origin::user, + F("the 'difference' function takes 2 arguments, not %d") % args.size()); + set lhs = args[0]->complete(project); + set rhs = args[1]->complete(project); + + set ret; + set_difference(lhs.begin(), lhs.end(), + rhs.begin(), rhs.end(), + inserter(ret, ret.end())); + return ret; + } + else if (name == "lca") + { + E(args.size() == 2, origin::user, + F("the 'lca' function takes 2 arguments, not %d") % args.size()); + set lhs = get_ancestors(project, args[0]->complete(project)); + set rhs = get_ancestors(project, args[1]->complete(project)); + set common; + set_intersection(lhs.begin(), lhs.end(), + rhs.begin(), rhs.end(), + inserter(common, common.end())); + erase_ancestors(project.db, common); + return common; + } + else if (name == "max") + { + E(args.size() == 1, origin::user, + F("the 'max' function takes 1 argument, not %d") % args.size()); + set ret = args[0]->complete(project); + erase_ancestors(project.db, ret); + return ret; + } + else if (name == "ancestors") + { + E(args.size() == 1, origin::user, + F("the 'ancestors' function takes 1 argument, not %d") % args.size()); + return get_ancestors(project, args[0]->complete(project)); + } + else if (name == "descendants") + { + E(args.size() == 1, origin::user, + F("the 'descendants' function takes 1 argument, not %d") % args.size()); + set frontier = args[0]->complete(project); + set ret; + while (!frontier.empty()) + { + revision_id revid = *frontier.begin(); + frontier.erase(frontier.begin()); + set c; + project.db.get_revision_children(revid, c); + frontier.insert(c.begin(), c.end()); + ret.insert(c.begin(), c.end()); + } + return ret; + } + else if (name == "parents") + { + E(args.size() == 1, origin::user, + F("the 'parents' function takes 1 argument, not %d") % args.size()); + set ret; + set tmp = args[0]->complete(project); + for (set::const_iterator i = tmp.begin(); + i != tmp.end(); ++i) + { + set p; + project.db.get_revision_parents(*i, p); + ret.insert(p.begin(), p.end()); + } + ret.erase(revision_id()); + return ret; + } + else if (name == "children") + { + E(args.size() == 1, origin::user, + F("the 'children' function takes 1 argument, not %d") % args.size()); + set ret; + set tmp = args[0]->complete(project); + for (set::const_iterator i = tmp.begin(); + i != tmp.end(); ++i) + { + set c; + project.db.get_revision_children(*i, c); + ret.insert(c.begin(), c.end()); + } + ret.erase(revision_id()); + return ret; + } + else + { + E(false, origin::user, + F("unknown selection function '%s'") % name); + } + } +}; + +struct parse_item +{ + shared_ptr sel; + string str; + explicit parse_item(shared_ptr const & s) : sel(s) { } + explicit parse_item(string const & s) : str(s) { } +}; + +shared_ptr +selector::create_simple_selector(options const & opts, + lua_hooks & lua, + project_t & project, + string const & orig) +{ + string sel = orig; + if (sel.find_first_not_of(constants::legal_id_bytes) == string::npos + && sel.size() == constants::idlen) + return shared_ptr(new ident_selector(sel)); + + if (sel.size() < 2 || sel[1] != ':') + { + string tmp; + if (!lua.hook_expand_selector(sel, tmp)) + { + L(FL("expansion of selector '%s' failed") % sel); + } + else + { + P(F("expanded selector '%s' -> '%s'") % sel % tmp); + sel = tmp; + } + } + if (sel.size() < 2 || sel[1] != ':') + return shared_ptr(new unknown_selector(sel)); + char sel_type = sel[0]; + sel.erase(0,2); + switch (sel_type) + { + case 'a': + return shared_ptr(new author_selector(sel)); + case 'b': + return shared_ptr(new branch_selector(sel, opts)); + case 'c': + return shared_ptr(new cert_selector(sel)); + case 'd': + return shared_ptr(new date_selector(sel, lua)); + case 'e': + return shared_ptr(new earlier_than_selector(sel, lua)); + case 'h': + return shared_ptr(new head_selector(sel, opts)); + case 'i': + return shared_ptr(new ident_selector(sel)); + case 'l': + return shared_ptr(new later_than_selector(sel, lua)); + case 'm': + return shared_ptr(new message_selector(sel)); + case 'p': + return shared_ptr(new parent_selector(sel, opts, lua, project)); + case 't': + return shared_ptr(new tag_selector(sel)); + case 'u': + return shared_ptr(new update_selector(sel, lua)); + case 'w': + return shared_ptr(new working_base_selector(sel, project, lua)); + default: + E(false, origin::user, F("unknown selector type: %c") % sel_type); + } +} + +shared_ptr selector::create(options const & opts, + lua_hooks & lua, + project_t & project, + string const & orig) +{ + // I would try to use lex/yacc for this, but they kinda look like a mess + // with lots of global variables and icky macros and such in the output. + // Using bisonc++ with flex in c++ mode might be better, except that + // bisonc++ is GPLv3 *without* (as far as I can see) an exception for use + // of the parser skeleton as included in the output. + string const special_chars("(),\\/|"); + boost::char_separator splitter("", special_chars.c_str()); + typedef boost::tokenizer > tokenizer_t; + tokenizer_t tokenizer(orig, splitter); + vector splitted; + L(FL("tokenizing selector '%s'") % orig); + bool dont_advance = false; + for (tokenizer_t::const_iterator iter = tokenizer.begin(); + iter != tokenizer.end(); dont_advance || (++iter, true)) + { + dont_advance = false; + string const & val = *iter; + if (val != "\\") + splitted.push_back(val); + else + { + if (splitted.empty()) + splitted.push_back(string()); + + ++iter; + E(iter != tokenizer.end(), origin::user, + F("selector '%s' is invalid, it ends with the escape character '\\'") + % orig); + string const & val2 = *iter; + I(!val2.empty()); + E(special_chars.find(val2) != string::npos, origin::user, + F("selector '%s' is invalid, it contains an unknown escape sequence '%s%s'") + % val % '\\' % val2.substr(0,1)); + splitted.back().append(val2); + + ++iter; + if (iter != tokenizer.end()) + { + string const & val3 = *iter; + if (val3.size() != 1 || special_chars.find(val3) == string::npos) + splitted.back().append(val3); + else + dont_advance = true; + } + } + } + if (splitted.empty()) + splitted.push_back(string()); + for (vector::const_iterator i = splitted.begin(); + i != splitted.end(); ++i) + { + L(FL("tokens: '%s'") % *i); + } + + vector items; + for (vector::const_iterator tok = splitted.begin(); + tok != splitted.end(); ++tok) + { + if (*tok == "(") { + items.push_back(parse_item(*tok)); + } else if (*tok == ")") { + unsigned int lparen_pos = 0; + while (lparen_pos <= items.size() && items[items.size() - lparen_pos].str != "(") + { + --lparen_pos; + } + E(items[items.size() - lparen_pos].str == "(", origin::user, + F("selector '%s' is invalid, unmatched '('") % orig); + unsigned int name_idx = items.size() - lparen_pos - 1; + if (lparen_pos < items.size() && !items[name_idx].str.empty()) + { + // looks like a function call + shared_ptr to_add(new fn_selector(items[name_idx].str)); + for (unsigned int idx = items.size() - lparen_pos + 1; + idx < items.size() - 1; idx += 2) + { + shared_ptr arg = items[idx].sel; + string sep = items[idx + 1].str; + E(sep == "," || (sep == ")" && idx == items.size() - 2), origin::user, + F("selector '%s' is invalid, function argument doesn't look like an arg-list")); + to_add->add(arg); + } + while (name_idx < items.size()) + items.pop_back(); + items.push_back(parse_item(to_add)); + } + else + { + // just parentheses for grouping + E(lparen_pos == 3 && items[items.size() - 2].sel, origin::user, + F("selector '%s' is invalid, grouping parentheses contain something that doesn't look like an expr") % orig); + shared_ptr to_add(new nested_selector(items[items.size() - 2].sel)); + items.pop_back(); + items.pop_back(); + items.pop_back(); + items.push_back(parse_item(to_add)); + } + } else if (*tok == ",") { + items.push_back(parse_item(*tok)); + } else if (*tok == "/") { + E(!items.empty(), origin::user, + F("selector '%s' is invalid, because it starts with a '/'") % orig); + items.push_back(parse_item(*tok)); + } else if (*tok == "|") { + E(!items.empty(), origin::user, + F("selector '%s' is invalid, because it starts with a '|'") % orig); + items.push_back(parse_item(*tok)); + } else { + vector::const_iterator next = tok; + ++next; + bool next_is_oparen = false; + if (next != splitted.end()) + next_is_oparen = (*next == "("); + if (next_is_oparen) + items.push_back(parse_item(*tok)); + else + items.push_back(parse_item(create_simple_selector(opts, lua, + project, + *tok))); + } + + // may have an infix operator to reduce + if (items.size() >= 3 && items.back().sel) + { + string op = items[items.size() - 2].str; + if (op == "|" || op == "/") + { + shared_ptr lhs = items[items.size() - 3].sel; + shared_ptr rhs = items[items.size() - 1].sel; + E(lhs, origin::user, + F("selector '%s is invalid, because there is a '%s' someplace it shouldn't be") + % orig % op); + shared_ptr lhs_as_or = boost::dynamic_pointer_cast(lhs); + shared_ptr lhs_as_and = boost::dynamic_pointer_cast(lhs); + E(op == "/" || !lhs_as_and, origin::user, + F("selector '%s' is invalid, don't mix '/' and '|' operators without parentheses") + % orig); + E(op == "|" || !lhs_as_or, origin::user, + F("selector '%s' is invalid, don't mix '/' and '|' operators without parentheses") + % orig); + shared_ptr new_item; + if (lhs_as_or) + { + lhs_as_or->add(rhs); + new_item = lhs; + } + else if (lhs_as_and) + { + lhs_as_and->add(rhs); + new_item = lhs; + } + else + { + if (op == "/") + { + shared_ptr x(new and_selector()); + x->add(lhs); + x->add(rhs); + new_item = x; + } + else + { + shared_ptr x(new or_selector()); + x->add(lhs); + x->add(rhs); + new_item = x; + } + } + I(new_item); + items.pop_back(); + items.pop_back(); + items.pop_back(); + items.push_back(parse_item(new_item)); + } + } + } + E(items.size() == 1 && items[0].sel, origin::user, + F("selector '%s' is invalid, it doesn't look like an expr") % orig); + return items[0].sel; +} + enum selector_type { sel_author, @@ -421,22 +1194,20 @@ complete(options const & opts, lua_hooks string const & str, set & completions) { - selector_list sels; - parse_selector(opts, lua, project, str, sels); + shared_ptr sel = selector::create(opts, lua, project, str); // avoid logging if there's no expansion to be done - if (sels.size() == 1 - && sels[0].first == sel_ident - && sels[0].second.size() == constants::idlen) + shared_ptr isel = boost::dynamic_pointer_cast(sel); + if (isel && isel->is_full_length()) { - completions.insert(decode_hexenc_as(sels[0].second, origin::user)); + completions.insert(isel->get_assuming_full_length()); E(project.db.revision_exists(*completions.begin()), origin::user, F("no such revision '%s'") % *completions.begin()); return; } P(F("expanding selection '%s'") % str); - complete_selector(opts, lua, project, sels, completions); + completions = sel->complete(project); E(!completions.empty(), origin::user, F("no match for selection '%s'") % str); @@ -476,20 +1247,17 @@ expand_selector(options const & opts, lu string const & str, set & completions) { - selector_list sels; - parse_selector(opts, lua, project, str, sels); + shared_ptr sel = selector::create(opts, lua, project, str); // avoid logging if there's no expansion to be done - if (sels.size() == 1 - && sels[0].first == sel_ident - && sels[0].second.size() == constants::idlen) + shared_ptr isel = boost::dynamic_pointer_cast(sel); + if (isel && isel->is_full_length()) { - completions.insert(decode_hexenc_as(sels[0].second, - origin::user)); + completions.insert(isel->get_assuming_full_length()); return; } - complete_selector(opts, lua, project, sels, completions); + completions = sel->complete(project); } void