# # # rename "tests/commit_using__MTN_log/commit_log.lua" # to "tests/commit_using__MTN_log/commit_cancelled.lua" # # rename "tests/commit_using__MTN_log/commit_log_modified_return.lua" # to "tests/commit_using__MTN_log/commit_confirmed.lua" # # add_dir "tests/changelog_editor" # # add_dir "tests/db_check_branch_leaves" # # add_dir "tests/log_recognizes_invalid_workspace_parent" # # add_file "rev_output.cc" # content [3dd86fa3304e75f321bab7892357dce2dfac33ec] # # add_file "rev_output.hh" # content [666dd3ed35e16d8b122b4932c2aad05a21a22e25] # # add_file "tests/changelog_editor/__driver__.lua" # content [96e979a9afb3cdab37abc18f09b32044d8f5a4ab] # # add_file "tests/changelog_editor/changelog.lua" # content [395e1d1d8755263c93cd5f66486baa1d5e7957a4] # # add_file "tests/db_check_branch_leaves/__driver__.lua" # content [283652fe9b2ccf7d834f74e28aed18efb29ccb59] # # add_file "tests/log_recognizes_invalid_workspace_parent/__driver__.lua" # content [6f35ab85a83153a157bbefff0e3f65c9a5dbb0b0] # # patch "Makefile.am" # from [a0ed2ec6eda766d2da11b80abf961781e4c486b9] # to [2edf48887f6a0df8e512dffef242d9c3c9c7aa49] # # patch "NEWS" # from [96cc469d41eda0ab62590dbbaa1aa0143e7d9ec1] # to [e8fc0a6f14177e59c3ee6e2d8900fd448ae2bfc4] # # patch "cmd_diff_log.cc" # from [1f1811415796db5326a6c6be183acb4bf72b900b] # to [8e1fe32880b51238d9352b547b201321b3befb2a] # # patch "cmd_key_cert.cc" # from [4f67608a826cf01297dd297ad9aaf5a18c85a82a] # to [0e5707233eb6e8687029ee792dfb75ad5668e3e2] # # patch "cmd_list.cc" # from [001d3407a8d30b05a29f7e6bf63138696022fac9] # to [03d285c5e0f6c9889f427e4d189a6f542629368b] # # patch "cmd_ws_commit.cc" # from [10d33763a838c34d36cdacf987678905f77762be] # to [e3eed43a97da68628c4fabc80ced9ea3327871f8] # # patch "contrib/edit_comment_from_changelog.lua" # from [392821013c884731eaef31836acab2c2c5938373] # to [fa734b0c03818ea1790aa939ac3db59441eec9fc] # # patch "database.cc" # from [026b0f32727c3e31be115a47a4b8de251078151a] # to [25a7d09b691fa29743249d4151dc27bab18c7923] # # patch "database.hh" # from [c7ae2340052b1205f3615f416adb1c58917abfd5] # to [654c356dcdbe48064357eaa66fb6e7c9b5a9d9b5] # # patch "database_check.cc" # from [55d80d4456c96b241ceabd77be9872cfb7285ab6] # to [eb2f10d33d8a4b345982a09ccb29a3d4235c06ab] # # patch "dates.cc" # from [8a01802be5f9f514b73a29a9480a09d8415df3c4] # to [24dd854d8c1be66b65a57d813b756ddf13550848] # # patch "dates.hh" # from [e25c6a24f11ef3e948b44d7adb694661183b1d1a] # to [3a730e5536cece2437e9caad0e4f8f2767e4887d] # # patch "key_store.cc" # from [6ea38c00f7da18dd8136f0a4080b2fb7a8cbedba] # to [30d4b56d5274333246ece56ac4f1fd2b2e86c33a] # # patch "keys.cc" # from [c422791c7c491e8cd0360ddc4f93f2a59b8651ae] # to [e27fd43f8730dc5ada1f4aaddfb55e910ac659ac] # # patch "keys.hh" # from [f48a9a4a28dd60cc8b89cb4f0fc9ecf0dd8613c9] # to [6b5412008a7158b8abfb8228163b687538d7879f] # # patch "lua_hooks.cc" # from [d4f06492ebfd19893156cf0021b7afef29009332] # to [13ee547d9fe9b6b303a9e68636a8f784856c490b] # # patch "lua_hooks.hh" # from [e4bfa01a091e43739f11ecfc0b6c096e089b127e] # to [40d020a0b33504b110181d8272ad664a4e622e4e] # # patch "migrate_ancestry.cc" # from [87b81a3b30c9299f5919f8f89b154843a7171989] # to [790cf2e18a4653af131bdb350e9c3ee4e223e294] # # patch "monotone.texi" # from [ce013122c5f540df1e48aa56e7452d73887efa35] # to [25cf3d90449d7a6304c612671738c12cc49ba0e9] # # patch "packet.cc" # from [36b0f681ca92776c2e60e16529ef7cd7d826a03f] # to [7a0c4e9c49cba8d651e2ba8a69c8eee80769bc47] # # patch "simplestring_xform.cc" # from [4d7bb0ac67e722f14a3f81722e05f249d009f645] # to [de3c230648303fb799bdd907bb90985d2ed7e97a] # # patch "simplestring_xform.hh" # from [5a6e7a65e2564f2a01321844a1ff6d98e6f58646] # to [fb077a97ed6338787a652441b97c809750aac454] # # patch "std_hooks.lua" # from [a8ce32582e19e799eef97ddb45e91418c8b24fcc] # to [bc071c124deba2c01c60120e449c861ece126b81] # # patch "tests/_--author,_--date/__driver__.lua" # from [32996584305ed55d69e88dd14f0570e6da40b827] # to [39f17d4b68507906b2c98cefda79c2c0d9fc9ba3] # # patch "tests/_MTN_files_handled_correctly_in_aborted_commit/bad_edit_comment.lua" # from [98cfa1aefd465bf9b3e8169c40c33ccc204fdbfd] # to [bb3533964abf2e17d7d7ca490589c497a50285d8] # # patch "tests/checkout_creates__MTN_log/commit_log.lua" # from [a9af6de4500c2fddab1853eebc185a055156f8cd] # to [e1b07045d69de57d75e5b73df41124c2c94b315d] # # patch "tests/clone_creates__MTN_log/commit_log.lua" # from [a9af6de4500c2fddab1853eebc185a055156f8cd] # to [e1b07045d69de57d75e5b73df41124c2c94b315d] # # patch "tests/commit_default_editor/test_hooks.lua" # from [bf4c9c22f206ed2ada4074eaac831948a36b6c87] # to [acc4afeabd22d8a4dbe2329ca5907202d875b89f] # # patch "tests/commit_using__MTN_log/__driver__.lua" # from [a9b708bd811300cc5c4ca78b932f50f7f93341cf] # to [64507df85499593c9de2e854a3add3b4d6683c47] # # patch "tests/commit_using__MTN_log/commit_cancelled.lua" # from [09c926209c317efc447dbe6448fe891c2d86e958] # to [1b64ffe9a275897fe60c629350ac44b5bb3afc62] # # patch "tests/commit_using__MTN_log/commit_confirmed.lua" # from [a9af6de4500c2fddab1853eebc185a055156f8cd] # to [13f324e5f74d60c595cbf45b2eb5f85115829967] # # patch "tests/commit_using__MTN_log_and_--message/commit_log.lua" # from [09c926209c317efc447dbe6448fe891c2d86e958] # to [e1b07045d69de57d75e5b73df41124c2c94b315d] # # patch "tests/commit_validation_lua_hook/errmsg" # from [33334887b6211bde36d406f5fceaf4c8fca84170] # to [b1f67b9ea367e6bffd33f28da4996b73bdc2bd21] # # patch "tests/commit_with_--message-file/commit_log.lua" # from [09c926209c317efc447dbe6448fe891c2d86e958] # to [e1b07045d69de57d75e5b73df41124c2c94b315d] # # patch "tests/commit_writes_message_back_to__MTN_log/__driver__.lua" # from [7758bd1d4316f2c3e80cc4018a4b3ab4067137b7] # to [7bee4d10f6e1cd255e8a6501ad8d93d5efa1a48b] # # patch "tests/commit_writes_message_back_to__MTN_log/my_hook.lua" # from [2c27a968d793839ad3c7dade630eb58837ea2f79] # to [2a7cb74c1a3c220ce8f4446357d87f70e8988e22] # # patch "tests/db_kill_tag_locally/__driver__.lua" # from [eb96c1472d7ef266ce84d3fcfb4beb75b3c57661] # to [412ff7d7544bb55aee63e6e6333ae23b1acc12e6] # # patch "tests/git_export/__driver__.lua" # from [ca3862a9039ee4e3f52329a6d0cfad08c347cc33] # to [d7ba0d936847de3d24d493df4167d687b3f63618] # # patch "tests/i18n_commit_messages/__driver__.lua" # from [2e3be837715a30fa20743f0f4a54fceaf4a7dbfe] # to [4d8c64139a02a00aa3a3aca1e2e703849adf4d15] # # patch "tests/i18n_commit_messages/euc-jp.txt" # from [e42d3979ee6d233a489f9a21697abf8786bc8b65] # to [0894414faade0c80e732c757474203f9de358469] # # patch "tests/i18n_commit_messages/extra_hooks.lua" # from [ca81a1f657530409dbbe1ec50b52a9e253b0f411] # to [d80fb2f538a58b7ba6430e6b8af6305edaa2abb8] # # patch "tests/i18n_commit_messages/utf8.txt" # from [681c6291b3f365c477e93d5bd77192d88253b486] # to [7cb4bf2f531c9b04b50e7c120e87465087b88cbd] # # patch "tests/importing_cvs_metadata/expected_log_output" # from [2a3629825db9242abe52c0107f5269d34c125494] # to [1cd19bc2ebd7cf09842b51879d71d3a75812d39b] # # patch "tests/importing_cvs_tricky_repo_with_tags/test.tags" # from [b8838f53fc6b7e0aee11074fa2e466e3615aa148] # to [7e382e03da27d4a3309a4661e5154669078f4dfe] # # patch "tests/ls_tags_with_ambiguous_tags/__driver__.lua" # from [86ca8c16ab1e8b7c071c1e3dd73f47852059a31e] # to [5478308acb6e9d10f39a2411e5cfae4637cfe737] # # patch "tests/merge_into_workspace/expected-log" # from [77915a878636ca16ee32f5385d88a7dc8bd0054a] # to [e43ddd168775004aa49e0dc8afc1ba8604b8678f] # # patch "tests/merge_into_workspace/expected-log-left" # from [6f396f4e143f0b58ef18131c13549622eea39cfb] # to [ff2b6b8ad837acf826dffa1ad07cd67714279805] # # patch "tests/tags_and_tagging_of_revisions/__driver__.lua" # from [417a74a8f678051c254c7fc3a041d9f703987239] # to [81e6f1af6cbe3b957c7a8680a5539a2d14da858d] # # patch "unit-tests/dates.cc" # from [af8a777e15c6cccc98f7c006dfe4d84ffd5636ba] # to [fc614466a2130277bb98e89db1398d6b85cde1a1] # # patch "unit-tests/simplestring_xform.cc" # from [ce5669547d1c501aae6e7619c37cde5fd25358cc] # to [6aae93f492f427a10ccdd220a92e21b5e8dc8302] # # patch "win32/main.cc" # from [283e7fe96c5964a4408e17344a94b5a87b0ce6af] # to [8b14cc20945d524354751fd745044d96cfe69f96] # # patch "work.cc" # from [28266a7fbb698d94c998047e52842b220b37756d] # to [7e3829c75634ee5bbf555209dfa152d0bd88138c] # # patch "work.hh" # from [021031510460f3cdc3762460937d94b575f04154] # to [3f94623eb296347caca9ed341f0bee2ef63481be] # ============================================================ --- rev_output.cc 3dd86fa3304e75f321bab7892357dce2dfac33ec +++ rev_output.cc 3dd86fa3304e75f321bab7892357dce2dfac33ec @@ -0,0 +1,199 @@ +// Copyright (C) 2010 Derek Scherger +// +// This program is made available under the GNU GPL version 2.0 or +// greater. See the accompanying file COPYING for details. +// +// This program is distributed WITHOUT ANY WARRANTY; without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. + +#include "base.hh" +#include +#include +#include +#include + +#include "cert.hh" +#include "cset.hh" +#include "dates.hh" +#include "project.hh" +#include "rev_output.hh" +#include "revision.hh" + +using std::map; +using std::ostringstream; +using std::pair; +using std::set; +using std::string; +using std::vector; + +void +revision_header(revision_id const rid, revision_t const & rev, + string const & author, date_t const date, + branch_name const & branch, utf8 const & changelog, + string const & date_fmt, utf8 & header) +{ + vector certs; + key_id empty_key; + certs.push_back(cert(rid, author_cert_name, + cert_value(author, origin::user), empty_key)); + certs.push_back(cert(rid, date_cert_name, + cert_value(date.as_iso_8601_extended(), origin::user), + empty_key)); + certs.push_back(cert(rid, branch_cert_name, + cert_value(branch(), origin::user), empty_key)); + + if (!changelog().empty()) + certs.push_back(cert(rid, changelog_cert_name, + cert_value(changelog(), origin::user), empty_key)); + + revision_header(rid, rev, certs, date_fmt, header); +} + +void +revision_header(revision_id const rid, revision_t const & rev, + vector const & certs, string const & date_fmt, + utf8 & header) +{ + ostringstream out; + + out << string(70, '-') << '\n' + << _("Revision: ") << rid << '\n'; + + for (edge_map::const_iterator i = rev.edges.begin(); i != rev.edges.end(); ++i) + { + revision_id parent = edge_old_revision(*i); + if (!null_id(parent)) + out << _("Parent: ") << parent << '\n'; + } + + cert_name const author(author_cert_name); + cert_name const date(date_cert_name); + cert_name const branch(branch_cert_name); + cert_name const tag(tag_cert_name); + cert_name const changelog(changelog_cert_name); + cert_name const comment(comment_cert_name); + + for (vector::const_iterator i = certs.begin(); i != certs.end(); ++i) + if (i->name == author) + out << _("Author: ") << i->value << '\n'; + + for (vector::const_iterator i = certs.begin(); i != certs.end(); ++i) + if (i->name == date) + { + if (date_fmt.empty()) + out << _("Date: ") << i->value << '\n'; + else + { + date_t date(i->value()); + out << _("Date: ") << date.as_formatted_localtime(date_fmt) << '\n'; + } + } + + for (vector::const_iterator i = certs.begin(); i != certs.end(); ++i) + if (i->name == branch) + out << _("Branch: ") << i->value << '\n'; + + for (vector::const_iterator i = certs.begin(); i != certs.end(); ++i) + if (i->name == tag) + out << _("Tag: ") << i->value << '\n'; + + out << "\n"; + + for (vector::const_iterator i = certs.begin(); i != certs.end(); ++i) + if (i->name == changelog) + { + out << _("Changelog: ") << "\n\n" << i->value << '\n'; + if (!i->value().empty() && i->value()[i->value().length()-1] != '\n') + out << '\n'; + } + + for (vector::const_iterator i = certs.begin(); i != certs.end(); ++i) + if (i->name == comment) + { + out << _("Comments: ") << "\n\n" << i->value << '\n'; + if (!i->value().empty() && i->value()[i->value().length()-1] != '\n') + out << '\n'; + } + + header = utf8(out.str(), origin::internal); +} + +void +revision_summary(revision_t const & rev, utf8 & summary) +{ + // We intentionally do not collapse the final \n into the format + // strings here, for consistency with newline conventions used by most + // other format strings. + + ostringstream out; + revision_id rid; + calculate_ident(rev, rid); + + for (edge_map::const_iterator i = rev.edges.begin(); i != rev.edges.end(); ++i) + { + revision_id parent = edge_old_revision(*i); + cset const & cs = edge_changes(*i); + + // A colon at the end of this string looked nicer, but it made + // double-click copying from terminals annoying. + if (null_id(parent)) + out << _("Changes") << "\n\n"; + else + out << _("Changes against parent ") << parent << "\n\n"; + + // presumably a merge rev could have an empty edge if one side won + if (cs.empty()) + out << _("no changes") << '\n'; + + for (set::const_iterator i = cs.nodes_deleted.begin(); + i != cs.nodes_deleted.end(); ++i) + out << _(" dropped ") << *i << '\n'; + + for (map::const_iterator + i = cs.nodes_renamed.begin(); + i != cs.nodes_renamed.end(); ++i) + out << _(" renamed ") << i->first << '\n' + << _(" to ") << i->second << '\n'; + + for (set::const_iterator i = cs.dirs_added.begin(); + i != cs.dirs_added.end(); ++i) + out << _(" added ") << *i << '\n'; + + for (map::const_iterator i = cs.files_added.begin(); + i != cs.files_added.end(); ++i) + out << _(" added ") << i->first << '\n'; + + for (map >::const_iterator + i = cs.deltas_applied.begin(); i != cs.deltas_applied.end(); ++i) + out << _(" patched ") << i->first << '\n'; + + for (map, attr_value >::const_iterator + i = cs.attrs_set.begin(); i != cs.attrs_set.end(); ++i) + out << _(" attr on ") << i->first.first << '\n' + << _(" set ") << i->first.second << '\n' + << _(" to ") << i->second << '\n'; + + // FIXME: naming here could not be more inconsistent + // the cset calls it attrs_cleared + // the command is attr drop + // here it is called unset + // the revision text uses attr clear + + for (set >::const_iterator + i = cs.attrs_cleared.begin(); i != cs.attrs_cleared.end(); ++i) + out << _(" attr on ") << i->first << '\n' + << _(" unset ") << i->second << '\n'; + + out << '\n'; + } + summary = utf8(out.str(), origin::internal); +} + +// Local Variables: +// mode: C++ +// fill-column: 76 +// c-file-style: "gnu" +// indent-tabs-mode: nil +// End: +// vim: et:sw=2:sts=2:ts=2:cino=>2s,{s,\:s,+s,t0,g0,^-2,e-2,n-2,p2s,(0,=s: ============================================================ --- rev_output.hh 666dd3ed35e16d8b122b4932c2aad05a21a22e25 +++ rev_output.hh 666dd3ed35e16d8b122b4932c2aad05a21a22e25 @@ -0,0 +1,41 @@ +// Copyright (C) 2010 Derek Scherger +// +// This program is made available under the GNU GPL version 2.0 or +// greater. See the accompanying file COPYING for details. +// +// This program is distributed WITHOUT ANY WARRANTY; without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. + +#ifndef __REV_SUMMARY_HH__ +#define __REV_SUMMARY_HH__ + +#include "rev_types.hh" +#include "vocab.hh" + +struct date_t; +struct cert; + +void +revision_header(revision_id const rid, revision_t const & rev, + std::string const & author, date_t const date, + branch_name const & branch, utf8 const & changelog, + std::string const & date_fmt, utf8 & header); + +void +revision_header(revision_id const rid, revision_t const & rev, + std::vector const & certs, std::string const & date_fmt, + utf8 & header); + +void +revision_summary(revision_t const & rev, utf8 & summary); + +#endif // header guard + +// Local Variables: +// mode: C++ +// fill-column: 76 +// c-file-style: "gnu" +// indent-tabs-mode: nil +// End: +// vim: et:sw=2:sts=2:ts=2:cino=>2s,{s,\:s,+s,t0,g0,^-2,e-2,n-2,p2s,(0,=s: ============================================================ --- tests/changelog_editor/__driver__.lua 96e979a9afb3cdab37abc18f09b32044d8f5a4ab +++ tests/changelog_editor/__driver__.lua 96e979a9afb3cdab37abc18f09b32044d8f5a4ab @@ -0,0 +1,237 @@ +mtn_setup() +addfile("a", "hello world") +commit() +writefile("a", "aaa") + +check(get("changelog.lua")) + +-- status warns with bad date format + +check(mtn("status"), 0, false, false) +check(mtn("status", "--date-format", "%F"), 0, false, true) +check(qgrep("date format", "stderr")) + + +-- commits that fail + + +-- commit fails with bad date format + +check(mtn("commit", "--date-format", "%F"), 1, false, true) +check(qgrep("date format", "stderr")) +check(not exists("_MTN/commit")) + +-- commit fails with empty message + +writefile("_MTN/log", "empty message") +check(mtn("commit", "--rcfile=changelog.lua"), 1, false, true) +check(qgrep("empty log message", "stderr")) +check(not exists("_MTN/commit")) + +-- commit fails with modified/missing instructions + +writefile("_MTN/log", "missing instructions") +check(mtn("commit", "--rcfile=changelog.lua"), 1, false, true) +check(qgrep("Instructions not found", "stderr")) +check(exists("_MTN/commit")) +check(fsize("_MTN/commit") > 0) +id1=sha1("_MTN/commit") + +-- commit fails if _MTN/commit exists from previously failed commit +check(mtn("commit"), 1, false, true) +check(qgrep("previously failed commit", "stderr")) +check(exists("_MTN/commit")) +check(fsize("_MTN/commit") > 0) +id2=sha1("_MTN/commit") +check(id1 == id2) +remove("_MTN/commit") + +-- commit can be cancelled + +writefile("_MTN/log", "cancel") +check(mtn("commit", "--rcfile=changelog.lua"), 1, false, true) +check(qgrep("Commit cancelled.", "stderr")) +check(not exists("_MTN/commit")) + +-- commit fails with modified/missing separator, Revision: or Parent: lines + +writefile("_MTN/log", "missing separator") +check(mtn("commit", "--rcfile=changelog.lua"), 1, false, true) +check(qgrep("Revision/Parent header not found", "stderr")) +check(exists("_MTN/commit")) +check(fsize("_MTN/commit") > 0) +remove("_MTN/commit") + +writefile("_MTN/log", "missing revision") +check(mtn("commit", "--rcfile=changelog.lua"), 1, false, true) +check(qgrep("Revision/Parent header not found", "stderr")) +check(exists("_MTN/commit")) +check(fsize("_MTN/commit") > 0) +remove("_MTN/commit") + +writefile("_MTN/log", "missing parent") +check(mtn("commit", "--rcfile=changelog.lua"), 1, false, true) +check(qgrep("Revision/Parent header not found", "stderr")) +check(exists("_MTN/commit")) +check(fsize("_MTN/commit") > 0) +remove("_MTN/commit") + +-- commit fails with modified/missing Author: line + +writefile("_MTN/log", "missing author") +check(mtn("commit", "--rcfile=changelog.lua"), 1, false, true) +check(qgrep("Author header not found", "stderr")) +check(exists("_MTN/commit")) +check(fsize("_MTN/commit") > 0) +remove("_MTN/commit") + +-- commit fails with empty Author: line + +writefile("_MTN/log", "empty author") +check(mtn("commit", "--rcfile=changelog.lua"), 1, false, true) +check(qgrep("Author value empty", "stderr")) +check(exists("_MTN/commit")) +check(fsize("_MTN/commit") > 0) +remove("_MTN/commit") + +-- commit fails with modified/missing Date: line + +writefile("_MTN/log", "missing date") +check(mtn("commit", "--rcfile=changelog.lua"), 1, false, true) +check(qgrep("Date header not found", "stderr")) +check(exists("_MTN/commit")) +check(fsize("_MTN/commit") > 0) +remove("_MTN/commit") + +-- commit fails with empty Date: line + +writefile("_MTN/log", "empty date") +check(mtn("commit", "--rcfile=changelog.lua"), 1, false, true) +check(qgrep("Date value empty", "stderr")) +check(exists("_MTN/commit")) +check(fsize("_MTN/commit") > 0) +remove("_MTN/commit") + +-- commit fails with modified/missing Branch: line + +writefile("_MTN/log", "missing branch") +check(mtn("commit", "--rcfile=changelog.lua"), 1, false, true) +check(qgrep("Branch header not found", "stderr")) +check(exists("_MTN/commit")) +check(fsize("_MTN/commit") > 0) +remove("_MTN/commit") + +-- commit fails with empty Branch: line + +writefile("_MTN/log", "empty branch") +check(mtn("commit", "--rcfile=changelog.lua"), 1, false, true) +check(qgrep("Branch value empty", "stderr")) +check(exists("_MTN/commit")) +check(fsize("_MTN/commit") > 0) +remove("_MTN/commit") + +-- commit fails with modified/missing blank line before Changelog section + +writefile("_MTN/log", "missing blank line") +check(mtn("commit", "--rcfile=changelog.lua"), 1, false, true) +check(qgrep("Changelog header not found", "stderr")) +check(exists("_MTN/commit")) +check(fsize("_MTN/commit") > 0) +remove("_MTN/commit") + +-- commit fails with modified/missing Changelog section + +writefile("_MTN/log", "missing changelog") +check(mtn("commit", "--rcfile=changelog.lua"), 1, false, true) +check(qgrep("Changelog header not found", "stderr")) +check(exists("_MTN/commit")) +check(fsize("_MTN/commit") > 0) +remove("_MTN/commit") + +-- commit fails with missing Change summary section + +writefile("_MTN/log", "missing summary") +check(mtn("commit", "--rcfile=changelog.lua"), 1, false, true) +check(qgrep("Change summary not found", "stderr")) +check(exists("_MTN/commit")) +check(fsize("_MTN/commit") > 0) +remove("_MTN/commit") + +-- commit fails with duplicated Change summary section + +writefile("_MTN/log", "duplicated summary") +check(mtn("commit", "--rcfile=changelog.lua"), 1, false, true) +check(qgrep("Text following Change summary", "stderr")) +check(exists("_MTN/commit")) +check(fsize("_MTN/commit") > 0) +remove("_MTN/commit") + +-- commit fails with new text after Change summary section + +writefile("_MTN/log", "trailing text") +check(mtn("commit", "--rcfile=changelog.lua"), 1, false, true) +check(qgrep("Text following Change summary", "stderr")) +check(exists("_MTN/commit")) +check(fsize("_MTN/commit") > 0) +remove("_MTN/commit") + + +-- commits that succeed + + +-- test unchanged --date, --author and --branch options + +writefile("a", "a2.1") +writefile("_MTN/log", "fine") +check(mtn("commit", "--rcfile=changelog.lua", "--date", "2010-01-01T01:01:01", "--author", "bobo", "--branch", "left"), 0, false, false) +check(mtn("log", "--last", "1", "--no-graph"), 0, true, false) +check(qgrep("Date: 2010-01-01T01:01:01", "stdout")) +check(qgrep("Author: bobo", "stdout")) +check(qgrep("Branch: left", "stdout")) +check(not exists("_MTN/commit")) + +-- test changed --date, --author and --branch options + +writefile("a", "a2.2") +writefile("_MTN/log", "change author/date/branch") +check(mtn("commit", "--rcfile=changelog.lua", "--date", "2010-01-01T01:01:01", "--author", "bobo", "--branch", "left"), 0, false, false) +check(mtn("log", "--last", "1", "--no-graph"), 0, true, false) +check(not qgrep("Date: 2010-01-01T01:01:01", "stdout")) +check(not qgrep("Author: bobo", "stdout")) +check(not qgrep("Branch: left", "stdout")) +check(not exists("_MTN/commit")) + +-- test unchanged date gets updated to reflect current time + +writefile("a", "a3.1") +writefile("_MTN/log", "sleep") +check(mtn("commit", "--rcfile=changelog.lua"), 0, false, false) +check(mtn("log", "--last", "1", "--no-graph"), 0, true, false) +log = readfile("stdout") +old = string.match(log, "Old: ([^\n]*)") +new = string.match(log, "Date: ([^\n]*)") +check(old ~= new) +check(not exists("_MTN/commit")) + +-- test changed date does not get updated + +writefile("a", "a3.2") +writefile("_MTN/log", "change date") +check(mtn("commit", "--rcfile=changelog.lua"), 0, false, false) +check(mtn("log", "--last", "1", "--no-graph"), 0, true, false) +check(qgrep("Date: 2010-01-01T01:01:01", "stdout")) +check(not exists("_MTN/commit")) + +-- message on same line as Changelog: header + +writefile("a", "a4") +writefile("_MTN/log", "changelog line") +check(mtn("commit", "--rcfile=changelog.lua"), 0, false, false) +check(not exists("_MTN/commit")) + +-- message filling entire Changelog section (no leading/trailing blank lines) + +writefile("a", "a5") +writefile("_MTN/log", "full changelog") +check(mtn("commit", "--rcfile=changelog.lua"), 0, false, false) +check(not exists("_MTN/commit")) ============================================================ --- tests/changelog_editor/changelog.lua 395e1d1d8755263c93cd5f66486baa1d5e7957a4 +++ tests/changelog_editor/changelog.lua 395e1d1d8755263c93cd5f66486baa1d5e7957a4 @@ -0,0 +1,62 @@ +function edit_comment(user_log_file) + + -- commits that fail + + if (string.find(user_log_file, "\nempty message\n")) then + return string.gsub(user_log_file, "\nempty message\n", "") + elseif (string.find(user_log_file, "\nmissing instructions\n")) then + return "foobar" .. user_log_file + elseif (string.find(user_log_file, "\ncancel\n")) then + return string.gsub(user_log_file, "... REMOVE THIS LINE TO CANCEL THE COMMIT ...\n", "") + elseif (string.find(user_log_file, "\nmissing separator\n")) then + return string.gsub(user_log_file, "---------------\n", "\n") + elseif (string.find(user_log_file, "\nmissing revision\n")) then + return string.gsub(user_log_file, "\nRevision:", "\nrevision:") + elseif (string.find(user_log_file, "\nmissing parent\n")) then + return string.gsub(user_log_file, "\nAuthor:", "foobar\nAuthor:") + elseif (string.find(user_log_file, "\nmissing author\n")) then + return string.gsub(user_log_file, "\nAuthor:", "\nAutor:") + elseif (string.find(user_log_file, "\nempty author\n")) then + return string.gsub(user_log_file, "\nAuthor: [^\n]*\n", "\nAuthor: \n") + elseif (string.find(user_log_file, "\nmissing date\n")) then + return string.gsub(user_log_file, "\nDate:", "\nDate") + elseif (string.find(user_log_file, "\nempty date\n")) then + return string.gsub(user_log_file, "\nDate: [^\n]*\n", "\nDate: \n") + elseif (string.find(user_log_file, "\nmissing branch\n")) then + return string.gsub(user_log_file, "\nBranch:", "\nranch:") + elseif (string.find(user_log_file, "\nempty branch\n")) then + return string.gsub(user_log_file, "\nBranch: [^\n]*\n", "\nBranch: \n") + elseif (string.find(user_log_file, "\nmissing blank line\n")) then + return string.gsub(user_log_file, "\n\nChangelog:", "\nChangelog:") + elseif (string.find(user_log_file, "\nmissing changelog\n")) then + return string.gsub(user_log_file, "\nChangelog:", "\n") + elseif (string.find(user_log_file, "\nmissing summary\n")) then + return string.gsub(user_log_file, "\nChanges against parent ", "\nChange foobar") + elseif (string.find(user_log_file, "\nduplicated summary\n")) then + return string.gsub(user_log_file, "(Changes against parent .*)", "%1%1") + elseif (string.find(user_log_file, "\ntrailing text\n")) then + return user_log_file .. "foobar" + end + + -- commits that succeed + + if (string.find(user_log_file, "\nchange author/date/branch\n")) then + result = user_log_file + result = string.gsub(result, "\nDate: [^\n]*\n", "\nDate: 2010-02-02T02:02:02\n") + result = string.gsub(result, "\nAuthor: bobo\n", "\nAuthor: baba\n") + result = string.gsub(result, "\nBranch: left\n", "\nBranch: right\n") + return result + elseif (string.find(user_log_file, "\nsleep\n")) then + date = string.match(user_log_file, "\nDate: ([^\n]*)") + sleep(2) + return string.gsub(user_log_file, "\nChangelog: \n\nsleep", "\nChangelog: \n\nOld: " .. date) + elseif (string.find(user_log_file, "\nchange date\n")) then + return string.gsub(user_log_file, "\nDate: [^\n]*\n", "\nDate: 2010-01-01T01:01:01\n") + elseif (string.find(user_log_file, "\nchangelog line\n")) then + return string.gsub(user_log_file, "\nChangelog: \n\nchangelog line", "\nChangelog:message on changelog line") + elseif (string.find(user_log_file, "\nfull changelog\n")) then + return string.gsub(user_log_file, "\nChangelog: \n\nfull changelog\n\n", "\nChangelog:no\nspace\naround\nthis\nchangelog\n-----") + end + + return user_log_file +end ============================================================ --- tests/db_check_branch_leaves/__driver__.lua 283652fe9b2ccf7d834f74e28aed18efb29ccb59 +++ tests/db_check_branch_leaves/__driver__.lua 283652fe9b2ccf7d834f74e28aed18efb29ccb59 @@ -0,0 +1,67 @@ +-- db check checks branch_leaves table + +-- a bug in mtn 0.46 caused extra entries to be left in the +-- branch_leaves table; this shows that 'db check' identifies that, +-- and 'regenerate_caches' fixes it. + +mtn_setup() + +-- create two heads on testbranch, one head on otherbranch +addfile("file1", "rev_A") +commit("testbranch", "rev_A") +base = base_revision() + +writefile("file1", "rev_a") +commit("testbranch", "rev_a") +rev_a = base_revision() + +revert_to(base) + +addfile("file2", "rev_b") +commit("testbranch", "rev_b") +rev_b = base_revision() + +writefile("file2", "rev_c") +commit("otherbranch", "rev_c") +rev_c = base_revision() + +-- db should be ok +check(mtn("db", "check"), 0, false, false) + +-- branch names are stored as binary blobs in the database; +-- these values are from sqlite3 test.db .dump branch_leaves. +testbranch_name = "X'746573746272616E6368'" +otherbranch_name = "X'6F746865726272616E6368'" + +-- add an extra head on 'testbranch' in branch_leaves +check(mtn("db", "execute", "INSERT INTO branch_leaves (branch, revision_id) VALUES (" .. testbranch_name .. ", x'" .. base .. "');"), false, false) + +-- delete 'otherbranch' from branch_leaves +check(mtn("db", "execute", "DELETE FROM branch_leaves WHERE branch=" .. otherbranch_name .. ";"), false, false) + +-- add an extra branch in branch_leaves; it doesn't matter that the +-- name is not actually binary +check(mtn("db", "execute", "INSERT INTO branch_leaves (branch, revision_id) VALUES ('bogusbranch', x'" .. base .. "');"), false, false) + +-- db is not ok +check(mtn("db", "check"), 1, false, true) +check(qgrep("cached branch 'bogusbranch' not used", "stderr")) +check(qgrep("branch 'otherbranch' not cached", "stderr")) +check(qgrep("branch 'testbranch' wrong head count", "stderr")) + +-- fix it +check(mtn("db", "regenerate_caches"), 0, false, false) + +check(mtn("db", "check"), 0, false, false) + +-- double check +check(mtn("automate", "heads", "testbranch"), 0, true, false) +check(readfile("stdout") == rev_a .. "\n" .. rev_b .. "\n") + +check(mtn("automate", "heads", "otherbranch"), 0, true, false) +check(readfile("stdout") == rev_c .. "\n") + +check(mtn("automate", "heads", "bogusbranch"), 0, true, false) +check(readfile("stdout") == "") + +-- end of file ============================================================ --- tests/log_recognizes_invalid_workspace_parent/__driver__.lua 6f35ab85a83153a157bbefff0e3f65c9a5dbb0b0 +++ tests/log_recognizes_invalid_workspace_parent/__driver__.lua 6f35ab85a83153a157bbefff0e3f65c9a5dbb0b0 @@ -0,0 +1,21 @@ + +mtn_setup() + +addfile("foo", "bar") +commit() + +check(mtn("log"), 0, false, false) + +rev = readfile("_MTN/revision") +-- change the parent revision to an invalid one +rev = string.gsub(rev, + "old_revision %[(%w+)%]", + "old_revision [0000000000000000000000000000000000000002]") +writefile("_MTN/revision", rev) + +check(mtn("log"), 1, false, true) +check(qgrep( + "workspace parent revision '0000000000000000000000000000000000000002' not found", + "stderr" +)) + ============================================================ --- Makefile.am a0ed2ec6eda766d2da11b80abf961781e4c486b9 +++ Makefile.am 2edf48887f6a0df8e512dffef242d9c3c9c7aa49 @@ -103,6 +103,7 @@ MOST_SOURCES = \ sha1.cc \ pcrewrap.cc pcrewrap.hh \ rev_height.cc rev_height.hh \ + rev_output.cc rev_output.hh \ asciik.cc asciik.hh \ dates.cc dates.hh \ \ ============================================================ --- NEWS 96cc469d41eda0ab62590dbbaa1aa0143e7d9ec1 +++ NEWS e8fc0a6f14177e59c3ee6e2d8900fd448ae2bfc4 @@ -7,6 +7,30 @@ xxx xxx xx xx:xx:xx UTC 2010 - The 'setup' command now creates a new internal database if no database is given either as command line or workspace option. + - 'db check' now checks for errors in the branch heads cache, + and 'db regenerate_caches' fixes them. + + - Much more information is now passed to the editor when composing a + commit message for a new revision. The Author, Date, Branch and + Changelog values may now all be changed directly in the editor + allowing new branches to be created without using the --branch option. + Changes to other lines of this information must not be made or the + commit will abort. + + - The edit_comment lua hook now only takes one argument which is the + text to be passed to the editor to edit a commit. Existing hooks that + override the default hook will need to be changed to work properly. + + - The long date/time format used by 'status', 'commit' and 'log' must be + sufficient to preserve a date through a formatting and parsing + cycle. The 'status' command now checks for this and warns if the + format is unsuitable and 'commit' will refuse to operate with an + unsuitable format. + + - The output of the 'status' and 'log' commands has changed to align + with the new information displayed by 'commit' so that all three + commands display revisions similarly. + New features - New automation command 'update' which behaves identical to @@ -43,6 +67,10 @@ xxx xxx xx xx:xx:xx UTC 2010 in 0.47 but not noted in the release notes at that time (fixes monotone bug #28991). + - monotone on Windows will now have a non-zero exit code when + interrupted (^C). This was broken in 0.47 when it was fixed to not + throw an exception on being interrupted. + Other - Support for the diffuse merger (http://diffuse.sourceforge.net) ============================================================ --- cmd_diff_log.cc 1f1811415796db5326a6c6be183acb4bf72b900b +++ cmd_diff_log.cc 8e1fe32880b51238d9352b547b201321b3befb2a @@ -22,6 +22,7 @@ #include "restrictions.hh" #include "revision.hh" #include "rev_height.hh" +#include "rev_output.hh" #include "simplestring_xform.hh" #include "transforms.hh" #include "app_state.hh" @@ -41,150 +42,7 @@ using std::priority_queue; using std::vector; using std::priority_queue; -using boost::lexical_cast; - -// The changes_summary structure holds a list all of files and directories -// affected in a revision, and is useful in the 'log' command to print this -// information easily. It has to be constructed from all cset objects -// that belong to a revision. - -struct -changes_summary -{ - cset cs; - changes_summary(void); - void add_change_set(cset const & cs); - void print(ostream & os, size_t max_cols) const; -}; - -changes_summary::changes_summary(void) -{ -} - -void -changes_summary::add_change_set(cset const & c) -{ - if (c.empty()) - return; - - // FIXME: not sure whether it matters for an informal summary - // object like this, but the pre-state names in deletes and renames - // are not really sensible to union; they refer to different trees - // so mixing them up in a single set is potentially ambiguous. - - copy(c.nodes_deleted.begin(), c.nodes_deleted.end(), - inserter(cs.nodes_deleted, cs.nodes_deleted.begin())); - - copy(c.files_added.begin(), c.files_added.end(), - inserter(cs.files_added, cs.files_added.begin())); - - copy(c.dirs_added.begin(), c.dirs_added.end(), - inserter(cs.dirs_added, cs.dirs_added.begin())); - - copy(c.nodes_renamed.begin(), c.nodes_renamed.end(), - inserter(cs.nodes_renamed, cs.nodes_renamed.begin())); - - copy(c.deltas_applied.begin(), c.deltas_applied.end(), - inserter(cs.deltas_applied, cs.deltas_applied.begin())); - - copy(c.attrs_cleared.begin(), c.attrs_cleared.end(), - inserter(cs.attrs_cleared, cs.attrs_cleared.begin())); - - copy(c.attrs_set.begin(), c.attrs_set.end(), - inserter(cs.attrs_set, cs.attrs_set.begin())); -} - static void -print_indented_set(ostream & os, - set const & s, - size_t max_cols) -{ - size_t cols = 8; - os << " "; - for (set::const_iterator i = s.begin(); - i != s.end(); i++) - { - string str = lexical_cast(*i); - if (str.empty()) - str = "."; // project root - if (cols > 8 && cols + str.size() + 1 >= max_cols) - { - cols = 8; - os << "\n "; - } - os << ' ' << str; - cols += str.size() + 1; - } - os << '\n'; -} - -void -changes_summary::print(ostream & os, size_t max_cols) const -{ - - if (! cs.nodes_deleted.empty()) - { - os << _("Deleted entries:") << '\n'; - print_indented_set(os, cs.nodes_deleted, max_cols); - } - - if (! cs.nodes_renamed.empty()) - { - os << _("Renamed entries:") << '\n'; - for (map::const_iterator - i = cs.nodes_renamed.begin(); - i != cs.nodes_renamed.end(); i++) - os << " " << i->first - << " to " << i->second << '\n'; - } - - if (! cs.files_added.empty()) - { - set tmp; - for (map::const_iterator - i = cs.files_added.begin(); - i != cs.files_added.end(); ++i) - tmp.insert(i->first); - os << _("Added files:") << '\n'; - print_indented_set(os, tmp, max_cols); - } - - if (! cs.dirs_added.empty()) - { - os << _("Added directories:") << '\n'; - print_indented_set(os, cs.dirs_added, max_cols); - } - - if (! cs.deltas_applied.empty()) - { - set tmp; - for (map >::const_iterator - i = cs.deltas_applied.begin(); - i != cs.deltas_applied.end(); ++i) - tmp.insert(i->first); - os << _("Modified files:") << '\n'; - print_indented_set(os, tmp, max_cols); - } - - if (! cs.attrs_set.empty() || ! cs.attrs_cleared.empty()) - { - set tmp; - for (set >::const_iterator - i = cs.attrs_cleared.begin(); - i != cs.attrs_cleared.end(); ++i) - tmp.insert(i->first); - - for (map, attr_value>::const_iterator - i = cs.attrs_set.begin(); - i != cs.attrs_set.end(); ++i) - tmp.insert(i->first.first); - - os << _("Modified attrs:") << '\n'; - print_indented_set(os, tmp, max_cols); - } -} - -static void do_external_diff(options & opts, lua_hooks & lua, database & db, cset const & cs, bool new_is_archived) { @@ -613,96 +471,32 @@ log_certs(vector const & certs, os static void log_certs(vector const & certs, ostream & os, cert_name const & name, - char const * label, char const * separator, - bool multiline, bool newline) + string const date_fmt = "") { bool first = true; - if (multiline) - newline = true; - - for (vector::const_iterator i = certs.begin(); - i != certs.end(); ++i) + for (vector::const_iterator i = certs.begin(); i != certs.end(); ++i) { if (i->name == name) { if (first) - os << label; + os << " "; else - os << separator; + os << ","; - if (multiline) - os << "\n\n"; - os << i->value; - if (newline) - os << '\n'; - - first = false; - } - } -} - -static void -log_certs(vector const & certs, ostream & os, cert_name const & name, - char const * label, bool multiline) -{ - log_certs(certs, os, name, label, label, multiline, true); -} - -static void -log_certs(vector const & certs, ostream & os, cert_name const & name) -{ - log_certs(certs, os, name, " ", ",", false, false); -} - -static void -log_date_certs(vector const & certs, ostream & os, string const & fmt, - char const * label, char const * separator, - bool multiline, bool newline) -{ - cert_name const date_name(date_cert_name); - - bool first = true; - if (multiline) - newline = true; - - for (vector::const_iterator i = certs.begin(); - i != certs.end(); ++i) - { - if (i->name == date_name) - { - if (first) - os << label; - else - os << separator; - - if (multiline) - os << "\n\n"; - if (fmt.empty()) + if (date_fmt.empty()) os << i->value; else - os << date_t(i->value()).as_formatted_localtime(fmt); - if (newline) - os << '\n'; - + { + I(name == date_cert_name); + os << date_t(i->value()).as_formatted_localtime(date_fmt); + } + first = false; } } } -static void -log_date_certs(vector const & certs, ostream & os, string const & fmt, - char const * label, bool multiline) -{ - log_date_certs(certs, os, fmt, label, label, multiline, true); -} - -static void -log_date_certs(vector const & certs, ostream & os, string const & fmt) -{ - log_date_certs(certs, os, fmt, " ", ",", false, false); -} - enum log_direction { log_forward, log_reverse }; struct rev_cmp @@ -748,7 +542,7 @@ CMD(log, "log", "", CMD_REF(informative) if (!app.opts.date_fmt.empty()) date_fmt = app.opts.date_fmt; else - app.lua.hook_get_date_format_spec(date_time_short, date_fmt); + app.lua.hook_get_date_format_spec(date_time_long, date_fmt); } long last = app.opts.last; @@ -922,6 +716,7 @@ CMD(log, "log", "", CMD_REF(informative) } cert_name const author_name(author_cert_name); + cert_name const date_name(date_cert_name); cert_name const branch_name(branch_cert_name); cert_name const tag_name(tag_cert_name); cert_name const changelog_name(changelog_cert_name); @@ -1040,49 +835,32 @@ CMD(log, "log", "", CMD_REF(informative) out << rid; log_certs(certs, out, author_name); if (app.opts.no_graph) - log_date_certs(certs, out, date_fmt); + log_certs(certs, out, date_name, date_fmt); else { out << '\n'; - log_date_certs(certs, out, date_fmt, "", "", false, false); + log_certs(certs, out, date_name, date_fmt); } log_certs(certs, out, branch_name); out << '\n'; } else { - out << string(65, '-') << '\n'; - out << _("Revision: ") << rid << '\n'; + utf8 header; + revision_header(rid, rev, certs, date_fmt, header); - changes_summary csum; + external header_external; + utf8_to_system_best_effort(header, header_external); + out << header_external; - set ancestors; - - for (edge_map::const_iterator e = rev.edges.begin(); - e != rev.edges.end(); ++e) + if (!app.opts.no_files) { - ancestors.insert(edge_old_revision(e)); - csum.add_change_set(edge_changes(e)); + utf8 summary; + revision_summary(rev, summary); + external summary_external; + utf8_to_system_best_effort(summary, summary_external); + out << summary_external; } - - for (set::const_iterator anc = ancestors.begin(); - anc != ancestors.end(); ++anc) - out << _("Ancestor: ") << *anc << '\n'; - - log_certs(certs, out, author_name, _("Author: "), false); - log_date_certs(certs, out, date_fmt, _("Date: "), false); - log_certs(certs, out, branch_name, _("Branch: "), false); - log_certs(certs, out, tag_name, _("Tag: "), false); - - if (!app.opts.no_files && !csum.cs.empty()) - { - out << '\n'; - csum.print(out, 70); - out << '\n'; - } - - log_certs(certs, out, changelog_name, _("ChangeLog: "), true); - log_certs(certs, out, comment_name, _("Comments: "), true); } if (app.opts.diffs) ============================================================ --- cmd_key_cert.cc 4f67608a826cf01297dd297ad9aaf5a18c85a82a +++ cmd_key_cert.cc 0e5707233eb6e8687029ee792dfb75ad5668e3e2 @@ -362,7 +362,7 @@ CMD(comment, "comment", "", CMD_REF(revi else { external comment_external; - E(app.lua.hook_edit_comment(external(""), external(""), comment_external), + E(app.lua.hook_edit_comment(external(""), comment_external), origin::user, F("edit comment failed")); system_to_utf8(comment_external, comment); ============================================================ --- cmd_list.cc 001d3407a8d30b05a29f7e6bf63138696022fac9 +++ cmd_list.cc 03d285c5e0f6c9889f427e4d189a6f542629368b @@ -531,14 +531,22 @@ CMD(epochs, "epochs", "", CMD_REF(list), } } -CMD(tags, "tags", "", CMD_REF(list), "", +CMD(tags, "tags", "", CMD_REF(list), "[PATTERN]", N_("Lists all tags in the database"), "", - options::opts::none) + options::opts::exclude) { + globish inc("*", origin::internal); + if (args.size() == 1) + inc = globish(idx(args,0)(), origin::user); + else if (args.size() > 1) + throw usage(execid); + database db(app); set tags; project_t project(db); + cert_name branch = branch_cert_name; + project.get_tags(tags); for (set::const_iterator i = tags.begin(); i != tags.end(); ++i) @@ -546,9 +554,30 @@ CMD(tags, "tags", "", CMD_REF(list), "", key_identity_info identity; identity.id = i->key; project.complete_key_identity(app.lua, identity); - cout << i->name << ' ' - << i->ident << ' ' - << format_key(identity) << '\n'; + + vector certs; + project.get_revision_certs(i->ident, certs); + + globish exc(app.opts.exclude_patterns); + + if (inc.matches(i->name()) && !exc.matches(i->name())) + { + hexenc hexid; + encode_hexenc(i->ident.inner(), hexid); + + cout << i->name << ' ' << hexid().substr(0,10) << "... "; + + for (vector::const_iterator c = certs.begin(); + c != certs.end(); ++c) + { + if (c->name == branch) + { + cout << c->value << ' '; + } + } + + cout << format_key(identity) << '\n'; + } } } ============================================================ --- cmd_ws_commit.cc 10d33763a838c34d36cdacf987678905f77762be +++ cmd_ws_commit.cc e3eed43a97da68628c4fabc80ced9ea3327871f8 @@ -11,6 +11,7 @@ #include #include #include +#include #include "cmd.hh" #include "merge_content.hh" @@ -30,13 +31,15 @@ #include "simplestring_xform.hh" #include "database.hh" #include "roster.hh" +#include "rev_output.hh" #include "vocab_cast.hh" using std::cout; using std::make_pair; -using std::pair; using std::make_pair; using std::map; +using std::ostringstream; +using std::pair; using std::set; using std::string; using std::vector; @@ -44,153 +47,264 @@ static void using boost::shared_ptr; static void -revision_summary(revision_t const & rev, branch_name const & branch, - set const & old_branch_names, - utf8 & summary) +get_old_branch_names(database & db, parent_map const & parents, + set & old_branch_names) { - string out; - // We intentionally do not collapse the final \n into the format - // strings here, for consistency with newline conventions used by most - // other format strings. - - revision_id rid; - calculate_ident(rev, rid); - - out += (F("Current revision: %s") % rid).str() += '\n'; - if (old_branch_names.find(branch) == old_branch_names.end()) + for (parent_map::const_iterator i = parents.begin(); + i != parents.end(); ++i) { - for (set::const_iterator i = old_branch_names.begin(); - i != old_branch_names.end(); ++i) + vector branches; + db.get_revision_certs(parent_id(i), branch_cert_name, branches); + for (vector::const_iterator b = branches.begin(); + b != branches.end(); ++b) { - out += (F("Old branch: %s") % *i).str() += '\n'; + old_branch_names.insert(typecast_vocab(b->value)); } - out += (F("New branch: %s") % branch).str() += '\n'; } - else - out += (F("Current branch: %s") % branch).str() += '\n'; +} +class message_reader +{ +public: + message_reader(string const & message) : + message(message), offset(0) {} - for (edge_map::const_iterator i = rev.edges.begin(); i != rev.edges.end(); ++i) - { - revision_id parent = edge_old_revision(*i); - // A colon at the end of this string looked nicer, but it made - // double-click copying from terminals annoying. - out += (F("Changes against parent %s") % parent).str() += '\n'; + bool read(string const & text) + { + size_t len = text.length(); + if (message.compare(offset, len, text) == 0) + { + offset += len; + return true; + } + else + return false; + } - cset const & cs = edge_changes(*i); + string readline() + { + size_t eol = message.find_first_of("\r\n", offset); + if (eol == string::npos) + return ""; - if (cs.empty()) - out += F(" no changes").str() += '\n'; + size_t len = eol - offset; + string line = message.substr(offset, len); + offset = eol+1; - for (set::const_iterator i = cs.nodes_deleted.begin(); - i != cs.nodes_deleted.end(); ++i) - out += (F(" dropped %s") % *i).str() += '\n'; + if (message[eol] == '\r' && message.length() > eol+1 && + message[eol+1] == '\n') + offset++; - for (map::const_iterator - i = cs.nodes_renamed.begin(); - i != cs.nodes_renamed.end(); ++i) - out += (F(" renamed %s\n" - " to %s") % i->first % i->second).str() += '\n'; + return trim(line); + } - for (set::const_iterator i = cs.dirs_added.begin(); - i != cs.dirs_added.end(); ++i) - out += (F(" added %s") % *i).str() += '\n'; + bool contains(string const & summary) + { + return message.find(summary, offset) != string::npos; + } - for (map::const_iterator i = cs.files_added.begin(); - i != cs.files_added.end(); ++i) - out += (F(" added %s") % i->first).str() += '\n'; + 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; + } - for (map >::const_iterator - i = cs.deltas_applied.begin(); i != cs.deltas_applied.end(); ++i) - out += (F(" patched %s") % (i->first)).str() += '\n'; + string content() + { + return message.substr(offset); + } - for (map, attr_value >::const_iterator - i = cs.attrs_set.begin(); i != cs.attrs_set.end(); ++i) - out += (F(" attr on %s\n" - " attr %s\n" - " value %s") - % (i->first.first) % (i->first.second) % (i->second) - ).str() += "\n"; +private: + string message; + size_t offset; +}; - for (set >::const_iterator - i = cs.attrs_cleared.begin(); i != cs.attrs_cleared.end(); ++i) - out += (F(" unset on %s\n" - " attr %s") - % (i->first) % (i->second)).str() += "\n"; +static void +get_log_message_interactively(lua_hooks & lua, workspace & work, + revision_id const rid, revision_t const & rev, + string & author, date_t & date, branch_name & branch, + set const & old_branches, + string const & date_fmt, utf8 & log_message) +{ + utf8 backup; + work.load_commit_text(backup); + + E(backup().empty(), origin::user, + F("A backup from a previously failed commit exists in _MTN/commit.\n" + "This file must be removed before commit will proceed.\n" + "You may recover the previous message from this file if necessary.")); + + utf8 instructions( + _("Enter a description of this change following the Changelog line below.\n" + "The values of Author, Date and Branch may be modified as required.\n" + "\n")); + + utf8 cancel(_("*** REMOVE THIS LINE TO CANCEL THE COMMIT ***\n")); + + utf8 changelog; + work.read_user_log(changelog); + + // ensure the changelog message is non-empty so that the Changelog: cert + // line is produced by revision_header and there is somewhere to enter a + // message + + string text = changelog(); + + if (text.empty() || text[text.length()-1] != '\n') + { + text += '\n'; + changelog = utf8(text, origin::user); } - summary = utf8(out, origin::internal); -} -static void -get_old_branch_names(database & db, parent_map const & parents, - set & old_branch_names) -{ - for (parent_map::const_iterator i = parents.begin(); - i != parents.end(); ++i) + ostringstream oss; + + oss << string(70, '-') << '\n'; + if (!old_branches.empty() && old_branches.find(branch) == old_branches.end()) { - vector branches; - db.get_revision_certs(parent_id(i), branch_cert_name, branches); - for (vector::const_iterator b = branches.begin(); - b != branches.end(); ++b) - { - old_branch_names.insert(typecast_vocab(b->value)); - } + oss << _("*** THIS REVISION WILL CREATE A NEW BRANCH ***") << "\n\n"; + for (set::const_iterator i = old_branches.begin(); + i != old_branches.end(); ++i) + oss << _("Old Branch: ") << *i << '\n'; + oss << _("New Branch: ") << branch << "\n\n"; } -} -static void -get_log_message_interactively(lua_hooks & lua, workspace & work, - revision_t const & cs, - branch_name const & branchname, - set const & old_branch_names, - utf8 & log_message) -{ + utf8 notes(oss.str().c_str()); + + utf8 header; utf8 summary; - revision_summary(cs, branchname, old_branch_names, summary); - external summary_external; - utf8_to_system_best_effort(summary, summary_external); - utf8 branch_comment = utf8((F("branch \"%s\"\n\n") % branchname).str(), - branchname.made_from); - external branch_external; - utf8_to_system_best_effort(branch_comment, branch_external); + revision_header(rid, rev, author, date, branch, changelog, date_fmt, header); + revision_summary(rev, summary); - string magic_line = _("*****DELETE THIS LINE TO CONFIRM YOUR COMMIT*****"); - string commentary_str; - commentary_str += string(70, '-') + "\n"; - commentary_str += _("Enter a description of this change.\n" - "Lines beginning with `MTN:' " - "are removed automatically."); - commentary_str += "\n\n"; - commentary_str += summary_external(); - commentary_str += string(70, '-') + "\n"; + utf8 full_message(instructions() + cancel() + header() + notes() + summary(), + origin::internal); + + external input_message; + external output_message; - external commentary(commentary_str, origin::internal); + utf8_to_system_best_effort(full_message, input_message); - utf8 user_log_message; - work.read_user_log(user_log_message); + E(lua.hook_edit_comment(input_message, output_message), + origin::user, + F("edit of log message failed")); - //if the _MTN/log file was non-empty, we'll append the 'magic' line - utf8 user_log; - if (user_log_message().length() > 0) - user_log = utf8( magic_line + "\n" + user_log_message(), origin::internal); + system_to_utf8(output_message, full_message); + + // save the message in _MTN/commit so its not lost if something fails below + work.save_commit_text(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 + // between the "Changelog: " line from the header and the following line + // of dashes preceeding the summary is where they should be adding + // lines. Ideally, there is a blank line following "Changelog:" + // (preceeding the changelog message) and another blank line preceeding + // the next line of dashes (following the changelog message) but both of + // these are optional. + + E(message.read(instructions()), origin::user, + F("Commit failed. Instructions not found.")); + + if (!message.read(cancel())) + { + // clear the backup file if the commit was explicitly cancelled + work.clear_commit_text(); + E(message.read(cancel()), origin::user, + F("Commit cancelled.")); + } + + utf8 const AUTHOR(trim_right(_("Author: ")).c_str()); + utf8 const DATE(trim_right(_("Date: ")).c_str()); + utf8 const BRANCH(trim_right(_("Branch: ")).c_str()); + utf8 const CHANGELOG(trim_right(_("Changelog: ")).c_str()); + + // ---------------------------------------------------------------------- + // Revision: + // Parent: + // Parent: + // Author: + + size_t pos = header().find(AUTHOR()); // look in unedited header + I(pos != string::npos); + + string prefix = header().substr(0, pos); + E(message.read(prefix), origin::user, + F("Commit failed. Revision/Parent header not found.")); + + // Author: + + E(message.read(AUTHOR()), origin::user, + F("Commit failed. Author header not found.")); + + author = message.readline(); + + E(!author.empty(), origin::user, + F("Commit failed. Author value empty.")); + + // Date: + + E(message.read(DATE()), origin::user, + F("Commit failed. Date header not found.")); + + string d = message.readline(); + + E(!d.empty(), origin::user, + F("Commit failed. Date value empty.")); + + if (date_fmt.empty()) + date = date_t(d); else - user_log = user_log_message; + date = date_t::from_formatted_localtime(d, date_fmt); - external user_log_message_external; - utf8_to_system_best_effort(user_log, user_log_message_external); + // Branch: - external log_message_external; - E(lua.hook_edit_comment(commentary, user_log_message_external, - log_message_external), - origin::user, - F("edit of log message failed")); + E(message.read(BRANCH()), origin::user, + F("Commit failed. Branch header not found.")); - E(log_message_external().find(magic_line) == string::npos, - origin::user, - F("failed to remove magic line; commit cancelled")); + string b = message.readline(); - system_to_utf8(log_message_external, log_message); + E(!b.empty(), origin::user, + F("Commit failed. Branch value empty.")); + + branch = branch_name(b, origin::user); + + string blank = message.readline(); + E(blank == "", origin::user, + F("Commit failed. Blank line before Changelog header not found.")); + + // Changelog: + + E(message.read(CHANGELOG()), origin::user, + F("Commit failed. Changelog header not found.")); + + // remove the summary before extracting the changelog content + + string footer = notes() + summary(); + + if (!footer.empty()) + { + E(message.contains(footer), origin::user, + F("Commit failed. Change summary not found.")); + + E(message.remove(footer), origin::user, + F("Commit failed. Text following Change summary.")); + } + + string content = trim(message.content()) + '\n'; + + log_message = utf8(content, origin::user); + + // remove the backup file now that all values have been extracted + work.clear_commit_text(); } CMD(revert, "revert", "", CMD_REF(workspace), N_("[PATH]..."), @@ -617,7 +731,40 @@ CMD(status, "status", "", CMD_REF(inform temp_node_id_source nis; database db(app); + project_t project(db); workspace work(app); + + string date_fmt; + if (app.opts.format_dates) + { + if (!app.opts.date_fmt.empty()) + date_fmt = app.opts.date_fmt; + else + app.lua.hook_get_date_format_spec(date_time_long, date_fmt); + } + + if (!date_fmt.empty()) + { + // check that the specified date format can be parsed (for commit) + date_t now = date_t::now(); + date_t parsed; + try + { + string formatted = now.as_formatted_localtime(date_fmt); + parsed = date_t::from_formatted_localtime(formatted, date_fmt); + } + catch (recoverable_failure const & e) + { + L(FL("date check failed: %s") % e.what()); + } + + if (parsed != now) + { + L(FL("date check failed: %s != %s") % now % parsed); + W(F("date format '%s' cannot be used for commit") % date_fmt); + } + } + work.get_parent_rosters(db, old_rosters); work.get_current_roster_shape(db, nis, new_roster); @@ -645,13 +792,51 @@ CMD(status, "status", "", CMD_REF(inform } } - set old_branch_names; - get_old_branch_names(db, old_rosters, old_branch_names); + revision_id rid; + string author; + key_store keys(app); + key_identity_info key; + get_user_key(app.opts, app.lua, db, keys, project, key.id, cache_disable); + project.complete_key_identity(keys, app.lua, key); + + if (!app.lua.hook_get_author(app.opts.branch, key, author)) + author = key.official_name(); + + calculate_ident(rev, rid); + + set old_branches; + get_old_branch_names(db, old_rosters, old_branches); + + utf8 changelog; + work.read_user_log(changelog); + + utf8 header; utf8 summary; - revision_summary(rev, app.opts.branch, old_branch_names, summary); + + revision_header(rid, rev, author, date_t::now(), app.opts.branch, changelog, + date_fmt, header); + revision_summary(rev, summary); + + external header_external; external summary_external; + + utf8_to_system_best_effort(header, header_external); utf8_to_system_best_effort(summary, summary_external); + + cout << header_external; + + if (!old_branches.empty() && + old_branches.find(app.opts.branch) == old_branches.end()) + { + cout << string(70, '-') << '\n' + << _("*** THIS REVISION WILL CREATE A NEW BRANCH ***") << "\n\n"; + for (set::const_iterator i = old_branches.begin(); + i != old_branches.end(); ++i) + cout << _("Old Branch: ") << *i << '\n'; + cout << _("New Branch: ") << app.opts.branch << "\n\n"; + } + cout << summary_external; } @@ -1108,9 +1293,9 @@ CMD(commit, "commit", "ci", CMD_REF(work CMD(commit, "commit", "ci", CMD_REF(workspace), N_("[PATH]..."), N_("Commits workspace changes to the database"), "", - options::opts::branch | options::opts::message | options::opts::msgfile - | options::opts::date | options::opts::author | options::opts::depth - | options::opts::exclude) + options::opts::branch | options::opts::message | options::opts::msgfile | + options::opts::date | options::opts::author | options::opts::depth | + options::opts::exclude) { database db(app); key_store keys(app); @@ -1125,6 +1310,15 @@ CMD(commit, "commit", "ci", CMD_REF(work temp_node_id_source nis; cset excluded; + string date_fmt; + if (app.opts.format_dates) + { + if (!app.opts.date_fmt.empty()) + date_fmt = app.opts.date_fmt; + else + app.lua.hook_get_date_format_spec(date_time_long, date_fmt); + } + work.get_parent_rosters(db, old_rosters); work.get_current_roster_shape(db, nis, new_roster); @@ -1139,6 +1333,9 @@ CMD(commit, "commit", "ci", CMD_REF(work restricted_rev.check_sane(); E(restricted_rev.is_nontrivial(), origin::user, F("no changes to commit")); + set old_branches; + get_old_branch_names(db, old_rosters, old_branches); + revision_id restricted_rev_id; calculate_ident(restricted_rev, restricted_rev_id); @@ -1165,8 +1362,6 @@ CMD(commit, "commit", "ci", CMD_REF(work app.opts.branch = branchname; } - P(F("beginning commit on branch '%s'") % app.opts.branch); - if (global_sanity.debug_p()) { L(FL("new manifest '%s'\n" @@ -1177,25 +1372,74 @@ CMD(commit, "commit", "ci", CMD_REF(work process_commit_message_args(app.opts, log_message_given, log_message); - E(!(log_message_given && work.has_contents_user_log() && app.opts.msgfile() != "_MTN/log"), origin::user, + E(!(log_message_given && work.has_contents_user_log() && + app.opts.msgfile() != "_MTN/log"), origin::user, F("_MTN/log is non-empty and log message " "was specified on command line\n" "perhaps move or delete _MTN/log,\n" "or remove --message/--message-file from the command line?")); - if (!log_message_given) + date_t date; + date_t now = date_t::now(); + string author = app.opts.author(); + + if (app.opts.date_given) { - set old_branch_names; - get_old_branch_names(db, old_rosters, old_branch_names); + date = app.opts.date; + L(FL("using specified commit date %s") % date); + } + else + { + date = now; + L(FL("using current commit date %s") % date); + } + if (author.empty()) + { + key_identity_info key; + get_user_key(app.opts, app.lua, db, keys, project, key.id, cache_disable); + project.complete_key_identity(keys, app.lua, key); + + if (!app.lua.hook_get_author(app.opts.branch, key, author)) + author = key.official_name(); + } + + if (!date_fmt.empty()) + { + // check that the current date format can be parsed + date_t parsed; + try + { + string formatted = date.as_formatted_localtime(date_fmt); + parsed = date_t::from_formatted_localtime(formatted, date_fmt); + } + catch (recoverable_failure const & e) + { + L(FL("date check failed: %s") % e.what()); + } + + if (parsed != date) + { + L(FL("date check failed: %s != %s") % date % parsed); + } + + E(parsed == date, origin::user, + F("date format '%s' cannot be used for commit") % date_fmt); + } + + if (!log_message_given) + { // This call handles _MTN/log. - get_log_message_interactively(app.lua, work, restricted_rev, - app.opts.branch, old_branch_names, - log_message); + get_log_message_interactively(app.lua, work, + restricted_rev_id, restricted_rev, + author, date, app.opts.branch, old_branches, + date_fmt, log_message); // 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")); @@ -1231,6 +1475,8 @@ CMD(commit, "commit", "ci", CMD_REF(work app.opts.ignore_suspend_certs); unsigned int old_head_size = heads.size(); + P(F("beginning commit on branch '%s'") % app.opts.branch); + { transaction_guard guard(db); @@ -1320,10 +1566,21 @@ CMD(commit, "commit", "ci", CMD_REF(work db.put_revision(restricted_rev_id, rdat); } - project.put_standard_certs_from_options(app.opts, app.lua, keys, - restricted_rev_id, - app.opts.branch, - log_message); + // if no --date option was specified and the user didn't edit the date + // update it to reflect the current time. + + if (date == now && !app.opts.date_given) + { + date = date_t::now(); + L(FL("updating commit date %s") % date); + } + + project.put_standard_certs(keys, + restricted_rev_id, + app.opts.branch, + log_message, + date, + author); guard.commit(); } ============================================================ --- contrib/edit_comment_from_changelog.lua 392821013c884731eaef31836acab2c2c5938373 +++ contrib/edit_comment_from_changelog.lua fa734b0c03818ea1790aa939ac3db59441eec9fc @@ -1,11 +1,11 @@ std_edit_comment = edit_comment std_edit_comment = edit_comment -function edit_comment(basetext, user_log_message) +function edit_comment(user_log_message) local tmp, tname = temp_file() if (tmp == nil) then return nil end if (user_log_message == "") then local ChangeLog = io.open("ChangeLog", "r") if ChangeLog == nil then - return std_edit_comment(basetext, user_log_message) + return std_edit_comment(user_log_message) end local line = ChangeLog:read() local msg = "" @@ -23,5 +23,5 @@ function edit_comment(basetext, user_log user_log_message = msg io.close(ChangeLog) end - return std_edit_comment(basetext, user_log_message) + return std_edit_comment(user_log_message) end ============================================================ --- database.cc 026b0f32727c3e31be115a47a4b8de251078151a +++ database.cc 25a7d09b691fa29743249d4151dc27bab18c7923 @@ -1,3 +1,4 @@ +// Copyright (C) 2010 Stephen Leake // Copyright (C) 2002 Graydon Hoare // // This program is made available under the GNU GPL version 2.0 or @@ -2995,6 +2996,12 @@ database::delete_existing_heights() imp->execute(query("DELETE FROM heights")); } +void +database::delete_existing_branch_leaves() +{ + imp->execute(query("DELETE FROM branch_leaves")); +} + /// Deletes one revision from the local database. /// @see kill_rev_locally void @@ -3039,16 +3046,23 @@ void } void -database::recalc_branch_leaves(cert_value const & name) +database::compute_branch_leaves(cert_value const & branch_name, set & revs) { - imp->execute(query("DELETE FROM branch_leaves WHERE branch = ?") % blob(name())); + imp->execute(query("DELETE FROM branch_leaves WHERE branch = ?") % blob(branch_name())); + get_revisions_with_cert(cert_name("branch"), branch_name, revs); + erase_ancestors(*this, revs); +} + +void +database::recalc_branch_leaves(cert_value const & branch_name) +{ + imp->execute(query("DELETE FROM branch_leaves WHERE branch = ?") % blob(branch_name())); set revs; - get_revisions_with_cert(cert_name("branch"), name, revs); - erase_ancestors(*this, revs); + compute_branch_leaves(branch_name, revs); for (set::const_iterator i = revs.begin(); i != revs.end(); ++i) { imp->execute(query("INSERT INTO branch_leaves (branch, revision_id) " - "VALUES (?, ?)") % blob(name()) % blob((*i).inner()())); + "VALUES (?, ?)") % blob(branch_name()) % blob((*i).inner()())); } } @@ -3608,6 +3622,14 @@ database::record_as_branch_leaf(cert_val } } + // This check is needed for this case: + // + // r1 (branch1) + // | + // r2 (branch2) + // | + // r3 (branch1) + if (!all_parents_were_leaves) { for (set::const_iterator r = current_leaves.begin(); ============================================================ --- database.hh c7ae2340052b1205f3615f416adb1c58917abfd5 +++ database.hh 654c356dcdbe48064357eaa66fb6e7c9b5a9d9b5 @@ -1,3 +1,4 @@ +// Copyright (C) 2010 Stephen Leake // Copyright (C) 2002 Graydon Hoare // // This program is made available under the GNU GPL version 2.0 or @@ -312,9 +313,10 @@ public: outdated_indicator get_branch_leaves(cert_value const & value, std::set & revisions); -private: - void recalc_branch_leaves(cert_value const & value); -public: + // used by check_db, regenerate_caches + void compute_branch_leaves(cert_value const & branch_name, std::set & revs); + void recalc_branch_leaves(cert_value const & branch_name); + void delete_existing_branch_leaves(); // Used through project.cc outdated_indicator get_revision_certs(revision_id const & ident, ============================================================ --- database_check.cc 55d80d4456c96b241ceabd77be9872cfb7285ab6 +++ database_check.cc eb2f10d33d8a4b345982a09ccb29a3d4235c06ab @@ -1,3 +1,4 @@ +// Copyright (C) 2010 Stephen Leake // Copyright (C) 2005 Derek Scherger // // This program is made available under the GNU GPL version 2.0 or @@ -133,6 +134,14 @@ struct checked_height { checked_height(): found(false), unique(false), sensible(true) {} }; +struct checked_branch { + bool used; + bool heads_ok; + bool cached; + + checked_branch(): used(false), heads_ok(false), cached(false) {} +}; + /* * check integrity of the SQLite database */ @@ -597,6 +606,62 @@ static void } static void +check_branch_leaves(database & db, map & checked_branches) +{ + // We don't assume db.get_branches is right, because that uses + // branch_leaves, and we are checking to see if branch_leaves is ok. + + vector all_branch_certs; + set seen_branches; + vector cached_branches; + + db.get_branches (cached_branches); + + L(FL("checking %d branches") % cached_branches.size()); + + db.get_revision_certs(branch_cert_name, all_branch_certs); + + // we assume cached_branches is close enough for the ticker. + ticker ticks(_("branches"), "b", cached_branches.size()); + + for (vector::const_iterator i = all_branch_certs.begin(); i != all_branch_certs.end(); ++i) + { + string const name = i->value(); + + std::pair::iterator, bool> inserted = seen_branches.insert(name); + + if (inserted.second) + { + checked_branches[name].used = true; + + checked_branches[name].cached = + find(cached_branches.begin(), cached_branches.end(), name) != cached_branches.end(); + + set cached_leaves; + set computed_leaves; + + db.get_branch_leaves(i->value, cached_leaves); + db.compute_branch_leaves(i->value, computed_leaves); + + checked_branches[name].heads_ok = cached_leaves == computed_leaves; + ++ticks; + } + } + + for (vector::const_iterator i = cached_branches.begin(); i != cached_branches.end(); ++i) + { + string const name = *i; + + if (seen_branches.find(name) == seen_branches.end()) + { + checked_branches[name].used = false; + checked_branches[name].cached = true; + checked_branches[name].heads_ok = false; + } + } +} + +static void report_files(map const & checked_files, size_t & missing_files, size_t & unreferenced_files) @@ -893,6 +958,32 @@ report_heights(map const & checked_branches, + size_t & extra_branches, + size_t & bad_branches, + size_t & missing_branches) +{ + for (map::const_iterator i = checked_branches.begin(); i != checked_branches.end(); ++i) + { + if (!i->second.used) + { + extra_branches++; + P(F("cached branch '%s' not used") % i->first); + } + else if (!i->second.cached) + { + missing_branches++; + P(F("branch '%s' not cached") % i->first); + } + else if (!i->second.heads_ok) + { + bad_branches ++; + P(F("branch '%s' wrong head count") % i->first); + } + } +} + void check_db(database & db) { @@ -902,6 +993,7 @@ check_db(database & db) map checked_revisions; map checked_keys; map checked_heights; + map checked_branches; size_t missing_files = 0; size_t unreferenced_files = 0; @@ -931,6 +1023,10 @@ check_db(database & db) size_t duplicate_heights = 0; size_t incorrect_heights = 0; + size_t extra_branches = 0; + size_t bad_branches = 0; + size_t missing_branches = 0; + transaction_guard guard(db, false); check_db_integrity_check(db); @@ -945,6 +1041,7 @@ check_db(database & db) check_certs(db, checked_revisions, checked_keys, total_certs); check_heights(db, checked_heights); check_heights_relation(db, checked_heights); + check_branch_leaves(db, checked_branches); report_files(checked_files, missing_files, unreferenced_files); @@ -968,8 +1065,10 @@ check_db(database & db) report_heights(checked_heights, missing_heights, duplicate_heights, incorrect_heights); + report_branches(checked_branches, extra_branches, bad_branches, missing_branches); + // NOTE: any new sorts of problems need to have added: - // -- a message here, that tells the use about them + // -- a message here, that tells the user about them // -- entries in one _or both_ of the sums calculated at the end // -- an entry added to the manual, which describes in detail why the // error occurs and what it means to the user @@ -1024,6 +1123,13 @@ check_db(database & db) if (incorrect_heights > 0) W(F("%d incorrect heights") % incorrect_heights); + if (extra_branches > 0) + W(F("%d branches cached but not used") % extra_branches); + if (bad_branches > 0) + W(F("%d branches with incorrect head count") % bad_branches); + if (missing_branches > 0) + W(F("%d branches missing from branch cache") % missing_branches); + size_t total = missing_files + unreferenced_files + unreferenced_rosters + incomplete_rosters + missing_revisions + incomplete_revisions + @@ -1034,7 +1140,9 @@ check_db(database & db) missing_certs + mismatched_certs + unchecked_sigs + bad_sigs + missing_keys + - missing_heights + duplicate_heights + incorrect_heights; + missing_heights + duplicate_heights + incorrect_heights + + extra_branches + bad_branches + missing_branches; + // unreferenced files and rosters and mismatched certs are not actually // serious errors; odd, but nothing will break. size_t serious = missing_files + @@ -1046,15 +1154,17 @@ check_db(database & db) missing_certs + unchecked_sigs + bad_sigs + missing_keys + - missing_heights + duplicate_heights + incorrect_heights; + missing_heights + duplicate_heights + incorrect_heights+ + extra_branches + bad_branches + missing_branches; - P(F("check complete: %d files; %d rosters; %d revisions; %d keys; %d certs; %d heights") + P(F("check complete: %d files; %d rosters; %d revisions; %d keys; %d certs; %d heights; %d branches") % checked_files.size() % checked_rosters.size() % checked_revisions.size() % checked_keys.size() % total_certs - % checked_heights.size()); + % checked_heights.size() + % checked_branches.size()); P(F("total problems detected: %d (%d serious)") % total % serious); if (serious) { ============================================================ --- dates.cc 8a01802be5f9f514b73a29a9480a09d8415df3c4 +++ dates.cc 24dd854d8c1be66b65a57d813b756ddf13550848 @@ -50,6 +50,9 @@ #undef SEC #undef MILLISEC +using std::localtime; +using std::mktime; +using std::numeric_limits; using std::ostream; using std::string; using std::time_t; @@ -143,7 +146,7 @@ our_gmtime(s64 ts, broken_down_time & tb our_gmtime(s64 ts, broken_down_time & tb) { // validate our assumptions about which basic type is u64 (see above). - I(PROBABLE_S64_MAX == std::numeric_limits::max()); + I(PROBABLE_S64_MAX == numeric_limits::max()); I(LATEST_SUPPORTED_DATE < PROBABLE_S64_MAX); I(valid_ms_count(ts)); @@ -312,7 +315,8 @@ date_t::date_t(int year, int month, int broken_down_time t; t.millisec = millisec; t.sec = sec; - t.min = min; t.hour = hour; + t.min = min; + t.hour = hour; t.day = day; t.month = month; t.year = year; @@ -327,7 +331,7 @@ date_t::now() date_t::now() { time_t t = std::time(0); - s64 tu = s64(t) * 1000 + get_epoch_offset(); + s64 tu = MILLISEC(t) + get_epoch_offset(); E(valid_ms_count(tu), origin::system, F("current date '%s' is outside usable range\n" "(your system clock may not be set correctly)") @@ -363,8 +367,41 @@ date_t::as_formatted_localtime(string co string date_t::as_formatted_localtime(string const & fmt) const { - time_t t(d/1000 - get_epoch_offset()); - tm tb(*std::localtime(&t)); + L(FL("formatting date '%s' with format '%s'") % *this % fmt); + + // note that the time_t value here may underflow or overflow if our date + // is outside of the representable range. for 32 bit time_t's the earliest + // representable time is 1901-12-13 20:45:52 UTC and the latest + // representable time is 2038-01-19 03:14:07 UTC. assert that the value is + // within range for the current time_t type so that localtime doesn't + // produce a bad result. + + s64 seconds = d/1000 - get_epoch_offset(); + + L(FL("%s seconds UTC since unix epoch") % seconds); + + E(seconds >= numeric_limits::min(), origin::user, + F("date '%s' is out of range and cannot be formatted") + % as_iso_8601_extended()); + + E(seconds <= numeric_limits::max(), origin::user, + F("date '%s' is out of range and cannot be formatted") + % as_iso_8601_extended()); + + time_t t(seconds); // seconds since unix epoch in UTC + tm tb(*localtime(&t)); // converted to local timezone values + + L(FL("localtime %4s/%02s/%02s %02s:%02s:%02s WD %s YD %s DST %d") + % (tb.tm_year + 1900) + % (tb.tm_mon + 1) + % tb.tm_mday + % tb.tm_hour + % tb.tm_min + % tb.tm_sec + % tb.tm_wday + % tb.tm_yday + % tb.tm_isdst); + char buf[128]; // Poison the buffer so we can tell whether strftime() produced @@ -374,7 +411,11 @@ date_t::as_formatted_localtime(string co size_t wrote = strftime(buf, sizeof buf, fmt.c_str(), &tb); if (wrote > 0) - return string(buf); // yay, it worked + { + string formatted(buf); + L(FL("formatted date '%s'") % formatted); + return formatted; // yay, it worked + } if (wrote == 0 && buf[0] == '\0') // no output { @@ -393,6 +434,77 @@ date_t::as_formatted_localtime(string co % (sizeof buf - 1)); } +date_t +date_t::from_formatted_localtime(string const & s, string const & fmt) +{ + tm tb; + memset(&tb, 0, sizeof(tb)); + + L(FL("parsing date '%s' with format '%s'") % s % fmt); + + char *p = strptime(s.c_str(), fmt.c_str(), &tb); // local timezone values + + E(p, origin::user, // strptime failed to match all of the format string + F("unable to parse date '%s' with format '%s'") % s % fmt); + + E(*p == 0, origin::user, // extraneous characters in input string + F("invalid date '%s' not matched by format '%s'") % s % fmt); + + // strptime does *not* set the tm_isdst field in the broken down time + // struct. setting it to -1 is apparently the way to tell mktime to + // determine whether DST is in effect or not. + + tb.tm_isdst = -1; + + L(FL("localtime %4s/%02s/%02s %02s:%02s:%02s WD %s YD %s DST %d") + % (tb.tm_year + 1900) + % (tb.tm_mon + 1) + % tb.tm_mday + % tb.tm_hour + % tb.tm_min + % tb.tm_sec + % tb.tm_wday + % tb.tm_yday + % tb.tm_isdst); + + // note that the time_t value here may underflow or overflow if our date + // is outside of the representable range. for 32 bit time_t's the earliest + // representable time is 1901-12-13 20:45:52 UTC and the latest + // representable time is 2038-01-19 03:14:07 UTC. mktime seems to detect + // this and return -1 for values it cannot handle, which strptime will + // happily produce. + + time_t t = mktime(&tb); // convert to seconds since unix epoch in UTC + + L(FL("%s seconds UTC since unix epoch") % t); + + // mktime may return a time_t that has the value -1 to indicate an error. + // however this is also the valid date 1969-12-31 23:59:59. so we ignore this + // error indication and convert the resulting time_t back to a struct tm + // for comparison with the input to mktime to detect out of range errors. + + tm check(*localtime(&t)); // back to local timezone values + + E(tb.tm_sec == check.tm_sec && + tb.tm_min == check.tm_min && + tb.tm_hour == check.tm_hour && + tb.tm_mday == check.tm_mday && + tb.tm_mon == check.tm_mon && + tb.tm_year == check.tm_year && + tb.tm_wday == check.tm_wday && + tb.tm_yday == check.tm_yday && + tb.tm_isdst == check.tm_isdst, + origin::user, + F("date '%s' is out of range and cannot be parsed") + % s); + + date_t date(MILLISEC(t) + get_epoch_offset()); + + L(FL("parsed date '%s'") % date); + + return date; +} + s64 date_t::as_millisecs_since_unix_epoch() const { ============================================================ --- dates.hh e25c6a24f11ef3e948b44d7adb694661183b1d1a +++ dates.hh 3a730e5536cece2437e9caad0e4f8f2767e4887d @@ -45,6 +45,9 @@ struct date_t // display only. std::string as_formatted_localtime(std::string const & fmt) const; + static date_t from_formatted_localtime(std::string const & s, + std::string const & fmt); + // Retrieve the internal milliseconds count since the Unix epoch. s64 as_millisecs_since_unix_epoch() const; ============================================================ --- key_store.cc 6ea38c00f7da18dd8136f0a4080b2fb7a8cbedba +++ key_store.cc 30d4b56d5274333246ece56ac4f1fd2b2e86c33a @@ -238,7 +238,8 @@ key_store_state::maybe_read_key_dir() data dat; read_data(*i, dat); istringstream is(dat()); - read_packets(is, kr); + if (read_packets(is, kr) == 0) + W(F("ignored invalid key file ('%s') in key store") % (*i) ); } } ============================================================ --- keys.cc c422791c7c491e8cd0360ddc4f93f2a59b8651ae +++ keys.cc e27fd43f8730dc5ada1f4aaddfb55e910ac659ac @@ -118,7 +118,8 @@ get_user_key(options const & opts, lua_h void get_user_key(options const & opts, lua_hooks & lua, database & db, key_store & keys, - project_t & project, key_id & key) + project_t & project, key_id & key, + key_cache_flag const cache) { if (keys.have_signing_key()) { @@ -149,7 +150,8 @@ get_user_key(options const & opts, lua_h get_only_key(keys, true, key); } - check_and_save_chosen_key(db, keys, key); + if (cache == cache_enable) + check_and_save_chosen_key(db, keys, key); } void ============================================================ --- keys.hh f48a9a4a28dd60cc8b89cb4f0fc9ecf0dd8613c9 +++ keys.hh 6b5412008a7158b8abfb8228163b687538d7879f @@ -20,17 +20,20 @@ class globish; struct keypair; class globish; +enum key_cache_flag { cache_disable, cache_enable }; + // keys.{hh,cc} does all the "delicate" crypto (meaning: that which needs // to read passphrases and manipulate raw, decrypted private keys). it // could in theory be in transforms.cc too, but that file's already kinda // big and this stuff "feels" different, imho. // Find the key to be used for signing certs. If possible, ensure the -// database and the key_store agree on that key, and cache it in decrypted -// form, so as not to bother the user for their passphrase later. +// database and the key_store agree on that key, and optionally cache it in +// decrypted form, so as not to bother the user for their passphrase later. void get_user_key(options const & opts, lua_hooks & lua, database & db, key_store & keys, - project_t & project, key_id & key); + project_t & project, key_id & key, + key_cache_flag const cache = cache_enable); // As above, but does not report which key has been selected; for use when // the important thing is to have selected one and cached the decrypted key. ============================================================ --- lua_hooks.cc d4f06492ebfd19893156cf0021b7afef29009332 +++ lua_hooks.cc 13ee547d9fe9b6b303a9e68636a8f784856c490b @@ -354,16 +354,14 @@ bool } bool -lua_hooks::hook_edit_comment(external const & commentary, - external const & user_log_message, +lua_hooks::hook_edit_comment(external const & user_log_message, external & result) { string result_str; bool is_ok = Lua(st) .func("edit_comment") - .push_str(commentary()) .push_str(user_log_message()) - .call(2,1) + .call(1,1) .extract_str(result_str) .ok(); result = external(result_str, origin::user); ============================================================ --- lua_hooks.hh e4bfa01a091e43739f11ecfc0b6c096e089b127e +++ lua_hooks.hh 40d020a0b33504b110181d8272ad664a4e622e4e @@ -60,8 +60,7 @@ public: bool hook_get_author(branch_name const & branchname, key_identity_info const & info, std::string & author); - bool hook_edit_comment(external const & commentary, - external const & user_log_message, + bool hook_edit_comment(external const & user_log_message, external & result); bool hook_persist_phrase_ok(); bool hook_get_revision_cert_trust(std::set const & signers, ============================================================ --- migrate_ancestry.cc 87b81a3b30c9299f5919f8f89b154843a7171989 +++ migrate_ancestry.cc 790cf2e18a4653af131bdb350e9c3ee4e223e294 @@ -1,3 +1,4 @@ +// Copyright (C) 2010 Stephen Leake // Copyright (C) 2004 Graydon Hoare // // This program is made available under the GNU GPL version 2.0 or @@ -986,31 +987,58 @@ regenerate_caches(database & db) db.ensure_open_for_cache_reset(); - transaction_guard guard(db); + { + transaction_guard guard(db); - db.delete_existing_rosters(); - db.delete_existing_heights(); + db.delete_existing_rosters(); + db.delete_existing_heights(); - vector sorted_ids; - allrevs_toposorted(db, sorted_ids); + vector sorted_ids; + allrevs_toposorted(db, sorted_ids); - ticker done(_("regenerated"), "r", 5); - done.set_total(sorted_ids.size()); + ticker done(_("regenerated"), "r", 5); + done.set_total(sorted_ids.size()); - for (std::vector::const_iterator i = sorted_ids.begin(); - i != sorted_ids.end(); ++i) - { - revision_t rev; - revision_id const & rev_id = *i; - db.get_revision(rev_id, rev); - db.put_roster_for_revision(rev_id, rev); - db.put_height_for_revision(rev_id, rev); - ++done; - } + for (std::vector::const_iterator i = sorted_ids.begin(); + i != sorted_ids.end(); ++i) + { + revision_t rev; + revision_id const & rev_id = *i; + db.get_revision(rev_id, rev); + db.put_roster_for_revision(rev_id, rev); + db.put_height_for_revision(rev_id, rev); + ++done; + } - guard.commit(); + guard.commit(); + } P(F("finished regenerating cached rosters and heights")); + + P(F("regenerating cached branches")); + { + transaction_guard guard(db); + + db.delete_existing_branch_leaves(); + + vector all_branch_certs; + db.get_revision_certs(branch_cert_name, all_branch_certs); + set seen_branches; + for (vector::const_iterator i = all_branch_certs.begin(); i != all_branch_certs.end(); ++i) + { + string const name = i->value(); + + std::pair::iterator, bool> inserted = seen_branches.insert(name); + + if (inserted.second) + { + db.recalc_branch_leaves (i->value); + } + } + guard.commit(); + } + P(F("finished regenerating cached branches")); + } // Local Variables: ============================================================ --- monotone.texi ce013122c5f540df1e48aa56e7452d73887efa35 +++ monotone.texi 25cf3d90449d7a6304c612671738c12cc49ba0e9 @@ -33,7 +33,7 @@ Copyright @copyright{} 2003, 2004 Graydon Hoare @* Copyright @copyright{} 2004, 2005, 2006 Nathaniel Smith @* -Copyright @copyright{} 2005, 2008, 2009 Derek Scherger @* +Copyright @copyright{} 2005 - 2010 Derek Scherger @* Copyright @copyright{} 2005, 2006 Daniel Carosone @* Copyright @copyright{} 2006 Jeronimo Pellegrini @* Copyright @copyright{} 2006 Alex Queiroz @* @@ -310,7 +310,7 @@ @section Versions of trees file, or different contents for the same name. Other entries in the manifest format name directories or store file -attrs, which we will cover later. +attributes, which we will cover later. @ifinfo @smallexample @@ -610,11 +610,6 @@ @section Certificates @image{figures/cert} @end ifnotinfo address@hidden:} because our certificates give the @i{name} of the -signing key instead of the @i{hash} (ID), all key names must be -globally unique forever. If you lose a key and need to generate a -replacement, make sure that you give the replacement a different name. - Monotone uses certs extensively. Any ``extra'' information which needs to be stored, transmitted or retrieved --- above and beyond files, manifests, and revisions --- is kept in the form of certs. This @@ -1452,21 +1447,26 @@ @section Adding Files @smallexample @group $ cat _MTN/revision - format_version "1" -new_manifest [2098eddbe833046174de28172a813150a6cbda7b] +new_manifest [0000000000000000000000000000000000000002] old_revision [] +add_dir "" + +add_dir "include" + +add_dir "src" + add_file "include/jb.h" - content [3b12b2d0b31439bd50976633db1895cff8b19da0] + content [f6996ce2dfc5d32bda8b574c3e9ce75db8d55492] add_file "src/apple.c" - content [2650ffc660dd00a08b659b883b65a060cac7e560] + content [1ce885d2cc59842ff16785834391e864068fbc3c] add_file "src/banana.c" - content [e8f147e5b4d5667f3228b7bba1c5c1e639f5db9f] + content [ad88bbbb1b7507ddff26be67efd91d95e069afb6] @end group @end smallexample @@ -1479,19 +1479,29 @@ @section Adding Files @smallexample @group $ mtn status -Current branch: jp.co.juicebot.jb7 -Changes against parent : - added include/jb.h - added src/apple.c - added src/banana.c +---------------------------------------------------------------------- +Revision: 493bda86628fd72c992eb56f73899db9ead3cf6f +Author: jim@@juicebot.co.jp +Date: 2004-10-26T02:53:08 +Branch: jp.co.juicebot.jb7 + +Changes + + added + added include + added src + added include/jb.h + added src/apple.c + added src/banana.c + @end group @end smallexample The output of this command tells Jim that his edits, so far, -constitute only the addition of some files. +constitute only the addition of some files and directories. Jim wants to see the actual details of the files he added, however, so -he runs a command which prints out the status @emph{and} a GNU +he runs a command which prints out the revision @emph{and} a GNU ``unified diff'' of the patches involved in the changeset: @smallexample @@ -1500,18 +1510,24 @@ @section Adding Files # # old_revision [] # +# add_dir "" +# +# add_dir "include" +# +# add_dir "src" +# # add_file "include/jb.h" -# content [3b12b2d0b31439bd50976633db1895cff8b19da0] -# +# content [f6996ce2dfc5d32bda8b574c3e9ce75db8d55492] +# # add_file "src/apple.c" -# content [2650ffc660dd00a08b659b883b65a060cac7e560] -# +# content [1ce885d2cc59842ff16785834391e864068fbc3c] +# # add_file "src/banana.c" -# content [e8f147e5b4d5667f3228b7bba1c5c1e639f5db9f] +# content [ad88bbbb1b7507ddff26be67efd91d95e069afb6] # -============================================================================ ---- include/jb.h -+++ include/jb.h 3b12b2d0b31439bd50976633db1895cff8b19da0 +============================================================ +--- include/jb.h f6996ce2dfc5d32bda8b574c3e9ce75db8d55492 ++++ include/jb.h f6996ce2dfc5d32bda8b574c3e9ce75db8d55492 @@ -0,0 +1,13 @@ +/* Standard JuiceBot hw interface */ + @@ -1526,9 +1542,9 @@ @section Adding Files +#define BANANA_SPOUT 0x7f +void dispense_apple_juice (); +void dispense_banana_juice (); -============================================================================ ---- src/apple.c -+++ src/apple.c 2650ffc660dd00a08b659b883b65a060cac7e560 +============================================================ +--- src/apple.c 1ce885d2cc59842ff16785834391e864068fbc3c ++++ src/apple.c 1ce885d2cc59842ff16785834391e864068fbc3c @@ -0,0 +1,7 @@ +#include "jb.h" + @@ -1537,9 +1553,9 @@ @section Adding Files address@hidden + /* Fill this in please, Abe. */ address@hidden -============================================================================ ---- src/banana.c -+++ src/banana.c e8f147e5b4d5667f3228b7bba1c5c1e639f5db9f +============================================================ +--- src/banana.c ad88bbbb1b7507ddff26be67efd91d95e069afb6 ++++ src/banana.c ad88bbbb1b7507ddff26be67efd91d95e069afb6 @@ -0,0 +1,7 @@ +#include "jb.h" + @@ -1567,7 +1583,7 @@ @section Committing Work @group $ mtn commit --message="initial checkin of project" mtn: beginning commit on branch 'jp.co.juicebot.jb7' -mtn: committed revision 2e24d49a48adf9acf3a1b6391a080008cbef9c21 +mtn: committed revision 493bda86628fd72c992eb56f73899db9ead3cf6f @end group @end smallexample @@ -1579,7 +1595,7 @@ @section Committing Work @smallexample @group $ mtn automate get_base_revision_id -2e24d49a48adf9acf3a1b6391a080008cbef9c21 +493bda86628fd72c992eb56f73899db9ead3cf6f @end group @end smallexample @@ -1590,7 +1606,7 @@ @section Committing Work @smallexample @group -$ mtn ls certs 2e24d49a48adf9acf3a1b6391a080008cbef9c21 +$ mtn ls certs 493bda86628fd72c992eb56f73899db9ead3cf6f ----------------------------------------------------------------- Key : jim@@juicebot.co.jp (398cb10d...) Sig : ok @@ -1622,7 +1638,7 @@ @section Committing Work the cert name, and the fourth is the cert value. This list shows us that monotone has confirmed that, according to @code{jim@@juicebot.co.jp}, the revision address@hidden is a member of the address@hidden is a member of the branch @code{jp.co.juicebot.jb7}, written by @code{jim@@juicebot.co.jp}, with the given date and changelog. @@ -1640,7 +1656,7 @@ @section Committing Work @group $ mtn heads branch 'jp.co.juicebot.jb7' is currently merged: -2e24d49a48adf9acf3a1b6391a080008cbef9c21 jim@@juicebot.co.jp 2004-10-26T02:53:08 +493bda86628fd72c992eb56f73899db9ead3cf6f jim@@juicebot.co.jp 2004-10-26T02:53:08 @end group @end smallexample @@ -1834,18 +1850,16 @@ @section Making Changes @group $ mtn diff # -# old_revision [2e24d49a48adf9acf3a1b6391a080008cbef9c21] +# old_revision [493bda86628fd72c992eb56f73899db9ead3cf6f] # # patch "src/apple.c" -# from [2650ffc660dd00a08b659b883b65a060cac7e560] -# to [e2c418703c863eabe70f9bde988765406f885fd0] +# from [1ce885d2cc59842ff16785834391e864068fbc3c] +# to [e2c64f6bde75a192d48d2256385df3dd7a963349] # -============================================================================ ---- src/apple.c 2650ffc660dd00a08b659b883b65a060cac7e560 -+++ src/apple.c e2c418703c863eabe70f9bde988765406f885fd0 -@@ -1,7 +1,10 @@ - #include "jb.h" - +============================================================ +--- src/apple.c 1ce885d2cc59842ff16785834391e864068fbc3c ++++ src/apple.c e2c64f6bde75a192d48d2256385df3dd7a963349 +@@ -3,5 +3,8 @@ dispense_apple_juice() void dispense_apple_juice() @{ @@ -1863,7 +1877,6 @@ @section Making Changes @smallexample @group $ mtn commit -mtn: beginning commit on branch 'jp.co.juicebot.jb7' @end group @end smallexample @@ -1876,28 +1889,40 @@ @section Making Changes @smallexample @group +Enter a description of this change following the Changelog line below. +The values of Author, Date and Branch may be modified as required. + +*** REMOVE THIS LINE TO CANCEL THE COMMIT *** +---------------------------------------------------------------------- +Revision: 42eae36587508faa664b111cefc291f0b85ef83a +Parent: 493bda86628fd72c992eb56f73899db9ead3cf6f +Author: abe@@juicebot.co.jp +Date: 2004-10-26T02:53:08 +Branch: jp.co.juicebot.jb7 + +Changelog: + polling implementation of src/apple.c -MTN: ---------------------------------------------------------------------- -MTN: Enter a description of this change. -MTN: Lines beginning with `MTN:' are removed automatically. -MTN: -MTN: Current branch: jp.co.juicebot.jb7 -MTN: Changes against parent 2e24d49a48adf9acf3a1b6391a080008cbef9c21 -MTN: patched src/apple.c -MTN: ---------------------------------------------------------------------- -MTN: + +---------------------------------------------------------------------- +Changes against parent 493bda86628fd72c992eb56f73899db9ead3cf6f + + patched src/apple.c + @end group @end smallexample -Abe enters a single line above the explanatory message, saying -``polling implementation of src/apple.c''. He then saves the file and -quits the editor. Monotone deletes all the lines beginning with -``MTN:'' and leaves only Abe's short message. Returning to the shell, -Abe's commit completes: +Abe enters a single line below the Changelog: header, saying ``polling +implementation of src/apple.c''. He then saves the file and quits the +editor. Monotone confirms that no other lines have been changed and +extracts the message to be stored in the associated ``changelog'' +cert, leaving only Abe's short message. Returning to the shell, Abe's +commit completes: @smallexample @group -mtn: committed revision 70decb4b31a8227a629c0e364495286c5c75f979 +mtn: beginning commit on branch 'jp.co.juicebot.jb7' +mtn: committed revision 42eae36587508faa664b111cefc291f0b85ef83a @end group @end smallexample @@ -1976,7 +2001,7 @@ @section Making Changes @group $ mtn commit mtn: beginning commit on branch 'jp.co.juicebot.jb7' -mtn: committed revision 80ef9c9d251d39074d37e72abf4897e0bbae1cfb +mtn: committed revision 85573a54105cd3220db10aa6a0713643cdf5ce6f @end group @end smallexample @@ -2018,9 +2043,9 @@ @section Dealing with a Fork produced purely derivative, ``down-stream'' work: @enumerate address@hidden Jim made revision 2e24d... address@hidden Abe changed revision 2e24d... into revision 70dec... address@hidden Beth derived revision 70dec... into revision 80ef9... address@hidden Jim made revision 493bd... address@hidden Abe changed revision 493bd... into revision 42eae... address@hidden Beth derived revision 42eae... into revision 85573... @end enumerate This is a simple, but sadly unrealistic, ordering of events. In real @@ -2041,7 +2066,7 @@ @section Dealing with a Fork sends out an email saying that the current polling juice dispensers use too much CPU time, and must be rewritten to use the JuiceBot's interrupt system. Beth wakes up first and begins working immediately, -basing her work off the revision 80ef9... which is currently in her +basing her work off the revision 85573... which is currently in her workspace: @smallexample @@ -2057,15 +2082,15 @@ @section Dealing with a Fork @group $ mtn diff # -# old_revision [80ef9c9d251d39074d37e72abf4897e0bbae1cfb] +# old_revision [85573a54105cd3220db10aa6a0713643cdf5ce6f] # # patch "src/banana.c" -# from [7381d6b3adfddaf16dc0fdb05e0f2d1873e3132a] -# to [5e6622cf5c8805bcbd50921ce7db86dad40f2ec6] +# from [d7e28a01cf6fc0f9ac04c6901dcafd77c2d32fb8] +# to [dd979c3c880e6a7221fcecd7148bd4afcfb3e964] # -============================================================================ ---- src/banana.c 7381d6b3adfddaf16dc0fdb05e0f2d1873e3132a -+++ src/banana.c 5e6622cf5c8805bcbd50921ce7db86dad40f2ec6 +============================================================ +--- src/banana.c d7e28a01cf6fc0f9ac04c6901dcafd77c2d32fb8 ++++ src/banana.c dd979c3c880e6a7221fcecd7148bd4afcfb3e964 @@ -1,10 +1,15 @@ #include "jb.h" @@ -2077,8 +2102,7 @@ @section Dealing with a Fork address@hidden + void --dispense_banana_juice() -+dispense_banana_juice() + dispense_banana_juice() @{ + spoutctl(BANANA_SPOUT, SET_INTR, &shut_off_banana); spoutctl(BANANA_SPOUT, FLOW_JUICE, 1); @@ -2095,7 +2119,7 @@ @section Dealing with a Fork @group $ mtn commit --message="interrupt implementation of src/banana.c" mtn: beginning commit on branch 'jp.co.juicebot.jb7' -mtn: committed revision 8b41b5399a564494993063287a737d26ede3dee4 +mtn: committed revision 90abe0f1bc354a73d42d3bff1b02946559682bd9 @end group @end smallexample @@ -2110,8 +2134,7 @@ @section Dealing with a Fork Unfortunately, before Beth managed to sync with Jim, Abe had woken up and implemented a similar interrupt-based apple juice dispenser, but -his workspace is 70dec..., which is still ``upstream'' of -Beth's. +his workspace is 42eae..., which is still ``upstream'' of Beth's. @smallexample @group @@ -2146,8 +2169,8 @@ @section Dealing with a Fork @group $ mtn heads mtn: branch 'jp.co.juicebot.jb7' is currently unmerged: -39969614e5a14316c7ffefc588771f491c709152 abe@@juicebot.co.jp 2004-10-26T02:53:16 -8b41b5399a564494993063287a737d26ede3dee4 beth@@juicebot.co.jp 2004-10-26T02:53:15 +90abe0f1bc354a73d42d3bff1b02946559682bd9 abe@@juicebot.co.jp 2004-10-26T02:53:16 +951da88860a4cf7419d66ed9094d8bf24df5fb8b beth@@juicebot.co.jp 2004-10-26T02:53:15 @end group @end smallexample @@ -2160,14 +2183,13 @@ @section Dealing with a Fork @smallexample @group $ mtn merge -mtn: starting with revision 1 / 2 -mtn: merging with revision 2 / 2 -mtn: [source] 39969614e5a14316c7ffefc588771f491c709152 -mtn: [source] 8b41b5399a564494993063287a737d26ede3dee4 -mtn: common ancestor 70decb4b31a8227a629c0e364495286c5c75f979 abe@@juicebot.co.jp 2004-10-26T:02:50:01 found -mtn: trying 3-way merge -mtn: [merged] da499b9d9465a0e003a4c6b2909102ef98bf4e6d -mtn: your workspaces have not been updated +mtn: 2 heads on branch 'jp.co.juicebot.jb7' +mtn: merge 1 / 1: +mtn: calculating best pair of heads to merge next +mtn: [left] 90abe0f1bc354a73d42d3bff1b02946559682bd9 +mtn: [right] 951da88860a4cf7419d66ed9094d8bf24df5fb8b +mtn: [merged] 3aca69c7749bde9bd07fe4c92bb868bd69b2e421 +mtn: note: your workspaces have not been updated @end group @end smallexample @@ -2185,10 +2207,13 @@ @section Dealing with a Fork @smallexample @group $ mtn update -mtn: selected update target da499b9d9465a0e003a4c6b2909102ef98bf4e6d -mtn: updating src/apple.c to f088e24beb43ab1468d7243e36ce214a559bdc96 -mtn: updating src/banana.c to 5e6622cf5c8805bcbd50921ce7db86dad40f2ec6 -mtn: updated to base revision da499b9d9465a0e003a4c6b2909102ef98bf4e6d +mtn: updating along branch 'jp.co.juicebot.jb7' +mtn: selected update target 3aca69c7749bde9bd07fe4c92bb868bd69b2e421 +mtn: [left] d60c18ec5e0cf1163b276f0bfdd908c1dfd53b4a +mtn: [right] 3aca69c7749bde9bd07fe4c92bb868bd69b2e421 +mtn: updating src/apple.c +mtn: updating src/banana.c +mtn: updated to base revision 3aca69c7749bde9bd07fe4c92bb868bd69b2e421 @end group @end smallexample @@ -5043,9 +5068,9 @@ @section Workspace This can be used with an empty directory to start a new blank project, or within an existing directory full of files, prior to using @command{mtn commit}. If no directory is specified, the current -directory is used. +directory is used. If no database is given, a new database is +initialized and saved to @file{_MTN/mtn.db}. - @item mtn add [--recursive | -R] [--no-respect-ignore] @var{pathname...} @item mtn add [--recursive | -R] [--no-respect-ignore] --unknown address@hidden This command places ``add'' entries for the paths specified in @@ -5173,14 +5198,14 @@ @section Workspace The @file{_MTN/log} file can be edited by the user during their daily work to record the changes made to the workspace. When running the address@hidden command without a @var{logmsg} supplied, the contents of -the @file{_MTN/log} file will be read and passed to the Lua hook address@hidden as a second parameter named @var{user_log_content}. -The log message will be prepended with a 'magic' string that must be -removed to confirm the commit. This allows the user to easily cancel a -commit, without emptying the entire log message. If the commit is -successful, the @file{_MTN/log} file is cleared of all content making it -ready for another edit/commit cycle. address@hidden command without a @var{logmsg} supplied, the contents +of the @file{_MTN/log} file will be read and passed to the Lua hook address@hidden as a parameter named @var{user_log_message}. The +log message passed to the user's editor will contain a line that the +user may remove allowing them user to easily cancel a commit, without +emptying the entire log message. If the commit is successful, the address@hidden/log} file is cleared of all content making it ready for +another edit/commit cycle. If @option{--message-file=_MTN/log} is specified, the contents of @file{_MTN/log} will be used without confirmation. @@ -5212,11 +5237,53 @@ @section Workspace A @code{changelog} cert, containing the ``log message'' for these changes. If you provided @var{logmsg} on the command line, this text will be used, otherwise @command{commit} will run the Lua hook address@hidden (@var{commentary}, @var{user_log_content})}, which -typically invokes an external editor program, in which you can compose -and/or review your log message for the change. address@hidden(@var{user_log_message})}, which typically invokes +an external editor program, in which you can compose and/or review the +revision and log message for the change. @end itemize +The @var{user_log_message} passed to the @code{edit_comment} hook will +contain a full summary of the revision as it would have been displayed +by the @command{status} command prior to committing and as the address@hidden command will display it after it has been committed: + address@hidden address@hidden +Enter a description of this change following the Changelog line below. +The values of Author, Date and Branch may be modified as required. + +*** REMOVE THIS LINE TO CANCEL THE COMMIT *** +---------------------------------------------------------------------- +Revision: 42eae36587508faa664b111cefc291f0b85ef83a +Parent: 493bda86628fd72c992eb56f73899db9ead3cf6f +Author: abe@@juicebot.co.jp +Date: 2004-10-26T02:53:08 +Branch: jp.co.juicebot.jb7 + +Changelog: + +polling implementation of src/apple.c + +---------------------------------------------------------------------- +Changes against parent 493bda86628fd72c992eb56f73899db9ead3cf6f + + patched src/apple.c + address@hidden group address@hidden smallexample + +Values specified in @option{--author}, @option{--date} or address@hidden options on the command line will be displayed in the +editor and may be further changed while editing the changelog +message. The changelog message will be extracted from between the address@hidden:} header and the trailing line of @code{----} +characters. Care must be taken to ensure no other lines are changed or +the commit will abort to avoid extracting an incorrect message. If the +commit needs to be cancelled the line following the leading +instructions should be removed. Any pending text in @file{MTN/log} +will be left untouched if the commit is cancelled. + + @item mtn revert @var{pathname...} @itemx mtn revert --missing @var{pathname...} This command changes your workspace, so that changes you have made @@ -5830,15 +5897,22 @@ @section Informative If @var{pattern} is provided, it is used as a glob to limit the keys listed. Otherwise all keys in your database are listed. address@hidden mtn list branches address@hidden mtn ls branches address@hidden mtn list branches address@hidden address@hidden address@hidden mtn ls branches address@hidden address@hidden -This command lists all known branches in your database. +This command lists all known branches in your database. If address@hidden is provided, it is used as a glob to select the branches +listed, otherwise all branches are listed. If @option{-exclude} +options are provided they are used as globs to exclude specified +branches. address@hidden mtn list tags address@hidden mtn ls tags address@hidden mtn list tags address@hidden address@hidden address@hidden mtn ls tags address@hidden address@hidden -This command lists all known tags in your database. +This command lists all known tags in your database. If @var{pattern} +is provided, it is used as a glob to select the tags listed, otherwise +all tags are listed. If @option{-exclude} options are provided they +are used as globs to exclude specified tags. @item mtn list vars @item mtn ls vars @@ -6377,6 +6451,12 @@ @section Database using @command{pubkey} on that database to create a public key packet, which can be loaded into your database with @command{read}. address@hidden branches not used or with incorrect head count or missing +in the cache of branch heads. This is a serious problem; it suggests +the presence of a bug in monotone's caching system, but does not +involve data loss; please report this on the monotone mailing +list. You can fix it by running @command{db regenerate_caches}. + @end itemize This command also verifies that the @sc{sha1} hash of every file, manifest, @@ -10586,21 +10666,22 @@ @subsection User Defaults @end group @end smallexample address@hidden edit_comment (@var{commentary}, @var{user_log_message}) address@hidden edit_comment (@var{user_log_message}) Returns a log entry for a given set of changes, described in address@hidden The commentary is identical to the output of address@hidden status}. This hook is intended to interface with -some sort of editor, so that you can interactively document each -change you make. The result is used as the value for a address@hidden certificate, automatically generated when you commit -changes. address@hidden The user_log_message is identical to the +output of @command{mtn status}. This hook is intended to interface +with some sort of editor, so that you can interactively document each +change you make. Values for @code{author}, @code{date}, @code{branch} +and the @code{changelog} are extracted from the result and used to +generate these certificates when you commit changes. -The contents of @file{_MTN/log} are read and passed as address@hidden This allows you to document your changes as -you proceed instead of waiting until you are ready to commit. Upon -a successful commit, the contents of @file{_MTN/log} are erased setting -the system up for another edit/commit cycle. +The contents of @file{_MTN/log} are read and passed as as the address@hidden value in @var{user_log_message}. This allows you to +document your changes as you proceed instead of waiting until you are +ready to commit. Upon a successful commit, the contents of address@hidden/log} are erased setting the system up for another +edit/commit cycle. For the default definition of this hook, see @ref{Default hooks}. @@ -10671,6 +10752,13 @@ @subsection User Defaults @samp{date_time_short}, though only @samp{date_short} and @samp{date_time_short} are currently used. +The @samp{date_time_long} format is used by the @command{status}, address@hidden and @command{log} commands to format revision dates +for printing. This format must preserve a date passed through a +formatting and parsing cycle. Otherwise the @command{status} command +will issue warnings that the format is unsuitable and the address@hidden command will abort. + @end ftable @subsection Netsync Permission Hooks ============================================================ --- packet.cc 36b0f681ca92776c2e60e16529ef7cd7d826a03f +++ packet.cc 7a0c4e9c49cba8d651e2ba8a69c8eee80769bc47 @@ -369,6 +369,22 @@ extract_packets(string const & s, packet return count; } +// this is same as rfind, but search area is haystack[start:] (from start to end of string) +// haystack is searched, needle is pattern +static size_t +rfind_in_substr(std::string const& haystack, size_t start, std::string const& needle) +{ + I(start <= haystack.size()); + const std::string::const_iterator result = + std::find_end(haystack.begin() + start, haystack.end(), + needle.begin(), needle.end()); + + if (result == haystack.end()) + return std::string::npos; + else + return distance(haystack.begin(), result); +} + size_t read_packets(istream & in, packet_consumer & cons) { @@ -379,10 +395,12 @@ read_packets(istream & in, packet_consum static string const end("[end]"); while(in) { + size_t const next_search_pos = (accum.size() >= end.size()) + ? accum.size() - end.size() : 0; in.read(buf, bufsz); accum.append(buf, in.gcount()); string::size_type endpos = string::npos; - endpos = accum.rfind(end); + endpos = rfind_in_substr(accum, next_search_pos, end); if (endpos != string::npos) { endpos += end.size(); ============================================================ --- simplestring_xform.cc 4d7bb0ac67e722f14a3f81722e05f249d009f645 +++ simplestring_xform.cc de3c230648303fb799bdd907bb90985d2ed7e97a @@ -146,16 +146,18 @@ join_lines(vector const & in, string & out, string const & linesep) { - ostringstream oss; - copy(in.begin(), in.end(), ostream_iterator(oss, linesep.c_str())); - out = oss.str(); + join_lines(in.begin(), in.end(), out, linesep); } void -join_lines(vector const & in, - string & out) +join_lines(vector::const_iterator begin, + vector::const_iterator end, + string & out, + string const & linesep) { - join_lines(in, out, "\n"); + ostringstream oss; + copy(begin, end, ostream_iterator(oss, linesep.c_str())); + out = oss.str(); } void @@ -216,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; } @@ -226,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; } @@ -239,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; } ============================================================ --- simplestring_xform.hh 5a6e7a65e2564f2a01321844a1ff6d98e6f58646 +++ simplestring_xform.hh fb077a97ed6338787a652441b97c809750aac454 @@ -33,12 +33,13 @@ void join_lines(std::vector void join_lines(std::vector const & in, std::string & out, - std::string const & linesep); + std::string const & linesep = "\n"); -void join_lines(std::vector const & in, - std::string & out); +void join_lines(std::vector::const_iterator begin, + std::vector::const_iterator end, + std::string & out, + std::string const & linesep = "\n"); - template inline origin::type get_made_from(Thing const & thing) { ============================================================ --- std_hooks.lua a8ce32582e19e799eef97ddb45e91418c8b24fcc +++ std_hooks.lua bc071c124deba2c01c60120e449c861ece126b81 @@ -277,7 +277,7 @@ end return "^[[:alnum:]$_]" end -function edit_comment(basetext, user_log_message) +function edit_comment(user_log_message) local exe = nil -- top priority is VISUAL, then EDITOR, then a series of hardcoded @@ -299,12 +299,10 @@ function edit_comment(basetext, user_log local tmp, tname = temp_file() if (tmp == nil) then return nil end - basetext = "MTN: " .. string.gsub(basetext, "\n", "\nMTN: ") .. "\n" tmp:write(user_log_message) if user_log_message == "" or string.sub(user_log_message, -1) ~= "\n" then tmp:write("\n") end - tmp:write(basetext) io.close(tmp) -- By historical convention, VISUAL and EDITOR can contain arguments @@ -316,22 +314,22 @@ function edit_comment(basetext, user_log if (not string.find(exe, "[^%w_.+-]")) then -- safe to call spawn directly if (execute(exe, tname) ~= 0) then - io.write(string.format(gettext("Error running editor '%s' ".. - "to enter log message\n"), + io.write(string.format(gettext("Error running editor '%s' ".. + "to enter log message\n"), exe)) - os.remove(tname) - return nil + os.remove(tname) + return nil end else -- must use shell local shell = os.getenv("SHELL") if (shell == nil) then shell = "sh" end if (not program_exists_in_path(shell)) then - io.write(string.format(gettext("Editor command '%s' needs a shell, ".. - "but '%s' is not to be found"), - exe, shell)) - os.remove(tname) - return nil + io.write(string.format(gettext("Editor command '%s' needs a shell, ".. + "but '%s' is not to be found"), + exe, shell)) + os.remove(tname) + return nil end -- Single-quoted strings in both Bourne shell and csh can contain @@ -339,24 +337,17 @@ function edit_comment(basetext, user_log local safe_tname = " '" .. string.gsub(tname, "'", "'\\''") .. "'" if (execute(shell, "-c", editor .. safe_tname) ~= 0) then - io.write(string.format(gettext("Error running editor '%s' ".. - "to enter log message\n"), + io.write(string.format(gettext("Error running editor '%s' ".. + "to enter log message\n"), exe)) - os.remove(tname) - return nil + os.remove(tname) + return nil end end tmp = io.open(tname, "r") if (tmp == nil) then os.remove(tname); return nil end - local res = "" - local line = tmp:read() - while(line ~= nil) do - if (not string.find(line, "^MTN:")) then - res = res .. line .. "\n" - end - line = tmp:read() - end + local res = tmp:read("*a") io.close(tmp) os.remove(tname) return res ============================================================ --- tests/_--author,_--date/__driver__.lua 32996584305ed55d69e88dd14f0570e6da40b827 +++ tests/_--author,_--date/__driver__.lua 39f17d4b68507906b2c98cefda79c2c0d9fc9ba3 @@ -6,8 +6,8 @@ check(mtn("log", "--from", rev), 0, true rev = base_revision() check(mtn("log", "--from", rev), 0, true, false) -check(qgrep('^[\\| ]+Author: the_author', "stdout")) -check(qgrep('^[\\| ]+Date: 1999-12-31T12:00:00', "stdout")) +check(qgrep('^[\\| ]+Author: the_author', "stdout")) +check(qgrep('^[\\| ]+Date: 1999-12-31T12:00:00', "stdout")) writefile("testfile", "oovel") check(mtn("commit", "--date=1999-12-31T12:00foo", "--branch=foo", "--message=foo"), 1, false, false) ============================================================ --- tests/_MTN_files_handled_correctly_in_aborted_commit/bad_edit_comment.lua 98cfa1aefd465bf9b3e8169c40c33ccc204fdbfd +++ tests/_MTN_files_handled_correctly_in_aborted_commit/bad_edit_comment.lua bb3533964abf2e17d7d7ca490589c497a50285d8 @@ -1,3 +1,3 @@ -function edit_comment(basetext, user_log_message) +function edit_comment(user_log_message) return "" end ============================================================ --- tests/checkout_creates__MTN_log/commit_log.lua a9af6de4500c2fddab1853eebc185a055156f8cd +++ tests/checkout_creates__MTN_log/commit_log.lua e1b07045d69de57d75e5b73df41124c2c94b315d @@ -1,6 +1,3 @@ -function edit_comment(summary, user_log_file) - -- this used to just return the variable user_log_file, - -- but now must return the original entry without the 'magic' line. - -- this avoids a failing test after the 'magic line' patch - return "Log Entry" +function edit_comment(user_log_file) + return user_log_file end ============================================================ --- tests/clone_creates__MTN_log/commit_log.lua a9af6de4500c2fddab1853eebc185a055156f8cd +++ tests/clone_creates__MTN_log/commit_log.lua e1b07045d69de57d75e5b73df41124c2c94b315d @@ -1,6 +1,3 @@ -function edit_comment(summary, user_log_file) - -- this used to just return the variable user_log_file, - -- but now must return the original entry without the 'magic' line. - -- this avoids a failing test after the 'magic line' patch - return "Log Entry" +function edit_comment(user_log_file) + return user_log_file end ============================================================ --- tests/commit_default_editor/test_hooks.lua bf4c9c22f206ed2ada4074eaac831948a36b6c87 +++ tests/commit_default_editor/test_hooks.lua acc4afeabd22d8a4dbe2329ca5907202d875b89f @@ -5,9 +5,17 @@ function execute(path,...) return 1 end if path == "editor" then + + tmp = io.open(tname, "r") + text = tmp:read("*a") + io.close(tmp) + + text = string.gsub(text, "\nChangelog: \n\n\n", "\nChangelog: \n\nHello\n") + tmp = io.open(tname, "w") - tmp:write("Hello\n") + tmp:write(text) io.close(tmp) + return 0 end return 1 ============================================================ --- tests/commit_using__MTN_log/__driver__.lua a9b708bd811300cc5c4ca78b932f50f7f93341cf +++ tests/commit_using__MTN_log/__driver__.lua 64507df85499593c9de2e854a3add3b4d6683c47 @@ -1,26 +1,25 @@ mtn_setup() mtn_setup() -check(get("commit_log.lua")) -check(get("commit_log_modified_return.lua")) +check(get("commit_cancelled.lua")) +check(get("commit_confirmed.lua")) -writefile("_MTN/log", "Log entry") - +writefile("_MTN/log", "Log Entry") writefile("input.txt", "version 0 of the file") check(mtn("add", "input.txt"), 0, false, false) ---this should now fail, given that the log file has content and we don't ---remove the 'magic' line -check(mtn("--branch=testbranch", "--rcfile=commit_log.lua", "commit"), 1, false, true) -check(qgrep('magic line; commit cancelled', "stderr")) +-- this should now fail, given that the log file has content and the cancel line +-- has been removed +check(mtn("--branch=testbranch", "--rcfile=commit_cancelled.lua", "commit"), 1, false, true) +check(qgrep('Commit cancelled.', "stderr")) check(exists("_MTN/log")) check(fsize("_MTN/log") > 0) ---this should pass, as the lua hook now returns a string that doesn't contain ---the 'magic' line -check(mtn("--branch=testbranch", "--rcfile=commit_log_modified_return.lua", "commit"), 0, false, false) +-- this should pass, as the lua hook now returns a string that still includes the +-- cancel line +check(mtn("--branch=testbranch", "--rcfile=commit_confirmed.lua", "commit"), 0, false, false) tsha = base_revision() check(exists("_MTN/log")) ============================================================ --- tests/commit_using__MTN_log/commit_log.lua 09c926209c317efc447dbe6448fe891c2d86e958 +++ tests/commit_using__MTN_log/commit_cancelled.lua 1b64ffe9a275897fe60c629350ac44b5bb3afc62 @@ -1,3 +1,3 @@ -function edit_comment(summary, user_log_file) - return user_log_file +function edit_comment(user_log_file) + return string.gsub(user_log_file, "... REMOVE THIS LINE TO CANCEL THE COMMIT ...\n", "") end ============================================================ --- tests/commit_using__MTN_log/commit_log_modified_return.lua a9af6de4500c2fddab1853eebc185a055156f8cd +++ tests/commit_using__MTN_log/commit_confirmed.lua 13f324e5f74d60c595cbf45b2eb5f85115829967 @@ -1,6 +1,3 @@ -function edit_comment(summary, user_log_file) - -- this used to just return the variable user_log_file, - -- but now must return the original entry without the 'magic' line. - -- this avoids a failing test after the 'magic line' patch - return "Log Entry" +function edit_comment(user_log_file) + return user_log_file end ============================================================ --- tests/commit_using__MTN_log_and_--message/commit_log.lua 09c926209c317efc447dbe6448fe891c2d86e958 +++ tests/commit_using__MTN_log_and_--message/commit_log.lua e1b07045d69de57d75e5b73df41124c2c94b315d @@ -1,3 +1,3 @@ -function edit_comment(summary, user_log_file) +function edit_comment(user_log_file) return user_log_file end ============================================================ --- tests/commit_validation_lua_hook/errmsg 33334887b6211bde36d406f5fceaf4c8fca84170 +++ tests/commit_validation_lua_hook/errmsg b1f67b9ea367e6bffd33f28da4996b73bdc2bd21 @@ -1,2 +1 @@ -mtn: beginning commit on branch 'testbranch' mtn: misuse: log message rejected by hook: input.txt ============================================================ --- tests/commit_with_--message-file/commit_log.lua 09c926209c317efc447dbe6448fe891c2d86e958 +++ tests/commit_with_--message-file/commit_log.lua e1b07045d69de57d75e5b73df41124c2c94b315d @@ -1,3 +1,3 @@ -function edit_comment(summary, user_log_file) +function edit_comment(user_log_file) return user_log_file end ============================================================ --- tests/commit_writes_message_back_to__MTN_log/__driver__.lua 7758bd1d4316f2c3e80cc4018a4b3ab4067137b7 +++ tests/commit_writes_message_back_to__MTN_log/__driver__.lua 7bee4d10f6e1cd255e8a6501ad8d93d5efa1a48b @@ -4,6 +4,13 @@ addfile("testfile", "blah blah") addfile("testfile", "blah blah") +-- when a public key is first accessed it is automatically added to the database +-- but since this test removes write permission from the database this fails and +-- aborts the commit before the edit_comment hook gets to run. running status +-- while the database is still writeable adds the key so that its available for +-- commit allowing the edit_comment hook to succeed. +check(mtn("status"), 0, false, false) + -- Make it unwriteable, so our edit_comment hook will have a chance to -- run, but the overall commit will fail. (How do I know this will -- work? Well, it did...) ============================================================ --- tests/commit_writes_message_back_to__MTN_log/my_hook.lua 2c27a968d793839ad3c7dade630eb58837ea2f79 +++ tests/commit_writes_message_back_to__MTN_log/my_hook.lua 2a7cb74c1a3c220ce8f4446357d87f70e8988e22 @@ -1,3 +1,3 @@ -function edit_comment(basetext, user_log_message) - return "foobar\n" +function edit_comment(user_log_file) + return string.gsub(user_log_file, "\nChangelog: \n\n\n", "\nChangelog: \n\nfoobar\n") end ============================================================ --- tests/db_kill_tag_locally/__driver__.lua eb96c1472d7ef266ce84d3fcfb4beb75b3c57661 +++ tests/db_kill_tag_locally/__driver__.lua 412ff7d7544bb55aee63e6e6333ae23b1acc12e6 @@ -22,36 +22,43 @@ check(mtn("tag", R4, "other_tag"), 0, fa check(mtn("tag", R3, "test_tag"), 0, false, false) check(mtn("tag", R4, "other_tag"), 0, false, false) +-- abbreviated revision ids + +a1=string.sub(R1, 0, 10) .. "..." +a2=string.sub(R2, 0, 10) .. "..." +a3=string.sub(R3, 0, 10) .. "..." +a4=string.sub(R4, 0, 10) .. "..." + check(mtn("ls", "tags"), 0, true, false) -check(qgrep(R1, "stdout")) +check(qgrep(a1, "stdout")) check(qgrep("ambig_tag", "stdout")) -check(qgrep(R2, "stdout")) +check(qgrep(a2, "stdout")) check(qgrep("ambig_tag", "stdout")) -check(qgrep(R3, "stdout")) +check(qgrep(a3, "stdout")) check(qgrep("test_tag", "stdout")) -check(qgrep(R4, "stdout")) +check(qgrep(a4, "stdout")) check(qgrep("other_tag", "stdout")) check(mtn("db", "kill_tag_locally", "test_tag"), 0, false, false) check(mtn("ls", "tags"), 0, true, false) -check(qgrep(R1, "stdout")) +check(qgrep(a1, "stdout")) check(qgrep("ambig_tag", "stdout")) -check(qgrep(R2, "stdout")) +check(qgrep(a2, "stdout")) check(qgrep("ambig_tag", "stdout")) -check(not qgrep(R3, "stdout")) +check(not qgrep(a3, "stdout")) check(not qgrep("test_tag", "stdout")) -check(qgrep(R4, "stdout")) +check(qgrep(a4, "stdout")) check(qgrep("other_tag", "stdout")) check(mtn("db", "kill_tag_locally", "ambig_tag"), 0, false, false) check(mtn("ls", "tags"), 0, true, false) -check(not qgrep(R1, "stdout")) +check(not qgrep(a1, "stdout")) check(not qgrep("ambig_tag", "stdout")) -check(not qgrep(R2, "stdout")) +check(not qgrep(a2, "stdout")) check(not qgrep("ambig_tag", "stdout")) -check(not qgrep(R3, "stdout")) +check(not qgrep(a3, "stdout")) check(not qgrep("test_tag", "stdout")) -check(qgrep(R4, "stdout")) +check(qgrep(a4, "stdout")) check(qgrep("other_tag", "stdout")) ============================================================ --- tests/git_export/__driver__.lua ca3862a9039ee4e3f52329a6d0cfad08c347cc33 +++ tests/git_export/__driver__.lua d7ba0d936847de3d24d493df4167d687b3f63618 @@ -79,7 +79,7 @@ check(indir("mtn.dir", mtn("log")), 0, t -- log both repos and check the author mapping check(indir("mtn.dir", mtn("log")), 0, true, false) -check(qgrep("Author: address@hidden", "stdout")) +check(qgrep("Author: address@hidden", "stdout")) check(indir("git.dir", {"git", "log", "--summary", "--pretty=raw"}), 0, true, false) check(qgrep("author other ", "stdout")) check(qgrep("committer other ", "stdout")) ============================================================ --- tests/i18n_commit_messages/__driver__.lua 2e3be837715a30fa20743f0f4a54fceaf4a7dbfe +++ tests/i18n_commit_messages/__driver__.lua 4d8c64139a02a00aa3a3aca1e2e703849adf4d15 @@ -35,7 +35,12 @@ check(qgrep("VALIDATE: REV GOOD", "stdou check(qgrep("VALIDATE: MSG GOOD", "stdout")) check(qgrep("VALIDATE: REV GOOD", "stdout")) +check(mtn("log"), 0, true, false) +eucjp = string.gsub(readfile("euc-jp.txt"), "\n", "") +check(qgrep(eucjp, "stdout")) + -- and if we look at the cert, it should be in unicode rev = base_revision() check(mtn("automate", "certs", rev), 0, true, false) -check(qgrep(readfile("utf8.txt"), "stdout")) +utf8 = string.gsub(readfile("utf8.txt"), "\n", "") +check(qgrep(utf8, "stdout")) ============================================================ --- tests/i18n_commit_messages/euc-jp.txt e42d3979ee6d233a489f9a21697abf8786bc8b65 +++ tests/i18n_commit_messages/euc-jp.txt 0894414faade0c80e732c757474203f9de358469 @@ -1 +1 @@ -¥ï¡¼¥¯¥¹¥Ú¡¼¥¹¤¬É¬ÍפǤ¹¤¬¤ß¤Ä¤«¤ê¤Þ¤»¤ó¤Ç¤·¤¿ \ No newline at end of file +¥ï¡¼¥¯¥¹¥Ú¡¼¥¹¤¬É¬ÍפǤ¹¤¬¤ß¤Ä¤«¤ê¤Þ¤»¤ó¤Ç¤·¤¿ ============================================================ --- tests/i18n_commit_messages/extra_hooks.lua ca81a1f657530409dbbe1ec50b52a9e253b0f411 +++ tests/i18n_commit_messages/extra_hooks.lua d80fb2f538a58b7ba6430e6b8af6305edaa2abb8 @@ -6,28 +6,21 @@ end end -- this should get the commit message in current locale -function edit_comment(basetext, user_log_message) - -- we now mangle this to become the same as the content of the text file, - -- as it will have the 'magic line' prepended to it, which will cause the - -- test to fail. - -- this is only done if the message was pre-specified in _MTN/log - if user_log_message ~= "" then - user_log_message = "¥ï¡¼¥¯¥¹¥Ú¡¼¥¹¤¬É¬ÍפǤ¹¤¬¤ß¤Ä¤«¤ê¤Þ¤»¤ó¤Ç¤·¤¿" - end - +function edit_comment(user_log_message) wanted = slurp("euc-jp.txt") - if string.find(basetext, wanted) ~= nil then + -- this is looking for the euc-jp string as an attribute value + if string.find(user_log_message, wanted) ~= nil then io.write("EDIT: BASE GOOD\n") else io.write("EDIT: BASE BAD\n") end - if user_log_message == "" then + if string.find(user_log_message, "\nChangelog: \n\n\n") ~= nil then io.write("EDIT: MSG NONESUCH\n") - return wanted + return string.gsub(user_log_message, "\nChangelog: \n\n\n", "\nChangelog: \n\n" .. wanted .. "\n") else - if wanted == user_log_message then + if string.find(user_log_message, "\nChangelog: \n\n" .. wanted .. "\n") ~= nil then io.write("EDIT: MSG GOOD\n") else io.write("EDIT: MSG BAD\n") ============================================================ --- tests/i18n_commit_messages/utf8.txt 681c6291b3f365c477e93d5bd77192d88253b486 +++ tests/i18n_commit_messages/utf8.txt 7cb4bf2f531c9b04b50e7c120e87465087b88cbd @@ -1 +1 @@ -ワークスペースが必要ですがみつかりませんでした \ No newline at end of file +ワークスペースが必要ですがみつかりませんでした ============================================================ --- tests/importing_cvs_metadata/expected_log_output 2a3629825db9242abe52c0107f5269d34c125494 +++ tests/importing_cvs_metadata/expected_log_output 1cd19bc2ebd7cf09842b51879d71d3a75812d39b @@ -1,16 +1,15 @@ ------------------------------------------------------------------ +---------------------------------------------------------------------- Revision: f31b5f6dfeddd51ebf19cd44c4176c68bff51709 -Ancestor: -Author: luigi -Date: 2009-09-11T16:37:48 -Branch: testbranch +Author: luigi +Date: 2009-09-11T16:37:48 +Branch: testbranch -Added files: - test.txt -Added directories: - . +Changelog: -ChangeLog: +test -test +Changes + added + added test.txt + ============================================================ --- tests/importing_cvs_tricky_repo_with_tags/test.tags b8838f53fc6b7e0aee11074fa2e466e3615aa148 +++ tests/importing_cvs_tricky_repo_with_tags/test.tags 7e382e03da27d4a3309a4661e5154669078f4dfe @@ -1,3 +1,3 @@ -initial 7be6a094cb29307bed5bbacf40b3f3642222f413 address@hidden (46ec58576f...) -portable-branch-base 32ba3f451dcf277762ca1c328043df2e2ceebc17 address@hidden (46ec58576f...) -portable-branch-fork-20050601T0139 32ba3f451dcf277762ca1c328043df2e2ceebc17 address@hidden (46ec58576f...) +initial 7be6a094cb... foo.bar.redhat address@hidden (46ec58576f...) +portable-branch-base 32ba3f451d... foo.bar address@hidden (46ec58576f...) +portable-branch-fork-20050601T0139 32ba3f451d... foo.bar address@hidden (46ec58576f...) ============================================================ --- tests/ls_tags_with_ambiguous_tags/__driver__.lua 86ca8c16ab1e8b7c071c1e3dd73f47852059a31e +++ tests/ls_tags_with_ambiguous_tags/__driver__.lua 5478308acb6e9d10f39a2411e5cfae4637cfe737 @@ -12,6 +12,11 @@ check(mtn("tag", R2, "ambig_tag"), 0, fa check(mtn("tag", R1, "ambig_tag"), 0, false, false) check(mtn("tag", R2, "ambig_tag"), 0, false, false) +-- abbreviated revision ids + +a1=string.sub(R1, 0, 10) .. "..." +a2=string.sub(R2, 0, 10) .. "..." + check(mtn("ls", "tags"), 0, true, false) -check(qgrep(R1, "stdout")) -check(qgrep(R2, "stdout")) +check(qgrep(a1, "stdout")) +check(qgrep(a2, "stdout")) ============================================================ --- tests/merge_into_workspace/expected-log 77915a878636ca16ee32f5385d88a7dc8bd0054a +++ tests/merge_into_workspace/expected-log e43ddd168775004aa49e0dc8afc1ba8604b8678f @@ -1,44 +1,46 @@ ------------------------------------------------------------------ +---------------------------------------------------------------------- Revision: 04093de26437e634d2b43f1722e22fcc6b3804c3 -Ancestor: 87c726bb950efb3d809662b86700a8566346970d -Author: address@hidden -Date: 2007-02-28T02:09:14 -Branch: testbranch +Parent: 87c726bb950efb3d809662b86700a8566346970d +Author: address@hidden +Date: 2007-02-28T02:09:14 +Branch: testbranch -Modified files: - testfile +Changelog: -ChangeLog: +blah-blah -blah-blah +Changes against parent 87c726bb950efb3d809662b86700a8566346970d ------------------------------------------------------------------ + patched testfile + +---------------------------------------------------------------------- Revision: 21320a960288df775cd62b01f1b40e981f4ecd65 -Ancestor: 87c726bb950efb3d809662b86700a8566346970d -Author: address@hidden -Date: 2007-02-28T02:09:13 -Branch: testbranch +Parent: 87c726bb950efb3d809662b86700a8566346970d +Author: address@hidden +Date: 2007-02-28T02:09:13 +Branch: testbranch -Modified files: - testfile +Changelog: -ChangeLog: +blah-blah -blah-blah +Changes against parent 87c726bb950efb3d809662b86700a8566346970d ------------------------------------------------------------------ + patched testfile + +---------------------------------------------------------------------- Revision: 87c726bb950efb3d809662b86700a8566346970d -Ancestor: -Author: address@hidden -Date: 2007-02-28T02:09:12 -Branch: testbranch +Author: address@hidden +Date: 2007-02-28T02:09:12 +Branch: testbranch -Added files: - otherfile testfile -Added directories: - . +Changelog: -ChangeLog: +blah-blah -blah-blah +Changes + added + added otherfile + added testfile + ============================================================ --- tests/merge_into_workspace/expected-log-left 6f396f4e143f0b58ef18131c13549622eea39cfb +++ tests/merge_into_workspace/expected-log-left ff2b6b8ad837acf826dffa1ad07cd67714279805 @@ -1,30 +1,31 @@ ------------------------------------------------------------------ +---------------------------------------------------------------------- Revision: 21320a960288df775cd62b01f1b40e981f4ecd65 -Ancestor: 87c726bb950efb3d809662b86700a8566346970d -Author: address@hidden -Date: 2007-02-28T02:11:20 -Branch: testbranch +Parent: 87c726bb950efb3d809662b86700a8566346970d +Author: address@hidden +Date: 2007-02-28T02:11:20 +Branch: testbranch -Modified files: - testfile +Changelog: -ChangeLog: +blah-blah -blah-blah +Changes against parent 87c726bb950efb3d809662b86700a8566346970d ------------------------------------------------------------------ + patched testfile + +---------------------------------------------------------------------- Revision: 87c726bb950efb3d809662b86700a8566346970d -Ancestor: -Author: address@hidden -Date: 2007-02-28T02:11:19 -Branch: testbranch +Author: address@hidden +Date: 2007-02-28T02:11:19 +Branch: testbranch -Added files: - otherfile testfile -Added directories: - . +Changelog: -ChangeLog: +blah-blah -blah-blah +Changes + added + added otherfile + added testfile + ============================================================ --- tests/tags_and_tagging_of_revisions/__driver__.lua 417a74a8f678051c254c7fc3a041d9f703987239 +++ tests/tags_and_tagging_of_revisions/__driver__.lua 81e6f1af6cbe3b957c7a8680a5539a2d14da858d @@ -13,30 +13,30 @@ check(mtn("add", "file1"), 0, false, fal -- make and tag revision 1 check(mtn("add", "file1"), 0, false, false) -commit() +commit("branch1") revs[1] = base_revision() check(mtn("tag", revs[1], "tag1"), 0, false, false) -- make and tag revision 2 check(mtn("add", "file2"), 0, false, false) -commit() +commit("branch2") revs[2] = base_revision() check(mtn("tag", revs[2], "tag2"), 0, false, false) -- make and tag revision 3 check(mtn("add", "file3"), 0, false, false) -commit() +commit("branch3") revs[3] = base_revision() check(mtn("tag", revs[3], "tag3"), 0, true, true) -- check tags created above check(mtn("ls", "tags"), 0, true, false) -check(qgrep("tag1", "stdout")) -check(qgrep("tag2", "stdout")) -check(qgrep("tag3", "stdout")) +check(qgrep("tag1 .* branch1 ", "stdout")) +check(qgrep("tag2 .* branch2 ", "stdout")) +check(qgrep("tag3 .* branch3 ", "stdout")) -- make sure 'ls tags' output is sorted if existsonpath("sort") then ============================================================ --- unit-tests/dates.cc af8a777e15c6cccc98f7c006dfe4d84ffd5636ba +++ unit-tests/dates.cc fc614466a2130277bb98e89db1398d6b85cde1a1 @@ -188,6 +188,128 @@ UNIT_TEST(from_string) #undef NO } +UNIT_TEST(roundtrip_localtimes) +{ +#define OK(x) do { \ + string iso8601 = x.as_iso_8601_extended(); \ + string formatted = x.as_formatted_localtime("%c"); \ + L(FL("iso 8601 date '%s' local date '%s'\n") % iso8601 % formatted); \ + date_t parsed = date_t::from_formatted_localtime(formatted, "%c"); \ + UNIT_TEST_CHECK(parsed == x); \ + } while (0) + + // 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"); + + OK(start); + OK(end); + + // stagger the millisecond values to hit different times of day + for (date_t date = start; date <= end; date += MILLISEC(DAY+HOUR+MIN+SEC)) + OK(date); + + start -= 1000; + end += 1000; + + // these tests run with LANG=C and TZ=UTC so the %c format seems to work + // however strptime does not like the timezone name when %c is used in + // other locales. with LANG=en_CA.UTF-8 this test fails. + + if (sizeof(time_t) <= 4) + { + UNIT_TEST_CHECK_THROW(start.as_formatted_localtime("%c"), + recoverable_failure); + UNIT_TEST_CHECK_THROW(date_t::from_formatted_localtime("Fri Dec 13 20:45:51 1901", "%c"), + recoverable_failure); + + UNIT_TEST_CHECK_THROW(end.as_formatted_localtime("%c"), + recoverable_failure); + UNIT_TEST_CHECK_THROW(date_t::from_formatted_localtime("Tue Jan 19 03:14:08 2038", "%c"), + recoverable_failure); + } + else + { + OK(start); + OK(end); + } + + // this date represents 1 second before the unix epoch which has a time_t + // value of -1. conveniently, mktime returns -1 to indicate that it was + // unable to convert a struct tm into a valid time_t value even though + // dates before/after this are valid. our date parsing code ignores this + // "error" from mktime, does a conversion back to localtime and compares + // this with the localtime value it called mktime with in the first place. + // allowing conversion of this date to succeed while still detecting dates + // that are out of range. + + OK(date_t("1969-12-31T23:59:59")); + +#undef OK +} + +UNIT_TEST(localtime_formats) +{ +#define OK(d, f) do { \ + string formatted = d.as_formatted_localtime(f); \ + date_t parsed = date_t::from_formatted_localtime(formatted, f); \ + UNIT_TEST_CHECK(parsed == d); \ + L(FL("date %s formatted %s parsed %s\n") % d % formatted % parsed); \ + } 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"); + + // The 'y' (year in century) specification is taken to specify a year in + // the 20th century by libc4 and libc5. It is taken to be a year in the + // range 1950-2049 by glibc 2.0. It is taken to be a year in 1969-2068 + // since glibc 2.1. + + // When a century is not otherwise specified, values in the range 69-99 + // refer to years in the twentieth century (1969-1999); values in the + // range 00-68 refer to years in the twenty-first century (2000-2068). + + // With glibc 2.1 or newer the 2 digit years in the %x format will fail + // for years before 1969 because strptime will assume they are in the 21st + // century. + date_t yy_ok("1969-01-01T00:00:00"); + + // 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("iso 8601 date '%s' end '%s'\n") % date % end); + + // these all seem to work with the test setup of LANG=C and TZ=UTC + + OK(date, "%F %X"); // YYYY-MM-DD hh:mm:ss + OK(date, "%X %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"); + + if (date >= yy_ok) + { + OK(date, "%x %X"); // YY-MM-DD hh:mm:ss + OK(date, "%X %x"); // hh:mm:ss YY-MM-DD + } + + // possibly 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: ============================================================ --- win32/main.cc 283e7fe96c5964a4408e17344a94b5a87b0ce6af +++ win32/main.cc 8b14cc20945d524354751fd745044d96cfe69f96 @@ -180,7 +180,7 @@ BOOL WINAPI ConsoleCtrlHandler(DWORD wha //ExitProcess(0); // this exits without calling cleanup routines (atexit() and the like) - TerminateProcess(GetCurrentProcess(), 0); + TerminateProcess(GetCurrentProcess(), 1); // returning FALSE crashes (calls ExitProcess()); TRUE exits or does nothing return FALSE; ============================================================ --- work.cc 28266a7fbb698d94c998047e52842b220b37756d +++ work.cc 7e3829c75634ee5bbf555209dfa152d0bd88138c @@ -53,6 +53,7 @@ static char const user_log_file_name[] = static char const local_dump_file_name[] = "debug"; static char const options_file_name[] = "options"; static char const user_log_file_name[] = "log"; +static char const commit_file_name[] = "commit"; static char const revision_file_name[] = "revision"; static char const update_file_name[] = "update"; static char const bisect_file_name[] = "bisect"; @@ -93,6 +94,13 @@ static void } static void +get_commit_path(bookkeeping_path & commit_path) +{ + commit_path = bookkeeping_root / commit_file_name; + L(FL("commit path is %s") % commit_path); +} + +static void get_update_path(bookkeeping_path & update_path) { update_path = bookkeeping_root / update_file_name; @@ -414,6 +422,41 @@ workspace::has_contents_user_log() return user_log_message().length() > 0; } +// commit buffer backup file + +void +workspace::load_commit_text(utf8 & dat) +{ + bookkeeping_path commit_path; + get_commit_path(commit_path); + + if (file_exists(commit_path)) + { + data tmp; + read_data(commit_path, tmp); + system_to_utf8(typecast_vocab(tmp), dat); + } +} + +void +workspace::save_commit_text(utf8 const & dat) +{ + bookkeeping_path commit_path; + get_commit_path(commit_path); + + external tmp; + utf8_to_system_best_effort(dat, tmp); + write_data(commit_path, typecast_vocab(tmp)); +} + +void +workspace::clear_commit_text() +{ + bookkeeping_path commit_path; + get_commit_path(commit_path); + delete_file(commit_path); +} + // _MTN/options handling. static void ============================================================ --- work.hh 021031510460f3cdc3762460937d94b575f04154 +++ work.hh 3f94623eb296347caca9ed341f0bee2ef63481be @@ -226,6 +226,16 @@ public: void blank_user_log(); bool has_contents_user_log(); + // The full commit text from the edit_comment lua hook is saved before + // attempting to extract the various Author: Date: Branch: and Changelog: + // values from it in case these values don't appear where they are + // expected. Once all the values have been extracted the backup file is + // removed. + + void load_commit_text(utf8 & dat); + void save_commit_text(utf8 const & dat); + void clear_commit_text(); + // 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 workspace and