# # add_file "tests/t_add_stomp_file.at" # # add_file "tests/t_database_check_minor.at" # # add_file "tests/t_db_kill_rev_locally.at" # # patch "ChangeLog" # from [8c7f41215c825e16f2a0a442042d9a73fa70f17e] # to [a9ca443d4b127d6848986edda8e120dd19fa2374] # # patch "change_set.cc" # from [8721281fd257c827409baf2df2ec98e7883e0710] # to [514f646e5cc061ba8bf85e92eebd65357b4c85bf] # # patch "commands.cc" # from [82e7eeb431c5bd765fd43da20f96c28b2b9b4814] # to [180e64e3feece8d9611ce2ab1eaaf85e44375cca] # # patch "commands.hh" # from [f2f45ab20f37d33f52054ae343a0b1a099494a35] # to [cce382e66cf7064f56e731f8e9a48fe8be62d71c] # # patch "contrib/monotone-notify.pl" # from [7b80bc10043bcd080e0a98742cd49a3c8aa7b8df] # to [5fe63044551809be3609ccdd179c51b68109ffb1] # # patch "database.cc" # from [5d62241bcc64569576fd10f45db8bf559d8c1769] # to [2f0d694a3ff58c86f98ae425dba01c9f0e467578] # # patch "database.hh" # from [6d54b527c19ca83ea17c04c733cc6f90b1e4722a] # to [7c532dedbdcb9c8dd9a19b7a80c3ce63a855f3c9] # # patch "database_check.cc" # from [924b949941d72064a292451e72c28bfb2e80755c] # to [63ce24deafa543d9f38e21ae826d8230e3d4d44c] # # patch "monotone.texi" # from [939f56de6aac68a630492031939ce4654f093203] # to [38e796ddafa66c78001fb53209d8c7e0dd8c0b75] # # patch "netsync.cc" # from [6c6cd98bcb3fe5a9c3c45af45cc47074f9e77fb8] # to [23798e72a6683bd620594f459f08ea19de8acdbe] # # patch "sanity.cc" # from [2fd252fa148582b35ba95b8ae368e2c500d4b01f] # to [87f3bbf0785ac602b26fc8f36ff4efdf475db220] # # patch "sanity.hh" # from [e7e2010f734f9e54499e14c700017b4f0577e9ac] # to [952408e83f22dc43392c7afcffc6e556cfd80d6b] # # patch "tests/t_add_stomp_file.at" # from [] # to [ff7f16f34c202ec044ad232af78786bd119b5e98] # # patch "tests/t_database_check.at" # from [3ea731d55685488c6dadb5580b4cd04dc130e855] # to [b02fe408e53bcd4d7219183e218bc21c864dee82] # # patch "tests/t_database_check_minor.at" # from [] # to [eaba52f05e60be3f4bfc420af0722fd5ed63df02] # # patch "tests/t_db_kill_rev_locally.at" # from [] # to [7833b3652bf45b72b9fb6490b86fe2b66d7105ee] # # patch "testsuite.at" # from [63f222bf1549f1b5ea773c38860809fd131a88e4] # to [78e75421c02747bc50d1966394370014a1e16858] # # patch "transforms.cc" # from [1d167b91aac0a32e6336692122db4b3a575a1a24] # to [0e8c4f90b28b45f9274f40eb9e40d866bc84f1e1] # --- ChangeLog +++ ChangeLog @@ -7,6 +7,70 @@ * Move base64 code as close to the database as possible, to avoid unnecessary inflating and deflating. +2005-04-17 Nathaniel Smith + + * monotone.texi (Branching and Merging): A few small edits. + +2005-04-17 Nathaniel Smith + + * change_set.cc (path_item, sanity_check_path_item): Mark things + inline. + +2005-04-17 Henrik Holmboe + + * contrib/monotone-notify.pl: Add signal handlers. Correct some + typos. + (my_exit): New function that does a cleanup and exit. + +2005-04-17 Olivier Andrieu + + * transforms.cc: fix glob_to_regexp assertions + +2005-04-17 Sebastian Spaeth + + * tests/t_db_kill_rev_locally.at: new test; + make sure that db kill_rev_locally works as intended + +2005-04-17 Sebastian Spaeth + + * commands.cc,database.cc: add 'db kill_rev_locally ' command + still missing: documentation and autotests. Otherwise seems ok. + +2005-04-17 Richard Levitte + + * transforms.cc: Remove tabs and make sure emacs doesn't add + them. + +2005-04-17 Nathaniel Smith + + * sanity.{hh,cc} (E, error_failure): New sort of invariant. + * netsync.cc (process_hello_cmd): Make initial pull message + more clear and friendly. + Also, if the key has changed, that is an error, not naughtiness. + * database_check.cc (check_db): Database problems are also errors, + not naughtiness. Revamp output in case of errors, to better + distinguish non-serious errors and serious errors. + * tests/t_database_check.at: Update accordingly. + * tests/t_database_check_minor.at: New test. + * testsuite.at: Add it. + +2005-04-17 Richard Levitte + + * transforms.cc (glob_to_regexp): New function that takes a glob + expression and transforms it into a regexp. This will be useful + for globbing branch expressions when collections are exchanged to + branch globs and regexps. + (glob_to_regexp_test): A unit test for glob_to_regexp(). + +2005-04-16 Emile Snyder + + * tests/t_add_stomp_file.at: New test for failing case. + If you have a file foo in your working dir (not monotone + controlled) and someone else adds a file foo and commits, + update should at least warn you before stomping your + non-recoverable foo file. + * testsuite.at: Add it. + 2005-04-17 Matt Johnston * commands.cc: warn that dropkey won't truly erase the privkey --- change_set.cc +++ change_set.cc @@ -61,11 +61,11 @@ tid parent; ptype ty; path_component name; - path_item() {} - path_item(tid p, ptype t, path_component n); - path_item(path_item const & other); - path_item const & operator=(path_item const & other); - bool operator==(path_item const & other) const; + inline path_item() {} + inline path_item(tid p, ptype t, path_component n); + inline path_item(path_item const & other); + inline path_item const & operator=(path_item const & other); + inline bool operator==(path_item const & other) const; }; @@ -506,7 +506,7 @@ } -static void +inline static void sanity_check_path_item(path_item const & pi) { } --- commands.cc +++ commands.cc @@ -2402,6 +2402,7 @@ "load\n" "migrate\n" "execute\n" + "kill_rev_locally \n" "check\n" "changesetify\n" "rebuild\n" @@ -2435,6 +2436,8 @@ { if (idx(args, 0)() == "execute") app.db.debug(idx(args, 1)(), cout); + else if (idx(args, 0)() == "kill_rev_locally") + kill_rev_locally(app,idx(args, 1)()); else if (idx(args, 0)() == "clear_epoch") app.db.clear_epoch(cert_value(idx(args, 1)())); else @@ -2452,6 +2455,24 @@ throw usage(name); } +/// Deletes a revision from the local database +/// This can be used to 'undo' a changed revision from a local database without +/// leaving a trace. +void kill_rev_locally(app_state& app, std::string const& id){ + revision_id ident; + complete(app, id, ident); + N(app.db.revision_exists(ident), + F("no revision %s found in database") % ident); + + //check that the revision does not have any children + set children; + app.db.get_revision_children(ident, children); + N(!children.size(), + F("revision %s already has children. We cannot kill it.") % ident); + + app.db.delete_existing_rev_and_certs(ident); +} + CMD(attr, "working copy", "set FILE ATTR VALUE\nget FILE [ATTR]", "get or set file attributes") { --- commands.hh +++ commands.hh @@ -25,6 +25,7 @@ namespace commands { void explain_usage(std::string const & cmd, std::ostream & out); int process(app_state & app, std::string const & cmd, std::vector const & args); + void kill_rev_locally(app_state& app, std::string const & id); typedef enum { sel_author, --- contrib/monotone-notify.pl +++ contrib/monotone-notify.pl @@ -73,6 +73,11 @@ 'quiet' => \$quiet, 'debug' => \$debug) or pod2usage(2); +$SIG{HUP} = \&my_exit; +$SIG{KILL} = \&my_exit; +$SIG{TERM} = \&my_exit; +$SIG{INT} = \&my_exit; + ###################################################################### # Respond to user input # @@ -97,7 +102,7 @@ pod2usage(2); } if (!defined $difflogs_to && !defined $nodifflogs_to) { - my_errlog("You need to specify a To address with --to"); + my_errlog("You need to specify a To address with --diffslogs-to or --nodiffslogs-to"); pod2usage(2); } } @@ -140,12 +145,15 @@ my $remove_workdir = 0; if (!defined $workdir) { - $workdir = "/var/tmp/monotone_motify.work.$$"; + $workdir = "/var/tmp/monotone_notify.work.$$"; mkdir $workdir; $remove_workdir = 1; } elsif (! file_name_is_absolute($workdir)) { $workdir = rel2abs($workdir); } +if (! -d $workdir && ! -w $workdir && ! -r $workdir) { + my_error("work directory $workdir not accessible, exiting"); +} my_debug("using work directory $workdir"); my_debug("(to be removed after I'm done)") if $remove_workdir; @@ -448,13 +456,8 @@ ###################################################################### # Clean up. # -my_log("cleaning up."); -unlink @files_to_clean_up; -rmdir $workdir if $remove_workdir; +my_exit(); -my_log("all done."); -exit(0); - ###################################################################### # Subroutines # @@ -594,6 +597,17 @@ return $return; } +# my_exit removes temporary files and gracefully closes network +# connections. +sub my_exit +{ + my_log("cleaning up."); + unlink @files_to_clean_up; + rmdir $workdir if $remove_workdir; + my_log("all done."); + exit(0); +} + # my_backtick does the same thing as backtick commands, but will print a bit # of debugging output when $debug is true. It will also die if the subprocess # returned an error code. @@ -738,7 +752,7 @@ two files F and F will be left in the work directory. -The default working directory is F, +The default working directory is F, and will be removed automatically unless F or F are left in it. --- database.cc +++ database.cc @@ -1480,7 +1480,25 @@ execute("DELETE from revision_certs"); } +/// Deletes one revision from the local database. +/// @see kill_rev_locally +void +database::delete_existing_rev_and_certs(revision_id const & rid){ + //check that the revision exists and doesn't have any children + I(revision_exists(rid)); + set children; + get_revision_children(rid, children); + I(!children.size()); + + // perform the actual SQL transactions to kill rev rid here + L(F("Killing revision %s locally\n") % rid); + execute("DELETE from revision_certs WHERE id = '%s'",rid.inner()().c_str()); + execute("DELETE from revision_ancestry WHERE child = '%s'", + rid.inner()().c_str()); + execute("DELETE from revisions WHERE id = '%s'",rid.inner()().c_str()); +} + // crypto key management void --- database.hh +++ database.hh @@ -288,6 +288,8 @@ void delete_existing_revs_and_certs(); + void delete_existing_rev_and_certs(revision_id const & rid); + // crypto key / cert operations void get_key_ids(std::string const & pattern, --- database_check.cc +++ database_check.cc @@ -669,22 +669,28 @@ missing_certs + mismatched_certs + unchecked_sigs + bad_sigs + missing_keys; + // unreferenced files and manifests and mismatched certs are not actually + // serious errors; odd, but nothing will break. + size_t serious = missing_files + + missing_manifests + incomplete_manifests + + missing_revisions + incomplete_revisions + + mismatched_parents + mismatched_children + + bad_history + + missing_certs + + unchecked_sigs + bad_sigs + + missing_keys; - if (total > 0) - N(total == 0, - F("check complete: %d files; %d manifests; %d revisions; %d keys; %d certs; %d problems detected\n") - % checked_files.size() - % checked_manifests.size() - % checked_revisions.size() - % checked_keys.size() - % total_certs - % total); + P(F("check complete: %d files; %d manifests; %d revisions; %d keys; %d certs\n") + % checked_files.size() + % checked_manifests.size() + % checked_revisions.size() + % checked_keys.size() + % total_certs); + P(F("total problems detected: %d (%d serious)\n") % total % serious); + if (serious) + E(false, F("serious problems detected")); + else if (total) + P(F("minor problems detected\n")); else - P(F("check complete: %d files; %d manifests; %d revisions; %d keys; %d certs; database is good\n") - % checked_files.size() - % checked_manifests.size() - % checked_revisions.size() - % checked_keys.size() - % total_certs - ); + P(F("database is good\n")); } --- monotone.texi +++ monotone.texi @@ -2115,7 +2115,7 @@ Continuing our example, suppose that Jim is so impressed by Beth's work on banana juice support that he assigns her to work on the JuiceBot 7's surprise new feature: muffins. In the mean time, Abe will continue -working on the JuiceBot's basic juice-related function. +working on the JuiceBot's basic juice-related functions. The changes required to support muffins are somewhat complicated, and Beth is worried that her work might destabilize the program, and @@ -2134,13 +2134,13 @@ @smallexample @group -$ monotone commit --branch=jp.co.juicebot.jb7.muffins --message='initial autobake framework' +$ monotone commit --branch=jp.co.juicebot.jb7.muffins --message='autobake framework' monotone: beginning commit on branch 'jp.co.juicebot.jb7.muffins' monotone: committed revision d33caefd61823ecbb605c39ffb84705dec449857 @end group @end smallexample -That's all there is to it --- now there is a +That's all there is to it --- there is now a @code{jp.co.juicebot.jb7.muffins} branch, with her initial checkin on it. She can make further checkins from the same working copy, and they will automatically go to the muffins branch; if anyone else wants to --- netsync.cc +++ netsync.cc @@ -1445,13 +1445,14 @@ P(F("I expected %s\n") % expected_key_hash); P(F("'monotone unset %s %s' overrides this check\n") % their_key_key.first % their_key_key.second); - N(false, F("server key changed")); + E(false, F("server key changed")); } } else { - W(F("first time connecting to server %s; authenticity can't be established\n") % peer_id); - W(F("their key is %s\n") % their_key_hash); + P(F("first time connecting to server %s\n") % peer_id); + P(F("I'll assume it's really them, but you might want to\n")); + P(F("double-check their key's fingerprint: %s\n") % their_key_hash); app.db.set_var(their_key_key, var_value(their_key_hash())); } if (!app.db.public_key_exists(their_key_hash)) --- sanity.cc +++ sanity.cc @@ -185,6 +185,17 @@ } void +sanity::error_failure(string const & expr, format const & explain, + string const & file, int line) +{ + string message; + log(format("%s:%d: detected error '%s' violated\n") % file % line % expr, + file.c_str(), line); + prefix_lines_with("error: ", explain.str(), message); + throw informative_failure(message); +} + +void sanity::invariant_failure(string const & expr, string const & file, int line) { --- sanity.hh +++ sanity.hh @@ -44,20 +44,22 @@ std::string filename; void log(boost::format const & fmt, - char const * file, int line); + char const * file, int line); void progress(boost::format const & fmt, - char const * file, int line); + char const * file, int line); void warning(boost::format const & fmt, - char const * file, int line); + char const * file, int line); void naughty_failure(std::string const & expr, boost::format const & explain, - std::string const & file, int line); + std::string const & file, int line); + void error_failure(std::string const & expr, boost::format const & explain, + std::string const & file, int line); void invariant_failure(std::string const & expr, - std::string const & file, int line); + std::string const & file, int line); void index_failure(std::string const & vec_expr, - std::string const & idx_expr, - unsigned long sz, - unsigned long idx, - std::string const & file, int line); + std::string const & idx_expr, + unsigned long sz, + unsigned long idx, + std::string const & file, int line); }; typedef std::runtime_error oops; @@ -107,7 +109,16 @@ } \ } while(0) +// E is for errors; they are normal (i.e., not a bug), but not necessarily +// attributable to user naughtiness +#define E(e, explain)\ +do { \ + if(UNLIKELY(!(e))) { \ + global_sanity.error_failure("E("#e")", (explain), __FILE__, __LINE__); \ + } \ +} while(0) + // we're interested in trapping index overflows early and precisely, // because they usually represent *very significant* logic errors. we use // an inline template function because the idx(...) needs to be used as an @@ -115,11 +126,11 @@ template inline T & checked_index(std::vector & v, - typename std::vector::size_type i, - char const * vec, - char const * index, - char const * file, - int line) + typename std::vector::size_type i, + char const * vec, + char const * index, + char const * file, + int line) { if (UNLIKELY(i >= v.size())) global_sanity.index_failure(vec, index, v.size(), i, file, line); @@ -128,11 +139,11 @@ template inline T const & checked_index(std::vector const & v, - typename std::vector::size_type i, - char const * vec, - char const * index, - char const * file, - int line) + typename std::vector::size_type i, + char const * vec, + char const * index, + char const * file, + int line) { if (UNLIKELY(i >= v.size())) global_sanity.index_failure(vec, index, v.size(), i, file, line); --- tests/t_add_stomp_file.at +++ tests/t_add_stomp_file.at @@ -0,0 +1,44 @@ +AT_SETUP([make sure update does not stomp non-monotone file]) +MONOTONE_SETUP + +# This test is a bug report +AT_XFAIL_IF(true) + +# 1. Alice checks out project, creates file foo +# 2. Bob checks out project, creates foo, adds foo, and commits +# 3. Now Alice does an update +# +# monotone should warn her before stomping her non-revision controlled 'foo' file +# + +AT_DATA(initial, [some initial data +]) + +AT_DATA(foo.alice, [foo +not revision controlled +]) + +AT_DATA(foo.bob, [foo +checked into project +]) + +# Alice make project, writes foo, but doesn't check it in +AT_CHECK(mkdir alicewd) +AT_CHECK(cp initial alicewd/initial) +AT_CHECK(MONOTONE --branch=testbranch setup alicewd, [], [ignore], [ignore]) +AT_CHECK( (cd alicewd; MONOTONE --branch=testbranch --root=. add initial), [], [ignore], [ignore]) +AT_CHECK( (cd alicewd; MONOTONE --branch=testbranch --root=. commit -m 'initial commit'), [], [ignore], [ignore]) +AT_CHECK(cp foo.alice alicewd/foo) + +# Bob does add of file foo, and commits +AT_CHECK(MONOTONE --branch=testbranch checkout bobwd, [], [ignore], [ignore]) +AT_CHECK(cp foo.bob bobwd/foo) +AT_CHECK( (cd bobwd; MONOTONE --branch=testbranch --root=. add foo), [], [ignore], [ignore]) +AT_CHECK( (cd bobwd; MONOTONE --branch=testbranch --root=. commit -m 'bob commit'), [], [ignore], [ignore]) +REV=`BASE_REVISION` + +# Alice does her update, discovers foo has been stomped! +AT_CHECK( (cd alicewd; MONOTONE --branch=testbranch --root=. update $REV), [], [ignore], [ignore]) +AT_CHECK(cmp foo.alice alicewd/foo) + +AT_CLEANUP --- tests/t_database_check.at +++ tests/t_database_check.at @@ -66,7 +66,8 @@ AT_CHECK(grep '1 missing file' stderr, [0], [ignore], [ignore]) AT_CHECK(grep '2 incomplete manifest' stderr, [0], [ignore], [ignore]) AT_CHECK(grep '2 incomplete revision' stderr, [0], [ignore], [ignore]) -AT_CHECK(grep '5 problems' stderr, [0], [ignore], [ignore]) +AT_CHECK(grep 'total problems detected: 5' stderr, [0], [ignore], [ignore]) +AT_CHECK(grep '5 serious' stderr, [0], [ignore], [ignore]) # add an unreferenced file, and an unreferenced manifest that # references several files, none of which exist in the db --- tests/t_database_check_minor.at +++ tests/t_database_check_minor.at @@ -0,0 +1,40 @@ +AT_SETUP([db check and non-serious errors]) +MONOTONE_SETUP + +# Make sure that db check detects minor problems, but doesn't complain +# about them too loudly (and doesn't exit with error status). + +AT_DATA(fileX, [blah blah +]) +AT_DATA(fileY, [stuff stuff +]) +# manifestX is: +# +# e3f2b0427b57241225ba1ffc2b67fecd64d07613 fileX +# +# where e3f2 etc. is as above + +AT_DATA(manifestX, [@<:@mdata 8e5d7e5ffca2393cfca5625ac9c1a19a46489f92@:>@ +H4sIAAAAAAAAAEs1TjNKMjAxMk8yNTcyMTQyMk1KNExLSzZKMjNPS01OMTNJMTA3MzRWUEjL +zEmN4AIA90gN9zAAAAA= +@<:@end@:>@ +]) + +ADD_FILE(testfile, [more stuff +]) +COMMIT(testbranch) +REV=`BASE_REVISION` + +AT_CHECK(MONOTONE cert $REV author extra_author, [], [ignore], [ignore]) + +AT_CHECK(MONOTONE fload < fileX, [], [ignore], [ignore]) +AT_CHECK(MONOTONE fload < fileY, [], [ignore], [ignore]) +AT_CHECK(MONOTONE read < manifestX, [], [ignore], [ignore]) + +AT_CHECK(MONOTONE db check, [], [ignore], [stderr]) + +AT_CHECK(grep -q 'problems detected: 3' stderr) +AT_CHECK(grep -q '0 serious' stderr) +AT_CHECK(grep -q 'minor problems detected' stderr) + +AT_CLEANUP --- tests/t_db_kill_rev_locally.at +++ tests/t_db_kill_rev_locally.at @@ -0,0 +1,24 @@ +AT_SETUP([db kill_rev_locally command]) +MONOTONE_SETUP + +# This tests the db kill_rev_locally command + +# Prepare a db with two revisions +AT_CHECK(echo "test">tmp, [], ignore, ignore) +AT_CHECK(MONOTONE add tmp, [], ignore, ignore) +AT_CHECK(MONOTONE --branch="test" --message="logtext" commit, [], ignore, stderr) +ANCESTOR=`cat stderr|awk '/committed revision/ {print $4}'` +AT_CHECK(echo "test2">>tmp, [], ignore, ignore) +AT_CHECK(MONOTONE --message="logtext" commit, [], ignore, stderr) +CHILD=`cat stderr|awk '/committed revision/ {print $4}'` + +# trying to kill the ancestor. This *is supposed to fail* +AT_CHECK(MONOTONE db kill_rev_locally $ANCESTOR, [1], ignore, ignore) +AT_CHECK(MONOTONE db check, [], ignore, ignore) + +# killing children is ok, though :) +AT_CHECK(MONOTONE db kill_rev_locally $CHILD, [], ignore, ignore) +AT_CHECK(MONOTONE db check, [], ignore, ignore) + +AT_CLEANUP +(END) --- testsuite.at +++ testsuite.at @@ -562,3 +562,6 @@ m4_include(tests/t_add_vs_commit.at) m4_include(tests/t_update_nonexistent.at) m4_include(tests/t_override_author_date.at) +m4_include(tests/t_add_stomp_file.at) +m4_include(tests/t_database_check_minor.at) +m4_include(tests/t_db_kill_rev_locally.at) --- transforms.cc +++ transforms.cc @@ -1,3 +1,4 @@ +// -*- mode: C++; c-file-style: "gnu"; indent-tabs-mode: nil -*- // copyright (C) 2002, 2003 graydon hoare // all rights reserved. // licensed to the public under the terms of the GNU GPL (>= 2) @@ -680,7 +681,142 @@ dst += linesep_str; } +// glob_to_regexp converts a sh file glob to a regexp. The regexp should +// be usable by the Boost regexp library. +// +// Pattern tranformation: +// +// - Any character except those described below are copied as they are. +// - The backslash (\) escapes the following character. The escaping +// backslash is copied to the regexp along with the following character. +// - * is transformed to .* in the regexp. +// - ? is transformed to . in the regexp. +// - { is transformed to ( in the regexp, unless within [ and ]. +// - } is transformed to ) in the regexp, unless within [ and ]. +// - , is transformed to | in the regexp, if within { and } and not +// within [ and ]. +// - ^ is escaped unless it comes directly after an unescaped [. +// - ! is transformed to ^ in the regexp if it comes directly after an +// unescaped [. +// - ] directly following an unescaped [ is escaped. +string glob_to_regexp(const string & glob) +{ + int in_braces = 0; // counter for levels if {} + bool in_brackets = false; // flags if we're inside a [], which + // has higher precedence than {}. + // Also, [ is accepted inside [] unescaped. + bool this_was_opening_bracket = false; + string tmp; + + tmp.reserve(glob.size() * 2); + #ifdef BUILD_UNIT_TESTS + cerr << "DEBUG[glob_to_regexp]: input = \"" << glob << "\"" << endl; +#endif + + for (string::const_iterator i = glob.begin(); i != glob.end(); ++i) + { + char c = *i; + bool last_was_opening_bracket = this_was_opening_bracket; + this_was_opening_bracket = false; + + // Special case ^ and ! at the beginning of a [] expression. + if (in_brackets && last_was_opening_bracket + && (c == '!' || c == '^')) + { + tmp += '^'; + if (++i == glob.end()) + break; + c = *i; + } + + if (c == '\\') + { + tmp += c; + if (++i == glob.end()) + break; + tmp += *i; + } + else if (in_brackets) + { + switch(c) + { + case ']': + if (!last_was_opening_bracket) + { + in_brackets = false; + tmp += c; + break; + } + // Trickling through to the standard character conversion, + // because ] as the first character of a set is regarded as + // a normal character. + default: + if (!(isalnum(c) || c == '_')) + { + tmp += '\\'; + } + tmp += c; + break; + } + } + else + { + switch(c) + { + case '*': + tmp += ".*"; + break; + case '?': + tmp += '.'; + break; + case '{': + in_braces++; + tmp += '('; + break; + case '}': + N(in_braces != 0, + F("trying to end a brace expression in a glob when none is started")); + tmp += ')'; + in_braces--; + break; + case '[': + in_brackets = true; + this_was_opening_bracket = true; + tmp += c; + break; + case ',': + if (in_braces > 0) + { + tmp += '|'; + break; + } + // Trickling through to default: here, since a comma outside of + // brace notation is just a normal character. + default: + if (!(isalnum(c) || c == '_')) + { + tmp += '\\'; + } + tmp += c; + break; + } + } + } + + N(!in_brackets, + F("run-away bracket expression in glob")); + N(in_braces == 0, + F("run-away brace expression in glob")); + +#ifdef BUILD_UNIT_TESTS + cerr << "DEBUG[glob_to_regexp]: output = \"" << tmp << "\"" << endl; +#endif + + return tmp; +} + +#ifdef BUILD_UNIT_TESTS #include "unit_tests.hh" static void @@ -966,6 +1102,15 @@ check_idna_encoding(); } +static void glob_to_regexp_test() +{ + BOOST_CHECK(glob_to_regexp("abc,v") == "abc\\,v"); + BOOST_CHECK(glob_to_regexp("foo[12m,]") == "foo[12m\\,]"); + // A full fledged, use all damn features test... + BOOST_CHECK(glob_to_regexp("foo.{bar*,cookie?{haha,hehe[^\\123!,]}}[!]a^b]") + == "foo\\.(bar.*|cookie.(haha|hehe[^\\123\\!\\,]))[^\\]a\\^b]"); +} + void add_transform_tests(test_suite * suite) { @@ -977,6 +1122,7 @@ suite->add(BOOST_TEST_CASE(&join_lines_test)); suite->add(BOOST_TEST_CASE(&strip_ws_test)); suite->add(BOOST_TEST_CASE(&encode_test)); + suite->add(BOOST_TEST_CASE(&glob_to_regexp_test)); } #endif // BUILD_UNIT_TESTS