# # # patch "NEWS" # from [09f664e0c61cd32fb378957aba4b6c23e48596fc] # to [0be91dde3db297ab74d81378f7eb1f61fee066c8] # # patch "cmd_diff_log.cc" # from [ddcd0dfc3c690b04ce83cb0fb68b8cce2795137e] # to [b9ffb18ae4714e4c979c0c52a46ebc5ba3e31348] # # patch "dates.cc" # from [1e7df6dc769636e26962563569dac8d1d4b1d39f] # to [1223edca99a21b63136c5ec576a2b61c5bf686e2] # # patch "dates.hh" # from [69bde209cb015527e9716f9b52a3b56f25b6bd95] # to [e25c6a24f11ef3e948b44d7adb694661183b1d1a] # # patch "lua_hooks.cc" # from [14c47b49b38e31bc04dd47c1ea5d53231c772f8d] # to [a3f97d745061aebfee49041d273bff165030e35c] # # patch "lua_hooks.hh" # from [13b02250e084d7e1d04d9df095f4409d3b6189bb] # to [626f87628c6b603749a5b7f44505a69dd34682d2] # # patch "monotone.texi" # from [b788c4a30206f2eacd2011dac65f79584488bd1a] # to [e791cde3620d4ed4489952d28398a28970bc18df] # # patch "options_list.hh" # from [fe3b7714d2c58e05d75fefc8d28bbe5935d22051] # to [105ee0f7162f5ea8a0c91e982c8190d72a649740] # # patch "std_hooks.lua" # from [eff8bcbd98c0643e183c5a1ee043a2af914129ac] # to [52aa565e0f7dad93d2e7fd0ffef4ac9d9ee7a9a6] # # patch "tests/_--author,_--date/__driver__.lua" # from [32996584305ed55d69e88dd14f0570e6da40b827] # to [7fd3ac3cb95874381f43dd1182a23bc22dd02b5a] # # patch "tests/log_--brief/__driver__.lua" # from [5fe08b4182a0dd9349743363da14eb0c4582fcea] # to [e693530a91048029954b4dc66a6caca1973ca08c] # ============================================================ --- NEWS 09f664e0c61cd32fb378957aba4b6c23e48596fc +++ NEWS 0be91dde3db297ab74d81378f7eb1f61fee066c8 @@ -1,3 +1,27 @@ +??? ?? ?? ??:??:?? UTC ???? + + 0.45 release. + + Changes + + New features + + - The 'log' command now, by default, converts all dates it + prints to your timezone instead of leaving them in UTC, and + uses a somewhat more friendly format for the dates. + + You can customize the date format with the new + "get_date_format_spec" Lua hook, which returns a strftime(3) + format string. You can also override the format for one + command with the new --date-format option, disable date + conversion for one command with --no-format-dates, or + disable it by default by having the above Lua hook return an + empty string. + + Bugs fixed + + Internal + Tue May 12 20:44:00 UTC 2009 0.44 release. ============================================================ --- cmd_diff_log.cc ddcd0dfc3c690b04ce83cb0fb68b8cce2795137e +++ cmd_diff_log.cc b9ffb18ae4714e4c979c0c52a46ebc5ba3e31348 @@ -585,8 +585,9 @@ static void static void -log_certs(vector const & certs, ostream & os, revision_id id, cert_name name, - string label, string separator, bool multiline, bool newline) +log_certs(vector const & certs, ostream & os, cert_name const & name, + char const * label, char const * separator, + bool multiline, bool newline) { bool first = true; @@ -615,18 +616,66 @@ static void } static void -log_certs(vector const & certs, ostream & os, revision_id id, cert_name name, - string label, bool multiline) +log_certs(vector const & certs, ostream & os, cert_name const & name, + char const * label, bool multiline) { - log_certs(certs, os, id, name, label, label, multiline, true); + log_certs(certs, os, name, label, label, multiline, true); } static void -log_certs(vector const & certs, ostream & os, revision_id id, cert_name name) +log_certs(vector const & certs, ostream & os, cert_name const & name) { - log_certs(certs, os, id, name, " ", ",", false, false); + 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()) + os << i->value; + else + os << date_t(i->value()).as_formatted_localtime(fmt); + if (newline) + os << '\n'; + + 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 @@ -710,6 +759,7 @@ CMD(log, "log", "", CMD_REF(informative) options::opts::last | options::opts::next | options::opts::from | options::opts::to | options::opts::revision | options::opts::brief | options::opts::diffs | + options::opts::format_dates | options::opts::date_fmt | options::opts::depth | options::opts::exclude | options::opts::no_merges | options::opts::no_files | options::opts::no_graph) @@ -717,6 +767,15 @@ CMD(log, "log", "", CMD_REF(informative) database db(app); project_t project(db); + 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_fmt); + } + long last = app.opts.last; long next = app.opts.next; @@ -874,12 +933,11 @@ CMD(log, "log", "", CMD_REF(informative) L(FL("log %d starting revisions") % starting_revs.size()); } - cert_name author_name(author_cert_name); - cert_name date_name(date_cert_name); - cert_name branch_name(branch_cert_name); - cert_name tag_name(tag_cert_name); - cert_name changelog_name(changelog_cert_name); - cert_name comment_name(comment_cert_name); + cert_name const author_name(author_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); + cert_name const comment_name(comment_cert_name); // we can use the markings if we walk backwards for a restricted log bool use_markings(direction == log_reverse && !mask.empty()); @@ -986,16 +1044,15 @@ CMD(log, "log", "", CMD_REF(informative) if (app.opts.brief) { out << rid; - log_certs(certs, out, rid, author_name); + log_certs(certs, out, author_name); if (app.opts.no_graph) - log_certs(certs, out, rid, date_name); + log_date_certs(certs, out, date_fmt); else { out << '\n'; - log_certs(certs, out, rid, date_name, - string(), string(), false, false); + log_date_certs(certs, out, date_fmt, "", "", false, false); } - log_certs(certs, out, rid, branch_name); + log_certs(certs, out, branch_name); out << '\n'; } else @@ -1018,10 +1075,10 @@ CMD(log, "log", "", CMD_REF(informative) anc != ancestors.end(); ++anc) out << "Ancestor: " << *anc << '\n'; - log_certs(certs, out, rid, author_name, "Author: ", false); - log_certs(certs, out, rid, date_name, "Date: ", false); - log_certs(certs, out, rid, branch_name, "Branch: ", false); - log_certs(certs, out, rid, tag_name, "Tag: ", false); + 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()) { @@ -1030,8 +1087,8 @@ CMD(log, "log", "", CMD_REF(informative) out << '\n'; } - log_certs(certs, out, rid, changelog_name, "ChangeLog: ", true); - log_certs(certs, out, rid, comment_name, "Comments: ", true); + log_certs(certs, out, changelog_name, "ChangeLog: ", true); + log_certs(certs, out, comment_name, "Comments: ", true); } if (app.opts.diffs) ============================================================ --- dates.cc 1e7df6dc769636e26962563569dac8d1d4b1d39f +++ dates.cc 1223edca99a21b63136c5ec576a2b61c5bf686e2 @@ -1,5 +1,5 @@ -// Copyright (C) 2007, 2008 Zack Weinberg -// Markus Wanner +// Copyright (C) 2007-2009 Zack Weinberg +// Markus Wanner // // This program is made available under the GNU GPL version 2.0 or // greater. See the accompanying file COPYING for details. @@ -36,14 +36,24 @@ // for 'time_t'. This is only a problem because we support reading // CVS/RCS ,v files, which encode times as decimal seconds since the Unix // epoch; so we must support that epoch regardless of what the system does. +// +// Note that while we track dates to the millisecond in memory, we do not +// record milliseconds in the database, nor do we ask the system for +// sub-second resolution when retrieving the current time, nor do we display +// milliseconds to the user. There isn't much point in fixing one of these +// problems if we don't fix all of them, and while the first two would be +// straightforward, the third is very hard -- it would require us to +// reimplement strftime() with our own extension for the purpose. - // On Solaris, these macros are already defined by system includes. We want // to use our own, so we undef them here. #undef SEC #undef MILLISEC +using std::ostream; using std::string; +using std::time_t; +using std::tm; // Our own "struct tm"-like struct to represent broken-down times struct broken_down_time { @@ -121,7 +131,7 @@ days_in_year(s32 year) days_in_year(s32 year) { return is_leap_year(year) ? 366 : 365; - } +} inline bool valid_ms_count(s64 d) @@ -130,7 +140,7 @@ static void } static void -our_gmtime(s64 ts, broken_down_time & tm) +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()); @@ -149,14 +159,14 @@ our_gmtime(s64 ts, broken_down_time & tm u64 days = t / MILLISEC(DAY); u32 ms_in_day = t % MILLISEC(DAY); - tm.millisec = ms_in_day % 1000; + tb.millisec = ms_in_day % 1000; ms_in_day /= 1000; - tm.sec = ms_in_day % 60; + tb.sec = ms_in_day % 60; ms_in_day /= 60; - tm.min = ms_in_day % 60; - tm.hour = ms_in_day / 60; + tb.min = ms_in_day % 60; + tb.hour = ms_in_day / 60; // This is the result of inverting the equation // yb = y*365 + y/4 - y/100 + y/400 @@ -181,7 +191,7 @@ our_gmtime(s64 ts, broken_down_time & tm } I(0 <= delta && delta < days_in_year(year)); - tm.year = year; + tb.year = year; days = delta; // Now, the months digit! @@ -198,47 +208,47 @@ our_gmtime(s64 ts, broken_down_time & tm month++; I(month <= 12); } - tm.month = month; - tm.day = days + 1; + tb.month = month; + tb.day = days + 1; } static s64 -our_timegm(broken_down_time const & tm) +our_timegm(broken_down_time const & tb) { s64 d; // range checks - I(tm.year > 0 && tm.year <= 292278994); - I(tm.month >= 1 && tm.month <= 12); - I(tm.day >= 1 && tm.day <= 31); - I(tm.hour >= 0 && tm.hour <= 23); - I(tm.min >= 0 && tm.min <= 59); - I(tm.sec >= 0 && tm.sec <= 60); - I(tm.millisec >= 0 && tm.millisec <= 999); + I(tb.year > 0 && tb.year <= 292278994); + I(tb.month >= 1 && tb.month <= 12); + I(tb.day >= 1 && tb.day <= 31); + I(tb.hour >= 0 && tb.hour <= 23); + I(tb.min >= 0 && tb.min <= 59); + I(tb.sec >= 0 && tb.sec <= 60); + I(tb.millisec >= 0 && tb.millisec <= 999); // years (since 1970) - d = YEAR * (tm.year - 1970); + d = YEAR * (tb.year - 1970); // leap days to add (or subtract) - int add_leap_days = (tm.year - 1) / 4 - 492; - add_leap_days -= (tm.year - 1) / 100 - 19; - add_leap_days += (tm.year - 1) / 400 - 4; + int add_leap_days = (tb.year - 1) / 4 - 492; + add_leap_days -= (tb.year - 1) / 100 - 19; + add_leap_days += (tb.year - 1) / 400 - 4; d += add_leap_days * DAY; // months - for (int m = 1; m < tm.month; ++m) + for (int m = 1; m < tb.month; ++m) { d += DAYS_PER_MONTH[m-1] * DAY; - if (m == 2 && is_leap_year(tm.year)) + if (m == 2 && is_leap_year(tb.year)) d += DAY; } // days within month, and so on - d += (tm.day - 1) * DAY; - d += tm.hour * HOUR; - d += tm.min * MIN; - d += tm.sec * SEC; + d += (tb.day - 1) * DAY; + d += tb.hour * HOUR; + d += tb.min * MIN; + d += tb.sec * SEC; - return MILLISEC(d) + tm.millisec; + return MILLISEC(d) + tb.millisec; } // In a few places we need to know the offset between the Unix epoch and the @@ -253,8 +263,8 @@ get_epoch_offset() if (know_epoch_offset) return epoch_offset; - std::time_t epoch = 0; - std::tm t = *std::gmtime(&epoch); + time_t epoch = 0; + tm t = *std::gmtime(&epoch); our_t.millisec = 0; our_t.sec = t.tm_sec; @@ -304,8 +314,7 @@ 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; @@ -317,7 +326,7 @@ date_t::now() date_t date_t::now() { - std::time_t t = std::time(0); + time_t t = std::time(0); s64 tu = s64(t) * 1000 + get_epoch_offset(); E(valid_ms_count(tu), origin::system, F("current date '%s' is outside usable range\n" @@ -329,26 +338,59 @@ date_t::as_iso_8601_extended() const string date_t::as_iso_8601_extended() const { - broken_down_time tm; + broken_down_time tb; I(valid()); - our_gmtime(d, tm); + our_gmtime(d, tb); return (FL("%04u-%02u-%02uT%02u:%02u:%02u") - % tm.year % tm.month % tm.day - % tm.hour % tm.min % tm.sec).str(); + % tb.year % tb.month % tb.day + % tb.hour % tb.min % tb.sec).str(); } -std::ostream & -operator<< (std::ostream & o, date_t const & d) +ostream & +operator<< (ostream & o, date_t const & d) { return o << d.as_iso_8601_extended(); } template <> void -dump(date_t const & d, std::string & s) +dump(date_t const & d, string & s) { s = d.as_iso_8601_extended(); } +string +date_t::as_formatted_localtime(string const & fmt) const +{ + time_t t(d/1000 - get_epoch_offset()); + tm tb(*std::localtime(&t)); + char buf[128]; + + // Poison the buffer so we can tell whether strftime() produced + // no output at all. + buf[0] = '#'; + + size_t wrote = strftime(buf, sizeof buf, fmt.c_str(), &tb); + + if (wrote > 0) + return string(buf); // yay, it worked + + if (wrote == 0 && buf[0] == '\0') // no output + { + static bool warned = false; + if (!warned) + { + warned = true; + W(F("time format specification '%s' produces no output") % fmt); + } + return string(); + } + + E(false, origin::user, + F("date '%s' is too long when formatted using '%s'" + " (the result must fit in %d characters)") + % (sizeof buf - 1)); +} + s64 date_t::as_millisecs_since_unix_epoch() const { ============================================================ --- dates.hh 69bde209cb015527e9716f9b52a3b56f25b6bd95 +++ dates.hh e25c6a24f11ef3e948b44d7adb694661183b1d1a @@ -40,6 +40,11 @@ struct date_t // Retrieve the date as a string. std::string as_iso_8601_extended() const; + // Retrieve the date as a string, formatted using the strftime(3) + // specification in 'fmt', and converted to local time. For user + // display only. + std::string as_formatted_localtime(std::string const & fmt) const; + // Retrieve the internal milliseconds count since the Unix epoch. s64 as_millisecs_since_unix_epoch() const; ============================================================ --- lua_hooks.cc 14c47b49b38e31bc04dd47c1ea5d53231c772f8d +++ lua_hooks.cc a3f97d745061aebfee49041d273bff165030e35c @@ -559,6 +559,23 @@ lua_hooks::hook_get_default_command_opti return ll.ok() && !args.empty(); } +bool +lua_hooks::hook_get_date_format_spec(std::string & spec) +{ + bool exec_ok + = Lua(st) + .func("get_date_format_spec") + .call(0, 1) + .extract_str(spec) + .ok(); + + // If the hook fails, disable date formatting. + if (!exec_ok) + spec = ""; + return exec_ok; +} + + bool lua_hooks::hook_hook_wrapper(std::string const & func_name, std::vector const & args, std::string & out) ============================================================ --- lua_hooks.hh 13b02250e084d7e1d04d9df095f4409d3b6189bb +++ lua_hooks.hh 626f87628c6b603749a5b7f44505a69dd34682d2 @@ -113,6 +113,8 @@ public: bool hook_get_default_command_options(commands::command_id const & cmd, args_vector & args); + bool hook_get_date_format_spec(std::string & spec); + // workspace hooks bool hook_use_inodeprints(); ============================================================ --- monotone.texi b788c4a30206f2eacd2011dac65f79584488bd1a +++ monotone.texi e791cde3620d4ed4489952d28398a28970bc18df @@ -10024,6 +10024,14 @@ @subsection User Defaults branches. Otherwise returns @code{false}. This hook has no default definition, therefore the default behavior is to list all branches. address@hidden get_date_format_spec () + +Returns a @code{strftime} format specification, which @command{mtn +log} and similar commands will use to format dates, unless instructed +otherwise. The default definition returns @address@hidden"%d %b %Y, +%I:%M:%S %p"}}, which produces output like this: @address@hidden May 2009, +09:06:14 AM}}. + @end ftable @subsection Netsync Permission Hooks ============================================================ --- options_list.hh fe3b7714d2c58e05d75fefc8d28bbe5935d22051 +++ options_list.hh 105ee0f7162f5ea8a0c91e982c8190d72a649740 @@ -180,6 +180,22 @@ OPT(date, "date", date_t, , } #endif +OPT(date_fmt, "date-format", std::string, , + gettext_noop("strftime(3) format specification for printing dates")) +#ifdef option_bodies +{ + date_fmt = arg; +} +#endif + +OPT(format_dates, "no-format-dates", bool, true, + gettext_noop("print date certs exactly as stored in the database")) +#ifdef option_bodies +{ + format_dates = false; +} +#endif + GOPT(dbname, "db,d", system_path, , gettext_noop("set name of database")) #ifdef option_bodies { ============================================================ --- std_hooks.lua eff8bcbd98c0643e183c5a1ee043a2af914129ac +++ std_hooks.lua 52aa565e0f7dad93d2e7fd0ffef4ac9d9ee7a9a6 @@ -372,7 +372,20 @@ end return false end +function get_date_format_spec() + -- Return the strftime(3) specification to be used to print dates + -- in human-readable format after conversion to the local timezone. + -- The default produces output like this: 22 May 2009, 09:06:14 AM + -- (the month names are localized). + return "%d %b %Y, %I:%M:%S %p" + -- A sampling of other possible formats you might want: + -- default for your locale: "%c" (may include a confusing timezone label) + -- like ctime(3): "%a %b %d %H:%M:%S %Y" + -- email style: "%a, %d %b %Y %H:%M:%S" + -- ISO 8601: "%Y-%m-%d %H:%M:%S" or "%Y-%m-%dT%H:%M:%S" +end + -- trust evaluation hooks function intersection(a,b) @@ -1199,6 +1212,10 @@ end return "mtn" end +function get_remote_unix_socket_command(host) + return "socat" +end + function get_default_command_options(command) local default_args = {} return default_args @@ -1251,11 +1268,6 @@ end return hook_wrapper_dump._table(res) end - -function get_remote_unix_socket_command(host) - return "socat" -end - do -- Hook functions are tables containing any of the following 6 items -- with associated functions: ============================================================ --- tests/_--author,_--date/__driver__.lua 32996584305ed55d69e88dd14f0570e6da40b827 +++ tests/_--author,_--date/__driver__.lua 7fd3ac3cb95874381f43dd1182a23bc22dd02b5a @@ -4,7 +4,7 @@ rev = base_revision() addfile("testfile", "floooooo") check(mtn("commit", "--author=the_author", "--date=1999-12-31T12:00:00", "--branch=foo", "--message=foo"), 0, false, false) rev = base_revision() -check(mtn("log", "--from", rev), 0, true, false) +check(mtn("log", "--from", rev, "--no-format-dates"), 0, true, false) check(qgrep('^[\\| ]+Author: the_author', "stdout")) check(qgrep('^[\\| ]+Date: 1999-12-31T12:00:00', "stdout")) ============================================================ --- tests/log_--brief/__driver__.lua 5fe08b4182a0dd9349743363da14eb0c4582fcea +++ tests/log_--brief/__driver__.lua e693530a91048029954b4dc66a6caca1973ca08c @@ -13,7 +13,7 @@ R2=base_revision() check(mtn("commit", "-b", "otherbranch", "--date", "2005-08-16T03:16:05", "-m", "foo"), 0, false, false) R2=base_revision() -check(mtn("log", "--brief", "--no-graph"), 0, true, false) +check(mtn("log", "--brief", "--no-graph", "--no-format-dates"), 0, true, false) check(samelines("stdout", {R2.." address@hidden 2005-08-16T03:16:05 otherbranch", R1.." address@hidden 2005-08-16T03:16:00 testbranch", R0.." address@hidden 2005-08-16T03:16:00 testbranch"}))