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 "base.hh"␊ |
11 | #include <deque>␊ |
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 "diff_patch.hh"␊ |
21 | #include "file_io.hh"␊ |
22 | #include "restrictions.hh"␊ |
23 | #include "revision.hh"␊ |
24 | #include "rev_height.hh"␊ |
25 | #include "simplestring_xform.hh"␊ |
26 | #include "transforms.hh"␊ |
27 | #include "app_state.hh"␊ |
28 | #include "project.hh"␊ |
29 | #include "database.hh"␊ |
30 | #include "work.hh"␊ |
31 | #include "roster.hh"␊ |
32 | ␊ |
33 | using std::cout;␊ |
34 | using std::deque;␊ |
35 | using std::make_pair;␊ |
36 | using std::map;␊ |
37 | using std::ostream;␊ |
38 | using std::ostringstream;␊ |
39 | using std::pair;␊ |
40 | using std::set;␊ |
41 | using std::string;␊ |
42 | using std::vector;␊ |
43 | using std::priority_queue;␊ |
44 | ␊ |
45 | using boost::lexical_cast;␊ |
46 | ␊ |
47 | // The changes_summary structure holds a list all of files and directories␊ |
48 | // affected in a revision, and is useful in the 'log' command to print this␊ |
49 | // information easily. It has to be constructed from all cset objects␊ |
50 | // that belong to a revision.␊ |
51 | ␊ |
52 | struct␊ |
53 | changes_summary␊ |
54 | {␊ |
55 | cset cs;␊ |
56 | changes_summary(void);␊ |
57 | void add_change_set(cset const & cs);␊ |
58 | void print(ostream & os, size_t max_cols) const;␊ |
59 | };␊ |
60 | ␊ |
61 | changes_summary::changes_summary(void)␊ |
62 | {␊ |
63 | }␊ |
64 | ␊ |
65 | void␊ |
66 | changes_summary::add_change_set(cset const & c)␊ |
67 | {␊ |
68 | if (c.empty())␊ |
69 | return;␊ |
70 | ␊ |
71 | // FIXME: not sure whether it matters for an informal summary␊ |
72 | // object like this, but the pre-state names in deletes and renames␊ |
73 | // are not really sensible to union; they refer to different trees␊ |
74 | // so mixing them up in a single set is potentially ambiguous.␊ |
75 | ␊ |
76 | copy(c.nodes_deleted.begin(), c.nodes_deleted.end(),␊ |
77 | inserter(cs.nodes_deleted, cs.nodes_deleted.begin()));␊ |
78 | ␊ |
79 | copy(c.files_added.begin(), c.files_added.end(),␊ |
80 | inserter(cs.files_added, cs.files_added.begin()));␊ |
81 | ␊ |
82 | copy(c.dirs_added.begin(), c.dirs_added.end(),␊ |
83 | inserter(cs.dirs_added, cs.dirs_added.begin()));␊ |
84 | ␊ |
85 | copy(c.nodes_renamed.begin(), c.nodes_renamed.end(),␊ |
86 | inserter(cs.nodes_renamed, cs.nodes_renamed.begin()));␊ |
87 | ␊ |
88 | copy(c.deltas_applied.begin(), c.deltas_applied.end(),␊ |
89 | inserter(cs.deltas_applied, cs.deltas_applied.begin()));␊ |
90 | ␊ |
91 | copy(c.attrs_cleared.begin(), c.attrs_cleared.end(),␊ |
92 | inserter(cs.attrs_cleared, cs.attrs_cleared.begin()));␊ |
93 | ␊ |
94 | copy(c.attrs_set.begin(), c.attrs_set.end(),␊ |
95 | inserter(cs.attrs_set, cs.attrs_set.begin()));␊ |
96 | }␊ |
97 | ␊ |
98 | static void␊ |
99 | print_indented_set(ostream & os,␊ |
100 | set<file_path> const & s,␊ |
101 | size_t max_cols)␊ |
102 | {␊ |
103 | size_t cols = 8;␊ |
104 | os << " ";␊ |
105 | for (set<file_path>::const_iterator i = s.begin();␊ |
106 | i != s.end(); i++)␊ |
107 | {␊ |
108 | string str = lexical_cast<string>(*i);␊ |
109 | if (str.empty())␊ |
110 | str = "."; // project root␊ |
111 | if (cols > 8 && cols + str.size() + 1 >= max_cols)␊ |
112 | {␊ |
113 | cols = 8;␊ |
114 | os << "\n ";␊ |
115 | }␊ |
116 | os << ' ' << str;␊ |
117 | cols += str.size() + 1;␊ |
118 | }␊ |
119 | os << '\n';␊ |
120 | }␊ |
121 | ␊ |
122 | void␊ |
123 | changes_summary::print(ostream & os, size_t max_cols) const␊ |
124 | {␊ |
125 | ␊ |
126 | if (! cs.nodes_deleted.empty())␊ |
127 | {␊ |
128 | os << _("Deleted entries:") << '\n';␊ |
129 | print_indented_set(os, cs.nodes_deleted, max_cols);␊ |
130 | }␊ |
131 | ␊ |
132 | if (! cs.nodes_renamed.empty())␊ |
133 | {␊ |
134 | os << _("Renamed entries:") << '\n';␊ |
135 | for (map<file_path, file_path>::const_iterator␊ |
136 | i = cs.nodes_renamed.begin();␊ |
137 | i != cs.nodes_renamed.end(); i++)␊ |
138 | os << " " << i->first␊ |
139 | << " to " << i->second << '\n';␊ |
140 | }␊ |
141 | ␊ |
142 | if (! cs.files_added.empty())␊ |
143 | {␊ |
144 | set<file_path> tmp;␊ |
145 | for (map<file_path, file_id>::const_iterator␊ |
146 | i = cs.files_added.begin();␊ |
147 | i != cs.files_added.end(); ++i)␊ |
148 | tmp.insert(i->first);␊ |
149 | os << _("Added files:") << '\n';␊ |
150 | print_indented_set(os, tmp, max_cols);␊ |
151 | }␊ |
152 | ␊ |
153 | if (! cs.dirs_added.empty())␊ |
154 | {␊ |
155 | os << _("Added directories:") << '\n';␊ |
156 | print_indented_set(os, cs.dirs_added, max_cols);␊ |
157 | }␊ |
158 | ␊ |
159 | if (! cs.deltas_applied.empty())␊ |
160 | {␊ |
161 | set<file_path> tmp;␊ |
162 | for (map<file_path, pair<file_id, file_id> >::const_iterator␊ |
163 | i = cs.deltas_applied.begin();␊ |
164 | i != cs.deltas_applied.end(); ++i)␊ |
165 | tmp.insert(i->first);␊ |
166 | os << _("Modified files:") << '\n';␊ |
167 | print_indented_set(os, tmp, max_cols);␊ |
168 | }␊ |
169 | ␊ |
170 | if (! cs.attrs_set.empty() || ! cs.attrs_cleared.empty())␊ |
171 | {␊ |
172 | set<file_path> tmp;␊ |
173 | for (set<pair<file_path, attr_key> >::const_iterator␊ |
174 | i = cs.attrs_cleared.begin();␊ |
175 | i != cs.attrs_cleared.end(); ++i)␊ |
176 | tmp.insert(i->first);␊ |
177 | ␊ |
178 | for (map<pair<file_path, attr_key>, attr_value>::const_iterator␊ |
179 | i = cs.attrs_set.begin();␊ |
180 | i != cs.attrs_set.end(); ++i)␊ |
181 | tmp.insert(i->first.first);␊ |
182 | ␊ |
183 | os << _("Modified attrs:") << '\n';␊ |
184 | print_indented_set(os, tmp, max_cols);␊ |
185 | }␊ |
186 | }␊ |
187 | ␊ |
188 | static void␊ |
189 | do_external_diff(options & opts, lua_hooks & lua, database & db,␊ |
190 | cset const & cs, bool new_is_archived)␊ |
191 | {␊ |
192 | for (map<file_path, pair<file_id, file_id> >::const_iterator␊ |
193 | i = cs.deltas_applied.begin();␊ |
194 | i != cs.deltas_applied.end(); ++i)␊ |
195 | {␊ |
196 | data data_old;␊ |
197 | data data_new;␊ |
198 | ␊ |
199 | file_data f_old;␊ |
200 | db.get_file_version(delta_entry_src(i), f_old);␊ |
201 | data_old = f_old.inner();␊ |
202 | ␊ |
203 | if (new_is_archived)␊ |
204 | {␊ |
205 | file_data f_new;␊ |
206 | db.get_file_version(delta_entry_dst(i), f_new);␊ |
207 | data_new = f_new.inner();␊ |
208 | }␊ |
209 | else␊ |
210 | {␊ |
211 | read_data(delta_entry_path(i), data_new);␊ |
212 | }␊ |
213 | ␊ |
214 | bool is_binary = false;␊ |
215 | if (guess_binary(data_old()) ||␊ |
216 | guess_binary(data_new()))␊ |
217 | is_binary = true;␊ |
218 | ␊ |
219 | lua.hook_external_diff(delta_entry_path(i),␊ |
220 | data_old,␊ |
221 | data_new,␊ |
222 | is_binary,␊ |
223 | opts.external_diff_args_given,␊ |
224 | opts.external_diff_args,␊ |
225 | encode_hexenc(delta_entry_src(i).inner()()),␊ |
226 | encode_hexenc(delta_entry_dst(i).inner()()));␊ |
227 | }␊ |
228 | }␊ |
229 | ␊ |
230 | static void␊ |
231 | dump_diffs(lua_hooks & lua,␊ |
232 | database & db,␊ |
233 | cset const & cs,␊ |
234 | set<file_path> const & paths,␊ |
235 | std::ostream & output,␊ |
236 | diff_type diff_format,␊ |
237 | bool new_is_archived,␊ |
238 | bool show_encloser,␊ |
239 | bool limit_paths = false)␊ |
240 | {␊ |
241 | // 60 is somewhat arbitrary, but less than 80␊ |
242 | string patch_sep = string(60, '=');␊ |
243 | ␊ |
244 | for (map<file_path, file_id>::const_iterator␊ |
245 | i = cs.files_added.begin();␊ |
246 | i != cs.files_added.end(); ++i)␊ |
247 | {␊ |
248 | if (limit_paths && paths.find(i->first) == paths.end())␊ |
249 | continue;␊ |
250 | ␊ |
251 | output << patch_sep << '\n';␊ |
252 | data unpacked;␊ |
253 | vector<string> lines;␊ |
254 | ␊ |
255 | if (new_is_archived)␊ |
256 | {␊ |
257 | file_data dat;␊ |
258 | db.get_file_version(i->second, dat);␊ |
259 | unpacked = dat.inner();␊ |
260 | }␊ |
261 | else␊ |
262 | {␊ |
263 | read_data(i->first, unpacked);␊ |
264 | }␊ |
265 | ␊ |
266 | std::string pattern("");␊ |
267 | if (show_encloser)␊ |
268 | lua.hook_get_encloser_pattern(i->first, pattern);␊ |
269 | ␊ |
270 | make_diff(i->first.as_internal(),␊ |
271 | i->first.as_internal(),␊ |
272 | i->second,␊ |
273 | i->second,␊ |
274 | data(), unpacked,␊ |
275 | output, diff_format, pattern);␊ |
276 | }␊ |
277 | ␊ |
278 | map<file_path, file_path> reverse_rename_map;␊ |
279 | ␊ |
280 | for (map<file_path, file_path>::const_iterator␊ |
281 | i = cs.nodes_renamed.begin();␊ |
282 | i != cs.nodes_renamed.end(); ++i)␊ |
283 | {␊ |
284 | reverse_rename_map.insert(make_pair(i->second, i->first));␊ |
285 | }␊ |
286 | ␊ |
287 | for (map<file_path, pair<file_id, file_id> >::const_iterator␊ |
288 | i = cs.deltas_applied.begin();␊ |
289 | i != cs.deltas_applied.end(); ++i)␊ |
290 | {␊ |
291 | if (limit_paths && paths.find(i->first) == paths.end())␊ |
292 | continue;␊ |
293 | ␊ |
294 | file_data f_old;␊ |
295 | data data_old, data_new;␊ |
296 | ␊ |
297 | output << patch_sep << '\n';␊ |
298 | ␊ |
299 | db.get_file_version(delta_entry_src(i), f_old);␊ |
300 | data_old = f_old.inner();␊ |
301 | ␊ |
302 | if (new_is_archived)␊ |
303 | {␊ |
304 | file_data f_new;␊ |
305 | db.get_file_version(delta_entry_dst(i), f_new);␊ |
306 | data_new = f_new.inner();␊ |
307 | }␊ |
308 | else␊ |
309 | {␊ |
310 | read_data(delta_entry_path(i), data_new);␊ |
311 | }␊ |
312 | ␊ |
313 | file_path dst_path = delta_entry_path(i);␊ |
314 | file_path src_path = dst_path;␊ |
315 | map<file_path, file_path>::const_iterator re;␊ |
316 | re = reverse_rename_map.find(dst_path);␊ |
317 | if (re != reverse_rename_map.end())␊ |
318 | src_path = re->second;␊ |
319 | ␊ |
320 | std::string pattern("");␊ |
321 | if (show_encloser)␊ |
322 | lua.hook_get_encloser_pattern(src_path, pattern);␊ |
323 | ␊ |
324 | make_diff(src_path.as_internal(),␊ |
325 | dst_path.as_internal(),␊ |
326 | delta_entry_src(i),␊ |
327 | delta_entry_dst(i),␊ |
328 | data_old, data_new,␊ |
329 | output, diff_format, pattern);␊ |
330 | }␊ |
331 | }␊ |
332 | ␊ |
333 | static void␊ |
334 | dump_diffs(lua_hooks & lua,␊ |
335 | database & db,␊ |
336 | cset const & cs,␊ |
337 | std::ostream & output,␊ |
338 | diff_type diff_format,␊ |
339 | bool new_is_archived,␊ |
340 | bool show_encloser)␊ |
341 | {␊ |
342 | set<file_path> dummy;␊ |
343 | dump_diffs(lua, db, cs, dummy, output,␊ |
344 | diff_format, new_is_archived, show_encloser);␊ |
345 | }␊ |
346 | ␊ |
347 | // common functionality for diff and automate content_diff to determine␊ |
348 | // revisions and rosters which should be diffed␊ |
349 | // FIXME needs app_state in order to create workspace objects (sometimes)␊ |
350 | static void␊ |
351 | prepare_diff(app_state & app,␊ |
352 | database & db,␊ |
353 | cset & included,␊ |
354 | args_vector args,␊ |
355 | bool & new_is_archived,␊ |
356 | std::string & revheader)␊ |
357 | {␊ |
358 | temp_node_id_source nis;␊ |
359 | ostringstream header;␊ |
360 | cset excluded;␊ |
361 | ␊ |
362 | // initialize before transaction so we have a database to work with.␊ |
363 | project_t project(db);␊ |
364 | ␊ |
365 | N(app.opts.revision_selectors.size() <= 2,␊ |
366 | F("more than two revisions given"));␊ |
367 | ␊ |
368 | if (app.opts.revision_selectors.size() == 0)␊ |
369 | {␊ |
370 | roster_t old_roster, restricted_roster, new_roster;␊ |
371 | revision_id old_rid;␊ |
372 | parent_map parents;␊ |
373 | workspace work(app);␊ |
374 | ␊ |
375 | work.get_parent_rosters(db, parents);␊ |
376 | ␊ |
377 | // With no arguments, which parent should we diff against?␊ |
378 | N(parents.size() == 1,␊ |
379 | F("this workspace has more than one parent\n"␊ |
380 | "(specify a revision to diff against with --revision)"));␊ |
381 | ␊ |
382 | old_rid = parent_id(parents.begin());␊ |
383 | old_roster = parent_roster(parents.begin());␊ |
384 | work.get_current_roster_shape(db, nis, new_roster);␊ |
385 | ␊ |
386 | node_restriction mask(work, args_to_paths(args),␊ |
387 | args_to_paths(app.opts.exclude_patterns),␊ |
388 | app.opts.depth,␊ |
389 | old_roster, new_roster);␊ |
390 | ␊ |
391 | work.update_current_roster_from_filesystem(new_roster, mask);␊ |
392 | ␊ |
393 | make_restricted_roster(old_roster, new_roster, restricted_roster, ␊ |
394 | mask);␊ |
395 | ␊ |
396 | make_cset(old_roster, restricted_roster, included);␊ |
397 | make_cset(restricted_roster, new_roster, excluded);␊ |
398 | ␊ |
399 | new_is_archived = false;␊ |
400 | header << "# old_revision [" << old_rid << "]\n";␊ |
401 | }␊ |
402 | else if (app.opts.revision_selectors.size() == 1)␊ |
403 | {␊ |
404 | roster_t old_roster, restricted_roster, new_roster;␊ |
405 | revision_id r_old_id;␊ |
406 | workspace work(app);␊ |
407 | ␊ |
408 | complete(app.opts, app.lua, project, idx(app.opts.revision_selectors, 0)(), r_old_id);␊ |
409 | ␊ |
410 | db.get_roster(r_old_id, old_roster);␊ |
411 | work.get_current_roster_shape(db, nis, new_roster);␊ |
412 | ␊ |
413 | node_restriction mask(work, args_to_paths(args),␊ |
414 | args_to_paths(app.opts.exclude_patterns),␊ |
415 | app.opts.depth,␊ |
416 | old_roster, new_roster);␊ |
417 | ␊ |
418 | work.update_current_roster_from_filesystem(new_roster, mask);␊ |
419 | ␊ |
420 | make_restricted_roster(old_roster, new_roster, restricted_roster, ␊ |
421 | mask);␊ |
422 | ␊ |
423 | make_cset(old_roster, restricted_roster, included);␊ |
424 | make_cset(restricted_roster, new_roster, excluded);␊ |
425 | ␊ |
426 | new_is_archived = false;␊ |
427 | header << "# old_revision [" << r_old_id << "]\n";␊ |
428 | }␊ |
429 | else if (app.opts.revision_selectors.size() == 2)␊ |
430 | {␊ |
431 | roster_t old_roster, restricted_roster, new_roster;␊ |
432 | revision_id r_old_id, r_new_id;␊ |
433 | ␊ |
434 | complete(app.opts, app.lua, project, idx(app.opts.revision_selectors, 0)(), r_old_id);␊ |
435 | complete(app.opts, app.lua, project, idx(app.opts.revision_selectors, 1)(), r_new_id);␊ |
436 | ␊ |
437 | db.get_roster(r_old_id, old_roster);␊ |
438 | db.get_roster(r_new_id, new_roster);␊ |
439 | ␊ |
440 | // FIXME: this is *possibly* a UI bug, insofar as we␊ |
441 | // look at the restriction name(s) you provided on the command␊ |
442 | // line in the context of new and old, *not* the working copy.␊ |
443 | // One way of "fixing" this is to map the filenames on the command␊ |
444 | // line to node_ids, and then restrict based on those. This␊ |
445 | // might be more intuitive; on the other hand it would make it␊ |
446 | // impossible to restrict to paths which are dead in the working␊ |
447 | // copy but live between old and new. So ... no rush to "fix" it;␊ |
448 | // discuss implications first.␊ |
449 | //␊ |
450 | // Let the discussion begin...␊ |
451 | //␊ |
452 | // - "map filenames on the command line to node_ids" needs to be done␊ |
453 | // in the context of some roster, possibly the working copy base or␊ |
454 | // the current working copy (or both)␊ |
455 | // - diff with two --revision's may be done with no working copy␊ |
456 | // - some form of "peg" revision syntax for paths that would allow␊ |
457 | // for each path to specify which revision it is relevant to is␊ |
458 | // probably the "right" way to go eventually. something like file@rev␊ |
459 | // (which fails for paths with @'s in them) or possibly //rev/file␊ |
460 | // since versioned paths are required to be relative.␊ |
461 | ␊ |
462 | node_restriction mask(args_to_paths(args),␊ |
463 | args_to_paths(app.opts.exclude_patterns),␊ |
464 | app.opts.depth,␊ |
465 | old_roster, new_roster);␊ |
466 | ␊ |
467 | make_restricted_roster(old_roster, new_roster, restricted_roster, ␊ |
468 | mask);␊ |
469 | ␊ |
470 | make_cset(old_roster, restricted_roster, included);␊ |
471 | make_cset(restricted_roster, new_roster, excluded);␊ |
472 | ␊ |
473 | new_is_archived = true;␊ |
474 | }␊ |
475 | else␊ |
476 | {␊ |
477 | I(false);␊ |
478 | }␊ |
479 | ␊ |
480 | revheader = header.str();␊ |
481 | }␊ |
482 | ␊ |
483 | CMD(diff, "diff", "di", CMD_REF(informative), N_("[PATH]..."),␊ |
484 | N_("Shows current differences"),␊ |
485 | N_("Compares the current tree with the files in the repository and "␊ |
486 | "prints the differences on the standard output.\n"␊ |
487 | "If one revision is given, the diff between the workspace and "␊ |
488 | "that revision is shown. If two revisions are given, the diff "␊ |
489 | "between them is given. If no format is specified, unified is "␊ |
490 | "used by default."),␊ |
491 | options::opts::revision | options::opts::depth | options::opts::exclude␊ |
492 | | options::opts::diff_options)␊ |
493 | {␊ |
494 | if (app.opts.external_diff_args_given)␊ |
495 | N(app.opts.diff_format == external_diff,␊ |
496 | F("--diff-args requires --external\n"␊ |
497 | "try adding --external or removing --diff-args?"));␊ |
498 | ␊ |
499 | cset included;␊ |
500 | std::string revs;␊ |
501 | bool new_is_archived;␊ |
502 | database db(app);␊ |
503 | ␊ |
504 | prepare_diff(app, db, included, args, new_is_archived, revs);␊ |
505 | ␊ |
506 | data summary;␊ |
507 | write_cset(included, summary);␊ |
508 | ␊ |
509 | vector<string> lines;␊ |
510 | split_into_lines(summary(), lines);␊ |
511 | cout << "#\n";␊ |
512 | if (summary().size() > 0)␊ |
513 | {␊ |
514 | cout << revs << "#\n";␊ |
515 | for (vector<string>::iterator i = lines.begin();␊ |
516 | i != lines.end(); ++i)␊ |
517 | cout << "# " << *i << '\n';␊ |
518 | }␊ |
519 | else␊ |
520 | {␊ |
521 | cout << "# " << _("no changes") << '\n';␊ |
522 | }␊ |
523 | cout << "#\n";␊ |
524 | ␊ |
525 | if (app.opts.diff_format == external_diff)␊ |
526 | {␊ |
527 | do_external_diff(app.opts, app.lua, db, included, new_is_archived);␊ |
528 | }␊ |
529 | else␊ |
530 | {␊ |
531 | dump_diffs(app.lua, db, included, cout,␊ |
532 | app.opts.diff_format, new_is_archived,␊ |
533 | !app.opts.no_show_encloser);␊ |
534 | }␊ |
535 | }␊ |
536 | ␊ |
537 | ␊ |
538 | // Name: content_diff␊ |
539 | // Arguments:␊ |
540 | // (optional) one or more files to include␊ |
541 | // Added in: 4.0␊ |
542 | // Purpose: Availability of mtn diff as automate command.␊ |
543 | //␊ |
544 | // Output format: Like mtn diff, but with the header part omitted (as this is␊ |
545 | // doubles the output of automate get_revision). If no content changes happened,␊ |
546 | // the output is empty. All file operations beside mtn add are omitted,␊ |
547 | // as they don't change the content of the file.␊ |
548 | CMD_AUTOMATE(content_diff, N_("[FILE [...]]"),␊ |
549 | N_("Calculates diffs of files"),␊ |
550 | "",␊ |
551 | options::opts::revision | options::opts::depth |␊ |
552 | options::opts::exclude)␊ |
553 | {␊ |
554 | cset included;␊ |
555 | std::string dummy_header;␊ |
556 | bool new_is_archived;␊ |
557 | database db(app);␊ |
558 | ␊ |
559 | prepare_diff(app, db, included, args, new_is_archived, dummy_header);␊ |
560 | ␊ |
561 | dump_diffs(app.lua, db, included, output,␊ |
562 | app.opts.diff_format, new_is_archived, !app.opts.no_show_encloser);␊ |
563 | }␊ |
564 | ␊ |
565 | ␊ |
566 | static void␊ |
567 | log_certs(project_t & project, ostream & os, revision_id id, cert_name name,␊ |
568 | string label, string separator, bool multiline, bool newline)␊ |
569 | {␊ |
570 | vector< revision<cert> > certs;␊ |
571 | bool first = true;␊ |
572 | ␊ |
573 | if (multiline)␊ |
574 | newline = true;␊ |
575 | ␊ |
576 | project.get_revision_certs_by_name(id, name, certs);␊ |
577 | for (vector< revision<cert> >::const_iterator i = certs.begin();␊ |
578 | i != certs.end(); ++i)␊ |
579 | {␊ |
580 | if (first)␊ |
581 | os << label;␊ |
582 | else␊ |
583 | os << separator;␊ |
584 | ␊ |
585 | if (multiline)␊ |
586 | os << "\n\n";␊ |
587 | os << i->inner().value;␊ |
588 | if (newline)␊ |
589 | os << '\n';␊ |
590 | ␊ |
591 | first = false;␊ |
592 | }␊ |
593 | }␊ |
594 | ␊ |
595 | static void␊ |
596 | log_certs(project_t & project, ostream & os, revision_id id, cert_name name,␊ |
597 | string label, bool multiline)␊ |
598 | {␊ |
599 | log_certs(project, os, id, name, label, label, multiline, true);␊ |
600 | }␊ |
601 | ␊ |
602 | static void␊ |
603 | log_certs(project_t & project, ostream & os, revision_id id, cert_name name)␊ |
604 | {␊ |
605 | log_certs(project, os, id, name, " ", ",", false, false);␊ |
606 | }␊ |
607 | ␊ |
608 | ␊ |
609 | struct rev_cmp␊ |
610 | {␊ |
611 | bool dir;␊ |
612 | rev_cmp(bool _dir) : dir(_dir) {}␊ |
613 | bool operator() (pair<rev_height, revision_id> const & x,␊ |
614 | pair<rev_height, revision_id> const & y) const␊ |
615 | {␊ |
616 | return dir ? (x.first < y.first) : (x.first > y.first);␊ |
617 | }␊ |
618 | };␊ |
619 | ␊ |
620 | typedef priority_queue<pair<rev_height, revision_id>,␊ |
621 | vector<pair<rev_height, revision_id> >,␊ |
622 | rev_cmp> frontier_t;␊ |
623 | ␊ |
624 | CMD(log, "log", "", CMD_REF(informative), N_("[FILE] ..."),␊ |
625 | N_("Prints history in reverse order"),␊ |
626 | N_("This command prints history in reverse order, filtering it by "␊ |
627 | "FILE if given. If one or more revisions are given, uses them as "␊ |
628 | "a starting point."),␊ |
629 | options::opts::last | options::opts::next␊ |
630 | | options::opts::from | options::opts::to␊ |
631 | | options::opts::brief | options::opts::diffs␊ |
632 | | options::opts::no_merges | options::opts::no_files␊ |
633 | | options::opts::no_graph)␊ |
634 | {␊ |
635 | database db(app);␊ |
636 | project_t project(db);␊ |
637 | ␊ |
638 | long last = app.opts.last;␊ |
639 | long next = app.opts.next;␊ |
640 | ␊ |
641 | N(last == -1 || next == -1,␊ |
642 | F("only one of --last/--next allowed"));␊ |
643 | ␊ |
644 | frontier_t frontier(rev_cmp(!(next>0)));␊ |
645 | revision_id first_rid; // for mapping paths to node ids when restricted␊ |
646 | ␊ |
647 | if (app.opts.from.size() == 0)␊ |
648 | {␊ |
649 | workspace work(app,␊ |
650 | F("try passing a --from revision to start at"));␊ |
651 | ␊ |
652 | revision_t rev;␊ |
653 | work.get_work_rev(rev);␊ |
654 | for (edge_map::const_iterator i = rev.edges.begin();␊ |
655 | i != rev.edges.end(); i++)␊ |
656 | {␊ |
657 | rev_height height;␊ |
658 | db.get_rev_height(edge_old_revision(i), height);␊ |
659 | frontier.push(make_pair(height, edge_old_revision(i)));␊ |
660 | }␊ |
661 | }␊ |
662 | else␊ |
663 | {␊ |
664 | for (args_vector::const_iterator i = app.opts.from.begin();␊ |
665 | i != app.opts.from.end(); i++)␊ |
666 | {␊ |
667 | set<revision_id> rids;␊ |
668 | complete(app.opts, app.lua, project, (*i)(), rids);␊ |
669 | for (set<revision_id>::const_iterator j = rids.begin();␊ |
670 | j != rids.end(); ++j)␊ |
671 | {␊ |
672 | rev_height height;␊ |
673 | db.get_rev_height(*j, height);␊ |
674 | frontier.push(make_pair(height, *j));␊ |
675 | }␊ |
676 | if (i == app.opts.from.begin())␊ |
677 | first_rid = *rids.begin();␊ |
678 | }␊ |
679 | }␊ |
680 | ␊ |
681 | node_restriction mask;␊ |
682 | ␊ |
683 | if (args.size() > 0)␊ |
684 | {␊ |
685 | // User wants to trace only specific files␊ |
686 | if (app.opts.from.size() == 0)␊ |
687 | {␊ |
688 | workspace work(app);␊ |
689 | roster_t new_roster;␊ |
690 | parent_map parents;␊ |
691 | temp_node_id_source nis;␊ |
692 | ␊ |
693 | work.get_parent_rosters(db, parents);␊ |
694 | work.get_current_roster_shape(db, nis, new_roster);␊ |
695 | ␊ |
696 | mask = node_restriction(work, args_to_paths(args),␊ |
697 | args_to_paths(app.opts.exclude_patterns), ␊ |
698 | app.opts.depth, parents, new_roster);␊ |
699 | }␊ |
700 | else␊ |
701 | {␊ |
702 | // FIXME_RESTRICTIONS: should this add paths from the rosters of␊ |
703 | // all selected revs?␊ |
704 | roster_t roster;␊ |
705 | db.get_roster(first_rid, roster);␊ |
706 | ␊ |
707 | mask = node_restriction(args_to_paths(args),␊ |
708 | args_to_paths(app.opts.exclude_patterns), ␊ |
709 | app.opts.depth, roster);␊ |
710 | }␊ |
711 | }␊ |
712 | ␊ |
713 | // If --to was given, don't log past those revisions.␊ |
714 | set<revision_id> disallowed;␊ |
715 | bool use_disallowed(!app.opts.to.empty());␊ |
716 | if (use_disallowed)␊ |
717 | {␊ |
718 | std::deque<revision_id> to;␊ |
719 | for (args_vector::const_iterator i = app.opts.to.begin();␊ |
720 | i != app.opts.to.end(); i++)␊ |
721 | {␊ |
722 | MM(*i);␊ |
723 | set<revision_id> rids;␊ |
724 | complete(app.opts, app.lua, project, (*i)(), rids);␊ |
725 | for (set<revision_id>::const_iterator j = rids.begin();␊ |
726 | j != rids.end(); ++j)␊ |
727 | {␊ |
728 | I(!null_id(*j));␊ |
729 | pair<set<revision_id>::iterator, bool> res = disallowed.insert(*j);␊ |
730 | if (res.second)␊ |
731 | {␊ |
732 | to.push_back(*j);␊ |
733 | }␊ |
734 | }␊ |
735 | }␊ |
736 | ␊ |
737 | while (!to.empty())␊ |
738 | {␊ |
739 | revision_id const & rid(to.front());␊ |
740 | MM(rid);␊ |
741 | ␊ |
742 | set<revision_id> relatives;␊ |
743 | MM(relatives);␊ |
744 | if (next > 0)␊ |
745 | {␊ |
746 | db.get_revision_children(rid, relatives);␊ |
747 | }␊ |
748 | else␊ |
749 | {␊ |
750 | db.get_revision_parents(rid, relatives);␊ |
751 | }␊ |
752 | ␊ |
753 | for (set<revision_id>::const_iterator i = relatives.begin();␊ |
754 | i != relatives.end(); ++i)␊ |
755 | {␊ |
756 | if (null_id(*i))␊ |
757 | continue;␊ |
758 | pair<set<revision_id>::iterator, bool> res = disallowed.insert(*i);␊ |
759 | if (res.second)␊ |
760 | {␊ |
761 | to.push_back(*i);␊ |
762 | }␊ |
763 | }␊ |
764 | ␊ |
765 | to.pop_front();␊ |
766 | }␊ |
767 | }␊ |
768 | ␊ |
769 | cert_name author_name(author_cert_name);␊ |
770 | cert_name date_name(date_cert_name);␊ |
771 | cert_name branch_name(branch_cert_name);␊ |
772 | cert_name tag_name(tag_cert_name);␊ |
773 | cert_name changelog_name(changelog_cert_name);␊ |
774 | cert_name comment_name(comment_cert_name);␊ |
775 | ␊ |
776 | // we can use the markings if we walk backwards for a restricted log␊ |
777 | bool use_markings(!(next>0) && !mask.empty());␊ |
778 | ␊ |
779 | set<revision_id> seen;␊ |
780 | revision_t rev;␊ |
781 | // this is instantiated even when not used, but it's lightweight␊ |
782 | asciik graph(cout);␊ |
783 | while(! frontier.empty() && (last == -1 || last > 0)␊ |
784 | && (next == -1 || next > 0))␊ |
785 | {␊ |
786 | revision_id const & rid = frontier.top().second;␊ |
787 | ␊ |
788 | bool print_this = mask.empty();␊ |
789 | set<file_path> diff_paths;␊ |
790 | ␊ |
791 | if (null_id(rid) || seen.find(rid) != seen.end())␊ |
792 | {␊ |
793 | frontier.pop();␊ |
794 | continue;␊ |
795 | }␊ |
796 | ␊ |
797 | seen.insert(rid);␊ |
798 | db.get_revision(rid, rev);␊ |
799 | ␊ |
800 | set<revision_id> marked_revs;␊ |
801 | ␊ |
802 | if (!mask.empty())␊ |
803 | {␊ |
804 | roster_t roster;␊ |
805 | marking_map markings;␊ |
806 | db.get_roster(rid, roster, markings);␊ |
807 | ␊ |
808 | // get all revision ids mentioned in one of the markings␊ |
809 | for (marking_map::const_iterator m = markings.begin();␊ |
810 | m != markings.end(); ++m)␊ |
811 | {␊ |
812 | node_id node = m->first;␊ |
813 | marking_t marking = m->second;␊ |
814 | ␊ |
815 | if (mask.includes(roster, node))␊ |
816 | {␊ |
817 | marked_revs.insert(marking.file_content.begin(), marking.file_content.end());␊ |
818 | marked_revs.insert(marking.parent_name.begin(), marking.parent_name.end());␊ |
819 | for (map<attr_key, set<revision_id> >::const_iterator a = marking.attrs.begin();␊ |
820 | a != marking.attrs.end(); ++a)␊ |
821 | marked_revs.insert(a->second.begin(), a->second.end());␊ |
822 | }␊ |
823 | }␊ |
824 | ␊ |
825 | // find out whether the current rev is to be printed␊ |
826 | // we don't care about changed paths if it is not marked␊ |
827 | if (!use_markings || marked_revs.find(rid) != marked_revs.end())␊ |
828 | {␊ |
829 | set<node_id> nodes_modified;␊ |
830 | select_nodes_modified_by_rev(db, rev, roster,␊ |
831 | nodes_modified);␊ |
832 | ␊ |
833 | for (set<node_id>::const_iterator n = nodes_modified.begin();␊ |
834 | n != nodes_modified.end(); ++n)␊ |
835 | {␊ |
836 | // a deleted node will be "modified" but won't␊ |
837 | // exist in the result.␊ |
838 | // we don't want to print them.␊ |
839 | if (roster.has_node(*n) && mask.includes(roster, *n))␊ |
840 | {␊ |
841 | print_this = true;␊ |
842 | if (app.opts.diffs)␊ |
843 | {␊ |
844 | file_path fp;␊ |
845 | roster.get_name(*n, fp);␊ |
846 | diff_paths.insert(fp);␊ |
847 | }␊ |
848 | }␊ |
849 | }␊ |
850 | }␊ |
851 | }␊ |
852 | ␊ |
853 | if (app.opts.no_merges && rev.is_merge_node())␊ |
854 | print_this = false;␊ |
855 | ␊ |
856 | set<revision_id> interesting;␊ |
857 | // if rid is not marked we can jump directly to the marked ancestors,␊ |
858 | // otherwise we need to visit the parents␊ |
859 | if (use_markings && marked_revs.find(rid) == marked_revs.end())␊ |
860 | {␊ |
861 | interesting.insert(marked_revs.begin(), marked_revs.end());␊ |
862 | }␊ |
863 | else␊ |
864 | {␊ |
865 | if (next > 0)␊ |
866 | db.get_revision_children(rid, interesting);␊ |
867 | else // walk backwards by default␊ |
868 | db.get_revision_parents(rid, interesting);␊ |
869 | }␊ |
870 | ␊ |
871 | if (print_this)␊ |
872 | {␊ |
873 | ostringstream out;␊ |
874 | if (app.opts.brief)␊ |
875 | {␊ |
876 | out << rid;␊ |
877 | log_certs(project, out, rid, author_name);␊ |
878 | if (app.opts.no_graph)␊ |
879 | log_certs(project, out, rid, date_name);␊ |
880 | else␊ |
881 | {␊ |
882 | out << '\n';␊ |
883 | log_certs(project, out, rid, date_name,␊ |
884 | string(), string(), false, false);␊ |
885 | }␊ |
886 | log_certs(project, out, rid, branch_name);␊ |
887 | out << '\n';␊ |
888 | }␊ |
889 | else␊ |
890 | {␊ |
891 | out << string(65, '-') << '\n';␊ |
892 | out << "Revision: " << rid << '\n';␊ |
893 | ␊ |
894 | changes_summary csum;␊ |
895 | ␊ |
896 | set<revision_id> ancestors;␊ |
897 | ␊ |
898 | for (edge_map::const_iterator e = rev.edges.begin();␊ |
899 | e != rev.edges.end(); ++e)␊ |
900 | {␊ |
901 | ancestors.insert(edge_old_revision(e));␊ |
902 | csum.add_change_set(edge_changes(e));␊ |
903 | }␊ |
904 | ␊ |
905 | for (set<revision_id>::const_iterator anc = ancestors.begin();␊ |
906 | anc != ancestors.end(); ++anc)␊ |
907 | out << "Ancestor: " << *anc << '\n';␊ |
908 | ␊ |
909 | log_certs(project, out, rid, author_name, "Author: ", false);␊ |
910 | log_certs(project, out, rid, date_name, "Date: ", false);␊ |
911 | log_certs(project, out, rid, branch_name, "Branch: ", false);␊ |
912 | log_certs(project, out, rid, tag_name, "Tag: ", false);␊ |
913 | ␊ |
914 | if (!app.opts.no_files && !csum.cs.empty())␊ |
915 | {␊ |
916 | out << '\n';␊ |
917 | csum.print(out, 70);␊ |
918 | out << '\n';␊ |
919 | }␊ |
920 | ␊ |
921 | log_certs(project, out, rid, changelog_name, "ChangeLog: ", true);␊ |
922 | log_certs(project, out, rid, comment_name, "Comments: ", true);␊ |
923 | }␊ |
924 | ␊ |
925 | if (app.opts.diffs)␊ |
926 | {␊ |
927 | for (edge_map::const_iterator e = rev.edges.begin();␊ |
928 | e != rev.edges.end(); ++e)␊ |
929 | dump_diffs(app.lua, db, edge_changes(e), diff_paths, out,␊ |
930 | app.opts.diff_format, true,␊ |
931 | !app.opts.no_show_encloser, !mask.empty());␊ |
932 | }␊ |
933 | ␊ |
934 | if (next > 0)␊ |
935 | next--;␊ |
936 | else if (last > 0)␊ |
937 | last--;␊ |
938 | ␊ |
939 | string out_system;␊ |
940 | utf8_to_system_best_effort(utf8(out.str()), out_system);␊ |
941 | if (app.opts.no_graph)␊ |
942 | cout << out_system;␊ |
943 | else␊ |
944 | graph.print(rid, interesting, out_system);␊ |
945 | }␊ |
946 | else if (use_markings && !app.opts.no_graph)␊ |
947 | graph.print(rid, interesting,␊ |
948 | (F("(Revision: %s)") % rid).str());␊ |
949 | ␊ |
950 | frontier.pop(); // beware: rid is invalid from now on␊ |
951 | ␊ |
952 | for (set<revision_id>::const_iterator i = interesting.begin();␊ |
953 | i != interesting.end(); ++i)␊ |
954 | {␊ |
955 | if (use_disallowed && (disallowed.find(*i) != disallowed.end()))␊ |
956 | {␊ |
957 | continue;␊ |
958 | }␊ |
959 | rev_height height;␊ |
960 | db.get_rev_height(*i, height);␊ |
961 | frontier.push(make_pair(height, *i));␊ |
962 | }␊ |
963 | }␊ |
964 | }␊ |
965 | ␊ |
966 | // Local Variables:␊ |
967 | // mode: C++␊ |
968 | // fill-column: 76␊ |
969 | // c-file-style: "gnu"␊ |
970 | // indent-tabs-mode: nil␊ |
971 | // End:␊ |
972 | // vim: et:sw=2:sts=2:ts=2:cino=>2s,{s,\:s,+s,t0,g0,^-2,e-2,n-2,p2s,(0,=s:␊ |