monotone

monotone Mtn Source Tree

Root/cmd_diff_log.cc

1// Copyright (C) 2002 Graydon Hoare <graydon@pobox.com>
2//
3// This program is made available under the GNU GPL version 2.0 or
4// greater. See the accompanying file COPYING for details.
5//
6// This program is distributed WITHOUT ANY WARRANTY; without even the
7// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
8// PURPOSE.
9
10#include <deque>
11#include <iostream>
12#include <map>
13#include <sstream>
14
15#include "cmd.hh"
16#include "diff_patch.hh"
17#include "localized_file_io.hh"
18#include "restrictions.hh"
19#include "revision.hh"
20#include "simplestring_xform.hh"
21#include "transforms.hh"
22
23using std::cout;
24using std::deque;
25using std::make_pair;
26using std::map;
27using std::ostream;
28using std::ostringstream;
29using std::pair;
30using std::set;
31using std::string;
32using std::vector;
33
34using boost::lexical_cast;
35
36// The changes_summary structure holds a list all of files and directories
37// affected in a revision, and is useful in the 'log' command to print this
38// information easily. It has to be constructed from all cset objects
39// that belong to a revision.
40
41struct
42changes_summary
43{
44 cset cs;
45 changes_summary(void);
46 void add_change_set(cset const & cs);
47 void print(ostream & os, size_t max_cols) const;
48};
49
50changes_summary::changes_summary(void)
51{
52}
53
54void
55changes_summary::add_change_set(cset const & c)
56{
57 if (c.empty())
58 return;
59
60 // FIXME: not sure whether it matters for an informal summary
61 // object like this, but the pre-state names in deletes and renames
62 // are not really sensible to union; they refer to different trees
63 // so mixing them up in a single set is potentially ambiguous.
64
65 copy(c.nodes_deleted.begin(), c.nodes_deleted.end(),
66 inserter(cs.nodes_deleted, cs.nodes_deleted.begin()));
67
68 copy(c.files_added.begin(), c.files_added.end(),
69 inserter(cs.files_added, cs.files_added.begin()));
70
71 copy(c.dirs_added.begin(), c.dirs_added.end(),
72 inserter(cs.dirs_added, cs.dirs_added.begin()));
73
74 copy(c.nodes_renamed.begin(), c.nodes_renamed.end(),
75 inserter(cs.nodes_renamed, cs.nodes_renamed.begin()));
76
77 copy(c.deltas_applied.begin(), c.deltas_applied.end(),
78 inserter(cs.deltas_applied, cs.deltas_applied.begin()));
79
80 copy(c.attrs_cleared.begin(), c.attrs_cleared.end(),
81 inserter(cs.attrs_cleared, cs.attrs_cleared.begin()));
82
83 copy(c.attrs_set.begin(), c.attrs_set.end(),
84 inserter(cs.attrs_set, cs.attrs_set.begin()));
85}
86
87static void
88print_indented_set(ostream & os,
89 path_set const & s,
90 size_t max_cols)
91{
92 size_t cols = 8;
93 os << " ";
94 for (path_set::const_iterator i = s.begin();
95 i != s.end(); i++)
96 {
97 const string str = lexical_cast<string>(file_path(*i));
98 if (cols > 8 && cols + str.size() + 1 >= max_cols)
99 {
100 cols = 8;
101 os << "\n" << " ";
102 }
103 os << " " << str;
104 cols += str.size() + 1;
105 }
106 os << "\n";
107}
108
109void
110changes_summary::print(ostream & os, size_t max_cols) const
111{
112
113 if (! cs.nodes_deleted.empty())
114 {
115 os << "Deleted entries:" << "\n";
116 print_indented_set(os, cs.nodes_deleted, max_cols);
117 }
118
119 if (! cs.nodes_renamed.empty())
120 {
121 os << "Renamed entries:" << "\n";
122 for (map<split_path, split_path>::const_iterator
123 i = cs.nodes_renamed.begin();
124 i != cs.nodes_renamed.end(); i++)
125 os << " " << file_path(i->first)
126 << " to " << file_path(i->second) << "\n";
127 }
128
129 if (! cs.files_added.empty())
130 {
131 path_set tmp;
132 for (map<split_path, file_id>::const_iterator
133 i = cs.files_added.begin();
134 i != cs.files_added.end(); ++i)
135 tmp.insert(i->first);
136 os << "Added files:" << "\n";
137 print_indented_set(os, tmp, max_cols);
138 }
139
140 if (! cs.dirs_added.empty())
141 {
142 os << "Added directories:" << "\n";
143 print_indented_set(os, cs.dirs_added, max_cols);
144 }
145
146 if (! cs.deltas_applied.empty())
147 {
148 path_set tmp;
149 for (map<split_path, pair<file_id, file_id> >::const_iterator
150 i = cs.deltas_applied.begin();
151 i != cs.deltas_applied.end(); ++i)
152 tmp.insert(i->first);
153 os << "Modified files:" << "\n";
154 print_indented_set(os, tmp, max_cols);
155 }
156
157 if (! cs.attrs_set.empty() || ! cs.attrs_cleared.empty())
158 {
159 path_set tmp;
160 for (set<pair<split_path, attr_key> >::const_iterator
161 i = cs.attrs_cleared.begin();
162 i != cs.attrs_cleared.end(); ++i)
163 tmp.insert(i->first);
164
165 for (map<pair<split_path, attr_key>, attr_value>::const_iterator
166 i = cs.attrs_set.begin();
167 i != cs.attrs_set.end(); ++i)
168 tmp.insert(i->first.first);
169
170 os << "Modified attrs:" << "\n";
171 print_indented_set(os, tmp, max_cols);
172 }
173}
174
175static void
176do_external_diff(cset const & cs,
177 app_state & app,
178 bool new_is_archived)
179{
180 for (map<split_path, pair<file_id, file_id> >::const_iterator
181 i = cs.deltas_applied.begin();
182 i != cs.deltas_applied.end(); ++i)
183 {
184 data data_old;
185 data data_new;
186
187 file_data f_old;
188 app.db.get_file_version(delta_entry_src(i), f_old);
189 data_old = f_old.inner();
190
191 if (new_is_archived)
192 {
193 file_data f_new;
194 app.db.get_file_version(delta_entry_dst(i), f_new);
195 data_new = f_new.inner();
196 }
197 else
198 {
199 read_localized_data(file_path(delta_entry_path(i)),
200 data_new, app.lua);
201 }
202
203 bool is_binary = false;
204 if (guess_binary(data_old()) ||
205 guess_binary(data_new()))
206 is_binary = true;
207
208 app.lua.hook_external_diff(file_path(delta_entry_path(i)),
209 data_old,
210 data_new,
211 is_binary,
212 app.diff_args_provided,
213 app.diff_args(),
214 delta_entry_src(i).inner()(),
215 delta_entry_dst(i).inner()());
216 }
217}
218
219static void
220dump_diffs(cset const & cs,
221 app_state & app,
222 bool new_is_archived,
223 set<split_path> const & paths,
224 bool limit_paths = false)
225{
226 // 60 is somewhat arbitrary, but less than 80
227 string patch_sep = string(60, '=');
228
229 for (map<split_path, file_id>::const_iterator
230 i = cs.files_added.begin();
231 i != cs.files_added.end(); ++i)
232 {
233 if (limit_paths && paths.find(i->first) == paths.end())
234 continue;
235
236 cout << patch_sep << "\n";
237 data unpacked;
238 vector<string> lines;
239
240 if (new_is_archived)
241 {
242 file_data dat;
243 app.db.get_file_version(i->second, dat);
244 unpacked = dat.inner();
245 }
246 else
247 {
248 read_localized_data(file_path(i->first),
249 unpacked, app.lua);
250 }
251
252 std::string pattern("");
253 if (app.diff_show_encloser)
254 app.lua.hook_get_encloser_pattern(file_path(i->first),
255 pattern);
256
257 make_diff(file_path(i->first).as_internal(),
258 file_path(i->first).as_internal(),
259 i->second,
260 i->second,
261 data(), unpacked,
262 cout, app.diff_format, pattern);
263 }
264
265 map<split_path, split_path> reverse_rename_map;
266
267 for (map<split_path, split_path>::const_iterator
268 i = cs.nodes_renamed.begin();
269 i != cs.nodes_renamed.end(); ++i)
270 {
271 reverse_rename_map.insert(make_pair(i->second, i->first));
272 }
273
274 for (map<split_path, pair<file_id, file_id> >::const_iterator
275 i = cs.deltas_applied.begin();
276 i != cs.deltas_applied.end(); ++i)
277 {
278 if (limit_paths && paths.find(i->first) == paths.end())
279 continue;
280
281 file_data f_old;
282 data data_old, data_new;
283
284 cout << patch_sep << "\n";
285
286 app.db.get_file_version(delta_entry_src(i), f_old);
287 data_old = f_old.inner();
288
289 if (new_is_archived)
290 {
291 file_data f_new;
292 app.db.get_file_version(delta_entry_dst(i), f_new);
293 data_new = f_new.inner();
294 }
295 else
296 {
297 read_localized_data(file_path(delta_entry_path(i)),
298 data_new, app.lua);
299 }
300
301 split_path dst_path = delta_entry_path(i);
302 split_path src_path = dst_path;
303 map<split_path, split_path>::const_iterator re;
304 re = reverse_rename_map.find(dst_path);
305 if (re != reverse_rename_map.end())
306 src_path = re->second;
307
308 std::string pattern("");
309 if (app.diff_show_encloser)
310 app.lua.hook_get_encloser_pattern(file_path(src_path),
311 pattern);
312
313 make_diff(file_path(src_path).as_internal(),
314 file_path(dst_path).as_internal(),
315 delta_entry_src(i),
316 delta_entry_dst(i),
317 data_old, data_new,
318 cout, app.diff_format, pattern);
319 }
320}
321
322static void
323dump_diffs(cset const & cs,
324 app_state & app,
325 bool new_is_archived)
326{
327 set<split_path> dummy;
328 dump_diffs(cs, app, new_is_archived, dummy);
329}
330
331CMD(diff, N_("informative"), N_("[PATH]..."),
332 N_("show current diffs on stdout.\n"
333 "If one revision is given, the diff between the workspace and\n"
334 "that revision is shown. If two revisions are given, the diff\n"
335 "between them is given. If no format is specified, unified\n"
336 "is used by default."),
337 OPT_REVISION % OPT_DEPTH % OPT_EXCLUDE %
338 OPT_UNIFIED_DIFF % OPT_CONTEXT_DIFF % OPT_EXTERNAL_DIFF %
339 OPT_EXTERNAL_DIFF_ARGS % OPT_NO_SHOW_ENCLOSER)
340{
341 bool new_is_archived;
342 ostringstream header;
343 temp_node_id_source nis;
344
345 if (app.diff_args_provided)
346 N(app.diff_format == external_diff,
347 F("--diff-args requires --external\n"
348 "try adding --external or removing --diff-args?"));
349
350 cset included, excluded;
351
352 // initialize before transaction so we have a database to work with.
353
354 if (app.revision_selectors.size() == 0)
355 app.require_workspace();
356 else if (app.revision_selectors.size() == 1)
357 app.require_workspace();
358
359 if (app.revision_selectors.size() == 0)
360 {
361 roster_t new_roster, old_roster;
362 revision_id old_rid;
363
364 get_base_and_current_roster_shape(old_roster, new_roster,
365 nis, app);
366 get_revision_id(old_rid);
367
368 node_restriction mask(args_to_paths(args),
369 args_to_paths(app.exclude_patterns),
370 old_roster, new_roster, app);
371
372 update_current_roster_from_filesystem(new_roster, mask, app);
373 make_restricted_csets(old_roster, new_roster,
374 included, excluded, mask);
375 check_restricted_cset(old_roster, included);
376
377 new_is_archived = false;
378 header << "# old_revision [" << old_rid << "]" << "\n";
379 }
380 else if (app.revision_selectors.size() == 1)
381 {
382 roster_t new_roster, old_roster;
383 revision_id r_old_id;
384
385 complete(app, idx(app.revision_selectors, 0)(), r_old_id);
386 N(app.db.revision_exists(r_old_id),
387 F("no such revision '%s'") % r_old_id);
388
389 get_base_and_current_roster_shape(old_roster,
390 new_roster,
391 nis, app);
392 // Clobber old_roster with the one specified
393 app.db.get_roster(r_old_id, old_roster);
394
395 // FIXME: handle no ancestor case
396 // N(r_new.edges.size() == 1, F("current revision has no ancestor"));
397
398 node_restriction mask(args_to_paths(args),
399 args_to_paths(app.exclude_patterns),
400 old_roster, new_roster, app);
401
402 update_current_roster_from_filesystem(new_roster, mask, app);
403 make_restricted_csets(old_roster, new_roster,
404 included, excluded, mask);
405 check_restricted_cset(old_roster, included);
406
407 new_is_archived = false;
408 header << "# old_revision [" << r_old_id << "]" << "\n";
409 }
410 else if (app.revision_selectors.size() == 2)
411 {
412 roster_t new_roster, old_roster;
413 revision_id r_old_id, r_new_id;
414
415 complete(app, idx(app.revision_selectors, 0)(), r_old_id);
416 complete(app, idx(app.revision_selectors, 1)(), r_new_id);
417
418 N(app.db.revision_exists(r_old_id),
419 F("no such revision '%s'") % r_old_id);
420 N(app.db.revision_exists(r_new_id),
421 F("no such revision '%s'") % r_new_id);
422
423 app.db.get_roster(r_old_id, old_roster);
424 app.db.get_roster(r_new_id, new_roster);
425
426 node_restriction mask(args_to_paths(args),
427 args_to_paths(app.exclude_patterns),
428 old_roster, new_roster, app);
429
430 // FIXME: this is *possibly* a UI bug, insofar as we
431 // look at the restriction name(s) you provided on the command
432 // line in the context of new and old, *not* the working copy.
433 // One way of "fixing" this is to map the filenames on the command
434 // line to node_ids, and then restrict based on those. This
435 // might be more intuitive; on the other hand it would make it
436 // impossible to restrict to paths which are dead in the working
437 // copy but live between old and new. So ... no rush to "fix" it;
438 // discuss implications first.
439 //
440 // Let the discussion begin...
441 //
442 // - "map filenames on the command line to node_ids" needs to be done
443 // in the context of some roster, possibly the working copy base or
444 // the current working copy (or both)
445 // - diff with two --revision's may be done with no working copy
446 // - some form of "peg" revision syntax for paths that would allow
447 // for each path to specify which revision it is relevant to is
448 // probably the "right" way to go eventually. something like file@rev
449 // (which fails for paths with @'s in them) or possibly //rev/file
450 // since versioned paths are required to be relative.
451
452 make_restricted_csets(old_roster, new_roster,
453 included, excluded, mask);
454 check_restricted_cset(old_roster, included);
455
456 new_is_archived = true;
457 }
458 else
459 {
460 throw usage(name);
461 }
462
463
464 data summary;
465 write_cset(included, summary);
466
467 vector<string> lines;
468 split_into_lines(summary(), lines);
469 cout << "# " << "\n";
470 if (summary().size() > 0)
471 {
472 cout << header.str() << "# " << "\n";
473 for (vector<string>::iterator i = lines.begin();
474 i != lines.end(); ++i)
475 cout << "# " << *i << "\n";
476 }
477 else
478 {
479 cout << "# " << _("no changes") << "\n";
480 }
481 cout << "# " << "\n";
482
483 if (app.diff_format == external_diff) {
484 do_external_diff(included, app, new_is_archived);
485 } else
486 dump_diffs(included, app, new_is_archived);
487}
488
489static void
490log_certs(app_state & app, revision_id id, cert_name name,
491 string label, string separator,
492 bool multiline, bool newline)
493{
494 vector< revision<cert> > certs;
495 bool first = true;
496
497 if (multiline)
498 newline = true;
499
500 app.db.get_revision_certs(id, name, certs);
501 erase_bogus_certs(certs, app);
502 for (vector< revision<cert> >::const_iterator i = certs.begin();
503 i != certs.end(); ++i)
504 {
505 cert_value tv;
506 decode_base64(i->inner().value, tv);
507
508 if (first)
509 cout << label;
510 else
511 cout << separator;
512
513 if (multiline)
514 {
515 cout << "\n" << "\n" << tv;
516 if (newline)
517 cout << "\n";
518 }
519 else
520 {
521 cout << tv;
522 if (newline)
523 cout << "\n";
524 }
525
526 first = false;
527 }
528}
529
530static void
531log_certs(app_state & app, revision_id id, cert_name name,
532 string label, bool multiline)
533{
534 log_certs(app, id, name, label, label, multiline, true);
535}
536
537static void
538log_certs(app_state & app, revision_id id, cert_name name)
539{
540 log_certs(app, id, name, " ", ",", false, false);
541}
542
543CMD(log, N_("informative"), N_("[FILE] ..."),
544 N_("print history in reverse order (filtering by 'FILE'). "
545 "If one or more revisions are given, "
546 "use them as a starting point."),
547 OPT_LAST % OPT_NEXT % OPT_REVISION % OPT_BRIEF % OPT_DIFFS
548 % OPT_NO_MERGES % OPT_NO_FILES)
549{
550 if (app.revision_selectors.size() == 0)
551 app.require_workspace("try passing a --revision to start at");
552
553 temp_node_id_source nis;
554 set<revision_id> frontier;
555 revision_id first_rid;
556
557 if (app.revision_selectors.size() == 0)
558 {
559 get_revision_id(first_rid);
560 frontier.insert(first_rid);
561 }
562 else
563 {
564 for (vector<utf8>::const_iterator i = app.revision_selectors.begin();
565 i != app.revision_selectors.end(); i++)
566 {
567 set<revision_id> rids;
568 complete(app, (*i)(), rids);
569 frontier.insert(rids.begin(), rids.end());
570 if (i == app.revision_selectors.begin())
571 first_rid = *rids.begin();
572 }
573 }
574
575 node_restriction mask(app);
576
577 if (args.size() > 0)
578 {
579 // User wants to trace only specific files
580 roster_t old_roster, new_roster;
581
582 if (app.revision_selectors.size() == 0)
583 get_base_and_current_roster_shape(old_roster,
584 new_roster, nis, app);
585 else
586 app.db.get_roster(first_rid, new_roster);
587
588 // FIXME_RESTRICTIONS: should this add paths from the rosters of
589 // all selected revs?
590 mask = node_restriction(args_to_paths(args),
591 args_to_paths(app.exclude_patterns),
592 old_roster, new_roster, app);
593 }
594
595 cert_name author_name(author_cert_name);
596 cert_name date_name(date_cert_name);
597 cert_name branch_name(branch_cert_name);
598 cert_name tag_name(tag_cert_name);
599 cert_name changelog_name(changelog_cert_name);
600 cert_name comment_name(comment_cert_name);
601
602 set<revision_id> seen;
603 long last = app.last;
604 long next = app.next;
605
606 N(last == -1 || next == -1,
607 F("only one of --last/--next allowed"));
608
609 revision_t rev;
610
611 while(! frontier.empty() && (last == -1 || last > 0)
612 && (next == -1 || next > 0))
613 {
614 set<revision_id> next_frontier;
615
616 for (set<revision_id>::const_iterator i = frontier.begin();
617 i != frontier.end(); ++i)
618 {
619 revision_id rid = *i;
620
621 bool print_this = mask.empty();
622 set< revision<id> > parents;
623 vector< revision<cert> > tmp;
624 set<split_path> diff_paths;
625
626 if (null_id(rid) || seen.find(rid) != seen.end())
627 continue;
628
629 seen.insert(rid);
630 app.db.get_revision(rid, rev);
631
632 if (!mask.empty())
633 {
634 // TODO: stop if the restriction is pre-dated by the
635 // current roster i.e. the restriction's nodes are not
636 // born in the current roster
637 roster_t roster;
638 app.db.get_roster(rid, roster);
639
640 set<node_id> nodes_modified;
641 select_nodes_modified_by_rev(rid, rev, roster,
642 nodes_modified,
643 app);
644
645 for (set<node_id>::const_iterator n = nodes_modified.begin();
646 n != nodes_modified.end(); ++n)
647 {
648 if (!roster.has_node(*n))
649 {
650 // include all deleted nodes
651 print_this = true;
652 }
653 else if (mask.includes(roster, *n))
654 {
655 print_this = true;
656 if (app.diffs)
657 {
658 split_path sp;
659 roster.get_name(*n, sp);
660 diff_paths.insert(sp);
661 }
662 }
663 }
664 }
665
666 if (next > 0)
667 {
668 set<revision_id> children;
669 app.db.get_revision_children(rid, children);
670 copy(children.begin(), children.end(),
671 inserter(next_frontier, next_frontier.end()));
672 }
673 else // work backwards by default
674 {
675 set<revision_id> parents;
676 app.db.get_revision_parents(rid, parents);
677 copy(parents.begin(), parents.end(),
678 inserter(next_frontier, next_frontier.end()));
679 }
680
681 if (app.no_merges && rev.is_merge_node())
682 print_this = false;
683
684 if (print_this)
685 {
686 if (global_sanity.brief)
687 {
688 cout << rid;
689 log_certs(app, rid, author_name);
690 log_certs(app, rid, date_name);
691 log_certs(app, rid, branch_name);
692 cout << "\n";
693 }
694 else
695 {
696 cout << string(65, '-') << "\n";
697 cout << "Revision: " << rid << "\n";
698
699 changes_summary csum;
700
701 set<revision_id> ancestors;
702
703 for (edge_map::const_iterator e = rev.edges.begin();
704 e != rev.edges.end(); ++e)
705 {
706 ancestors.insert(edge_old_revision(e));
707 csum.add_change_set(edge_changes(e));
708 }
709
710 for (set<revision_id>::const_iterator
711 anc = ancestors.begin();
712 anc != ancestors.end(); ++anc)
713 cout << "Ancestor: " << *anc << "\n";
714
715 log_certs(app, rid, author_name, "Author: ", false);
716 log_certs(app, rid, date_name, "Date: ", false);
717 log_certs(app, rid, branch_name, "Branch: ", false);
718 log_certs(app, rid, tag_name, "Tag: ", false);
719
720 if (!app.no_files && !csum.cs.empty())
721 {
722 cout << "\n";
723 csum.print(cout, 70);
724 cout << "\n";
725 }
726
727 log_certs(app, rid, changelog_name, "ChangeLog: ", true);
728 log_certs(app, rid, comment_name, "Comments: ", true);
729 }
730
731 if (app.diffs)
732 {
733 for (edge_map::const_iterator e = rev.edges.begin();
734 e != rev.edges.end(); ++e)
735 {
736 dump_diffs(edge_changes(e), app, true, diff_paths,
737 !mask.empty());
738 }
739 }
740
741 if (next > 0)
742 {
743 next--;
744 }
745 else if (last > 0)
746 {
747 last--;
748 }
749
750 }
751 }
752 frontier = next_frontier;
753 }
754}
755
756// Local Variables:
757// mode: C++
758// fill-column: 76
759// c-file-style: "gnu"
760// indent-tabs-mode: nil
761// End:
762// 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