# # # patch "cmd_key_cert.cc" # from [bf2253ced263b560310d15589503376b57cf9625] # to [4141cfef09ab7a8136a42e1c2e33369ccae105d4] # # patch "cmd_ws_commit.cc" # from [f19eb71e76506b5ac8a706f610e6a34b595266a1] # to [16c7d171d276058868ae4d5d94bdecfee634e8f9] # # patch "lua_hooks.cc" # from [2cea55853bda5f175dc82c227ee21947236327e8] # to [d3debb5b5c55f6a3adb023641774d19ac66fd261] # # patch "lua_hooks.hh" # from [a5ac6a6139a2672c9e8d16bb97162ac849179dda] # to [ba234fdeb304a67e521d7fb71e366b4972538488] # # patch "simplestring_xform.cc" # from [0ce9a86370da9e0043729c1bae322655035694be] # to [72af38549cca3fbba8df9214681a61740f0f785e] # # patch "simplestring_xform.hh" # from [564a305c99ca931c93956ac3ef903683dcb15db7] # to [0ecfcafa7986955b3d26f104219be5808ae48d28] # # patch "std_hooks.lua" # from [6fa95c0269b7e1b615bf5d2dad24e32948e48350] # to [a7b61ab7990a85cf0093a560e877278125a3bb6b] # ============================================================ --- cmd_key_cert.cc bf2253ced263b560310d15589503376b57cf9625 +++ cmd_key_cert.cc 4141cfef09ab7a8136a42e1c2e33369ccae105d4 @@ -341,7 +341,7 @@ CMD(comment, "comment", "", CMD_REF(revi else { external comment_external; - N(app.lua.hook_edit_comment(external(""), external(""), comment_external), + N(app.lua.hook_edit_comment(external(""), comment_external), F("edit comment failed")); system_to_utf8(comment_external, comment); } ============================================================ --- cmd_ws_commit.cc f19eb71e76506b5ac8a706f610e6a34b595266a1 +++ cmd_ws_commit.cc 16c7d171d276058868ae4d5d94bdecfee634e8f9 @@ -10,6 +10,7 @@ #include "base.hh" #include #include +#include #include "cmd.hh" #include "diff_patch.hh" @@ -32,9 +33,10 @@ using std::make_pair; 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; @@ -42,24 +44,48 @@ static void using boost::shared_ptr; static void -revision_summary(revision_t const & rev, branch_name const & branch, utf8 & summary) +revision_header(revision_id rid, revision_t const & rev, string const & author, + date_t const date, branch_name const & branch, utf8 & header) { - string out; + ostringstream out; + out << string(70, '-') << "\n" + << "Revision: " << rid << " (uncommitted)\n"; + + for (edge_map::const_iterator i = rev.edges.begin(); i != rev.edges.end(); ++i) + { + revision_id parent = edge_old_revision(*i); + out << "Parent: " << parent << "\n"; + } + out << "Author: " << author << "\n" + << "Date: " << date << "\n" + << "Branch: " << branch << "\n" + << "Changelog: \n\n"; + + header = utf8(out.str()); +} + +static 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. - out += (F("Current branch: %s") % branch).str() += '\n'; + + string out; + // FIXME: use an ostringstream here too? 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. + // FIXME: this is not great for root commits, with no parent rev id out += (F("Changes against parent %s") % parent).str() += '\n'; - cset const & cs = edge_changes(*i); - + // presumably a merge rev could have an empty edge if one side won if (cs.empty()) - out += F(" no changes").str() += '\n'; + out += F("no changes").str() += '\n'; for (set::const_iterator i = cs.nodes_deleted.begin(); i != cs.nodes_deleted.end(); ++i) @@ -69,7 +95,7 @@ revision_summary(revision_t const & rev, i = cs.nodes_renamed.begin(); i != cs.nodes_renamed.end(); ++i) out += (F(" renamed %s\n" - " to %s") % i->first % i->second).str() += '\n'; + " to %s") % i->first % i->second).str() += '\n'; for (set::const_iterator i = cs.dirs_added.begin(); i != cs.dirs_added.end(); ++i) @@ -86,15 +112,21 @@ revision_summary(revision_t const & rev, 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") + " set %s\n" + " to %s") % (i->first.first) % (i->first.second) % (i->second) ).str() += "\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 += (F(" unset on %s\n" - " attr %s") + out += (F(" attr on %s\n" + " unset %s") % (i->first) % (i->second)).str() += "\n"; } summary = utf8(out); @@ -102,53 +134,119 @@ get_log_message_interactively(lua_hooks static void get_log_message_interactively(lua_hooks & lua, workspace & work, - revision_t const & cs, - branch_name const & branchname, + revision_id const rid, revision_t const & rev, + string & author, date_t & date, branch_name & branch, utf8 & log_message) { + external instructions( + _("Ensure the values for Author, Date and Branch are correct, then enter\n" + "a description of this change following the Changelog line. Any other\n" + "modifications to the lines below or to the summary of changes will\n" + "cause the commit to fail.\n")); + + utf8 header; + utf8 message; utf8 summary; - revision_summary(cs, branchname, summary); - external summary_external; - utf8_to_system_best_effort(summary, summary_external); - utf8 branch_comment = utf8((F("branch \"%s\"\n\n") % branchname).str()); - external branch_external; - utf8_to_system_best_effort(branch_comment, branch_external); + revision_header(rid, rev, author, date, branch, header); + work.read_user_log(message); + 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"; + string text = message(); + if (text.empty() || text.substr(text.length()-1) != "\n") + { + text += "\n"; + message = utf8(text); + } - external commentary(commentary_str); + utf8 full_message(instructions() + header() + message() + summary()); + + external input_message; + external output_message; + utf8_to_system_best_effort(full_message, input_message); + + N(lua.hook_edit_comment(input_message, output_message), + F("edit of log message failed")); - utf8 user_log_message; - work.read_user_log(user_log_message); + system_to_utf8(output_message, full_message); - //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()); - else - user_log = user_log_message; + string raw(full_message()); - external user_log_message_external; - utf8_to_system_best_effort(user_log, user_log_message_external); + // 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 "Changes against + // parent ..." line from 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 + // "Changes against parent ..." (following the changelog message) but both + // of these are optional. - external log_message_external; - N(lua.hook_edit_comment(commentary, user_log_message_external, - log_message_external), - F("edit of log message failed")); + N(raw.find(instructions()) == 0, + F("Modifications outside of Author, Date, Branch or Changelog.\n" + "Commit failed (missing instructions).")); - N(log_message_external().find(magic_line) == string::npos, - F("failed to remove magic line; commit cancelled")); + if (!summary().empty()) + { + // ignore the initial blank line when looking for the summary + size_t pos = raw.find(summary().substr(1)); - system_to_utf8(log_message_external, log_message); + // ignore the trailing blank line from the header as well + N(pos >= instructions().length() + header().length() - 1, + F("Modifications outside of Author, Date, Branch or Changelog.\n" + "Commit failed (missing summary).")); + raw.resize(pos); // remove the change summary + } + + raw = raw.substr(instructions().length()); // remove the instructions + + // ensure the first 3 or 4 lines from the header still match + size_t pos = header().find("Author: "); + N(header().substr(0, pos) == raw.substr(0, pos), + F("Modifications outside of Author, Date, Branch or Changelog.\n" + "Commit failed (missing revision or parent header).")); + + raw = raw.substr(pos); // remove the leading unchanged header lines + + vector lines; + split_into_lines(raw, lines); + + N(lines.size() >= 4, + F("Modifications outside of Author, Date, Branch or Changelog.\n" + "Commit failed (missing lines).")); + + vector::const_iterator line = lines.begin(); + N(line->find("Author: ") == 0, + F("Modifications outside of Author, Date, Branch or Changelog.\n" + "Commit failed (missing author).")); + + author = trim_ws(line->substr(8)); + + ++line; + N(line->find("Date: ") == 0, + F("Modifications outside of Author, Date, Branch or Changelog.\n" + "Commit failed (missing date).")); + + date = trim_ws(line->substr(6)); + + ++line; + N(line->find("Branch: ") == 0, + F("Modifications outside of Author, Date, Branch or Changelog.\n" + "Commit failed (missing branch).")); + + branch = branch_name(trim_ws(line->substr(8))); + + ++line; + N(line->find("Changelog: ") == 0, + F("Modifications outside of Author, Date, Branch or Changelog.\n" + "Commit failed (missing changelog).")); + + // now pointing at the optional blank line after Changelog + ++line; + join_lines(line, lines.end(), raw); + + raw = trim_ws(raw) + "\n"; + + log_message = utf8(raw); } CMD(revert, "revert", "", CMD_REF(workspace), N_("[PATH]..."), @@ -588,11 +686,43 @@ CMD(status, "status", "", CMD_REF(inform work.update_current_roster_from_filesystem(new_roster, mask); make_restricted_revision(old_rosters, new_roster, mask, rev); + revision_id rid; + string author; + key_store keys(app); + rsa_keypair_id key; + + calculate_ident(rev, rid); + + get_user_key(app.opts, app.lua, db, keys, key); + if (!app.lua.hook_get_author(app.opts.branchname, key, author)) + author = key(); + + utf8 header; + utf8 message; utf8 summary; - revision_summary(rev, app.opts.branchname, summary); + + revision_header(rid, rev, author, date_t::now(), app.opts.branchname, header); + work.read_user_log(message); + revision_summary(rev, summary); + + string text = message(); + if (text.empty() || text.substr(text.length()-1) != "\n") + { + text += "\n"; + message = utf8(text); + } + + external header_external; + external message_external; external summary_external; + + utf8_to_system_best_effort(header, header_external); + utf8_to_system_best_effort(message, message_external); utf8_to_system_best_effort(summary, summary_external); - cout << summary_external; + + cout << header_external + << message_external + << summary_external; } CMD(checkout, "checkout", "co", CMD_REF(tree), N_("[DIRECTORY]"), @@ -1148,11 +1278,30 @@ CMD(commit, "commit", "ci", CMD_REF(work "perhaps move or delete _MTN/log,\n" "or remove --message/--message-file from the command line?")); + date_t date; + date_t now = date_t::now(); + string author = app.opts.author(); + + if (app.opts.date_given) + date = app.opts.date; + else + date = now; + + if (author.empty()) + { + rsa_keypair_id key; + get_user_key(app.opts, app.lua, db, keys, key); + if (!app.lua.hook_get_author(app.opts.branchname, key, author)) + author = key(); + } + if (!log_message_given) { // This call handles _MTN/log. - get_log_message_interactively(app.lua, work, restricted_rev, - app.opts.branchname, log_message); + get_log_message_interactively(app.lua, work, + restricted_rev_id, restricted_rev, + author, date, app.opts.branchname, + log_message); // We only check for empty log messages when the user entered them // interactively. Consensus was that if someone wanted to explicitly @@ -1279,10 +1428,18 @@ 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.branchname, - 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(); + + project.put_standard_certs(keys, + restricted_rev_id, + app.opts.branchname, + log_message, + date, + author); guard.commit(); } ============================================================ --- lua_hooks.cc 2cea55853bda5f175dc82c227ee21947236327e8 +++ lua_hooks.cc d3debb5b5c55f6a3adb023641774d19ac66fd261 @@ -311,16 +311,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); ============================================================ --- lua_hooks.hh a5ac6a6139a2672c9e8d16bb97162ac849179dda +++ lua_hooks.hh ba234fdeb304a67e521d7fb71e366b4972538488 @@ -53,8 +53,7 @@ public: bool hook_get_author(branch_name const & branchname, rsa_keypair_id const & k, 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, ============================================================ --- simplestring_xform.cc 0ce9a86370da9e0043729c1bae322655035694be +++ simplestring_xform.cc 72af38549cca3fbba8df9214681a61740f0f785e @@ -137,16 +137,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 ============================================================ --- simplestring_xform.hh 564a305c99ca931c93956ac3ef903683dcb15db7 +++ simplestring_xform.hh 0ecfcafa7986955b3d26f104219be5808ae48d28 @@ -24,10 +24,12 @@ 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< class T > std::vector< T > split_into_words(T const & in) ============================================================ --- std_hooks.lua 6fa95c0269b7e1b615bf5d2dad24e32948e48350 +++ std_hooks.lua a7b61ab7990a85cf0093a560e877278125a3bb6b @@ -265,7 +265,7 @@ end return "^[[:alnum:]$_]" end -function edit_comment(basetext, user_log_message) +function edit_comment(user_log_message) local exe = nil if (program_exists_in_path("vi")) then exe = "vi" end if (string.sub(get_ostype(), 1, 6) ~= "CYGWIN" and program_exists_in_path("notepad.exe")) then exe = "notepad.exe" end @@ -287,12 +287,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) if (execute(exe, tname) ~= 0) then @@ -304,14 +302,7 @@ function edit_comment(basetext, user_log 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