monotone

monotone Mtn Source Tree

Root/src/cmd_ws_commit.cc

1// Copyright (C) 2010, 2011, 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 <deque>
13#include <iostream>
14#include <map>
15#include <sstream>
16
17#include "cmd.hh"
18#include "merge_content.hh"
19#include "file_io.hh"
20#include "restrictions.hh"
21#include "revision.hh"
22#include "selectors.hh"
23#include "transforms.hh"
24#include "work.hh"
25#include "charset.hh"
26#include "app_state.hh"
27#include "project.hh"
28#include "basic_io.hh"
29#include "xdelta.hh"
30#include "keys.hh"
31#include "key_store.hh"
32#include "maybe_workspace_updater.hh"
33#include "simplestring_xform.hh"
34#include "database.hh"
35#include "date_format.hh"
36#include "roster.hh"
37#include "rev_output.hh"
38#include "vocab_cast.hh"
39
40using std::cout;
41using std::make_pair;
42using std::make_pair;
43using std::map;
44using std::ostringstream;
45using std::pair;
46using std::set;
47using std::string;
48using std::vector;
49
50using boost::shared_ptr;
51
52static void
53get_old_branch_names(database & db, parent_map const & parents,
54 set<branch_name> & old_branch_names)
55{
56 for (parent_map::const_iterator i = parents.begin();
57 i != parents.end(); ++i)
58 {
59 vector<cert> branches;
60 db.get_revision_certs(parent_id(i), branch_cert_name, branches);
61 for (vector<cert>::const_iterator b = branches.begin();
62 b != branches.end(); ++b)
63 {
64 old_branch_names.insert(typecast_vocab<branch_name>(b->value));
65 }
66 }
67}
68
69class message_reader
70{
71public:
72 message_reader(string const & message, size_t offset) :
73 message(message), offset(offset) {}
74
75 bool read(string const & text)
76 {
77 size_t len = text.length();
78 if (message.compare(offset, len, text) == 0)
79 {
80 offset += len;
81 return true;
82 }
83 else
84 return false;
85 }
86
87 string readline()
88 {
89 size_t eol = message.find_first_of("\r\n", offset);
90 if (eol == string::npos)
91 return "";
92
93 size_t len = eol - offset;
94 string line = message.substr(offset, len);
95 offset = eol+1;
96
97 if (message[eol] == '\r' && message.length() > eol+1 &&
98 message[eol+1] == '\n')
99 offset++;
100
101 return trim(line);
102 }
103
104 bool contains(string const & summary)
105 {
106 return message.find(summary, offset) != string::npos;
107 }
108
109 bool remove(string const & summary)
110 {
111 size_t pos = message.find(summary, offset);
112 I(pos != string::npos);
113 if (pos + summary.length() == message.length())
114 {
115 message.erase(pos);
116 return true;
117 }
118 else
119 return false;
120 }
121
122 string content()
123 {
124 return message.substr(offset);
125 }
126
127private:
128 string message;
129 size_t offset;
130};
131
132static bool
133date_fmt_valid(string date_fmt)
134{
135 if (date_fmt.empty())
136 {
137 return true;
138 }
139 else
140 {
141 // check that the specified date format can be used to format and
142 // parse a date
143 date_t now = date_t::now();
144 date_t parsed;
145 try
146 {
147 string formatted = now.as_formatted_localtime(date_fmt);
148 parsed = date_t::from_formatted_localtime(formatted, date_fmt);
149 }
150 catch (recoverable_failure const & e)
151 {
152 L(FL("date check failed: %s") % e.what());
153 }
154
155 if (parsed != now)
156 {
157 L(FL("date check failed: %s != %s") % now % parsed);
158 return false;
159 }
160 else
161 {
162 return true;
163 }
164 }
165}
166
167static void
168get_log_message_interactively(lua_hooks & lua, workspace & work,
169 project_t & project,
170 revision_id const rid, revision_t const & rev,
171 string & author, date_t & date, branch_name & branch,
172 set<branch_name> const & old_branches,
173 string const & date_fmt, utf8 & log_message)
174{
175 utf8 backup;
176 work.load_commit_text(backup);
177
178 E(backup().empty(), origin::user,
179 F("a backup from a previously failed commit exists in '_MTN/commit'.\n"
180 "This file must be removed before commit will proceed.\n"
181 "You may recover the previous message from this file if necessary."));
182
183 utf8 instructions(
184 _("-- Enter a description of this change above --\n"
185 "-- You may edit the fields below --\n"));
186
187 utf8 ignored(
188 _("\n-- Modifications below this line are ignored --\n"));
189
190 utf8 cancel(_("*** REMOVE THIS LINE TO CANCEL THE COMMIT ***\n"));
191
192 utf8 const BRANCH(_("Branch: "));
193 utf8 const AUTHOR(_("Author: "));
194 utf8 const DATE( _("Date: "));
195
196 bool is_date_fmt_valid = date_fmt_valid(date_fmt);
197
198 utf8 changelog;
199 work.read_user_log(changelog);
200
201 // ensure there are two blank lines after the changelog
202
203 changelog = utf8(changelog() + "\n\n", origin::user);
204
205 // build editable fields
206 utf8 editable;
207 {
208 ostringstream oss;
209
210 oss << BRANCH << ' ' << branch << '\n';
211 oss << AUTHOR << ' ' << author << '\n';
212
213 if (!is_date_fmt_valid)
214 {
215 W(F("date format '%s' cannot be parsed; using default instead") % date_fmt);
216 }
217
218 if (!is_date_fmt_valid || date_fmt.empty())
219 {
220 oss << DATE << ' ' << date << '\n';
221 }
222 else
223 {
224 oss << DATE << ' ' << date.as_formatted_localtime(date_fmt) << '\n';
225 }
226
227 editable = utf8(oss.str().c_str());
228 }
229
230 // Build notes
231 utf8 notes;
232 {
233 ostringstream oss;
234
235 if (!old_branches.empty() && old_branches.find(branch) == old_branches.end())
236 {
237 oss << _("*** THIS REVISION WILL CREATE A NEW BRANCH ***") << "\n\n";
238 for (set<branch_name>::const_iterator i = old_branches.begin();
239 i != old_branches.end(); ++i)
240 oss << _("Old Branch: ") << *i << '\n';
241 oss << _("New Branch: ") << branch << "\n\n";
242 }
243 set<revision_id> heads;
244 project.get_branch_heads(branch, heads, false);
245 if (!heads.empty())
246 {
247 for (edge_map::const_iterator e = rev.edges.begin();
248 e != rev.edges.end(); ++e)
249 {
250 if (heads.find(edge_old_revision(e)) == heads.end())
251 {
252 oss << _("*** THIS REVISION WILL CREATE DIVERGENCE ***") << "\n\n";
253 break;
254 }
255 }
256 }
257
258 notes = utf8(oss.str().c_str());
259 }
260
261 utf8 summary;
262 revision_summary(rev, summary);
263
264 utf8 full_message(changelog() + cancel() + instructions() + editable() + ignored() +
265 notes() + summary(),
266 origin::internal);
267
268 external input_message;
269 external output_message;
270
271 utf8_to_system_best_effort(full_message, input_message);
272
273 E(lua.hook_edit_comment(input_message, output_message),
274 origin::user,
275 F("edit of log message failed"));
276
277 system_to_utf8(output_message, full_message);
278
279 // Everything up to the cancel message is the changelog; trailing blank
280 // lines trimmed.
281 size_t changelog_end = full_message().find(cancel());
282 if (changelog_end == string::npos)
283 {
284 // try to save the edited changelog, now delimited by the instructions.
285 changelog_end = full_message().find(instructions());
286 if (changelog_end != string::npos)
287 work.write_user_log(utf8(trim_right(full_message().substr(0, changelog_end)) + '\n', origin::user));
288
289 E(false, origin::user, F("commit cancelled."));
290 }
291
292 // save the message in _MTN/commit so it's not lost if something fails
293 // below
294 work.save_commit_text(full_message);
295
296 string content = trim_right(full_message().substr(0, changelog_end)) + '\n';
297 log_message = utf8(content, origin::user);
298
299 message_reader message(full_message(), changelog_end);
300
301 // Parse the editable fields.
302
303 // this can't fail, since we start reading where we found it above.
304 message.read(cancel());
305
306 E(message.read(instructions()), origin::user,
307 F("commit failed. Instructions not found."));
308
309 // Branch:
310
311 E(message.read(trim_right(BRANCH())), origin::user,
312 F("commit failed. Branch header not found."));
313
314 string b = message.readline();
315
316 E(!b.empty(), origin::user,
317 F("commit failed. Branch value empty."));
318
319 branch = branch_name(b, origin::user);
320
321 // Author:
322
323 E(message.read(trim_right(AUTHOR())), origin::user,
324 F("commit failed. Author header not found."));
325
326 author = message.readline();
327
328 E(!author.empty(), origin::user,
329 F("commit failed. Author value empty."));
330
331 // Date:
332
333 E(message.read(trim_right(DATE())), origin::user,
334 F("commit failed. Date header not found."));
335
336 string d = message.readline();
337
338 E(!d.empty(), origin::user,
339 F("commit failed. Date value empty."));
340
341 if (!is_date_fmt_valid || date_fmt.empty())
342 date = date_t(d);
343 else
344 date = date_t::from_formatted_localtime(d, date_fmt);
345
346 // rest is ignored
347
348 // remove the backup file now that all values have been extracted
349 work.clear_commit_text();
350}
351
352void
353revert(app_state & app,
354 args_vector const & args,
355 bool undrop)
356{
357 roster_t old_roster, new_roster;
358 cset preserved;
359
360 E(app.opts.missing || !args.empty() || !app.opts.exclude.empty(),
361 origin::user,
362 F("you must pass at least one path to 'revert' (perhaps '.')"));
363
364 database db(app);
365 workspace work(app);
366
367 parent_map parents;
368 work.get_parent_rosters(db, parents);
369 E(parents.size() == 1, origin::user,
370 F("this command can only be used in a single-parent workspace"));
371 old_roster = parent_roster(parents.begin());
372
373 {
374 temp_node_id_source nis;
375 work.get_current_roster_shape(db, nis, new_roster);
376 }
377
378 node_restriction mask(args_to_paths(args),
379 args_to_paths(app.opts.exclude),
380 app.opts.depth,
381 old_roster, new_roster, ignored_file(work),
382 restriction::explicit_includes);
383
384 if (app.opts.missing)
385 {
386 // --missing is a further filter on the files included by a
387 // restriction we first find all missing files included by the
388 // specified args and then make a restriction that includes only
389 // these missing files.
390 set<file_path> missing;
391 work.find_missing(new_roster, mask, missing);
392 if (missing.empty())
393 {
394 P(F("no missing files to revert"));
395 return;
396 }
397
398 std::vector<file_path> missing_files;
399 for (set<file_path>::const_iterator i = missing.begin();
400 i != missing.end(); i++)
401 {
402 L(FL("reverting missing file: %s") % *i);
403 missing_files.push_back(*i);
404 }
405 // replace the original mask with a more restricted one
406 mask = node_restriction(missing_files,
407 std::vector<file_path>(), app.opts.depth,
408 old_roster, new_roster, ignored_file(work));
409 }
410
411 // We want the restricted roster to include all the changes
412 // that are to be *kept*. Then, the changes to revert are those
413 // from the new roster *back* to the restricted roster
414
415 // The arguments to revert are paths to be reverted *not* paths to be left
416 // intact. The restriction formed from these arguments will include the
417 // changes to be reverted and excludes the changes to be kept. To form
418 // the correct restricted roster this restriction must be applied to the
419 // old and new rosters in reverse order, from new *back* to old.
420
421 roster_t restricted_roster;
422 make_restricted_roster(new_roster, old_roster, restricted_roster,
423 mask);
424
425 make_cset(old_roster, restricted_roster, preserved);
426
427 // At this point, all three rosters have accounted for additions,
428 // deletions and renames but they all have content hashes from the
429 // original old roster. This is fine, when reverting files we want to
430 // revert them back to their original content.
431
432 // The preserved cset will be left pending in MTN/revision
433
434 // if/when reverting through the editable_tree interface use
435 // make_cset(new_roster, restricted_roster, reverted);
436 // to get a cset that gets us back to the restricted roster
437 // from the current workspace roster
438
439 node_map const & nodes = restricted_roster.all_nodes();
440
441 for (node_map::const_iterator i = nodes.begin();
442 i != nodes.end(); ++i)
443 {
444 node_id nid = i->first;
445 node_t node = i->second;
446
447 if (restricted_roster.is_root(nid))
448 continue;
449
450 if (!mask.includes(restricted_roster, nid))
451 continue;
452
453 file_path path;
454 restricted_roster.get_name(nid, path);
455
456 if (is_file_t(node))
457 {
458 file_t f = downcast_to_file_t(node);
459
460 bool revert = true;
461
462 if (file_exists(path))
463 {
464 file_id ident;
465 calculate_ident(path, ident);
466 // don't touch unchanged files
467 if (ident == f->content)
468 {
469 L(FL("skipping unchanged %s") % path);
470 revert = false;
471 }
472 else
473 {
474 revert = !undrop;
475 }
476 }
477
478 if (revert)
479 {
480 P(F("reverting %s") % path);
481 L(FL("reverting %s to [%s]") % path
482 % f->content);
483
484 E(db.file_version_exists(f->content), origin::user,
485 F("no file version %s found in database for '%s'")
486 % f->content % path);
487
488 file_data dat;
489 L(FL("writing file %s to %s")
490 % f->content % path);
491 db.get_file_version(f->content, dat);
492 write_data(path, dat.inner());
493 }
494 }
495 else
496 {
497 if (!directory_exists(path))
498 {
499 P(F("recreating '%s/'") % path);
500 mkdir_p(path);
501 }
502 else
503 {
504 L(FL("skipping existing %s/") % path);
505 }
506 }
507
508 // revert attributes on this node -- this doesn't quite catch all cases:
509 // if the execute bits are manually set on some path that doesn't have
510 // a dormant mtn:execute the execute bits will not be cleared
511 // FIXME: check execute bits against mtn:execute explicitly?
512
513 for (attr_map_t::const_iterator a = node->attrs.begin();
514 a != node->attrs.end(); ++a)
515 {
516 L(FL("reverting %s on %s") % a->first() % path);
517 if (a->second.first)
518 app.lua.hook_set_attribute(a->first(), path,
519 a->second.second());
520 else
521 app.lua.hook_clear_attribute(a->first(), path);
522 }
523 }
524
525 // Included_work is thrown away which effectively reverts any adds,
526 // drops and renames it contains. Drops and rename sources will have
527 // been rewritten above but this may leave rename targets laying
528 // around.
529
530 revision_t remaining;
531 make_revision_for_workspace(parent_id(parents.begin()), preserved, remaining);
532
533 work.put_work_rev(remaining);
534 work.maybe_update_inodeprints(db, mask);
535}
536
537CMD(revert, "revert", "", CMD_REF(workspace), N_("[PATH]..."),
538 N_("Reverts files and/or directories"),
539 N_("In order to revert the entire workspace, specify '.' as the "
540 "file name."),
541 options::opts::depth | options::opts::exclude | options::opts::missing)
542{
543 revert(app, args, false);
544}
545
546CMD(undrop, "undrop", "", CMD_REF(workspace), N_("PATH..."),
547 N_("Reverses a mistaken 'drop'"),
548 N_("If the file was deleted from the workspace, this is the same as 'revert'. "
549 "Otherwise, it just removes the 'drop' from the manifest."),
550 options::opts::none)
551{
552 revert(app, args, true);
553}
554
555static void
556walk_revisions(database & db, const revision_id & from_rev,
557 const revision_id & to_rev)
558{
559 revision_id r = from_rev;
560 revision_t rev;
561
562 do
563 {
564 E(!null_id(r), origin::user,
565 F("revision %s it not a child of %s, cannot invert")
566 % from_rev % to_rev);
567
568 db.get_revision(r, rev);
569 E(rev.edges.size() < 2, origin::user,
570 F("revision %s has %d parents, cannot invert")
571 % r % rev.edges.size());
572
573 E(rev.edges.size() > 0, origin::user,
574 F("revision %s it not a child of %s, cannot invert")
575 % from_rev % to_rev);
576 r = edge_old_revision (rev.edges.begin());
577 }
578 while (r != to_rev);
579}
580
581CMD(disapprove, "disapprove", "", CMD_REF(review),
582 N_("[PARENT-REVISION] CHILD-REVISION"),
583 N_("Disapproves a particular revision or revision range"),
584 "",
585 options::opts::branch | options::opts::messages | options::opts::date |
586 options::opts::author | options::opts::auto_update)
587{
588 database db(app);
589 key_store keys(app);
590 project_t project(db);
591
592 if (args.size() < 1 || args.size() > 2)
593 throw usage(execid);
594
595 maybe_workspace_updater updater(app, project);
596
597 utf8 log_message("");
598 bool log_message_given;
599 revision_id child_rev, parent_rev;
600 revision_t rev, rev_inverse;
601 shared_ptr<cset> cs_inverse(new cset());
602
603 if (args.size() == 1)
604 {
605 complete(app.opts, app.lua, project, idx(args, 0)(), child_rev);
606 db.get_revision(child_rev, rev);
607
608 E(rev.edges.size() == 1, origin::user,
609 F("revision %s has %d parents, cannot invert")
610 % child_rev % rev.edges.size());
611
612 guess_branch(app.opts, project, child_rev);
613 E(!app.opts.branch().empty(), origin::user,
614 F("need '--branch' argument for disapproval"));
615
616 process_commit_message_args(app.opts, log_message_given, log_message,
617 utf8((FL("disapproval of revision '%s'")
618 % child_rev).str(),
619 origin::internal));
620 }
621 else if (args.size() == 2)
622 {
623 complete(app.opts, app.lua, project, idx(args, 0)(), parent_rev);
624 complete(app.opts, app.lua, project, idx(args, 1)(), child_rev);
625
626 set<revision_id> rev_set;
627
628 rev_set.insert(child_rev);
629 rev_set.insert(parent_rev);
630
631 erase_ancestors(db, rev_set);
632 if (rev_set.size() > 1)
633 {
634 set<revision_id> ancestors;
635 db.get_common_ancestors (rev_set, ancestors);
636 E(ancestors.size() > 0, origin::user,
637 F("revisions %s and %s do not share common history, cannot invert")
638 % parent_rev % child_rev);
639 E(ancestors.size() < 1, origin::user,
640 F("revisions share common history"
641 ", but %s is not an ancestor of %s, cannot invert")
642 % parent_rev % child_rev);
643 }
644
645 walk_revisions(db, child_rev, parent_rev);
646 db.get_revision(parent_rev, rev);
647
648 E(rev.edges.size() == 1, origin::user,
649 F("revision %s has %d parents, cannot invert")
650 % child_rev % rev.edges.size());
651
652 guess_branch(app.opts, project, child_rev);
653 E(!app.opts.branch().empty(), origin::user,
654 F("need '--branch' argument for disapproval"));
655
656 process_commit_message_args(app.opts, log_message_given, log_message,
657 utf8((FL("disapproval of revisions "
658 "'%s'..'%s'")
659 % parent_rev % child_rev).str(),
660 origin::internal));
661 }
662
663 cache_user_key(app.opts, project, keys, app.lua);
664
665 // for the divergence check, below
666 set<revision_id> heads;
667 project.get_branch_heads(app.opts.branch, heads,
668 app.opts.ignore_suspend_certs);
669 unsigned int old_head_size = heads.size();
670
671 edge_entry const & old_edge (*rev.edges.begin());
672 E(!null_id(edge_old_revision(old_edge)), origin::user,
673 F("cannot disapprove root revision"));
674 db.get_revision_manifest(edge_old_revision(old_edge),
675 rev_inverse.new_manifest);
676 {
677 roster_t old_roster, new_roster;
678 db.get_roster(edge_old_revision(old_edge), old_roster);
679 db.get_roster(child_rev, new_roster);
680 make_cset(new_roster, old_roster, *cs_inverse);
681 }
682 rev_inverse.edges.insert(make_pair(child_rev, cs_inverse));
683
684 {
685 transaction_guard guard(db);
686
687 revision_id inv_id;
688 revision_data rdat;
689
690 write_revision(rev_inverse, rdat);
691 calculate_ident(rdat, inv_id);
692 db.put_revision(inv_id, rdat);
693
694 project.put_standard_certs_from_options(app.opts, app.lua, keys,
695 inv_id, app.opts.branch,
696 log_message);
697 guard.commit();
698 }
699
700 project.get_branch_heads(app.opts.branch, heads,
701 app.opts.ignore_suspend_certs);
702 if (heads.size() > old_head_size && old_head_size > 0) {
703 P(F("note: this revision creates divergence\n"
704 "note: you may (or may not) wish to run '%s merge'")
705 % prog_name);
706 }
707 updater.maybe_do_update();
708}
709
710CMD(mkdir, "mkdir", "", CMD_REF(workspace), N_("[DIRECTORY...]"),
711 N_("Creates directories and adds them to the workspace"),
712 "",
713 options::opts::no_ignore)
714{
715 if (args.size() < 1)
716 throw usage(execid);
717
718 database db(app);
719 workspace work(app);
720
721 set<file_path> paths;
722 // spin through args and try to ensure that we won't have any collisions
723 // before doing any real filesystem modification. we'll also verify paths
724 // against .mtn-ignore here.
725 for (args_vector::const_iterator i = args.begin(); i != args.end(); ++i)
726 {
727 file_path fp = file_path_external(*i);
728 require_path_is_nonexistent
729 (fp, F("directory '%s' already exists") % fp);
730
731 // we'll treat this as a user (fatal) error. it really wouldn't make
732 // sense to add a dir to .mtn-ignore and then try to add it to the
733 // project with a mkdir statement, but one never can tell...
734 E(app.opts.no_ignore || !work.ignore_file(fp),
735 origin::user,
736 F("ignoring directory '%s' (see '.mtn-ignore')") % fp);
737
738 paths.insert(fp);
739 }
740
741 // this time, since we've verified that there should be no collisions,
742 // we'll just go ahead and do the filesystem additions.
743 for (set<file_path>::const_iterator i = paths.begin(); i != paths.end(); ++i)
744 mkdir_p(*i);
745
746 work.perform_additions(db, paths, false, !app.opts.no_ignore);
747}
748
749void perform_add(app_state & app,
750 database & db,
751 workspace & work,
752 vector<file_path> roots)
753{
754 bool add_recursive = app.opts.recursive;
755 if (app.opts.unknown)
756 {
757 path_restriction mask(roots, args_to_paths(app.opts.exclude),
758 app.opts.depth, ignored_file(work));
759 set<file_path> unknown;
760 set<file_path> ignored;
761
762 // if no starting paths have been specified use the workspace root
763 if (roots.empty())
764 roots.push_back(file_path());
765
766 work.find_unknown_and_ignored(db, mask, add_recursive, roots, unknown, ignored);
767
768 // This does nothing unless --no-ignore is given
769 work.perform_additions(db, ignored, add_recursive, !app.opts.no_ignore);
770
771 // No need for recursion here; all paths to be added are explicit in unknown
772 work.perform_additions(db, unknown, false, true);
773 }
774 else
775 {
776 // There are at most two roots in a workspace
777 set<file_path> paths = set<file_path>(roots.begin(), roots.end());
778 work.perform_additions(db, paths, add_recursive, !app.opts.no_ignore);
779 }
780}
781
782CMD_PRESET_OPTIONS(add)
783{
784 opts.recursive=false; // match 'ls unknown' and 'add --unknown --recursive'
785}
786CMD(add, "add", "", CMD_REF(workspace), N_("[PATH]..."),
787 N_("Adds files to the workspace"),
788 "",
789 options::opts::unknown | options::opts::no_ignore |
790 options::opts::recursive)
791{
792 if (!app.opts.unknown && (args.size() < 1))
793 throw usage(execid);
794
795 database db(app);
796 workspace work(app);
797
798 vector<file_path> roots = args_to_paths(args);
799
800 perform_add(app, db, work, roots);
801}
802
803void perform_drop(app_state & app,
804 database & db,
805 workspace & work,
806 vector<file_path> roots)
807{
808 set<file_path> paths;
809 if (app.opts.missing)
810 {
811 temp_node_id_source nis;
812 roster_t current_roster_shape;
813 work.get_current_roster_shape(db, nis, current_roster_shape);
814 node_restriction mask(roots,
815 args_to_paths(app.opts.exclude),
816 app.opts.depth,
817 current_roster_shape, ignored_file(work));
818 work.find_missing(current_roster_shape, mask, paths);
819 }
820 else
821 {
822 paths = set<file_path>(roots.begin(), roots.end());
823 }
824
825 work.perform_deletions(db, paths,
826 app.opts.recursive, app.opts.bookkeep_only);
827}
828CMD(drop, "drop", "rm", CMD_REF(workspace), N_("[PATH]..."),
829 N_("Drops files from the workspace"),
830 "",
831 options::opts::bookkeep_only | options::opts::missing | options::opts::recursive)
832{
833 if (!app.opts.missing && (args.size() < 1))
834 throw usage(execid);
835
836 database db(app);
837 workspace work(app);
838
839 perform_drop(app, db, work, args_to_paths(args));
840}
841
842
843CMD(rename, "rename", "mv", CMD_REF(workspace),
844 N_("SRC DEST\n"
845 "SRC1 [SRC2 [...]] DEST_DIR"),
846 N_("Renames entries in the workspace"),
847 "",
848 options::opts::bookkeep_only)
849{
850 if (args.size() < 2)
851 throw usage(execid);
852
853 database db(app);
854 workspace work(app);
855
856 utf8 dstr = args.back();
857 file_path dst_path = file_path_external(dstr);
858
859 set<file_path> src_paths;
860 for (size_t i = 0; i < args.size()-1; i++)
861 {
862 file_path s = file_path_external(idx(args, i));
863 src_paths.insert(s);
864 }
865
866 //this catches the case where the user specifies a directory 'by convention'
867 //that doesn't exist. the code in perform_rename already handles the proper
868 //cases for more than one source item.
869 if (src_paths.size() == 1 && dstr()[dstr().size() -1] == '/')
870 if (get_path_status(*src_paths.begin()) != path::directory)
871 E(get_path_status(dst_path) == path::directory, origin::user,
872 F(_("the specified target directory '%s/' doesn't exist.")) % dst_path);
873
874 work.perform_rename(db, src_paths, dst_path, app.opts.bookkeep_only);
875}
876
877
878CMD(pivot_root, "pivot_root", "", CMD_REF(workspace), N_("NEW_ROOT PUT_OLD"),
879 N_("Renames the root directory"),
880 N_("After this command, the directory that currently "
881 "has the name NEW_ROOT "
882 "will be the root directory, and the directory "
883 "that is currently the root "
884 "directory will have name PUT_OLD.\n"
885 "Use of '--bookkeep-only' is NOT recommended."),
886 options::opts::bookkeep_only | options::opts::move_conflicting_paths)
887{
888 if (args.size() != 2)
889 throw usage(execid);
890
891 database db(app);
892 workspace work(app);
893 file_path new_root = file_path_external(idx(args, 0));
894 file_path put_old = file_path_external(idx(args, 1));
895 work.perform_pivot_root(db, new_root, put_old,
896 app.opts.bookkeep_only,
897 app.opts.move_conflicting_paths);
898}
899
900CMD(status, "status", "", CMD_REF(informative), N_("[PATH]..."),
901 N_("Shows workspace's status information"),
902 "",
903 options::opts::depth | options::opts::exclude)
904{
905 roster_t new_roster;
906 parent_map old_rosters;
907 revision_t rev;
908 temp_node_id_source nis;
909
910 database db(app);
911 project_t project(db);
912 workspace work(app);
913
914 string date_fmt = get_date_format(app.opts, app.lua, date_time_long);
915
916 work.get_parent_rosters(db, old_rosters);
917 work.get_current_roster_shape(db, nis, new_roster);
918
919 node_restriction mask(args_to_paths(args),
920 args_to_paths(app.opts.exclude),
921 app.opts.depth,
922 old_rosters, new_roster, ignored_file(work));
923
924 work.update_current_roster_from_filesystem(new_roster, mask);
925 make_restricted_revision(old_rosters, new_roster, mask, rev);
926
927 vector<bisect::entry> info;
928 work.get_bisect_info(info);
929
930 if (!info.empty())
931 {
932 bisect::entry start = *info.begin();
933 I(start.first == bisect::start);
934
935 if (old_rosters.size() == 1)
936 {
937 revision_id current_id = parent_id(*old_rosters.begin());
938 if (start.second != current_id)
939 P(F("bisection from revision %s in progress") % start.second);
940 }
941 }
942
943 revision_id rid;
944 string author;
945 key_store keys(app);
946 key_identity_info key;
947
948 try
949 {
950 get_user_key(app.opts, app.lua, db, keys, project, key.id, cache_disable);
951 project.complete_key_identity_from_id(keys, app.lua, key);
952
953 if (!app.lua.hook_get_author(app.opts.branch, key, author))
954 author = key.official_name();
955 }
956 catch (recoverable_failure & rf)
957 {
958 // If we can't figure out which key would be used for commit, that's no reason
959 // to make status fail.
960 if (rf.caused_by() == origin::user)
961 {
962 author = "???";
963 }
964 else
965 throw;
966 }
967
968 calculate_ident(rev, rid);
969
970 set<branch_name> old_branches;
971 get_old_branch_names(db, old_rosters, old_branches);
972
973 utf8 changelog;
974 work.read_user_log(changelog);
975
976 utf8 header;
977 utf8 summary;
978
979 revision_header(rid, rev, author, date_t::now(), app.opts.branch, changelog,
980 date_fmt, header);
981 revision_summary(rev, summary);
982
983 external header_external;
984 external summary_external;
985
986 utf8_to_system_best_effort(header, header_external);
987 utf8_to_system_best_effort(summary, summary_external);
988
989 cout << header_external;
990
991 if (!old_branches.empty() &&
992 old_branches.find(app.opts.branch) == old_branches.end())
993 {
994 cout << string(70, '-') << '\n'
995 << _("*** THIS REVISION WILL CREATE A NEW BRANCH ***") << "\n\n";
996 for (set<branch_name>::const_iterator i = old_branches.begin();
997 i != old_branches.end(); ++i)
998 cout << _("Old Branch: ") << *i << '\n';
999 cout << _("New Branch: ") << app.opts.branch << "\n\n";
1000 }
1001 set<revision_id> heads;
1002 project.get_branch_heads(app.opts.branch, heads, false);
1003 if (!heads.empty())
1004 {
1005 for (edge_map::const_iterator e = rev.edges.begin();
1006 e != rev.edges.end(); ++e)
1007 {
1008 if (heads.find(edge_old_revision(e)) == heads.end())
1009 {
1010 cout << _("*** THIS REVISION WILL CREATE DIVERGENCE ***") << "\n\n";
1011 break;
1012 }
1013 }
1014 }
1015
1016 cout << summary_external;
1017}
1018
1019static void
1020checkout_common(app_state & app,
1021 args_vector const & args)
1022{
1023 revision_id revid;
1024 system_path dir;
1025
1026 database db(app);
1027 project_t project(db);
1028 transaction_guard guard(db, false);
1029
1030 if (app.opts.revision.empty())
1031 {
1032 // use branch head revision
1033 E(!app.opts.branch().empty(), origin::user,
1034 F("use '--revision' or '--branch' to specify what to checkout"));
1035
1036 set<revision_id> heads;
1037 project.get_branch_heads(app.opts.branch, heads,
1038 app.opts.ignore_suspend_certs);
1039 E(!heads.empty(), origin::user,
1040 F("branch '%s' is empty") % app.opts.branch);
1041 if (heads.size() > 1)
1042 {
1043 P(F("branch '%s' has multiple heads:") % app.opts.branch);
1044 for (set<revision_id>::const_iterator i = heads.begin(); i != heads.end(); ++i)
1045 P(i18n_format(" %s")
1046 % describe_revision(app.opts, app.lua, project, *i));
1047 P(F("choose one with '%s checkout -r<id>'") % prog_name);
1048 E(false, origin::user,
1049 F("branch '%s' has multiple heads") % app.opts.branch);
1050 }
1051 revid = *(heads.begin());
1052 }
1053 else if (app.opts.revision.size() == 1)
1054 {
1055 // use specified revision
1056 complete(app.opts, app.lua, project, idx(app.opts.revision, 0)(), revid);
1057
1058 guess_branch(app.opts, project, revid);
1059
1060 I(!app.opts.branch().empty());
1061
1062 E(project.revision_is_in_branch(revid, app.opts.branch),
1063 origin::user,
1064 F("revision %s is not a member of branch %s")
1065 % revid % app.opts.branch);
1066 }
1067
1068 // we do this part of the checking down here, because it is legitimate to
1069 // do
1070 // $ mtn co -r h:net.venge.monotone
1071 // and have mtn guess the branch, and then use that branch name as the
1072 // default directory. But in this case the branch name will not be set
1073 // until after the guess_branch() call above:
1074 {
1075 bool checkout_dot = false;
1076
1077 if (args.empty())
1078 {
1079 // No checkout dir specified, use branch name for dir.
1080 E(!app.opts.branch().empty(), origin::user,
1081 F("you must specify a destination directory"));
1082 dir = system_path(app.opts.branch(), origin::user);
1083 }
1084 else
1085 {
1086 // Checkout to specified dir.
1087 dir = system_path(idx(args, 0));
1088 if (idx(args, 0) == utf8("."))
1089 checkout_dot = true;
1090 }
1091
1092 if (!checkout_dot)
1093 require_path_is_nonexistent
1094 (dir, F("checkout directory '%s' already exists") % dir);
1095 }
1096
1097 workspace::create_workspace(app.opts, app.lua, dir);
1098 workspace work(app);
1099
1100 roster_t empty_roster, current_roster;
1101
1102 L(FL("checking out revision %s to directory %s")
1103 % revid % dir);
1104 db.get_roster(revid, current_roster);
1105
1106 revision_t workrev;
1107 make_revision_for_workspace(revid, cset(), workrev);
1108 work.put_work_rev(workrev);
1109
1110 cset checkout;
1111 make_cset(empty_roster, current_roster, checkout);
1112
1113 content_merge_checkout_adaptor wca(db);
1114 work.perform_content_update(empty_roster, current_roster, checkout, wca, false,
1115 app.opts.move_conflicting_paths);
1116
1117 work.maybe_update_inodeprints(db);
1118 guard.commit();
1119}
1120
1121CMD(checkout, "checkout", "co", CMD_REF(tree), N_("[DIRECTORY]"),
1122 N_("Checks out a revision from the database into a directory"),
1123 N_("If a revision is given, that's the one that will be checked out. "
1124 "Otherwise, it will be the head of the branch (given or implicit). "
1125 "If no directory is given, the branch name will be used as directory."),
1126 options::opts::branch | options::opts::revision |
1127 options::opts::move_conflicting_paths)
1128{
1129 if (args.size() > 1 || app.opts.revision.size() > 1)
1130 throw usage(execid);
1131
1132 checkout_common(app, args);
1133}
1134
1135CMD_AUTOMATE(checkout, N_("[DIRECTORY]"),
1136 N_("Checks out a revision from the database into a directory"),
1137 N_("If a revision is given, that's the one that will be checked out. "
1138 "Otherwise, it will be the head of the branch (given or implicit). "
1139 "If no directory is given, the branch name will be used as directory."),
1140 options::opts::branch | options::opts::revision |
1141 options::opts::move_conflicting_paths)
1142{
1143 E(args.size() < 2, origin::user,
1144 F("wrong argument count"));
1145
1146 E(app.opts.revision.size() < 2, origin::user,
1147 F("wrong revision count"));
1148
1149 checkout_common(app, args);
1150}
1151
1152CMD_GROUP(attr, "attr", "", CMD_REF(workspace),
1153 N_("Manages file attributes"),
1154 N_("This command is used to set, get or drop file attributes."));
1155
1156// WARNING: this function is used by both attr_drop and AUTOMATE drop_attribute
1157// don't change anything that affects the automate interface contract
1158
1159static void
1160drop_attr(app_state & app, args_vector const & args)
1161{
1162 database db(app);
1163 workspace work(app);
1164
1165 roster_t old_roster;
1166 temp_node_id_source nis;
1167
1168 work.get_current_roster_shape(db, nis, old_roster);
1169
1170 file_path path = file_path_external(idx(args, 0));
1171
1172 E(old_roster.has_node(path), origin::user,
1173 F("unknown path '%s'") % path);
1174
1175 roster_t new_roster = old_roster;
1176 node_t node = new_roster.get_node_for_update(path);
1177
1178 // Clear all attrs (or a specific attr).
1179 if (args.size() == 1)
1180 {
1181 for (attr_map_t::iterator i = node->attrs.begin();
1182 i != node->attrs.end(); ++i)
1183 i->second = make_pair(false, "");
1184 }
1185 else
1186 {
1187 I(args.size() == 2);
1188 attr_key a_key = typecast_vocab<attr_key>(idx(args, 1));
1189 E(node->attrs.find(a_key) != node->attrs.end(), origin::user,
1190 F("path '%s' does not have attribute '%s'")
1191 % path % a_key);
1192 node->attrs[a_key] = make_pair(false, "");
1193 }
1194
1195 cset cs;
1196 make_cset(old_roster, new_roster, cs);
1197
1198 content_merge_empty_adaptor empty;
1199 work.perform_content_update(old_roster, new_roster, cs, empty);
1200
1201 parent_map parents;
1202 work.get_parent_rosters(db, parents);
1203
1204 revision_t new_work;
1205 make_revision_for_workspace(parents, new_roster, new_work);
1206 work.put_work_rev(new_work);
1207}
1208
1209CMD(attr_drop, "drop", "", CMD_REF(attr), N_("PATH [ATTR]"),
1210 N_("Removes attributes from a file"),
1211 N_("If no attribute is specified, this command removes all attributes "
1212 "attached to the file given in PATH. Otherwise only removes the "
1213 "attribute specified in ATTR."),
1214 options::opts::none)
1215{
1216 if (args.size() != 1 && args.size() != 2)
1217 throw usage(execid);
1218
1219 drop_attr(app, args);
1220}
1221
1222CMD(attr_get, "get", "", CMD_REF(attr), N_("PATH [ATTR]"),
1223 N_("Gets the values of a file's attributes"),
1224 N_("If no attribute is specified, this command prints all attributes "
1225 "attached to the file given in PATH. Otherwise it only prints the "
1226 "attribute specified in ATTR."),
1227 options::opts::none)
1228{
1229 if (args.size() != 1 && args.size() != 2)
1230 throw usage(execid);
1231
1232 roster_t new_roster;
1233 temp_node_id_source nis;
1234
1235 database db(app);
1236 workspace work(app);
1237 work.get_current_roster_shape(db, nis, new_roster);
1238
1239 file_path path = file_path_external(idx(args, 0));
1240
1241 E(new_roster.has_node(path), origin::user, F("unknown path '%s'") % path);
1242 const_node_t node = new_roster.get_node(path);
1243
1244 if (args.size() == 1)
1245 {
1246 bool has_any_live_attrs = false;
1247 for (attr_map_t::const_iterator i = node->attrs.begin();
1248 i != node->attrs.end(); ++i)
1249 if (i->second.first)
1250 {
1251 cout << path << " : "
1252 << i->first << '='
1253 << i->second.second << '\n';
1254 has_any_live_attrs = true;
1255 }
1256 if (!has_any_live_attrs)
1257 cout << F("no attributes for '%s'") % path << '\n';
1258 }
1259 else
1260 {
1261 I(args.size() == 2);
1262 attr_key a_key = typecast_vocab<attr_key>(idx(args, 1));
1263 attr_map_t::const_iterator i = node->attrs.find(a_key);
1264 if (i != node->attrs.end() && i->second.first)
1265 cout << path << " : "
1266 << i->first << '='
1267 << i->second.second << '\n';
1268 else
1269 cout << (F("no attribute '%s' on path '%s'")
1270 % a_key % path) << '\n';
1271 }
1272}
1273
1274// WARNING: this function is used by both attr_set and AUTOMATE set_attribute
1275// don't change anything that affects the automate interface contract
1276
1277static void
1278set_attr(app_state & app, args_vector const & args)
1279{
1280 database db(app);
1281 workspace work(app);
1282
1283 roster_t old_roster;
1284 temp_node_id_source nis;
1285
1286 work.get_current_roster_shape(db, nis, old_roster);
1287
1288 file_path path = file_path_external(idx(args, 0));
1289
1290 E(old_roster.has_node(path), origin::user,
1291 F("unknown path '%s'") % path);
1292
1293 roster_t new_roster = old_roster;
1294 node_t node = new_roster.get_node_for_update(path);
1295
1296 attr_key a_key = typecast_vocab<attr_key>(idx(args, 1));
1297 attr_value a_value = typecast_vocab<attr_value>(idx(args, 2));
1298
1299 node->attrs[a_key] = make_pair(true, a_value);
1300
1301 cset cs;
1302 make_cset(old_roster, new_roster, cs);
1303
1304 content_merge_empty_adaptor empty;
1305 work.perform_content_update(old_roster, new_roster, cs, empty);
1306
1307 parent_map parents;
1308 work.get_parent_rosters(db, parents);
1309
1310 revision_t new_work;
1311 make_revision_for_workspace(parents, new_roster, new_work);
1312 work.put_work_rev(new_work);
1313}
1314
1315CMD(attr_set, "set", "", CMD_REF(attr), N_("PATH ATTR VALUE"),
1316 N_("Sets an attribute on a file"),
1317 N_("Sets the attribute given on ATTR to the value specified in VALUE "
1318 "for the file mentioned in PATH."),
1319 options::opts::none)
1320{
1321 if (args.size() != 3)
1322 throw usage(execid);
1323
1324 set_attr(app, args);
1325}
1326
1327// Name: get_attributes
1328// Arguments:
1329// 1: file / directory name
1330// Added in: 1.0
1331// Renamed from attributes to get_attributes in: 5.0
1332// Changed to also work without a workspace in: 13.1
1333// Purpose: Prints all attributes for the specified path
1334// Output format: basic_io formatted output, each attribute has its own stanza:
1335//
1336// 'attr'
1337// represents an attribute entry
1338// format: ('attr', name, value), ('state', [unchanged|changed|added|dropped])
1339// occurs: zero or more times
1340//
1341// Error conditions: If the path has no attributes, prints nothing,
1342// if the file is unknown, escalates
1343CMD_AUTOMATE(get_attributes, N_("PATH"),
1344 N_("Prints all attributes for the specified path"),
1345 N_("If an explicit revision is given, the file's attributes "
1346 "at this specific revision are returned."),
1347 options::opts::revision)
1348{
1349 E(args.size() == 1, origin::user,
1350 F("wrong argument count"));
1351
1352 file_path path = file_path_external(idx(args,0));
1353
1354 database db(app);
1355 roster_t current, base;
1356
1357 bool from_database;
1358 if (app.opts.revision.size() == 0)
1359 {
1360 from_database = false;
1361 workspace work(app);
1362
1363 parent_map parents;
1364 temp_node_id_source nis;
1365
1366 // get the base and the current roster of this workspace
1367 work.get_current_roster_shape(db, nis, current);
1368 work.get_parent_rosters(db, parents);
1369 E(parents.size() == 1, origin::user,
1370 F("this command can only be used in a single-parent workspace"));
1371 base = parent_roster(parents.begin());
1372
1373 E(current.has_node(path), origin::user,
1374 F("unknown path '%s'") % path);
1375 }
1376 else if (app.opts.revision.size() == 1)
1377 {
1378 from_database = true;
1379 revision_id rid;
1380
1381 project_t project(db);
1382 complete(app.opts, app.lua, project, idx(app.opts.revision, 0)(), rid);
1383 db.get_roster(rid, current);
1384
1385 E(current.has_node(path), origin::user,
1386 F("unknown path '%s' in %s") % path % rid);
1387 }
1388 else
1389 E(false, origin::user, F("none or only one revision must be given"));
1390
1391 basic_io::printer pr;
1392
1393 // the current node holds all current attributes (unchanged and new ones)
1394 const_node_t n = current.get_node(path);
1395 for (attr_map_t::const_iterator i = n->attrs.begin();
1396 i != n->attrs.end(); ++i)
1397 {
1398 std::string value(i->second.second());
1399 std::string state = "unchanged";
1400
1401 if (!from_database)
1402 {
1403 // if the first value of the value pair is false this marks a
1404 // dropped attribute
1405 if (!i->second.first)
1406 {
1407 // if the attribute is dropped, we should have a base roster
1408 // with that node. we need to check that for the attribute as well
1409 // because if it is dropped there as well it was already deleted
1410 // in any previous revision
1411 I(base.has_node(path));
1412
1413 const_node_t prev_node = base.get_node(path);
1414
1415 // find the attribute in there
1416 attr_map_t::const_iterator j = prev_node->attrs.find(i->first);
1417 I(j != prev_node->attrs.end());
1418
1419 // was this dropped before? then ignore it
1420 if (!j->second.first) { continue; }
1421
1422 state = "dropped";
1423 // output the previous (dropped) value later
1424 value = j->second.second();
1425 }
1426 // this marks either a new or an existing attribute
1427 else
1428 {
1429 if (base.has_node(path))
1430 {
1431 const_node_t prev_node = base.get_node(path);
1432 attr_map_t::const_iterator j =
1433 prev_node->attrs.find(i->first);
1434
1435 // the attribute is new if it either hasn't been found
1436 // in the previous roster or has been deleted there
1437 if (j == prev_node->attrs.end() || !j->second.first)
1438 {
1439 state = "added";
1440 }
1441 // check if the attribute's value has been changed
1442 else if (i->second.second() != j->second.second())
1443 {
1444 state = "changed";
1445 }
1446 else
1447 {
1448 state = "unchanged";
1449 }
1450 }
1451 // its added since the whole node has been just added
1452 else
1453 {
1454 state = "added";
1455 }
1456 }
1457 }
1458 else
1459 {
1460 // skip previously dropped attributes in database mode, because that
1461 // has meaning only in uncommitted workspaces.
1462 if (!i->second.first)
1463 continue;
1464 }
1465
1466 basic_io::stanza st;
1467 st.push_str_triple(basic_io::syms::attr, i->first(), value);
1468 st.push_str_pair(symbol("state"), state);
1469 pr.print_stanza(st);
1470 }
1471
1472 // print the output
1473 output.write(pr.buf.data(), pr.buf.size());
1474}
1475
1476// Name: set_attribute
1477// Arguments:
1478// 1: file / directory name
1479// 2: attribute key
1480// 3: attribute value
1481// Added in: 5.0
1482// Purpose: Edits the workspace revision and sets an attribute on a certain path
1483//
1484// Error conditions: If PATH is unknown in the new roster, prints an error and
1485// exits with status 1.
1486CMD_AUTOMATE(set_attribute, N_("PATH KEY VALUE"),
1487 N_("Sets an attribute on a certain path"),
1488 "",
1489 options::opts::none)
1490{
1491 E(args.size() == 3, origin::user,
1492 F("wrong argument count"));
1493
1494 set_attr(app, args);
1495}
1496
1497// Name: drop_attribute
1498// Arguments:
1499// 1: file / directory name
1500// 2: attribute key (optional)
1501// Added in: 5.0
1502// Purpose: Edits the workspace revision and drops an attribute or all
1503// attributes of the specified path
1504//
1505// Error conditions: If PATH is unknown in the new roster or the specified
1506// attribute key is unknown, prints an error and exits with
1507// status 1.
1508CMD_AUTOMATE(drop_attribute, N_("PATH [KEY]"),
1509 N_("Drops an attribute or all of them from a certain path"),
1510 "",
1511 options::opts::none)
1512{
1513 E(args.size() ==1 || args.size() == 2, origin::user,
1514 F("wrong argument count"));
1515
1516 drop_attr(app, args);
1517}
1518void perform_commit(app_state & app,
1519 database & db,
1520 workspace & work,
1521 project_t & project,
1522 commands::command_id const & execid,
1523 vector<file_path> const & paths)
1524{
1525 key_store keys(app);
1526
1527 utf8 log_message("");
1528 bool log_message_given;
1529 revision_t restricted_rev;
1530 parent_map old_rosters;
1531 roster_t new_roster;
1532 temp_node_id_source nis;
1533 cset excluded;
1534
1535 string date_fmt = get_date_format(app.opts, app.lua, date_time_long);
1536
1537 work.get_parent_rosters(db, old_rosters);
1538 work.get_current_roster_shape(db, nis, new_roster);
1539
1540 node_restriction mask(paths,
1541 args_to_paths(app.opts.exclude),
1542 app.opts.depth,
1543 old_rosters, new_roster, ignored_file(work));
1544
1545 work.update_current_roster_from_filesystem(new_roster, mask);
1546 make_restricted_revision(old_rosters, new_roster, mask, restricted_rev,
1547 excluded, join_words(execid));
1548 restricted_rev.check_sane();
1549 E(restricted_rev.is_nontrivial(), origin::user, F("no changes to commit"));
1550
1551 set<branch_name> old_branches;
1552 get_old_branch_names(db, old_rosters, old_branches);
1553
1554 revision_id restricted_rev_id;
1555 calculate_ident(restricted_rev, restricted_rev_id);
1556
1557 // We need the 'if' because guess_branch will try to override any branch
1558 // picked up from _MTN/options.
1559 if (app.opts.branch().empty())
1560 {
1561 branch_name branchname, bn_candidate;
1562 for (edge_map::iterator i = restricted_rev.edges.begin();
1563 i != restricted_rev.edges.end();
1564 i++)
1565 {
1566 // this will prefer --branch if it was set
1567 guess_branch(app.opts, project, edge_old_revision(i),
1568 bn_candidate);
1569 E(branchname() == "" || branchname == bn_candidate, origin::user,
1570 F("parent revisions of this commit are in different branches:\n"
1571 "'%s' and '%s'.\n"
1572 "Please specify a branch name for the commit, with '--branch'.")
1573 % branchname % bn_candidate);
1574 branchname = bn_candidate;
1575 }
1576
1577 app.opts.branch = branchname;
1578 }
1579
1580 // now that we have an (unedited) branch name, let the hook decide if the
1581 // changes should get committed at all
1582 revision_data rev_data;
1583 write_revision(restricted_rev, rev_data);
1584
1585 bool changes_validated;
1586 string reason;
1587
1588 app.lua.hook_validate_changes(rev_data, app.opts.branch,
1589 changes_validated, reason);
1590
1591 E(changes_validated, origin::user,
1592 F("changes rejected by hook: %s") % reason);
1593
1594 if (global_sanity.debug_p())
1595 {
1596 L(FL("new manifest '%s'\n"
1597 "new revision '%s'\n")
1598 % restricted_rev.new_manifest
1599 % restricted_rev_id);
1600 }
1601
1602 process_commit_message_args(app.opts, log_message_given, log_message);
1603
1604 E(!(log_message_given && work.has_contents_user_log() &&
1605 app.opts.msgfile() != "_MTN/log"), origin::user,
1606 F("'_MTN/log' is non-empty and log message "
1607 "was specified on command line.\n"
1608 "Perhaps move or delete '_MTN/log',\n"
1609 "or remove '--message'/'--message-file' from the command line?"));
1610
1611 date_t date;
1612 date_t now = date_t::now();
1613 string author = app.opts.author();
1614
1615 if (app.opts.date_given)
1616 {
1617 date = app.opts.date;
1618 L(FL("using specified commit date %s") % date);
1619 }
1620 else
1621 {
1622 date = now;
1623 L(FL("using current commit date %s") % date);
1624 }
1625
1626 if (author.empty())
1627 {
1628 key_identity_info key;
1629 get_user_key(app.opts, app.lua, db, keys, project, key.id, cache_disable);
1630 project.complete_key_identity_from_id(keys, app.lua, key);
1631
1632 if (!app.lua.hook_get_author(app.opts.branch, key, author))
1633 author = key.official_name();
1634 }
1635
1636 if (!log_message_given)
1637 {
1638 // This call handles _MTN/log.
1639 get_log_message_interactively(app.lua, work, project,
1640 restricted_rev_id, restricted_rev,
1641 author, date, app.opts.branch, old_branches,
1642 date_fmt, log_message);
1643
1644 // We only check for empty log messages when the user entered them
1645 // interactively. Consensus was that if someone wanted to explicitly
1646 // type --message="", then there wasn't any reason to stop them.
1647 // FIXME: perhaps there should be no changelog cert in this case.
1648
1649 E(log_message().find_first_not_of("\n\r\t ") != string::npos,
1650 origin::user,
1651 F("empty log message; commit canceled"));
1652
1653 // We save interactively entered log messages to _MTN/log, so if
1654 // something goes wrong, the next commit will pop up their old
1655 // log message by default. We only do this for interactively
1656 // entered messages, because otherwise 'monotone commit -mfoo'
1657 // giving an error, means that after you correct that error and
1658 // hit up-arrow to try again, you get an "_MTN/log non-empty and
1659 // message given on command line" error... which is annoying.
1660
1661 work.write_user_log(log_message);
1662 }
1663
1664 // If the hook doesn't exist, allow the message to be used.
1665 bool message_validated;
1666 reason.clear();
1667
1668 app.lua.hook_validate_commit_message(log_message, rev_data, app.opts.branch,
1669 message_validated, reason);
1670 E(message_validated, origin::user,
1671 F("log message rejected by hook: %s") % reason);
1672
1673 cache_user_key(app.opts, project, keys, app.lua);
1674
1675 // for the divergence check, below
1676 set<revision_id> heads;
1677 project.get_branch_heads(app.opts.branch, heads,
1678 app.opts.ignore_suspend_certs);
1679 unsigned int old_head_size = heads.size();
1680
1681 P(F("beginning commit on branch '%s'") % app.opts.branch);
1682
1683 {
1684 transaction_guard guard(db);
1685
1686 if (db.revision_exists(restricted_rev_id))
1687 W(F("revision %s already in database")
1688 % restricted_rev_id);
1689 else
1690 {
1691 if (global_sanity.debug_p())
1692 L(FL("inserting new revision %s")
1693 % restricted_rev_id);
1694
1695 for (edge_map::const_iterator edge = restricted_rev.edges.begin();
1696 edge != restricted_rev.edges.end();
1697 edge++)
1698 {
1699 // process file deltas or new files
1700 cset const & cs = edge_changes(edge);
1701
1702 for (map<file_path, pair<file_id, file_id> >::const_iterator
1703 i = cs.deltas_applied.begin();
1704 i != cs.deltas_applied.end(); ++i)
1705 {
1706 file_path path = i->first;
1707
1708 file_id old_content = i->second.first;
1709 file_id new_content = i->second.second;
1710
1711 if (db.file_version_exists(new_content))
1712 {
1713 if (global_sanity.debug_p())
1714 L(FL("skipping file delta %s, already in database")
1715 % delta_entry_dst(i));
1716 }
1717 else if (db.file_version_exists(old_content))
1718 {
1719 if (global_sanity.debug_p())
1720 L(FL("inserting delta %s -> %s")
1721 % old_content % new_content);
1722
1723 file_data old_data;
1724 data new_data;
1725 db.get_file_version(old_content, old_data);
1726 read_data(path, new_data);
1727 // sanity check
1728 file_id tid;
1729 calculate_ident(file_data(new_data), tid);
1730 E(tid == new_content, origin::user,
1731 F("file '%s' modified during commit, aborting")
1732 % path);
1733 delta del;
1734 diff(old_data.inner(), new_data, del);
1735 db.put_file_version(old_content,
1736 new_content,
1737 file_delta(del));
1738 }
1739 else
1740 // If we don't err out here, the database will later.
1741 E(false, origin::no_fault,
1742 F("your database is missing version %s of file '%s'")
1743 % old_content % path);
1744 }
1745
1746 for (map<file_path, file_id>::const_iterator
1747 i = cs.files_added.begin();
1748 i != cs.files_added.end(); ++i)
1749 {
1750 file_path path = i->first;
1751 file_id new_content = i->second;
1752
1753 if (global_sanity.debug_p())
1754 L(FL("inserting full version %s") % new_content);
1755 data new_data;
1756 read_data(path, new_data);
1757 // sanity check
1758 file_id tid;
1759 calculate_ident(file_data(new_data), tid);
1760 E(tid == new_content, origin::user,
1761 F("file '%s' modified during commit, aborting")
1762 % path);
1763 db.put_file(new_content, file_data(new_data));
1764 }
1765 }
1766
1767 revision_data rdat;
1768 write_revision(restricted_rev, rdat);
1769 db.put_revision(restricted_rev_id, rdat);
1770 }
1771
1772 // if no --date option was specified and the user didn't edit the date
1773 // update it to reflect the current time.
1774
1775 if (date == now && !app.opts.date_given)
1776 {
1777 date = date_t::now();
1778 L(FL("updating commit date %s") % date);
1779 }
1780
1781 project.put_standard_certs(keys,
1782 restricted_rev_id,
1783 app.opts.branch,
1784 log_message,
1785 date,
1786 author);
1787 guard.commit();
1788 }
1789
1790 // the workspace should remember the branch we just committed to.
1791 work.set_options(app.opts, app.lua, true);
1792
1793 // the work revision is now whatever changes remain on top of the revision
1794 // we just checked in.
1795 revision_t remaining;
1796 make_revision_for_workspace(restricted_rev_id, excluded, remaining);
1797
1798 // small race condition here...
1799 work.put_work_rev(remaining);
1800 P(F("committed revision %s") % restricted_rev_id);
1801
1802 work.blank_user_log();
1803
1804 project.get_branch_heads(app.opts.branch, heads,
1805 app.opts.ignore_suspend_certs);
1806 if (heads.size() > old_head_size && old_head_size > 0) {
1807 P(F("note: this revision creates divergence\n"
1808 "note: you may (or may not) wish to run '%s merge'")
1809 % prog_name);
1810 }
1811
1812 work.maybe_update_inodeprints(db, mask);
1813
1814 {
1815 // Tell lua what happened. Yes, we might lose some information
1816 // here, but it's just an indicator for lua, eg. to post stuff to
1817 // a mailing list. If the user *really* cares about cert validity,
1818 // multiple certs with same name, etc. they can inquire further,
1819 // later.
1820 map<cert_name, cert_value> certs;
1821 vector<cert> ctmp;
1822 project.get_revision_certs(restricted_rev_id, ctmp);
1823 for (vector<cert>::const_iterator i = ctmp.begin();
1824 i != ctmp.end(); ++i)
1825 certs.insert(make_pair(i->name, i->value));
1826
1827 revision_data rdat;
1828 db.get_revision(restricted_rev_id, rdat);
1829 app.lua.hook_note_commit(restricted_rev_id, rdat, certs);
1830 }
1831}
1832
1833CMD_PRESET_OPTIONS(commit)
1834{
1835 // Dates are never parseable on Win32 (see win32/parse_date.cc),
1836 // so don't warn about that, just use the default format.
1837#ifdef WIN32
1838 opts.no_format_dates = true;
1839#else
1840 opts.no_format_dates = false;
1841#endif
1842}
1843
1844CMD(commit, "commit", "ci", CMD_REF(workspace), N_("[PATH]..."),
1845 N_("Commits workspace changes to the database"),
1846 "",
1847 options::opts::branch | options::opts::messages |
1848 options::opts::date | options::opts::author | options::opts::depth |
1849 options::opts::exclude)
1850{
1851 database db(app);
1852 workspace work(app);
1853 project_t project(db);
1854 perform_commit(app, db, work, project, execid, args_to_paths(args));
1855}
1856
1857CMD_NO_WORKSPACE(setup, "setup", "", CMD_REF(tree), N_("[DIRECTORY]"),
1858 N_("Sets up a new workspace directory"),
1859 N_("If no directory is specified, uses the current directory."),
1860 options::opts::branch)
1861{
1862 if (args.size() > 1)
1863 throw usage(execid);
1864
1865 E(!app.opts.branch().empty(), origin::user,
1866 F("need '--branch' argument for setup"));
1867
1868 string dir;
1869 if (args.size() == 1)
1870 dir = idx(args,0)();
1871 else
1872 dir = ".";
1873
1874 system_path workspace_dir(dir, origin::user);
1875 system_path _MTN_dir(workspace_dir / bookkeeping_root_component);
1876
1877 require_path_is_nonexistent
1878 (_MTN_dir, F("bookkeeping directory already exists in '%s'")
1879 % workspace_dir);
1880
1881 // only try to remove the complete workspace directory
1882 // if we're about to create it anyways
1883 directory_cleanup_helper remove_on_fail(
1884 directory_exists(workspace_dir) ? _MTN_dir : workspace_dir
1885 );
1886
1887 database_path_helper helper(app.lua);
1888 helper.maybe_set_default_alias(app.opts);
1889
1890 database db(app);
1891 db.create_if_not_exists();
1892 db.ensure_open();
1893
1894 workspace::create_workspace(app.opts, app.lua, workspace_dir);
1895
1896 workspace work(app);
1897 revision_t rev;
1898 make_revision_for_workspace(revision_id(), cset(), rev);
1899 work.put_work_rev(rev);
1900
1901 remove_on_fail.commit();
1902}
1903
1904CMD_NO_WORKSPACE(import, "import", "", CMD_REF(tree), N_("DIRECTORY"),
1905 N_("Imports the contents of a directory into a branch"),
1906 "",
1907 options::opts::branch | options::opts::revision |
1908 options::opts::messages |
1909 options::opts::dryrun |
1910 options::opts::no_ignore | options::opts::exclude |
1911 options::opts::author | options::opts::date)
1912{
1913 revision_id ident;
1914 system_path dir;
1915 database db(app);
1916 project_t project(db);
1917
1918 E(args.size() == 1, origin::user,
1919 F("you must specify a directory to import"));
1920
1921 if (app.opts.revision.size() == 1)
1922 {
1923 // use specified revision
1924 complete(app.opts, app.lua, project, idx(app.opts.revision, 0)(), ident);
1925
1926 guess_branch(app.opts, project, ident);
1927
1928 I(!app.opts.branch().empty());
1929
1930 E(project.revision_is_in_branch(ident, app.opts.branch),
1931 origin::user,
1932 F("revision %s is not a member of branch '%s'")
1933 % ident % app.opts.branch);
1934 }
1935 else
1936 {
1937 // use branch head revision
1938 E(!app.opts.branch().empty(), origin::user,
1939 F("use '--revision' or '--branch' to specify the parent revision for the import"));
1940
1941 set<revision_id> heads;
1942 project.get_branch_heads(app.opts.branch, heads,
1943 app.opts.ignore_suspend_certs);
1944 if (heads.size() > 1)
1945 {
1946 P(F("branch '%s' has multiple heads:") % app.opts.branch);
1947 for (set<revision_id>::const_iterator i = heads.begin(); i != heads.end(); ++i)
1948 P(i18n_format(" %s")
1949 % describe_revision(app.opts, app.lua, project, *i));
1950 P(F("choose one with '%s import -r<id>'") % prog_name);
1951 E(false, origin::user,
1952 F("branch '%s' has multiple heads") % app.opts.branch);
1953 }
1954 if (!heads.empty())
1955 ident = *(heads.begin());
1956 }
1957
1958 dir = system_path(idx(args, 0));
1959 require_path_is_directory
1960 (dir,
1961 F("import directory '%s' doesn't exists") % dir,
1962 F("import directory '%s' is a file") % dir);
1963
1964 system_path _MTN_dir = dir / path_component("_MTN");
1965
1966 require_path_is_nonexistent
1967 (_MTN_dir, F("bookkeeping directory already exists in '%s'") % dir);
1968
1969 directory_cleanup_helper remove_on_fail(_MTN_dir);
1970
1971 workspace::create_workspace(app.opts, app.lua, dir);
1972 workspace work(app);
1973
1974 revision_t rev;
1975 make_revision_for_workspace(ident, cset(), rev);
1976 work.put_work_rev(rev);
1977
1978 // prepare stuff for 'add' and so on.
1979 options save_opts;
1980 // add --unknown
1981 save_opts.exclude = app.opts.exclude;
1982 app.opts.exclude = args_vector();
1983 app.opts.unknown = true;
1984 app.opts.recursive = true;
1985 perform_add(app, db, work, vector<file_path>());
1986 app.opts.recursive = false;
1987 app.opts.unknown = false;
1988 app.opts.exclude = save_opts.exclude;
1989
1990 // drop --missing
1991 save_opts.no_ignore = app.opts.no_ignore;
1992 app.opts.missing = true;
1993 perform_drop(app, db, work, vector<file_path>());
1994 app.opts.missing = false;
1995 app.opts.no_ignore = save_opts.no_ignore;
1996
1997 // commit
1998 if (!app.opts.dryrun)
1999 {
2000 perform_commit(app, db, work, project,
2001 make_command_id("workspace commit"),
2002 vector<file_path>());
2003 remove_on_fail.commit();
2004 }
2005 else
2006 {
2007 // since the _MTN directory gets removed, don't try to write out
2008 // _MTN/options at the end
2009 workspace::used = false;
2010 }
2011}
2012
2013CMD_NO_WORKSPACE(migrate_workspace, "migrate_workspace", "", CMD_REF(tree),
2014 N_("[DIRECTORY]"),
2015 N_("Migrates a workspace directory's metadata to the latest format"),
2016 N_("If no directory is given, defaults to the current workspace."),
2017 options::opts::none)
2018{
2019 if (args.size() > 1)
2020 throw usage(execid);
2021
2022 if (args.size() == 1)
2023 {
2024 go_to_workspace(system_path(idx(args, 0)));
2025 workspace::found = true;
2026 }
2027
2028 workspace work(app);
2029 work.migrate_format();
2030
2031 // FIXME: it seems to be a bit backwards to use the workspace object
2032 // but reset its usage flag afterwards, but migrate_workspace is a
2033 // different case: we don't want that this command touches
2034 // _MTN/options for any other use case than possibly migrating its
2035 // format and the workspace_migration test enforces that
2036 workspace::used = false;
2037}
2038
2039CMD(refresh_inodeprints, "refresh_inodeprints", "", CMD_REF(tree), "",
2040 N_("Refreshes the inodeprint cache"),
2041 "",
2042 options::opts::none)
2043{
2044 database db(app);
2045 workspace work(app);
2046 work.enable_inodeprints();
2047 work.maybe_update_inodeprints(db);
2048}
2049
2050CMD_GROUP(bisect, "bisect", "", CMD_REF(informative),
2051 N_("Search revisions to find where a change first appeared"),
2052 N_("These commands subdivide a set of revisions into good, bad "
2053 "and untested subsets and successively narrow the untested set "
2054 "to find the first revision that introduced some change."));
2055
2056CMD(reset, "reset", "", CMD_REF(bisect), "",
2057 N_("Reset the current bisection search"),
2058 N_("Update the workspace back to the revision from which the bisection "
2059 "was started and remove all current search information, allowing a new "
2060 "search to be started."),
2061 options::opts::none)
2062{
2063 if (args.size() != 0)
2064 throw usage(execid);
2065
2066 database db(app);
2067 workspace work(app);
2068 project_t project(db);
2069
2070 vector<bisect::entry> info;
2071 work.get_bisect_info(info);
2072
2073 E(!info.empty(), origin::user, F("no bisection in progress"));
2074
2075 parent_map parents;
2076 work.get_parent_rosters(db, parents);
2077 E(parents.size() == 1, origin::user,
2078 F("this command can only be used in a single-parent workspace"));
2079
2080 revision_id current_id = parent_id(*parents.begin());
2081
2082 temp_node_id_source nis;
2083 roster_t current_roster;
2084 work.get_current_roster_shape(db, nis, current_roster);
2085 work.update_current_roster_from_filesystem(current_roster);
2086
2087 E(parent_roster(parents.begin()) == current_roster, origin::user,
2088 F("this command can only be used in a workspace with no pending changes"));
2089
2090 bisect::entry start = *info.begin();
2091 I(start.first == bisect::start);
2092
2093 revision_id starting_id = start.second;
2094 P(F("reset back to %s") % describe_revision(app.opts, app.lua, project, starting_id));
2095
2096 roster_t starting_roster;
2097 db.get_roster(starting_id, starting_roster);
2098
2099 cset update;
2100 make_cset(current_roster, starting_roster, update);
2101
2102 content_merge_checkout_adaptor adaptor(db);
2103 work.perform_content_update(current_roster, starting_roster, update, adaptor);
2104
2105 revision_t starting_rev;
2106 cset empty;
2107 make_revision_for_workspace(starting_id, empty, starting_rev);
2108
2109 work.put_work_rev(starting_rev);
2110 work.maybe_update_inodeprints(db);
2111
2112 // note that the various bisect commands didn't change the workspace
2113 // branch so this should not need to reset it.
2114
2115 work.remove_bisect_info();
2116}
2117
2118static void
2119bisect_select(options const & opts, lua_hooks & lua,
2120 project_t & project,
2121 vector<bisect::entry> const & info,
2122 revision_id const & current_id,
2123 revision_id & selected_id)
2124{
2125 graph_loader loader(project.db);
2126 set<revision_id> good, bad, skipped;
2127
2128 E(!info.empty(), origin::user,
2129 F("no bisection in progress"));
2130
2131 for (vector<bisect::entry>::const_iterator i = info.begin();
2132 i != info.end(); ++i)
2133 {
2134 switch (i->first)
2135 {
2136 case bisect::start:
2137 // ignored the for the purposes of bisection
2138 // used only by reset after bisection is complete
2139 break;
2140 case bisect::good:
2141 good.insert(i->second);
2142 break;
2143 case bisect::bad:
2144 bad.insert(i->second);
2145 break;
2146 case bisect::skipped:
2147 skipped.insert(i->second);
2148 break;
2149 case bisect::update:
2150 // this value is not persisted, it is only used by the bisect
2151 // update command to rerun a selection and update based on current
2152 // bisect information
2153 I(false);
2154 break;
2155 }
2156 }
2157
2158 if (good.empty() && !bad.empty())
2159 {
2160 P(F("bisecting revisions; %d good; %d bad; %d skipped; specify good revisions to start search")
2161 % good.size() % bad.size() % skipped.size());
2162 return;
2163 }
2164 else if (!good.empty() && bad.empty())
2165 {
2166 P(F("bisecting revisions; %d good; %d bad; %d skipped; specify bad revisions to start search")
2167 % good.size() % bad.size() % skipped.size());
2168 return;
2169 }
2170
2171 I(!good.empty());
2172 I(!bad.empty());
2173
2174 // the initial set of revisions to be searched is the intersection between
2175 // the good revisions and their descendants and the bad revisions and
2176 // their ancestors. this clamps the search set between these two sets of
2177 // revisions.
2178
2179 // NOTE: this also presupposes that the search is looking for a good->bad
2180 // transition rather than a bad->good transition.
2181
2182 set<revision_id> good_descendants(good), bad_ancestors(bad);
2183 loader.load_descendants(good_descendants);
2184 loader.load_ancestors(bad_ancestors);
2185
2186 set<revision_id> search;
2187 set_intersection(good_descendants.begin(), good_descendants.end(),
2188 bad_ancestors.begin(), bad_ancestors.end(),
2189 inserter(search, search.end()));
2190
2191 // the searchable set of revisions excludes those explicitly skipped
2192
2193 set<revision_id> searchable;
2194 set_difference(search.begin(), search.end(),
2195 skipped.begin(), skipped.end(),
2196 inserter(searchable, searchable.begin()));
2197
2198 // partition the searchable set into three subsets
2199 // - known good revisions
2200 // - remaining revisions
2201 // - known bad revisions
2202
2203 set<revision_id> good_ancestors(good), bad_descendants(bad);
2204 loader.load_ancestors(good_ancestors);
2205 loader.load_descendants(bad_descendants);
2206
2207 set<revision_id> known_good;
2208 set_intersection(searchable.begin(), searchable.end(),
2209 good_ancestors.begin(), good_ancestors.end(),
2210 inserter(known_good, known_good.end()));
2211
2212 set<revision_id> known_bad;
2213 set_intersection(searchable.begin(), searchable.end(),
2214 bad_descendants.begin(), bad_descendants.end(),
2215 inserter(known_bad, known_bad.end()));
2216
2217 // remove known good and known bad revisions from the searchable set
2218
2219 set<revision_id> removed;
2220 set_union(known_good.begin(), known_good.end(),
2221 known_bad.begin(), known_bad.end(),
2222 inserter(removed, removed.begin()));
2223
2224 set<revision_id> remaining;
2225 set_difference(searchable.begin(), searchable.end(),
2226 removed.begin(), removed.end(),
2227 inserter(remaining, remaining.end()));
2228
2229 P(F("bisecting %d revisions; %d good; %d bad; %d skipped; %d remaining")
2230 % search.size() % known_good.size() % known_bad.size() % skipped.size()
2231 % remaining.size());
2232
2233 // remove the current revision from the remaining set so it cannot be
2234 // chosen as the next update target. this may remove the top bad revision
2235 // and end the search.
2236 remaining.erase(current_id);
2237
2238 if (remaining.empty())
2239 {
2240 // when no revisions remain to be tested the bisection ends on the bad
2241 // revision that is the ancestor of all other bad revisions.
2242
2243 vector<revision_id> bad_sorted;
2244 toposort(project.db, bad, bad_sorted);
2245 revision_id first_bad = *bad_sorted.begin();
2246
2247 P(F("bisection finished at revision %s")
2248 % describe_revision(opts, lua, project, first_bad));
2249
2250 // if the workspace is not already at the ending revision return it as
2251 // the selected revision so that an update back to this revision
2252 // happens
2253
2254 if (current_id != first_bad)
2255 selected_id = first_bad;
2256 return;
2257 }
2258
2259 // bisection is done by toposorting the remaining revs and using the
2260 // midpoint of the result as the next revision to test
2261
2262 vector<revision_id> candidates;
2263 toposort(project.db, remaining, candidates);
2264
2265 selected_id = candidates[candidates.size()/2];
2266}
2267
2268std::ostream &
2269operator<<(std::ostream & os,
2270 bisect::type const type)
2271{
2272 switch (type)
2273 {
2274 case bisect::start:
2275 os << "start";
2276 break;
2277 case bisect::good:
2278 os << "good";
2279 break;
2280 case bisect::bad:
2281 os << "bad";
2282 break;
2283 case bisect::skipped:
2284 os << "skip";
2285 break;
2286 case bisect::update:
2287 // this value is not persisted, it is only used by the bisect
2288 // update command to rerun a selection and update based on current
2289 // bisect information
2290 I(false);
2291 break;
2292 }
2293 return os;
2294}
2295
2296static void
2297bisect_update(app_state & app, bisect::type type)
2298{
2299 database db(app);
2300 workspace work(app);
2301 project_t project(db);
2302
2303 parent_map parents;
2304 work.get_parent_rosters(db, parents);
2305 E(parents.size() == 1, origin::user,
2306 F("this command can only be used in a single-parent workspace"));
2307
2308 revision_id current_id = parent_id(*parents.begin());
2309
2310 temp_node_id_source nis;
2311 roster_t current_roster;
2312 work.get_current_roster_shape(db, nis, current_roster);
2313 work.update_current_roster_from_filesystem(current_roster);
2314
2315 E(parent_roster(parents.begin()) == current_roster, origin::user,
2316 F("this command can only be used in a workspace with no pending changes"));
2317
2318 set<revision_id> marked_ids;
2319
2320 // mark the current or specified revisions as good, bad or skipped
2321 if (app.opts.revision.empty())
2322 marked_ids.insert(current_id);
2323 else
2324 for (args_vector::const_iterator i = app.opts.revision.begin();
2325 i != app.opts.revision.end(); i++)
2326 {
2327 set<revision_id> rids;
2328 MM(rids);
2329 MM(*i);
2330 complete(app.opts, app.lua, project, (*i)(), rids);
2331 marked_ids.insert(rids.begin(), rids.end());
2332 }
2333
2334 vector<bisect::entry> info;
2335 work.get_bisect_info(info);
2336
2337 if (info.empty())
2338 {
2339 info.push_back(make_pair(bisect::start, current_id));
2340 P(F("bisection started at revision %s")
2341 % describe_revision(app.opts, app.lua, project, current_id));
2342 }
2343
2344 if (type != bisect::update)
2345 {
2346 // don't allow conflicting or redundant settings
2347 for (vector<bisect::entry>::const_iterator i = info.begin();
2348 i != info.end(); ++i)
2349 {
2350 if (i->first == bisect::start)
2351 continue;
2352 if (marked_ids.find(i->second) != marked_ids.end())
2353 {
2354 if (type == i->first)
2355 {
2356 W(F("ignored redundant bisect %s on revision %s")
2357 % type % i->second);
2358 marked_ids.erase(i->second);
2359 }
2360 else
2361 E(false, origin::user, F("conflicting bisect %s/%s on revision %s")
2362 % type % i->first % i->second);
2363 }
2364 }
2365
2366 // push back all marked revs with the appropriate type
2367 for (set<revision_id>::const_iterator i = marked_ids.begin();
2368 i != marked_ids.end(); ++i)
2369 info.push_back(make_pair(type, *i));
2370
2371 work.put_bisect_info(info);
2372 }
2373
2374 revision_id selected_id;
2375 bisect_select(app.opts, app.lua, project, info, current_id, selected_id);
2376 if (null_id(selected_id))
2377 return;
2378
2379 P(F("updating to %s") % describe_revision(app.opts, app.lua, project, selected_id));
2380
2381 roster_t selected_roster;
2382 db.get_roster(selected_id, selected_roster);
2383
2384 cset update;
2385 make_cset(current_roster, selected_roster, update);
2386
2387 content_merge_checkout_adaptor adaptor(db);
2388 work.perform_content_update(current_roster, selected_roster, update, adaptor,
2389 true, app.opts.move_conflicting_paths);
2390
2391 revision_t selected_rev;
2392 cset empty;
2393 make_revision_for_workspace(selected_id, empty, selected_rev);
2394
2395 work.put_work_rev(selected_rev);
2396 work.maybe_update_inodeprints(db);
2397
2398 // this may have updated to a revision not in the branch specified by
2399 // the workspace branch option. however it cannot update the workspace
2400 // branch option because the new revision may be in multiple branches.
2401}
2402
2403CMD(bisect_status, "status", "", CMD_REF(bisect), "",
2404 N_("Reports on the current status of the bisection search"),
2405 N_("Lists the total number of revisions in the search set, "
2406 "the number of revisions that have been determined to be good or bad, "
2407 "the number of revisions that have been skipped "
2408 "and the number of revisions remaining to be tested."),
2409 options::opts::none)
2410{
2411 if (args.size() != 0)
2412 throw usage(execid);
2413
2414 database db(app);
2415 workspace work(app);
2416 project_t project(db);
2417
2418 parent_map parents;
2419 work.get_parent_rosters(db, parents);
2420 E(parents.size() == 1, origin::user,
2421 F("this command can only be used in a single-parent workspace"));
2422
2423 revision_id current_id = parent_id(*parents.begin());
2424
2425 vector<bisect::entry> info;
2426 work.get_bisect_info(info);
2427
2428 revision_id selected_id;
2429 bisect_select(app.opts, app.lua, project, info, current_id, selected_id);
2430
2431 if (current_id != selected_id)
2432 {
2433 W(F("next revision for bisection testing is %s\n") % selected_id);
2434 W(F("however this workspace is currently at %s\n") % current_id);
2435 W(F("run 'bisect update' to update to this revision before testing"));
2436 }
2437}
2438
2439CMD(bisect_update, "update", "", CMD_REF(bisect), "",
2440 N_("Updates the workspace to the next revision to be tested by bisection"),
2441 N_("This command can be used if updates by good, bad or skip commands "
2442 "fail due to blocked paths or other problems."),
2443 options::opts::move_conflicting_paths)
2444{
2445 if (args.size() != 0)
2446 throw usage(execid);
2447 bisect_update(app, bisect::update);
2448}
2449
2450CMD(bisect_skip, "skip", "", CMD_REF(bisect), "",
2451 N_("Excludes the current revision or specified revisions from the search"),
2452 N_("Skipped revisions are removed from the set being searched. Revisions "
2453 "that cannot be tested for some reason should be skipped."),
2454 options::opts::revision | options::opts::move_conflicting_paths)
2455{
2456 if (args.size() != 0)
2457 throw usage(execid);
2458 bisect_update(app, bisect::skipped);
2459}
2460
2461CMD(bisect_bad, "bad", "", CMD_REF(bisect), "",
2462 N_("Marks the current revision or specified revisions as bad"),
2463 N_("Known bad revisions are removed from the set being searched."),
2464 options::opts::revision | options::opts::move_conflicting_paths)
2465{
2466 if (args.size() != 0)
2467 throw usage(execid);
2468 bisect_update(app, bisect::bad);
2469}
2470
2471CMD(bisect_good, "good", "", CMD_REF(bisect), "",
2472 N_("Marks the current revision or specified revisions as good"),
2473 N_("Known good revisions are removed from the set being searched."),
2474 options::opts::revision | options::opts::move_conflicting_paths)
2475{
2476 if (args.size() != 0)
2477 throw usage(execid);
2478 bisect_update(app, bisect::good);
2479}
2480
2481// Local Variables:
2482// mode: C++
2483// fill-column: 76
2484// c-file-style: "gnu"
2485// indent-tabs-mode: nil
2486// End:
2487// 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