monotone

monotone Mtn Source Tree

Root/src/cmd_diff_log.cc

1// Copyright (C) 2009, 2010, 2012 Stephen Leake <stephen_leake@stephe-leake.org>
2// Copyright (C) 2002 Graydon Hoare <graydon@pobox.com>
3//
4// This program is made available under the GNU GPL version 2.0 or
5// greater. See the accompanying file COPYING for details.
6//
7// This program is distributed WITHOUT ANY WARRANTY; without even the
8// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
9// PURPOSE.
10
11#include "base.hh"
12#include <map>
13#include <iostream>
14#include <sstream>
15#include <queue>
16
17#include "asciik.hh"
18#include "charset.hh"
19#include "cmd.hh"
20#include "date_format.hh"
21#include "diff_output.hh"
22#include "file_io.hh"
23#include "parallel_iter.hh"
24#include "restrictions.hh"
25#include "revision.hh"
26#include "rev_height.hh"
27#include "rev_output.hh"
28#include "simplestring_xform.hh"
29#include "transforms.hh"
30#include "app_state.hh"
31#include "project.hh"
32#include "database.hh"
33#include "work.hh"
34#include "roster.hh"
35#include "vocab_cast.hh"
36
37using std::cout;
38using std::make_pair;
39using std::map;
40using std::ostream;
41using std::ostringstream;
42using std::pair;
43using std::set;
44using std::string;
45using std::vector;
46using std::priority_queue;
47
48static void
49get_data(database & db,
50 file_path const & path, file_id const & id,
51 bool const from_db, data & unpacked)
52{
53 if (from_db)
54 {
55 file_data dat;
56 db.get_file_version(id, dat);
57 unpacked = dat.inner();
58 }
59 else
60 {
61 read_data(path, unpacked);
62 }
63}
64
65static void
66dump_diff(lua_hooks & lua,
67 file_path const & left_path, file_path const & right_path,
68 file_id const left_id, file_id const right_id,
69 data const & left_data, data const & right_data,
70 bool is_manual_merge,
71 diff_type const diff_format,
72 bool external_diff_args_given,
73 string external_diff_args,
74 string const & encloser,
75 ostream & output)
76{
77 if (diff_format == external_diff)
78 {
79 bool is_binary = is_manual_merge || guess_binary(left_data()) || guess_binary(right_data());
80
81 file_path path = right_path;
82 if (path.empty()) // use the left path for deletes
83 path = left_path;
84
85 lua.hook_external_diff(path,
86 left_data,
87 right_data,
88 is_binary,
89 external_diff_args_given,
90 external_diff_args,
91 encode_hexenc(left_id.inner()(),
92 left_id.inner().made_from),
93 encode_hexenc(right_id.inner()(),
94 right_id.inner().made_from));
95 }
96 else
97 {
98 // 60 is somewhat arbitrary, but less than 80
99 string patch_sep = string(60, '=');
100 output << patch_sep << '\n';
101
102 // see the big comment in diff_output.cc about what paths should be
103 string left = left_path.as_internal();
104 if (left.empty())
105 left = "/dev/null";
106
107 string right = right_path.as_internal();
108 if (right.empty())
109 right = "/dev/null";
110
111 make_diff(left, right,
112 left_id, right_id,
113 left_data, right_data,
114 is_manual_merge,
115 output, diff_format, encloser);
116 }
117
118}
119struct diff_node_data
120{
121 file_path left_path;
122 file_path right_path;
123 file_id left_id;
124 file_id right_id;
125 bool is_manual_merge;
126};
127
128static void
129dump_diffs(lua_hooks & lua,
130 database & db,
131 roster_t const & left_roster,
132 roster_t const & right_roster,
133 std::ostream & output,
134 diff_type diff_format,
135 bool external_diff_args_given,
136 string external_diff_args,
137 bool left_from_db,
138 bool right_from_db,
139 bool show_encloser)
140{
141 // Put all node data in a multimap with the file path of the node as key
142 // which gets automatically sorted. For removed nodes the file path is
143 // the left_path, for added, patched and renamed nodes it is the right_path.
144 attr_key manual_merge_key = typecast_vocab<attr_key>(utf8("mtn:manual_merge"));
145 std::multimap<file_path, diff_node_data> path_node_data;
146 parallel::iter<node_map> i(left_roster.all_nodes(), right_roster.all_nodes());
147 while (i.next())
148 {
149 MM(i);
150 switch (i.state())
151 {
152 case parallel::invalid:
153 I(false);
154
155 case parallel::in_left:
156 // deleted
157 if (is_file_t(i.left_data()))
158 {
159 diff_node_data dat;
160 left_roster.get_name(i.left_key(), dat.left_path);
161 // right_path is null
162
163 dat.left_id = downcast_to_file_t(i.left_data())->content;
164 // right_id is null
165
166 dat.is_manual_merge = (i.left_data()->attrs.find(manual_merge_key) != i.left_data()->attrs.end());
167
168 path_node_data.insert(make_pair(dat.left_path, dat));
169 }
170 break;
171
172 case parallel::in_right:
173 // added
174 if (is_file_t(i.right_data()))
175 {
176 diff_node_data dat;
177 // left_path is null
178 right_roster.get_name(i.right_key(), dat.right_path);
179
180 // left_id is null
181 dat.right_id = downcast_to_file_t(i.right_data())->content;
182
183 dat.is_manual_merge = (i.right_data()->attrs.find(manual_merge_key) != i.right_data()->attrs.end());
184
185 path_node_data.insert(make_pair(dat.right_path, dat));
186 }
187 break;
188
189 case parallel::in_both:
190 // moved/renamed/patched/attribute changes
191 if (is_file_t(i.left_data()))
192 {
193 diff_node_data dat;
194 dat.left_id = downcast_to_file_t(i.left_data())->content;
195 dat.right_id = downcast_to_file_t(i.right_data())->content;
196
197 if (dat.left_id == dat.right_id)
198 continue;
199
200 left_roster.get_name(i.left_key(), dat.left_path);
201 right_roster.get_name(i.right_key(), dat.right_path);
202
203 dat.is_manual_merge =
204 (i.left_data()->attrs.find(manual_merge_key) != i.left_data()->attrs.end()) or
205 (i.right_data()->attrs.find(manual_merge_key) != i.right_data()->attrs.end());
206
207 path_node_data.insert(make_pair(dat.right_path, dat));
208 }
209 break;
210 }
211 }
212
213 for (std::multimap<file_path, diff_node_data>::iterator i = path_node_data.begin();
214 i != path_node_data.end(); ++i)
215 {
216 diff_node_data & dat = (*i).second;
217 data left_data, right_data;
218
219 if (!null_id(dat.left_id))
220 get_data(db, dat.left_path, dat.left_id, left_from_db, left_data);
221
222 if (!null_id(dat.right_id))
223 get_data(db, dat.right_path, dat.right_id, right_from_db, right_data);
224
225 string encloser("");
226 if (show_encloser)
227 lua.hook_get_encloser_pattern((*i).first, encloser);
228
229 dump_diff(lua,
230 dat.left_path, dat.right_path,
231 dat.left_id, dat.right_id,
232 left_data, right_data,
233 dat.is_manual_merge,
234 diff_format, external_diff_args_given, external_diff_args,
235 encloser, output);
236 }
237}
238
239// common functionality for diff and automate content_diff to determine
240// revisions and rosters which should be diffed
241// FIXME needs app_state in order to create workspace objects (sometimes)
242static void
243prepare_diff(app_state & app,
244 database & db,
245 roster_t & old_roster,
246 roster_t & new_roster,
247 args_vector args,
248 bool & old_from_db,
249 bool & new_from_db,
250 std::string & revheader)
251{
252 temp_node_id_source nis;
253 ostringstream header;
254
255 // initialize before transaction so we have a database to work with.
256 project_t project(db);
257
258 E(app.opts.revision.size() <= 2, origin::user,
259 F("more than two revisions given"));
260
261 E(!app.opts.reverse || app.opts.revision.size() == 1, origin::user,
262 F("'--reverse' only allowed with exactly one revision"));
263
264 if (app.opts.revision.empty())
265 {
266 roster_t left_roster, restricted_roster, right_roster;
267 revision_id old_rid;
268 parent_map parents;
269 workspace work(app);
270
271 work.get_parent_rosters(db, parents);
272
273 // With no arguments, which parent should we diff against?
274 E(parents.size() == 1, origin::user,
275 F("this workspace has more than one parent\n"
276 "(specify a revision to diff against with '--revision')"));
277
278 old_rid = parent_id(parents.begin());
279 left_roster = parent_roster(parents.begin());
280 work.get_current_roster_shape(db, nis, right_roster);
281
282 node_restriction mask(args_to_paths(args),
283 args_to_paths(app.opts.exclude),
284 app.opts.depth,
285 left_roster, right_roster, ignored_file(work));
286
287 work.update_current_roster_from_filesystem(right_roster, mask);
288
289 make_restricted_roster(left_roster, right_roster, restricted_roster,
290 mask);
291
292 old_roster = left_roster;
293 new_roster = restricted_roster;
294
295 old_from_db = true;
296 new_from_db = false;
297
298 header << "# old_revision [" << old_rid << "]\n";
299 }
300 else if (app.opts.revision.size() == 1)
301 {
302 roster_t left_roster, restricted_roster, right_roster;
303 revision_id r_old_id;
304 workspace work(app);
305
306 complete(app.opts, app.lua, project, idx(app.opts.revision, 0)(), r_old_id);
307
308 db.get_roster(r_old_id, left_roster);
309 work.get_current_roster_shape(db, nis, right_roster);
310
311 node_restriction mask(args_to_paths(args),
312 args_to_paths(app.opts.exclude),
313 app.opts.depth,
314 left_roster, right_roster, ignored_file(work));
315
316 work.update_current_roster_from_filesystem(right_roster, mask);
317
318 make_restricted_roster(left_roster, right_roster, restricted_roster,
319 mask);
320
321 if (app.opts.reverse)
322 {
323 old_roster = restricted_roster;
324 new_roster = left_roster;
325 old_from_db = false;
326 new_from_db = true;
327 }
328 else
329 {
330 old_roster = left_roster;
331 new_roster = restricted_roster;
332 old_from_db = true;
333 new_from_db = false;
334 }
335
336 header << "# old_revision [" << r_old_id << "]\n";
337 }
338 else if (app.opts.revision.size() == 2)
339 {
340 roster_t left_roster, restricted_roster, right_roster;
341 revision_id r_old_id, r_new_id;
342
343 complete(app.opts, app.lua, project, idx(app.opts.revision, 0)(), r_old_id);
344 complete(app.opts, app.lua, project, idx(app.opts.revision, 1)(), r_new_id);
345
346 db.get_roster(r_old_id, left_roster);
347 db.get_roster(r_new_id, right_roster);
348
349 // FIXME: this is *possibly* a UI bug, insofar as we
350 // look at the restriction name(s) you provided on the command
351 // line in the context of new and old, *not* the working copy.
352 // One way of "fixing" this is to map the filenames on the command
353 // line to node_ids, and then restrict based on those. This
354 // might be more intuitive; on the other hand it would make it
355 // impossible to restrict to paths which are dead in the working
356 // copy but live between old and new. So ... no rush to "fix" it;
357 // discuss implications first.
358 //
359 // Let the discussion begin...
360 //
361 // - "map filenames on the command line to node_ids" needs to be done
362 // in the context of some roster, possibly the working copy base or
363 // the current working copy (or both)
364 // - diff with two --revision's may be done with no working copy
365 // - some form of "peg" revision syntax for paths that would allow
366 // for each path to specify which revision it is relevant to is
367 // probably the "right" way to go eventually. something like file@rev
368 // (which fails for paths with @'s in them) or possibly //rev/file
369 // since versioned paths are required to be relative.
370
371 node_restriction mask(args_to_paths(args),
372 args_to_paths(app.opts.exclude),
373 app.opts.depth,
374 left_roster, right_roster);
375
376 make_restricted_roster(left_roster, right_roster, restricted_roster,
377 mask);
378
379 old_roster = left_roster;
380 new_roster = restricted_roster;
381
382 old_from_db = true;
383 new_from_db = true;
384
385 header << "# old_revision [" << r_old_id << "]\n";
386 header << "# new_revision [" << r_new_id << "]\n";
387 }
388 else
389 {
390 I(false);
391 }
392
393 revheader = header.str();
394}
395
396void dump_header(std::string const & revs,
397 roster_t const & old_roster,
398 roster_t const & new_roster,
399 std::ostream & out,
400 bool show_if_empty)
401{
402 cset changes;
403 make_cset(old_roster, new_roster, changes);
404
405 data summary;
406 write_cset(changes, summary);
407 if (summary().empty() && !show_if_empty)
408 return;
409
410 vector<string> lines;
411 split_into_lines(summary(), lines);
412 out << "#\n";
413 if (!summary().empty())
414 {
415 out << revs << "#\n";
416 for (vector<string>::iterator i = lines.begin();
417 i != lines.end(); ++i)
418 out << "# " << *i << '\n';
419 }
420 else
421 {
422 out << "# " << _("no changes") << '\n';
423 }
424 out << "#\n";
425}
426
427CMD_PRESET_OPTIONS(diff)
428{
429 opts.with_header = true;
430}
431CMD(diff, "diff", "di", CMD_REF(informative), N_("[PATH]..."),
432 N_("Shows current differences"),
433 N_("Compares the current tree with the files in the repository and "
434 "prints the differences on the standard output.\n"
435 "If one revision is given, the diff between the workspace and "
436 "that revision is shown. If two revisions are given, the diff "
437 "between them is given. If no format is specified, unified is "
438 "used by default."),
439 options::opts::revision | options::opts::depth | options::opts::exclude |
440 options::opts::diff_options)
441{
442 if (app.opts.external_diff_args_given)
443 E(app.opts.diff_format == external_diff, origin::user,
444 F("'--diff-args' requires '--external'; try adding '--external' or remove '--diff-args'"));
445
446 roster_t old_roster, new_roster;
447 std::string revs;
448 bool old_from_db;
449 bool new_from_db;
450 database db(app);
451
452 prepare_diff(app, db, old_roster, new_roster, args, old_from_db, new_from_db, revs);
453
454 if (app.opts.with_header)
455 {
456 dump_header(revs, old_roster, new_roster, cout, true);
457 }
458
459 dump_diffs(app.lua, db, old_roster, new_roster, cout,
460 app.opts.diff_format,
461 app.opts.external_diff_args_given,
462 app.opts.external_diff_args,
463 old_from_db, new_from_db,
464 !app.opts.no_show_encloser);
465}
466
467
468// Name: content_diff
469// Arguments:
470// (optional) one or more files to include
471// Added in: 4.0
472// Purpose: Availability of mtn diff as automate command.
473//
474// Output format: Like mtn diff, but with the header part omitted by default.
475// If no content changes happened, the output is empty. All file operations
476// beside mtn add are omitted, as they don't change the content of the file.
477CMD_AUTOMATE(content_diff, N_("[FILE [...]]"),
478 N_("Calculates diffs of files"),
479 "",
480 options::opts::au_diff_options |
481 options::opts::revision | options::opts::depth |
482 options::opts::exclude)
483{
484 roster_t old_roster, new_roster;
485 string dummy_header;
486 bool old_from_db;
487 bool new_from_db;
488 database db(app);
489
490 prepare_diff(app, db, old_roster, new_roster, args, old_from_db, new_from_db,
491 dummy_header);
492
493
494 if (app.opts.with_header)
495 {
496 dump_header(dummy_header, old_roster, new_roster, output, false);
497 }
498
499 dump_diffs(app.lua, db, old_roster, new_roster, output,
500 app.opts.diff_format,
501 app.opts.external_diff_args_given, app.opts.external_diff_args,
502 old_from_db, new_from_db, !app.opts.no_show_encloser);
503}
504
505
506static void
507log_certs(vector<cert> const & certs, ostream & os, cert_name const & name,
508 string const date_fmt = "")
509{
510 bool first = true;
511
512 for (vector<cert>::const_iterator i = certs.begin(); i != certs.end(); ++i)
513 {
514 if (i->name == name)
515 {
516 if (first)
517 os << " ";
518 else
519 os << ",";
520
521 if (date_fmt.empty())
522 os << i->value;
523 else
524 {
525 I(name == date_cert_name);
526 os << date_t(i->value()).as_formatted_localtime(date_fmt);
527 }
528
529 first = false;
530 }
531 }
532}
533
534enum log_direction { log_forward, log_reverse };
535
536struct rev_cmp
537{
538 log_direction direction;
539 rev_cmp(log_direction const & direction) : direction(direction) {}
540 bool operator() (pair<rev_height, revision_id> const & x,
541 pair<rev_height, revision_id> const & y) const
542 {
543 switch (direction)
544 {
545 case log_forward:
546 return x.first > y.first; // optional with --next N
547 case log_reverse:
548 return x.first < y.first; // default and with --last N
549 default:
550 I(false);
551 }
552 }
553};
554
555typedef priority_queue<pair<rev_height, revision_id>,
556 vector<pair<rev_height, revision_id> >,
557 rev_cmp> frontier_t;
558
559void
560log_print_rev (app_state & app,
561 database & db,
562 project_t & project,
563 revision_id rid,
564 revision_t & rev,
565 string date_fmt,
566 node_restriction mask,
567 ostream & out)
568{
569 cert_name const author_name(author_cert_name);
570 cert_name const date_name(date_cert_name);
571 cert_name const branch_name(branch_cert_name);
572 cert_name const tag_name(tag_cert_name);
573 cert_name const changelog_name(changelog_cert_name);
574 cert_name const comment_name(comment_cert_name);
575 vector<cert> certs;
576 project.get_revision_certs(rid, certs);
577
578 if (app.opts.brief)
579 {
580 out << rid;
581 log_certs(certs, out, author_name);
582 if (app.opts.no_graph)
583 log_certs(certs, out, date_name, date_fmt);
584 else
585 {
586 out << '\n';
587 log_certs(certs, out, date_name, date_fmt);
588 }
589 log_certs(certs, out, branch_name);
590 out << '\n';
591 }
592 else
593 {
594 utf8 header;
595 revision_header(rid, rev, certs, date_fmt, header);
596
597 external header_external;
598 utf8_to_system_best_effort(header, header_external);
599 out << header_external;
600
601 if (!app.opts.no_files)
602 {
603 utf8 summary;
604 revision_summary(rev, summary);
605 external summary_external;
606 utf8_to_system_best_effort(summary, summary_external);
607 out << summary_external;
608 }
609 }
610
611 if (app.opts.diffs)
612 {
613 // if the current roster was loaded above this should hit the
614 // cache and not cost much... logging diffs isn't superfast
615 // regardless.
616 roster_t current_roster;
617 db.get_roster(rid, current_roster);
618
619 for (edge_map::const_iterator e = rev.edges.begin();
620 e != rev.edges.end(); ++e)
621 {
622 roster_t parent_roster, restricted_roster;
623
624 db.get_roster(edge_old_revision(e), parent_roster);
625
626 // always show forward diffs from the parent roster to
627 // the current roster regardless of the log direction
628 make_restricted_roster(parent_roster, current_roster,
629 restricted_roster, mask);
630
631 dump_diffs(app.lua, db, parent_roster, restricted_roster,
632 out, app.opts.diff_format,
633 app.opts.external_diff_args_given,
634 app.opts.external_diff_args,
635 true, true,
636 !app.opts.no_show_encloser);
637 }
638 }
639}
640
641void
642log_common (app_state & app,
643 args_vector args,
644 bool automate,
645 std::ostream & output)
646{
647 database db(app);
648 project_t project(db);
649
650 string date_fmt = get_date_format(app.opts, app.lua, date_time_long);
651
652 long last = app.opts.last;
653 long next = app.opts.next;
654
655 log_direction direction = log_reverse;
656
657 E(last == -1 || next == -1, origin::user,
658 F("only one of '--last'/'--next' allowed"));
659
660 if (next >= 0)
661 direction = log_forward;
662
663 graph_loader loader(db);
664
665 rev_cmp cmp(direction);
666 frontier_t frontier(cmp);
667 revision_id first_rid; // for mapping paths to node ids when restricted
668
669 // start at revisions specified and implied by --from selectors
670
671 set<revision_id> starting_revs;
672 if (app.opts.from.empty() && app.opts.revision.empty())
673 {
674 // only set default --from revs if no --revision selectors were specified
675 workspace work(app, F("try passing a '--from' revision to start at"));
676
677 revision_t rev;
678 work.get_work_rev(rev);
679 for (edge_map::const_iterator i = rev.edges.begin();
680 i != rev.edges.end(); i++)
681 {
682 revision_id rid = edge_old_revision(i);
683 if ((FL("%s") % rid).str().empty()) {
684 W(F("workspace has no parent revision, probably an empty branch"));
685 } else {
686 E(db.revision_exists(rid), origin::user,
687 F("workspace parent revision %s not found - "
688 "did you specify a wrong database?") % rid);
689 starting_revs.insert(rid);
690 if (i == rev.edges.begin())
691 first_rid = rid;
692 }
693 }
694 }
695 else if (!app.opts.from.empty())
696 {
697 for (args_vector::const_iterator i = app.opts.from.begin();
698 i != app.opts.from.end(); i++)
699 {
700 set<revision_id> rids;
701 MM(rids);
702 MM(*i);
703 complete(app.opts, app.lua, project, (*i)(), rids);
704 starting_revs.insert(rids.begin(), rids.end());
705 if (i == app.opts.from.begin())
706 first_rid = *rids.begin();
707 }
708 }
709
710 L(FL("%d starting revisions") % starting_revs.size());
711
712 // stop at revisions specified and implied by --to selectors
713
714 set<revision_id> ending_revs;
715 if (!app.opts.to.empty())
716 {
717 for (args_vector::const_iterator i = app.opts.to.begin();
718 i != app.opts.to.end(); i++)
719 {
720 set<revision_id> rids;
721 MM(rids);
722 MM(*i);
723 complete(app.opts, app.lua, project, (*i)(), rids);
724 ending_revs.insert(rids.begin(), rids.end());
725 }
726
727 if (direction == log_forward)
728 {
729 loader.load_descendants(ending_revs);
730 }
731 else if (direction == log_reverse)
732 {
733 loader.load_ancestors(ending_revs);
734 }
735 else
736 I(false);
737 }
738
739 L(FL("%d ending revisions") % ending_revs.size());
740
741 // select revisions specified by --revision selectors
742
743 set<revision_id> selected_revs;
744 if (!app.opts.revision.empty())
745 {
746 for (args_vector::const_iterator i = app.opts.revision.begin();
747 i != app.opts.revision.end(); i++)
748 {
749 set<revision_id> rids;
750 MM(rids);
751 MM(*i);
752 complete(app.opts, app.lua, project, (*i)(), rids);
753
754 // only select revs outside of the ending set
755 set_difference(rids.begin(), rids.end(),
756 ending_revs.begin(), ending_revs.end(),
757 inserter(selected_revs, selected_revs.end()));
758 if (null_id(first_rid) && i == app.opts.revision.begin())
759 first_rid = *rids.begin();
760 }
761 }
762
763 L(FL("%d selected revisions") % selected_revs.size());
764
765 // the first restriction mask only includes the actual selected nodes
766 // of the user, so he doesn't get revisions reported in which not the
767 // selected node, but only one of its parents changed
768 // the second restriction mask includes the parent nodes implicitely,
769 // so we can use it to make a restricted roster with it later on
770 node_restriction mask;
771 node_restriction mask_diff;
772
773 if (!args.empty() || !app.opts.exclude.empty())
774 {
775 // User wants to trace only specific files
776 if (app.opts.from.empty())
777 {
778 workspace work(app);
779 roster_t new_roster;
780 parent_map parents;
781 temp_node_id_source nis;
782
783 work.get_parent_rosters(db, parents);
784 work.get_current_roster_shape(db, nis, new_roster);
785
786 mask = node_restriction(args_to_paths(args),
787 args_to_paths(app.opts.exclude),
788 app.opts.depth, parents, new_roster,
789 ignored_file(work),
790 restriction::explicit_includes);
791
792 if (app.opts.diffs)
793 {
794 mask_diff = node_restriction(args_to_paths(args),
795 args_to_paths(app.opts.exclude),
796 app.opts.depth, parents, new_roster,
797 ignored_file(work),
798 restriction::implicit_includes);
799 }
800 }
801 else
802 {
803 // FIXME_RESTRICTIONS: should this add paths from the rosters of
804 // all selected revs?
805 I(!null_id(first_rid));
806 roster_t roster;
807 db.get_roster(first_rid, roster);
808
809 mask = node_restriction(args_to_paths(args),
810 args_to_paths(app.opts.exclude),
811 app.opts.depth, roster,
812 path_always_false<file_path>(),
813 restriction::explicit_includes);
814
815 if (app.opts.diffs)
816 {
817 mask_diff = node_restriction(args_to_paths(args),
818 args_to_paths(app.opts.exclude),
819 app.opts.depth, roster,
820 path_always_false<file_path>(),
821 restriction::explicit_includes);
822 }
823 }
824 }
825
826 // if --revision was specified without --from log only the selected revs
827 bool log_selected(!app.opts.revision.empty() &&
828 app.opts.from.empty());
829
830 if (log_selected)
831 {
832 for (set<revision_id>::const_iterator i = selected_revs.begin();
833 i != selected_revs.end(); ++i)
834 {
835 rev_height height;
836 db.get_rev_height(*i, height);
837 frontier.push(make_pair(height, *i));
838 }
839 L(FL("log %d selected revisions") % selected_revs.size());
840 }
841 else
842 {
843 for (set<revision_id>::const_iterator i = starting_revs.begin();
844 i != starting_revs.end(); ++i)
845 {
846 rev_height height;
847 db.get_rev_height(*i, height);
848 frontier.push(make_pair(height, *i));
849 }
850 L(FL("log %d starting revisions") % starting_revs.size());
851 }
852
853
854 // we can use the markings if we walk backwards for a restricted log
855 bool use_markings(direction == log_reverse && !mask.empty());
856
857 set<revision_id> seen;
858 revision_t rev;
859 // this is instantiated even when not used, but it's lightweight
860 asciik graph(output);
861 while(!frontier.empty() && last != 0 && next != 0)
862 {
863 revision_id const & rid = frontier.top().second;
864
865 bool print_this = mask.empty();
866
867 if (null_id(rid) || seen.find(rid) != seen.end())
868 {
869 frontier.pop();
870 continue;
871 }
872
873 seen.insert(rid);
874 db.get_revision(rid, rev);
875
876 set<revision_id> marked_revs;
877
878 if (!mask.empty())
879 {
880 roster_t roster;
881 marking_map markings;
882 db.get_roster(rid, roster, markings);
883
884 // get all revision ids mentioned in one of the markings
885 for (marking_map::const_iterator m = markings.begin();
886 m != markings.end(); ++m)
887 {
888 node_id const & node = m->first;
889 marking_t const & marks = m->second;
890
891 if (mask.includes(roster, node))
892 {
893 marked_revs.insert(marks->file_content.begin(),
894 marks->file_content.end());
895 marked_revs.insert(marks->parent_name.begin(),
896 marks->parent_name.end());
897 for (map<attr_key, set<revision_id> >::const_iterator
898 a = marks->attrs.begin(); a != marks->attrs.end(); ++a)
899 marked_revs.insert(a->second.begin(), a->second.end());
900 }
901 }
902
903 // find out whether the current rev is to be printed
904 // we don't care about changed paths if it is not marked
905 if (!use_markings || marked_revs.find(rid) != marked_revs.end())
906 {
907 set<node_id> nodes_modified;
908 select_nodes_modified_by_rev(db, rev, roster,
909 nodes_modified);
910
911 for (set<node_id>::const_iterator n = nodes_modified.begin();
912 n != nodes_modified.end(); ++n)
913 {
914 // a deleted node will be "modified" but won't
915 // exist in the result.
916 // we don't want to print them.
917 if (roster.has_node(*n) && mask.includes(roster, *n))
918 {
919 print_this = true;
920 }
921 }
922 }
923 }
924
925 if (app.opts.no_merges && rev.is_merge_node())
926 print_this = false;
927 else if (!app.opts.revision.empty() &&
928 selected_revs.find(rid) == selected_revs.end())
929 print_this = false;
930
931 set<revision_id> interesting;
932 // if rid is not marked we can jump directly to the marked ancestors,
933 // otherwise we need to visit the parents
934 if (use_markings && marked_revs.find(rid) == marked_revs.end())
935 {
936 interesting.insert(marked_revs.begin(), marked_revs.end());
937 }
938 else if (direction == log_forward)
939 {
940 loader.load_children(rid, interesting);
941 }
942 else if (direction == log_reverse)
943 {
944 loader.load_parents(rid, interesting);
945 }
946 else
947 I(false);
948
949 if (print_this)
950 {
951 if (automate)
952 output << rid << "\n";
953 else
954 {
955 ostringstream out;
956 log_print_rev (app, db, project, rid, rev, date_fmt, mask_diff, out);
957
958 string out_system;
959 utf8_to_system_best_effort(utf8(out.str(), origin::internal), out_system);
960 if (app.opts.no_graph)
961 output << out_system;
962 else
963 graph.print(rid, interesting, out_system);
964 }
965
966 if (next > 0)
967 next--;
968 else if (last > 0)
969 last--;
970 }
971 else if (!automate && use_markings && !app.opts.no_graph)
972 graph.print(rid, interesting,
973 (F("(Revision: %s)") % rid).str());
974
975 output.flush();
976
977 frontier.pop(); // beware: rid is invalid from now on
978
979 if (!log_selected)
980 {
981 // only add revs to the frontier when not logging specific selected revs
982 for (set<revision_id>::const_iterator i = interesting.begin();
983 i != interesting.end(); ++i)
984 {
985 if (!app.opts.to.empty() && (ending_revs.find(*i) != ending_revs.end()))
986 continue;
987 rev_height height;
988 db.get_rev_height(*i, height);
989 frontier.push(make_pair(height, *i));
990 }
991 }
992 }
993}
994
995CMD(log, "log", "", CMD_REF(informative), N_("[PATH] ..."),
996 N_("Prints selected history in forward or reverse order"),
997 N_("This command prints selected history in forward or reverse order, "
998 "filtering it by PATH if given."),
999 options::opts::last | options::opts::next |
1000 options::opts::from | options::opts::to | options::opts::revision |
1001 options::opts::brief | options::opts::diffs |
1002 options::opts::depth | options::opts::exclude |
1003 options::opts::no_merges | options::opts::no_files |
1004 options::opts::no_graph)
1005{
1006 log_common (app, args, false, cout);
1007}
1008
1009CMD_AUTOMATE(log, N_("[PATH] ..."),
1010 N_("Lists the selected revision history"),
1011 "",
1012 options::opts::last | options::opts::next |
1013 options::opts::from | options::opts::to |
1014 options::opts::depth | options::opts::exclude |
1015 options::opts::no_merges)
1016{
1017 log_common (app, args, true, output);
1018}
1019
1020// Local Variables:
1021// mode: C++
1022// fill-column: 76
1023// c-file-style: "gnu"
1024// indent-tabs-mode: nil
1025// End:
1026// vim: et:sw=2:sts=2:ts=2:cino=>2s,{s,\:s,+s,t0,g0,^-2,e-2,n-2,p2s,(0,=s:

Archive Download this file

Branches

Tags

Quick Links:     www.monotone.ca    -     Downloads    -     Documentation    -     Wiki    -     Code Forge    -     Build Status