# # # patch "cmd_ws_commit.cc" # from [1d0701613003c35e113c6ce99baa5328912c4992] # to [c2c9560d543334edf0025fb9ee0b9e308aabe03a] # # patch "simplestring_xform.cc" # from [c2c96565966cb3ccbbfbfaba124df173de6a6653] # to [706c09976618adc16c20cdafc98993d48dbd9617] # # patch "unit-tests/dates.cc" # from [8632e759182b511458d9e0ffe3ee7c7e5786ea36] # to [78271d63f77ab5d3e25ae95260b67f9a038e6b31] # # patch "unit-tests/simplestring_xform.cc" # from [ce5669547d1c501aae6e7619c37cde5fd25358cc] # to [6aae93f492f427a10ccdd220a92e21b5e8dc8302] # ============================================================ --- cmd_ws_commit.cc 1d0701613003c35e113c6ce99baa5328912c4992 +++ cmd_ws_commit.cc c2c9560d543334edf0025fb9ee0b9e308aabe03a @@ -63,6 +63,69 @@ get_old_branch_names(database & db, pare } } +class message_reader +{ +public: + message_reader(string const & message) : + message(message), offset(0) {} + + bool read(string const & text) + { + size_t len = text.length(); + if (message.compare(offset, len, text) == 0) + { + offset += len; + return true; + } + else + return false; + } + + string readline() + { + size_t eol = message.find_first_of("\r\n", offset); + if (eol == string::npos) + return ""; + + size_t len = eol - offset; + string line = message.substr(offset, len); + offset = eol+1; + + if (message[eol] == '\r' && message.length() > eol+1 && + message[eol+1] == '\n') + offset++; + + return trim(line); + } + + bool contains(string const & summary) + { + return message.find(summary, offset) != string::npos; + } + + bool remove(string const & summary) + { + size_t pos = message.find(summary, offset); + I(pos != string::npos); + if (pos + summary.length() == message.length()) + { + message.erase(pos); + return true; + } + else + return false; + } + + string content() + { + return message.substr(offset); + } + +private: + string message; + size_t offset; +}; + static void get_log_message_interactively(lua_hooks & lua, workspace & work, revision_id const rid, revision_t const & rev, @@ -111,7 +174,7 @@ get_log_message_interactively(lua_hooks // FIXME: save the full message in _MTN/changelog so its not lost - string raw(full_message()); + message_reader message(full_message()); // Check the message carefully to make sure the user didn't edit somewhere // outside of the author, date, branch or changelog values. The section @@ -122,95 +185,93 @@ get_log_message_interactively(lua_hooks // "Changes against parent ..." (following the changelog message) but both // of these are optional. - E(raw.find(instructions()) == 0, - origin::user, + E(message.read(instructions()), origin::user, F("Modifications outside of Author, Date, Branch or ChangeLog.\n" - "Commit failed (missing or modified instructions).")); + "Commit failed (missing/modified instructions).")); - if (!summary().empty()) - { - size_t pos = raw.find(summary()); + utf8 const AUTHOR(_("Author: ")); + utf8 const DATE(_("Date: ")); + utf8 const BRANCH(_("Branch: ")); + utf8 const CHANGELOG(_("ChangeLog: ")); - // ignore the blank lines around the changelog as well - E(pos != string::npos && - pos >= instructions().length() + header().length() - changelog().length() - 2, - origin::user, - F("Modifications outside of Author, Date, Branch or ChangeLog.\n" - "Commit failed (missing or modified summary).")); + // ---------------------------------------------------------------------- + // Revision: + // Parent: + // Parent: + + size_t pos = header().find(AUTHOR()); + I(pos != string::npos); - E(raw.length() == summary().length() + pos, - origin::user, - F("Modifications outside of Author, Date, Branch or ChangeLog.\n" - "Commit failed (text following summary).")); + string prefix = header().substr(0, pos); + E(message.read(prefix), origin::user, + F("Modifications outside of Author, Date, Branch or ChangeLog.\n" + "Commit failed (missing/modified Revision or Parent header).")); - raw.resize(pos); // remove the change summary - } + // Author: - raw = raw.substr(instructions().length()); // remove the instructions + E(message.read(AUTHOR()), origin::user, + F("Modifications outside of Author, Date, Branch or ChangeLog.\n" + "Commit failed (missing Author header).")); - string const AUTHOR(_("Author: ")); - string const DATE(_("Date: ")); - string const BRANCH(_("Branch: ")); - string const CHANGELOG(_("ChangeLog: ")); + author = message.readline(); - // ensure the first 3 or 4 lines from the header still match - size_t pos = header().find(AUTHOR); - E(header().substr(0, pos) == raw.substr(0, pos), - origin::user, + E(!author.empty(), origin::user, F("Modifications outside of Author, Date, Branch or ChangeLog.\n" - "Commit failed (missing Revision or Parent header).")); + "Commit failed (empty Author header).")); - raw = raw.substr(pos); // remove the leading unchanged header lines + // Date: - vector lines; - split_into_lines(raw, lines); - - E(lines.size() >= 4, - origin::user, + E(message.read(DATE()), origin::user, F("Modifications outside of Author, Date, Branch or ChangeLog.\n" - "Commit failed (missing lines).")); + "Commit failed (missing Date header).")); - vector::const_iterator line = lines.begin(); - E(line->find(AUTHOR) == 0, - origin::user, - F("Modifications outside of Author, Date, Branch or ChangeLog.\n" - "Commit failed (missing Author header).")); + string d = message.readline(); - author = trim(line->substr(AUTHOR.length())); - - ++line; - E(line->find(DATE) == 0, - origin::user, + E(!d.empty(), origin::user, F("Modifications outside of Author, Date, Branch or ChangeLog.\n" - "Commit failed (missing Date header).")); + "Commit failed (empty Date header).")); if (date_fmt.empty()) - date = date_t(trim(line->substr(DATE.length()))); + date = date_t(d); else - date = date_t::from_formatted_localtime(trim(line->substr(DATE.length())), - date_fmt); + date = date_t::from_formatted_localtime(d, date_fmt); - ++line; - E(line->find(BRANCH) == 0, - origin::user, + // Branch: + + E(message.read(BRANCH()), origin::user, F("Modifications outside of Author, Date, Branch or ChangeLog.\n" "Commit failed (missing Branch header).")); - branch = branch_name(trim(line->substr(BRANCH.length())), origin::user); + string b = message.readline(); - ++line; - E(*line == CHANGELOG, - origin::user, + E(!b.empty(), origin::user, F("Modifications outside of Author, Date, Branch or ChangeLog.\n" + "Commit failed (empty Branch header).")); + + branch = branch_name(b, origin::user); + + // ChangeLog: + + E(message.read(CHANGELOG()), origin::user, + F("Modifications outside of Author, Date, Branch or ChangeLog.\n" "Commit failed (missing ChangeLog header).")); - // now pointing at the optional blank line after ChangeLog - ++line; //FIXME if the optional blank line is missing this will probably - join_lines(line, lines.end(), raw); + // remove the summary before extracting the changelog content - raw = trim(raw) + "\n"; + if (!summary().empty()) + { + E(message.contains(summary()), origin::user, + F("Modifications outside of Author, Date, Branch or ChangeLog.\n" + "Commit failed (missing or modified summary).")); - log_message = utf8(raw, origin::user); + E(message.remove(summary()), origin::user, + F("Modifications outside of Author, Date, Branch or ChangeLog.\n" + "Commit failed (text following summary).")); + } + + string content = trim(message.content()) + '\n'; + + log_message = utf8(content, origin::user); } CMD(revert, "revert", "", CMD_REF(workspace), N_("[PATH]..."), @@ -1327,6 +1388,8 @@ CMD(commit, "commit", "ci", CMD_REF(work // We only check for empty log messages when the user entered them // interactively. Consensus was that if someone wanted to explicitly // type --message="", then there wasn't any reason to stop them. + // FIXME: perhaps there should be no changelog cert in this case. + E(log_message().find_first_not_of("\n\r\t ") != string::npos, origin::user, F("empty log message; commit canceled")); ============================================================ --- simplestring_xform.cc c2c96565966cb3ccbbfbfaba124df173de6a6653 +++ simplestring_xform.cc 706c09976618adc16c20cdafc98993d48dbd9617 @@ -218,6 +218,14 @@ trim_left(string const & s, string const string::size_type pos = tmp.find_first_not_of(chars); if (pos < string::npos) tmp = tmp.substr(pos); + + // if the first character in the string is still one of the specified + // characters then the entire string is made up of these characters + + pos = tmp.find_first_of(chars); + if (pos == 0) + tmp = ""; + return tmp; } @@ -228,6 +236,14 @@ trim_right(string const & s, string cons string::size_type pos = tmp.find_last_not_of(chars); if (pos < string::npos) tmp.erase(++pos); + + // if the last character in the string is still one of the specified + // characters then the entire string is made up of these characters + + pos = tmp.find_last_of(chars); + if (pos == tmp.size()-1) + tmp = ""; + return tmp; } @@ -241,6 +257,14 @@ trim(string const & s, string const & ch pos = tmp.find_first_not_of(chars); if (pos < string::npos) tmp = tmp.substr(pos); + + // if the first character in the string is still one of the specified + // characters then the entire string is made up of these characters + + pos = tmp.find_first_of(chars); + if (pos == 0) + tmp = ""; + return tmp; } ============================================================ --- unit-tests/dates.cc 8632e759182b511458d9e0ffe3ee7c7e5786ea36 +++ unit-tests/dates.cc 78271d63f77ab5d3e25ae95260b67f9a038e6b31 @@ -246,6 +246,55 @@ UNIT_TEST(roundtrip_localtimes) recoverable_failure); #undef OK } + +UNIT_TEST(localtime_formats) +{ +#define OK(d, f) do { \ + string formatted = d.as_formatted_localtime(f); \ + L(FL("format '%s' local date '%s'\n") % f % formatted); \ + date_t parsed = date_t::from_formatted_localtime(formatted, f); \ + UNIT_TEST_CHECK(parsed == d); \ + } while (0) + + // can we fiddle with LANG or TZ here? + + // note that %c doesn't work for en_CA.UTF-8 because it includes a timezone label + // that strptime doesn't parse. this leaves some of the input string unprocessed + // which date_t::from_formatted_localtime doesn't allow. + + // this is the valid range of dates supported by 32 bit time_t + date_t start("1901-12-13T20:45:52"); + date_t end("2038-01-19T03:14:07"); + + // test roughly 2 days per month in this entire range + for (date_t date = start; date <= end; date += MILLISEC(15*(DAY+HOUR+MIN+SEC))) + { + L(FL("\niso 8601 date '%s'\n") % date); + + // these all seem to work with the test setup of LANG=C and TZ=UTC + + OK(date, "%F %T"); // YYYY-MM-DD hh:mm:ss + OK(date, "%T %F"); // hh:mm:ss YYYY-MM-DD + OK(date, "%d %b %Y, %I:%M:%S %p"); + OK(date, "%a %b %d %H:%M:%S %Y"); + OK(date, "%a %d %b %Y %I:%M:%S %p %z"); + OK(date, "%a, %d %b %Y %H:%M:%S"); + OK(date, "%Y-%m-%d %H:%M:%S"); + OK(date, "%Y-%m-%dT%H:%M:%S"); + + // these do not + + //(date, "%x %X"); // YY-MM-DD hh:mm:ss + //(date, "%X %x"); // hh:mm:ss YY-MM-DD + //(date, "%+"); + + // posibly anything with a timezone label (%Z) will fail + //(date, "%a %d %b %Y %I:%M:%S %p %Z"); // the timezone label breaks this + } + +#undef OK +} + UNIT_TEST(from_unix_epoch) { #define OK_(x,y) do { \ ============================================================ --- unit-tests/simplestring_xform.cc ce5669547d1c501aae6e7619c37cde5fd25358cc +++ unit-tests/simplestring_xform.cc 6aae93f492f427a10ccdd220a92e21b5e8dc8302 @@ -130,8 +130,14 @@ UNIT_TEST(trimming) UNIT_TEST_CHECK(trim("trailing space \n") == "trailing space"); UNIT_TEST_CHECK(trim("\t\n both \r \n\r\n") == "both"); + // strings with nothing but whitespace should trim to nothing + UNIT_TEST_CHECK(trim_left(" \r\n\r\n\t\t\n\n\r\n ") == ""); + UNIT_TEST_CHECK(trim_right(" \r\n\r\n\t\t\n\n\r\n ") == ""); + UNIT_TEST_CHECK(trim(" \r\n\r\n\t\t\n\n\r\n ") == ""); + UNIT_TEST_CHECK(remove_ws(" I like going\tfor walks\n ") == "Ilikegoingforwalks"); + } // Local Variables: