monotone

monotone Mtn Source Tree

Root/cmd_ws_commit.cc

1// Copyright (C) 2002 Graydon Hoare <graydon@pobox.com>
2//
3// This program is made available under the GNU GPL version 2.0 or
4// greater. See the accompanying file COPYING for details.
5//
6// This program is distributed WITHOUT ANY WARRANTY; without even the
7// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
8// PURPOSE.
9
10#include "base.hh"
11#include <iostream>
12#include <map>
13
14#include "cmd.hh"
15#include "diff_patch.hh"
16#include "file_io.hh"
17#include "restrictions.hh"
18#include "revision.hh"
19#include "transforms.hh"
20#include "work.hh"
21#include "charset.hh"
22#include "ui.hh"
23#include "app_state.hh"
24#include "basic_io.hh"
25
26using std::cout;
27using std::make_pair;
28using std::pair;
29using std::make_pair;
30using std::map;
31using std::set;
32using std::string;
33using std::vector;
34
35using boost::shared_ptr;
36
37static void
38revision_summary(revision_t const & rev, branch_name const & branch, utf8 & summary)
39{
40 string out;
41 // We intentionally do not collapse the final \n into the format
42 // strings here, for consistency with newline conventions used by most
43 // other format strings.
44 out += (F("Current branch: %s") % branch).str() += '\n';
45 for (edge_map::const_iterator i = rev.edges.begin(); i != rev.edges.end(); ++i)
46 {
47 revision_id parent = edge_old_revision(*i);
48 // A colon at the end of this string looked nicer, but it made
49 // double-click copying from terminals annoying.
50 out += (F("Changes against parent %s") % parent).str() += '\n';
51
52 cset const & cs = edge_changes(*i);
53
54 if (cs.empty())
55 out += F(" no changes").str() += '\n';
56
57 for (set<file_path>::const_iterator i = cs.nodes_deleted.begin();
58 i != cs.nodes_deleted.end(); ++i)
59 out += (F(" dropped %s") % *i).str() += '\n';
60
61 for (map<file_path, file_path>::const_iterator
62 i = cs.nodes_renamed.begin();
63 i != cs.nodes_renamed.end(); ++i)
64 out += (F(" renamed %s\n"
65 " to %s") % i->first % i->second).str() += '\n';
66
67 for (set<file_path>::const_iterator i = cs.dirs_added.begin();
68 i != cs.dirs_added.end(); ++i)
69 out += (F(" added %s") % *i).str() += '\n';
70
71 for (map<file_path, file_id>::const_iterator i = cs.files_added.begin();
72 i != cs.files_added.end(); ++i)
73 out += (F(" added %s") % i->first).str() += '\n';
74
75 for (map<file_path, pair<file_id, file_id> >::const_iterator
76 i = cs.deltas_applied.begin(); i != cs.deltas_applied.end(); ++i)
77 out += (F(" patched %s") % (i->first)).str() += '\n';
78
79 for (map<pair<file_path, attr_key>, attr_value >::const_iterator
80 i = cs.attrs_set.begin(); i != cs.attrs_set.end(); ++i)
81 out += (F(" attr on %s\n"
82 " attr %s\n"
83 " value %s")
84 % (i->first.first) % (i->first.second) % (i->second)
85 ).str() += "\n";
86
87 for (set<pair<file_path, attr_key> >::const_iterator
88 i = cs.attrs_cleared.begin(); i != cs.attrs_cleared.end(); ++i)
89 out += (F(" unset on %s\n"
90 " attr %s")
91 % (i->first) % (i->second)).str() += "\n";
92 }
93 summary = utf8(out);
94}
95
96static void
97get_log_message_interactively(revision_t const & cs,
98 app_state & app,
99 utf8 & log_message)
100{
101 utf8 summary;
102 revision_summary(cs, app.opts.branchname, summary);
103 external summary_external;
104 utf8_to_system_best_effort(summary, summary_external);
105
106 utf8 branch_comment = utf8((F("branch \"%s\"\n\n") % app.opts.branchname).str());
107 external branch_external;
108 utf8_to_system_best_effort(branch_comment, branch_external);
109
110 string magic_line = _("*****DELETE THIS LINE TO CONFIRM YOUR COMMIT*****");
111 string commentary_str;
112 commentary_str += string(70, '-') + "\n";
113 commentary_str += _("Enter a description of this change.\n"
114 "Lines beginning with `MTN:' "
115 "are removed automatically.");
116 commentary_str += "\n\n";
117 commentary_str += summary_external();
118 commentary_str += string(70, '-') + "\n";
119
120 external commentary(commentary_str);
121
122 utf8 user_log_message;
123 app.work.read_user_log(user_log_message);
124
125 //if the _MTN/log file was non-empty, we'll append the 'magic' line
126 utf8 user_log;
127 if (user_log_message().length() > 0)
128 user_log = utf8( magic_line + "\n" + user_log_message());
129 else
130 user_log = user_log_message;
131
132 external user_log_message_external;
133 utf8_to_system_best_effort(user_log, user_log_message_external);
134
135 external log_message_external;
136 N(app.lua.hook_edit_comment(commentary, user_log_message_external,
137 log_message_external),
138 F("edit of log message failed"));
139
140 N(log_message_external().find(magic_line) == string::npos,
141 F("failed to remove magic line; commit cancelled"));
142
143 system_to_utf8(log_message_external, log_message);
144}
145
146CMD(revert, "revert", "", CMD_REF(workspace), N_("[PATH]..."),
147 N_("Reverts files and/or directories"),
148 N_("In order to revert the entire workspace, specify \".\" as the "
149 "file name."),
150 options::opts::depth | options::opts::exclude | options::opts::missing)
151{
152 roster_t old_roster, new_roster;
153 cset included, excluded;
154
155 N(app.opts.missing || !args.empty() || !app.opts.exclude_patterns.empty(),
156 F("you must pass at least one path to 'revert' (perhaps '.')"));
157
158 app.require_workspace();
159
160 parent_map parents;
161 app.work.get_parent_rosters(parents);
162 N(parents.size() == 1,
163 F("this command can only be used in a single-parent workspace"));
164 old_roster = parent_roster(parents.begin());
165
166 {
167 temp_node_id_source nis;
168 app.work.get_current_roster_shape(new_roster, nis);
169 }
170
171 node_restriction mask(args_to_paths(args),
172 args_to_paths(app.opts.exclude_patterns),
173 app.opts.depth,
174 old_roster, new_roster, app);
175
176 if (app.opts.missing)
177 {
178 // --missing is a further filter on the files included by a
179 // restriction we first find all missing files included by the
180 // specified args and then make a restriction that includes only
181 // these missing files.
182 set<file_path> missing;
183 app.work.find_missing(new_roster, mask, missing);
184 if (missing.empty())
185 {
186 P(F("no missing files to revert"));
187 return;
188 }
189
190 std::vector<file_path> missing_files;
191 for (set<file_path>::const_iterator i = missing.begin();
192 i != missing.end(); i++)
193 {
194 L(FL("reverting missing file: %s") % *i);
195 missing_files.push_back(*i);
196 }
197 // replace the original mask with a more restricted one
198 mask = node_restriction(missing_files, std::vector<file_path>(),
199 app.opts.depth,
200 old_roster, new_roster, app);
201 }
202
203 make_restricted_csets(old_roster, new_roster,
204 included, excluded, mask);
205
206 // The included cset will be thrown away (reverted) leaving the
207 // excluded cset pending in MTN/work which must be valid against the
208 // old roster.
209
210 check_restricted_cset(old_roster, excluded);
211
212 node_map const & nodes = old_roster.all_nodes();
213 for (node_map::const_iterator i = nodes.begin();
214 i != nodes.end(); ++i)
215 {
216 node_id nid = i->first;
217 node_t node = i->second;
218
219 if (old_roster.is_root(nid))
220 continue;
221
222 if (!mask.includes(old_roster, nid))
223 continue;
224
225 file_path fp;
226 old_roster.get_name(nid, fp);
227
228 if (is_file_t(node))
229 {
230 file_t f = downcast_to_file_t(node);
231 if (file_exists(fp))
232 {
233 hexenc<id> ident;
234 calculate_ident(fp, ident);
235 // don't touch unchanged files
236 if (ident == f->content.inner())
237 continue;
238 }
239
240 P(F("reverting %s") % fp);
241 L(FL("reverting %s to [%s]") % fp % f->content);
242
243 N(app.db.file_version_exists(f->content),
244 F("no file version %s found in database for %s")
245 % f->content % fp);
246
247 file_data dat;
248 L(FL("writing file %s to %s")
249 % f->content % fp);
250 app.db.get_file_version(f->content, dat);
251 write_data(fp, dat.inner());
252 }
253 else
254 {
255 if (!directory_exists(fp))
256 {
257 P(F("recreating %s/") % fp);
258 mkdir_p(fp);
259 }
260 }
261 }
262
263 // Included_work is thrown away which effectively reverts any adds,
264 // drops and renames it contains. Drops and rename sources will have
265 // been rewritten above but this may leave rename targets laying
266 // around.
267
268 revision_t remaining;
269 make_revision_for_workspace(parent_id(parents.begin()), excluded, remaining);
270
271 // Race.
272 app.work.put_work_rev(remaining);
273 app.work.update_any_attrs();
274 app.work.maybe_update_inodeprints();
275}
276
277CMD(disapprove, "disapprove", "", CMD_REF(review), N_("REVISION"),
278 N_("Disapproves a particular revision"),
279 "",
280 options::opts::branch | options::opts::messages | options::opts::date |
281 options::opts::author)
282{
283 if (args.size() != 1)
284 throw usage(execid);
285
286 utf8 log_message("");
287 bool log_message_given;
288 revision_id r;
289 revision_t rev, rev_inverse;
290 shared_ptr<cset> cs_inverse(new cset());
291 complete(app, idx(args, 0)(), r);
292 app.db.get_revision(r, rev);
293
294 N(rev.edges.size() == 1,
295 F("revision %s has %d changesets, cannot invert") % r % rev.edges.size());
296
297 guess_branch(r, app);
298 N(app.opts.branchname() != "", F("need --branch argument for disapproval"));
299
300 process_commit_message_args(log_message_given, log_message, app,
301 utf8((FL("disapproval of revision '%s'") % r).str()));
302
303 edge_entry const & old_edge (*rev.edges.begin());
304 app.db.get_revision_manifest(edge_old_revision(old_edge),
305 rev_inverse.new_manifest);
306 {
307 roster_t old_roster, new_roster;
308 app.db.get_roster(edge_old_revision(old_edge), old_roster);
309 app.db.get_roster(r, new_roster);
310 make_cset(new_roster, old_roster, *cs_inverse);
311 }
312 rev_inverse.edges.insert(make_pair(r, cs_inverse));
313
314 {
315 transaction_guard guard(app.db);
316
317 revision_id inv_id;
318 revision_data rdat;
319
320 write_revision(rev_inverse, rdat);
321 calculate_ident(rdat, inv_id);
322 app.db.put_revision(inv_id, rdat);
323
324 app.get_project().put_standard_certs_from_options(inv_id,
325 app.opts.branchname,
326 log_message);
327 guard.commit();
328 }
329}
330
331CMD(mkdir, "mkdir", "", CMD_REF(workspace), N_("[DIRECTORY...]"),
332 N_("Creates directories and adds them to the workspace"),
333 "",
334 options::opts::no_ignore)
335{
336 if (args.size() < 1)
337 throw usage(execid);
338
339 app.require_workspace();
340
341 set<file_path> paths;
342 // spin through args and try to ensure that we won't have any collisions
343 // before doing any real filesystem modification. we'll also verify paths
344 // against .mtn-ignore here.
345 for (args_vector::const_iterator i = args.begin(); i != args.end(); ++i)
346 {
347 file_path fp = file_path_external(*i);
348 require_path_is_nonexistent
349 (fp, F("directory '%s' already exists") % fp);
350
351 // we'll treat this as a user (fatal) error. it really wouldn't make
352 // sense to add a dir to .mtn-ignore and then try to add it to the
353 // project with a mkdir statement, but one never can tell...
354 N(app.opts.no_ignore || !app.lua.hook_ignore_file(fp),
355 F("ignoring directory '%s' [see .mtn-ignore]") % fp);
356
357 paths.insert(fp);
358 }
359
360 // this time, since we've verified that there should be no collisions,
361 // we'll just go ahead and do the filesystem additions.
362 for (set<file_path>::const_iterator i = paths.begin(); i != paths.end(); ++i)
363 mkdir_p(*i);
364
365 app.work.perform_additions(paths, false, !app.opts.no_ignore);
366}
367
368CMD(add, "add", "", CMD_REF(workspace), N_("[PATH]..."),
369 N_("Adds files to the workspace"),
370 "",
371 options::opts::unknown | options::opts::no_ignore |
372 options::opts::recursive)
373{
374 if (!app.opts.unknown && (args.size() < 1))
375 throw usage(execid);
376
377 app.require_workspace();
378
379 vector<file_path> roots = args_to_paths(args);
380
381 set<file_path> paths;
382 bool add_recursive = app.opts.recursive;
383 if (app.opts.unknown)
384 {
385 path_restriction mask(roots, args_to_paths(app.opts.exclude_patterns),
386 app.opts.depth, app);
387 set<file_path> ignored;
388
389 // if no starting paths have been specified use the workspace root
390 if (roots.empty())
391 roots.push_back(file_path());
392
393 app.work.find_unknown_and_ignored(mask, roots, paths, ignored);
394
395 app.work.perform_additions(ignored, add_recursive, !app.opts.no_ignore);
396 }
397 else
398 paths = set<file_path>(roots.begin(), roots.end());
399
400 app.work.perform_additions(paths, add_recursive, !app.opts.no_ignore);
401}
402
403CMD(drop, "drop", "rm", CMD_REF(workspace), N_("[PATH]..."),
404 N_("Drops files from the workspace"),
405 "",
406 options::opts::bookkeep_only | options::opts::missing | options::opts::recursive)
407{
408 if (!app.opts.missing && (args.size() < 1))
409 throw usage(execid);
410
411 app.require_workspace();
412
413 set<file_path> paths;
414 if (app.opts.missing)
415 {
416 temp_node_id_source nis;
417 roster_t current_roster_shape;
418 app.work.get_current_roster_shape(current_roster_shape, nis);
419 node_restriction mask(args_to_paths(args),
420 args_to_paths(app.opts.exclude_patterns),
421 app.opts.depth,
422 current_roster_shape, app);
423 app.work.find_missing(current_roster_shape, mask, paths);
424 }
425 else
426 {
427 vector<file_path> roots = args_to_paths(args);
428 paths = set<file_path>(roots.begin(), roots.end());
429 }
430
431 app.work.perform_deletions(paths, app.opts.recursive, app.opts.bookkeep_only);
432}
433
434
435CMD(rename, "rename", "mv", CMD_REF(workspace),
436 N_("SRC DEST\n"
437 "SRC1 [SRC2 [...]] DEST_DIR"),
438 N_("Renames entries in the workspace"),
439 "",
440 options::opts::bookkeep_only)
441{
442 if (args.size() < 2)
443 throw usage(execid);
444
445 app.require_workspace();
446
447 file_path dst_path = file_path_external(args.back());
448
449 set<file_path> src_paths;
450 for (size_t i = 0; i < args.size()-1; i++)
451 {
452 file_path s = file_path_external(idx(args, i));
453 src_paths.insert(s);
454 }
455 app.work.perform_rename(src_paths, dst_path, app.opts.bookkeep_only);
456}
457
458
459CMD(pivot_root, "pivot_root", "", CMD_REF(workspace), N_("NEW_ROOT PUT_OLD"),
460 N_("Renames the root directory"),
461 N_("After this command, the directory that currently "
462 "has the name NEW_ROOT "
463 "will be the root directory, and the directory "
464 "that is currently the root "
465 "directory will have name PUT_OLD.\n"
466 "Use of --bookkeep-only is NOT recommended."),
467 options::opts::bookkeep_only)
468{
469 if (args.size() != 2)
470 throw usage(execid);
471
472 app.require_workspace();
473 file_path new_root = file_path_external(idx(args, 0));
474 file_path put_old = file_path_external(idx(args, 1));
475 app.work.perform_pivot_root(new_root, put_old, app.opts.bookkeep_only);
476}
477
478CMD(status, "status", "", CMD_REF(informative), N_("[PATH]..."),
479 N_("Shows workspace's status information"),
480 "",
481 options::opts::depth | options::opts::exclude)
482{
483 roster_t new_roster;
484 parent_map old_rosters;
485 revision_t rev;
486 temp_node_id_source nis;
487
488 app.require_workspace();
489 app.work.get_parent_rosters(old_rosters);
490 app.work.get_current_roster_shape(new_roster, nis);
491
492 node_restriction mask(args_to_paths(args),
493 args_to_paths(app.opts.exclude_patterns),
494 app.opts.depth,
495 old_rosters, new_roster, app);
496
497 app.work.update_current_roster_from_filesystem(new_roster, mask);
498 make_restricted_revision(old_rosters, new_roster, mask, rev);
499
500 utf8 summary;
501 revision_summary(rev, app.opts.branchname, summary);
502 external summary_external;
503 utf8_to_system_best_effort(summary, summary_external);
504 cout << summary_external;
505}
506
507CMD(checkout, "checkout", "co", CMD_REF(tree), N_("[DIRECTORY]"),
508 N_("Checks out a revision from the database into a directory"),
509 N_("If a revision is given, that's the one that will be checked out. "
510 "Otherwise, it will be the head of the branch (given or implicit). "
511 "If no directory is given, the branch name will be used as directory."),
512 options::opts::branch | options::opts::revision)
513{
514 revision_id revid;
515 system_path dir;
516
517 transaction_guard guard(app.db, false);
518
519 if (args.size() > 1 || app.opts.revision_selectors.size() > 1)
520 throw usage(execid);
521
522 if (app.opts.revision_selectors.size() == 0)
523 {
524 // use branch head revision
525 N(!app.opts.branchname().empty(),
526 F("use --revision or --branch to specify what to checkout"));
527
528 set<revision_id> heads;
529 app.get_project().get_branch_heads(app.opts.branchname, heads);
530 N(heads.size() > 0,
531 F("branch '%s' is empty") % app.opts.branchname);
532 if (heads.size() > 1)
533 {
534 P(F("branch %s has multiple heads:") % app.opts.branchname);
535 for (set<revision_id>::const_iterator i = heads.begin(); i != heads.end(); ++i)
536 P(i18n_format(" %s") % describe_revision(app, *i));
537 P(F("choose one with '%s checkout -r<id>'") % ui.prog_name);
538 E(false, F("branch %s has multiple heads") % app.opts.branchname);
539 }
540 revid = *(heads.begin());
541 }
542 else if (app.opts.revision_selectors.size() == 1)
543 {
544 // use specified revision
545 complete(app, idx(app.opts.revision_selectors, 0)(), revid);
546 N(app.db.revision_exists(revid),
547 F("no such revision '%s'") % revid);
548
549 guess_branch(revid, app);
550
551 I(!app.opts.branchname().empty());
552
553 N(app.get_project().revision_is_in_branch(revid, app.opts.branchname),
554 F("revision %s is not a member of branch %s")
555 % revid % app.opts.branchname);
556 }
557
558 // we do this part of the checking down here, because it is legitimate to
559 // do
560 // $ mtn co -r h:net.venge.monotone
561 // and have mtn guess the branch, and then use that branch name as the
562 // default directory. But in this case the branch name will not be set
563 // until after the guess_branch() call above:
564 {
565 bool checkout_dot = false;
566
567 if (args.size() == 0)
568 {
569 // No checkout dir specified, use branch name for dir.
570 N(!app.opts.branchname().empty(),
571 F("you must specify a destination directory"));
572 dir = system_path(app.opts.branchname());
573 }
574 else
575 {
576 // Checkout to specified dir.
577 dir = system_path(idx(args, 0));
578 if (idx(args, 0) == utf8("."))
579 checkout_dot = true;
580 }
581
582 if (!checkout_dot)
583 require_path_is_nonexistent
584 (dir, F("checkout directory '%s' already exists") % dir);
585 }
586
587 app.create_workspace(dir);
588
589 shared_ptr<roster_t> empty_roster = shared_ptr<roster_t>(new roster_t());
590 roster_t current_roster;
591
592 L(FL("checking out revision %s to directory %s") % revid % dir);
593 app.db.get_roster(revid, current_roster);
594
595 revision_t workrev;
596 make_revision_for_workspace(revid, cset(), workrev);
597 app.work.put_work_rev(workrev);
598
599 cset checkout;
600 make_cset(*empty_roster, current_roster, checkout);
601
602 map<file_id, file_path> paths;
603 get_content_paths(*empty_roster, paths);
604
605 content_merge_workspace_adaptor wca(app, empty_roster, paths);
606
607 app.work.perform_content_update(checkout, wca, false);
608
609 app.work.update_any_attrs();
610 app.work.maybe_update_inodeprints();
611 guard.commit();
612}
613
614CMD_GROUP(attr, "attr", "", CMD_REF(workspace),
615 N_("Manages file attributes"),
616 N_("This command is used to set, get or drop file attributes."));
617
618CMD(attr_drop, "drop", "", CMD_REF(attr), N_("PATH [ATTR]"),
619 N_("Removes attributes from a file"),
620 N_("If no attribute is specified, this command removes all attributes "
621 "attached to the file given in PATH. Otherwise only removes the "
622 "attribute specified in ATTR."),
623 options::opts::none)
624{
625 N(args.size() > 0 && args.size() < 3,
626 F("wrong argument count"));
627
628 roster_t new_roster;
629 temp_node_id_source nis;
630
631 app.require_workspace();
632 app.work.get_current_roster_shape(new_roster, nis);
633
634 file_path path = file_path_external(idx(args, 0));
635
636 N(new_roster.has_node(path), F("Unknown path '%s'") % path);
637 node_t node = new_roster.get_node(path);
638
639 // Clear all attrs (or a specific attr).
640 if (args.size() == 1)
641 {
642 for (full_attr_map_t::iterator i = node->attrs.begin();
643 i != node->attrs.end(); ++i)
644 i->second = make_pair(false, "");
645 }
646 else
647 {
648 I(args.size() == 2);
649 attr_key a_key = attr_key(idx(args, 1)());
650 N(node->attrs.find(a_key) != node->attrs.end(),
651 F("Path '%s' does not have attribute '%s'")
652 % path % a_key);
653 node->attrs[a_key] = make_pair(false, "");
654 }
655
656 parent_map parents;
657 app.work.get_parent_rosters(parents);
658
659 revision_t new_work;
660 make_revision_for_workspace(parents, new_roster, new_work);
661 app.work.put_work_rev(new_work);
662 app.work.update_any_attrs();
663}
664
665CMD(attr_get, "get", "", CMD_REF(attr), N_("PATH [ATTR]"),
666 N_("Gets the values of a file's attributes"),
667 N_("If no attribute is specified, this command prints all attributes "
668 "attached to the file given in PATH. Otherwise it only prints the "
669 "attribute specified in ATTR."),
670 options::opts::none)
671{
672 N(args.size() > 0 && args.size() < 3,
673 F("wrong argument count"));
674
675 roster_t new_roster;
676 temp_node_id_source nis;
677
678 app.require_workspace();
679 app.work.get_current_roster_shape(new_roster, nis);
680
681 file_path path = file_path_external(idx(args, 0));
682
683 N(new_roster.has_node(path), F("Unknown path '%s'") % path);
684 node_t node = new_roster.get_node(path);
685
686 if (args.size() == 1)
687 {
688 bool has_any_live_attrs = false;
689 for (full_attr_map_t::const_iterator i = node->attrs.begin();
690 i != node->attrs.end(); ++i)
691 if (i->second.first)
692 {
693 cout << path << " : "
694 << i->first << '='
695 << i->second.second << '\n';
696 has_any_live_attrs = true;
697 }
698 if (!has_any_live_attrs)
699 cout << F("No attributes for '%s'") % path << '\n';
700 }
701 else
702 {
703 I(args.size() == 2);
704 attr_key a_key = attr_key(idx(args, 1)());
705 full_attr_map_t::const_iterator i = node->attrs.find(a_key);
706 if (i != node->attrs.end() && i->second.first)
707 cout << path << " : "
708 << i->first << '='
709 << i->second.second << '\n';
710 else
711 cout << (F("No attribute '%s' on path '%s'")
712 % a_key % path) << '\n';
713 }
714}
715
716CMD(attr_set, "set", "", CMD_REF(attr), N_("PATH ATTR VALUE"),
717 N_("Sets an attribute on a file"),
718 N_("Sets the attribute given on ATTR to the value specified in VALUE "
719 "for the file mentioned in PATH."),
720 options::opts::none)
721{
722 N(args.size() == 3,
723 F("wrong argument count"));
724
725 roster_t new_roster;
726 temp_node_id_source nis;
727
728 app.require_workspace();
729 app.work.get_current_roster_shape(new_roster, nis);
730
731 file_path path = file_path_external(idx(args, 0));
732
733 N(new_roster.has_node(path), F("Unknown path '%s'") % path);
734 node_t node = new_roster.get_node(path);
735
736 attr_key a_key = attr_key(idx(args, 1)());
737 attr_value a_value = attr_value(idx(args, 2)());
738
739 node->attrs[a_key] = make_pair(true, a_value);
740
741 parent_map parents;
742 app.work.get_parent_rosters(parents);
743
744 revision_t new_work;
745 make_revision_for_workspace(parents, new_roster, new_work);
746 app.work.put_work_rev(new_work);
747 app.work.update_any_attrs();
748}
749
750// Name: get_attributes
751// Arguments:
752// 1: file / directory name
753// Added in: 1.0
754// Renamed from attributes to get_attributes in: 5.0
755// Purpose: Prints all attributes for the specified path
756// Output format: basic_io formatted output, each attribute has its own stanza:
757//
758// 'format_version'
759// used in case this format ever needs to change.
760// format: ('format_version', the string "1" currently)
761// occurs: exactly once
762// 'attr'
763// represents an attribute entry
764// format: ('attr', name, value), ('state', [unchanged|changed|added|dropped])
765// occurs: zero or more times
766//
767// Error conditions: If the path has no attributes, prints only the
768// format version, if the file is unknown, escalates
769CMD_AUTOMATE(get_attributes, N_("PATH"),
770 N_("Prints all attributes for the specified path"),
771 "",
772 options::opts::none)
773{
774 N(args.size() > 0,
775 F("wrong argument count"));
776
777 // this command requires a workspace to be run on
778 app.require_workspace();
779
780 // retrieve the path
781 file_path path = file_path_external(idx(args,0));
782
783 roster_t base, current;
784 parent_map parents;
785 temp_node_id_source nis;
786
787 // get the base and the current roster of this workspace
788 app.work.get_current_roster_shape(current, nis);
789 app.work.get_parent_rosters(parents);
790 N(parents.size() == 1,
791 F("this command can only be used in a single-parent workspace"));
792 base = parent_roster(parents.begin());
793
794 N(current.has_node(path), F("Unknown path '%s'") % path);
795
796 // create the printer
797 basic_io::printer pr;
798
799 // print the format version
800 basic_io::stanza st;
801 st.push_str_pair(basic_io::syms::format_version, "1");
802 pr.print_stanza(st);
803
804 // the current node holds all current attributes (unchanged and new ones)
805 node_t n = current.get_node(path);
806 for (full_attr_map_t::const_iterator i = n->attrs.begin();
807 i != n->attrs.end(); ++i)
808 {
809 std::string value(i->second.second());
810 std::string state;
811
812 // if if the first value of the value pair is false this marks a
813 // dropped attribute
814 if (!i->second.first)
815 {
816 // if the attribute is dropped, we should have a base roster
817 // with that node. we need to check that for the attribute as well
818 // because if it is dropped there as well it was already deleted
819 // in any previous revision
820 I(base.has_node(path));
821
822 node_t prev_node = base.get_node(path);
823
824 // find the attribute in there
825 full_attr_map_t::const_iterator j = prev_node->attrs.find(i->first);
826 I(j != prev_node->attrs.end());
827
828 // was this dropped before? then ignore it
829 if (!j->second.first) { continue; }
830
831 state = "dropped";
832 // output the previous (dropped) value later
833 value = j->second.second();
834 }
835 // this marks either a new or an existing attribute
836 else
837 {
838 if (base.has_node(path))
839 {
840 node_t prev_node = base.get_node(path);
841 full_attr_map_t::const_iterator j =
842 prev_node->attrs.find(i->first);
843
844 // the attribute is new if it either hasn't been found
845 // in the previous roster or has been deleted there
846 if (j == prev_node->attrs.end() || !j->second.first)
847 {
848 state = "added";
849 }
850 // check if the attribute's value has been changed
851 else if (i->second.second() != j->second.second())
852 {
853 state = "changed";
854 }
855 else
856 {
857 state = "unchanged";
858 }
859 }
860 // its added since the whole node has been just added
861 else
862 {
863 state = "added";
864 }
865 }
866
867 basic_io::stanza st;
868 st.push_str_triple(basic_io::syms::attr, i->first(), value);
869 st.push_str_pair(symbol("state"), state);
870 pr.print_stanza(st);
871 }
872
873 // print the output
874 output.write(pr.buf.data(), pr.buf.size());
875}
876
877// Name: set_attribute
878// Arguments:
879// 1: file / directory name
880// 2: attribute key
881// 3: attribute value
882// Added in: 5.0
883// Purpose: Edits the workspace revision and sets an attribute on a certain path
884//
885// Error conditions: If PATH is unknown in the new roster, prints an error and
886// exits with status 1.
887CMD_AUTOMATE(set_attribute, N_("PATH KEY VALUE"),
888 N_("Sets an attribute on a certain path"),
889 "",
890 options::opts::none)
891{
892 N(args.size() == 3,
893 F("wrong argument count"));
894
895 roster_t new_roster;
896 temp_node_id_source nis;
897
898 app.require_workspace();
899 app.work.get_current_roster_shape(new_roster, nis);
900
901 file_path path = file_path_external(idx(args,0));
902
903 N(new_roster.has_node(path), F("Unknown path '%s'") % path);
904 node_t node = new_roster.get_node(path);
905
906 attr_key a_key = attr_key(idx(args,1)());
907 attr_value a_value = attr_value(idx(args,2)());
908
909 node->attrs[a_key] = make_pair(true, a_value);
910
911 parent_map parents;
912 app.work.get_parent_rosters(parents);
913
914 revision_t new_work;
915 make_revision_for_workspace(parents, new_roster, new_work);
916 app.work.put_work_rev(new_work);
917 app.work.update_any_attrs();
918}
919
920// Name: drop_attribute
921// Arguments:
922// 1: file / directory name
923// 2: attribute key (optional)
924// Added in: 5.0
925// Purpose: Edits the workspace revision and drops an attribute or all
926// attributes of the specified path
927//
928// Error conditions: If PATH is unknown in the new roster or the specified
929// attribute key is unknown, prints an error and exits with
930// status 1.
931CMD_AUTOMATE(drop_attribute, N_("PATH [KEY]"),
932 N_("Drops an attribute or all of them from a certain path"),
933 "",
934 options::opts::none)
935{
936 N(args.size() ==1 || args.size() == 2,
937 F("wrong argument count"));
938
939 roster_t new_roster;
940 temp_node_id_source nis;
941
942 app.require_workspace();
943 app.work.get_current_roster_shape(new_roster, nis);
944
945 file_path path = file_path_external(idx(args,0));
946
947 N(new_roster.has_node(path), F("Unknown path '%s'") % path);
948 node_t node = new_roster.get_node(path);
949
950 // Clear all attrs (or a specific attr).
951 if (args.size() == 1)
952 {
953 for (full_attr_map_t::iterator i = node->attrs.begin();
954 i != node->attrs.end(); ++i)
955 i->second = make_pair(false, "");
956 }
957 else
958 {
959 attr_key a_key = attr_key(idx(args,1)());
960 N(node->attrs.find(a_key) != node->attrs.end(),
961 F("Path '%s' does not have attribute '%s'")
962 % path % a_key);
963 node->attrs[a_key] = make_pair(false, "");
964 }
965
966 parent_map parents;
967 app.work.get_parent_rosters(parents);
968
969 revision_t new_work;
970 make_revision_for_workspace(parents, new_roster, new_work);
971 app.work.put_work_rev(new_work);
972 app.work.update_any_attrs();
973}
974
975CMD(commit, "commit", "ci", CMD_REF(workspace), N_("[PATH]..."),
976 N_("Commits workspace changes to the database"),
977 "",
978 options::opts::branch | options::opts::message | options::opts::msgfile
979 | options::opts::date | options::opts::author | options::opts::depth
980 | options::opts::exclude)
981{
982 utf8 log_message("");
983 bool log_message_given;
984 revision_t restricted_rev;
985 parent_map old_rosters;
986 roster_t new_roster;
987 temp_node_id_source nis;
988 cset excluded;
989
990 app.require_workspace();
991
992 {
993 // fail early if there isn't a key
994 rsa_keypair_id key;
995 get_user_key(key, app);
996 }
997
998 app.make_branch_sticky();
999 app.work.get_parent_rosters(old_rosters);
1000 app.work.get_current_roster_shape(new_roster, nis);
1001
1002 node_restriction mask(args_to_paths(args),
1003 args_to_paths(app.opts.exclude_patterns),
1004 app.opts.depth,
1005 old_rosters, new_roster, app);
1006
1007 app.work.update_current_roster_from_filesystem(new_roster, mask);
1008 make_restricted_revision(old_rosters, new_roster, mask, restricted_rev,
1009 excluded, execid);
1010 restricted_rev.check_sane();
1011 N(restricted_rev.is_nontrivial(), F("no changes to commit"));
1012
1013 revision_id restricted_rev_id;
1014 calculate_ident(restricted_rev, restricted_rev_id);
1015
1016 // We need the 'if' because guess_branch will try to override any branch
1017 // picked up from _MTN/options.
1018 if (app.opts.branchname().empty())
1019 {
1020 branch_name branchname, bn_candidate;
1021 for (edge_map::iterator i = restricted_rev.edges.begin();
1022 i != restricted_rev.edges.end();
1023 i++)
1024 {
1025 // this will prefer --branch if it was set
1026 guess_branch(edge_old_revision(i), app, bn_candidate);
1027 N(branchname() == "" || branchname == bn_candidate,
1028 F("parent revisions of this commit are in different branches:\n"
1029 "'%s' and '%s'.\n"
1030 "please specify a branch name for the commit, with --branch.")
1031 % branchname % bn_candidate);
1032 branchname = bn_candidate;
1033 }
1034
1035 app.opts.branchname = branchname;
1036 }
1037
1038
1039 P(F("beginning commit on branch '%s'") % app.opts.branchname);
1040 L(FL("new manifest '%s'\n"
1041 "new revision '%s'\n")
1042 % restricted_rev.new_manifest
1043 % restricted_rev_id);
1044
1045 process_commit_message_args(log_message_given, log_message, app);
1046
1047 N(!(log_message_given && app.work.has_contents_user_log()),
1048 F("_MTN/log is non-empty and log message "
1049 "was specified on command line\n"
1050 "perhaps move or delete _MTN/log,\n"
1051 "or remove --message/--message-file from the command line?"));
1052
1053 if (!log_message_given)
1054 {
1055 // This call handles _MTN/log.
1056
1057 get_log_message_interactively(restricted_rev, app, log_message);
1058
1059 // We only check for empty log messages when the user entered them
1060 // interactively. Consensus was that if someone wanted to explicitly
1061 // type --message="", then there wasn't any reason to stop them.
1062 N(log_message().find_first_not_of("\n\r\t ") != string::npos,
1063 F("empty log message; commit canceled"));
1064
1065 // We save interactively entered log messages to _MTN/log, so if
1066 // something goes wrong, the next commit will pop up their old
1067 // log message by default. We only do this for interactively
1068 // entered messages, because otherwise 'monotone commit -mfoo'
1069 // giving an error, means that after you correct that error and
1070 // hit up-arrow to try again, you get an "_MTN/log non-empty and
1071 // message given on command line" error... which is annoying.
1072
1073 app.work.write_user_log(log_message);
1074 }
1075
1076 // If the hook doesn't exist, allow the message to be used.
1077 bool message_validated;
1078 string reason, new_manifest_text;
1079
1080 revision_data new_rev;
1081 write_revision(restricted_rev, new_rev);
1082
1083 app.lua.hook_validate_commit_message(log_message, new_rev, app.opts.branchname,
1084 message_validated, reason);
1085 N(message_validated, F("log message rejected by hook: %s") % reason);
1086
1087 // for the divergence check, below
1088 set<revision_id> heads;
1089 app.get_project().get_branch_heads(app.opts.branchname, heads);
1090 unsigned int old_head_size = heads.size();
1091
1092 {
1093 transaction_guard guard(app.db);
1094
1095 if (app.db.revision_exists(restricted_rev_id))
1096 W(F("revision %s already in database") % restricted_rev_id);
1097 else
1098 {
1099 L(FL("inserting new revision %s") % restricted_rev_id);
1100
1101 for (edge_map::const_iterator edge = restricted_rev.edges.begin();
1102 edge != restricted_rev.edges.end();
1103 edge++)
1104 {
1105 // process file deltas or new files
1106 cset const & cs = edge_changes(edge);
1107
1108 for (map<file_path, pair<file_id, file_id> >::const_iterator
1109 i = cs.deltas_applied.begin();
1110 i != cs.deltas_applied.end(); ++i)
1111 {
1112 file_path path = i->first;
1113
1114 file_id old_content = i->second.first;
1115 file_id new_content = i->second.second;
1116
1117 if (app.db.file_version_exists(new_content))
1118 {
1119 L(FL("skipping file delta %s, already in database")
1120 % delta_entry_dst(i));
1121 }
1122 else if (app.db.file_version_exists(old_content))
1123 {
1124 L(FL("inserting delta %s -> %s")
1125 % old_content % new_content);
1126 file_data old_data;
1127 data new_data;
1128 app.db.get_file_version(old_content, old_data);
1129 read_data(path, new_data);
1130 // sanity check
1131 hexenc<id> tid;
1132 calculate_ident(new_data, tid);
1133 N(tid == new_content.inner(),
1134 F("file '%s' modified during commit, aborting")
1135 % path);
1136 delta del;
1137 diff(old_data.inner(), new_data, del);
1138 app.db.put_file_version(old_content,
1139 new_content,
1140 file_delta(del));
1141 }
1142 else
1143 // If we don't err out here, the database will later.
1144 E(false,
1145 F("Your database is missing version %s of file '%s'")
1146 % old_content % path);
1147 }
1148
1149 for (map<file_path, file_id>::const_iterator
1150 i = cs.files_added.begin();
1151 i != cs.files_added.end(); ++i)
1152 {
1153 file_path path = i->first;
1154 file_id new_content = i->second;
1155
1156 L(FL("inserting full version %s") % new_content);
1157 data new_data;
1158 read_data(path, new_data);
1159 // sanity check
1160 hexenc<id> tid;
1161 calculate_ident(new_data, tid);
1162 N(tid == new_content.inner(),
1163 F("file '%s' modified during commit, aborting")
1164 % path);
1165 app.db.put_file(new_content, file_data(new_data));
1166 }
1167 }
1168
1169 revision_data rdat;
1170 write_revision(restricted_rev, rdat);
1171 app.db.put_revision(restricted_rev_id, rdat);
1172 }
1173
1174 app.get_project().put_standard_certs_from_options(restricted_rev_id,
1175 app.opts.branchname,
1176 log_message);
1177 guard.commit();
1178 }
1179
1180 // the work revision is now whatever changes remain on top of the revision
1181 // we just checked in.
1182 revision_t remaining;
1183 make_revision_for_workspace(restricted_rev_id, excluded, remaining);
1184
1185 // small race condition here...
1186 app.work.put_work_rev(remaining);
1187 P(F("committed revision %s") % restricted_rev_id);
1188
1189 app.work.blank_user_log();
1190
1191 app.get_project().get_branch_heads(app.opts.branchname, heads);
1192 if (heads.size() > old_head_size && old_head_size > 0) {
1193 P(F("note: this revision creates divergence\n"
1194 "note: you may (or may not) wish to run '%s merge'")
1195 % ui.prog_name);
1196 }
1197
1198 app.work.update_any_attrs();
1199 app.work.maybe_update_inodeprints();
1200
1201 {
1202 // Tell lua what happened. Yes, we might lose some information
1203 // here, but it's just an indicator for lua, eg. to post stuff to
1204 // a mailing list. If the user *really* cares about cert validity,
1205 // multiple certs with same name, etc. they can inquire further,
1206 // later.
1207 map<cert_name, cert_value> certs;
1208 vector< revision<cert> > ctmp;
1209 app.get_project().get_revision_certs(restricted_rev_id, ctmp);
1210 for (vector< revision<cert> >::const_iterator i = ctmp.begin();
1211 i != ctmp.end(); ++i)
1212 {
1213 cert_value vtmp;
1214 decode_base64(i->inner().value, vtmp);
1215 certs.insert(make_pair(i->inner().name, vtmp));
1216 }
1217 revision_data rdat;
1218 app.db.get_revision(restricted_rev_id, rdat);
1219 app.lua.hook_note_commit(restricted_rev_id, rdat, certs);
1220 }
1221}
1222
1223CMD_NO_WORKSPACE(setup, "setup", "", CMD_REF(tree), N_("[DIRECTORY]"),
1224 N_("Sets up a new workspace directory"),
1225 N_("If no directory is specified, uses the current directory."),
1226 options::opts::branch)
1227{
1228 if (args.size() > 1)
1229 throw usage(execid);
1230
1231 N(!app.opts.branchname().empty(), F("need --branch argument for setup"));
1232 app.db.ensure_open();
1233
1234 string dir;
1235 if (args.size() == 1)
1236 dir = idx(args,0)();
1237 else
1238 dir = ".";
1239
1240 app.create_workspace(dir);
1241
1242 revision_t rev;
1243 make_revision_for_workspace(revision_id(), cset(), rev);
1244 app.work.put_work_rev(rev);
1245}
1246
1247CMD_NO_WORKSPACE(import, "import", "", CMD_REF(tree), N_("DIRECTORY"),
1248 N_("Imports the contents of a directory into a branch"),
1249 "",
1250 options::opts::branch | options::opts::revision |
1251 options::opts::message | options::opts::msgfile |
1252 options::opts::dryrun |
1253 options::opts::no_ignore | options::opts::exclude |
1254 options::opts::author | options::opts::date)
1255{
1256 revision_id ident;
1257 system_path dir;
1258
1259 N(args.size() == 1,
1260 F("you must specify a directory to import"));
1261
1262 if (app.opts.revision_selectors.size() == 1)
1263 {
1264 // use specified revision
1265 complete(app, idx(app.opts.revision_selectors, 0)(), ident);
1266 N(app.db.revision_exists(ident),
1267 F("no such revision '%s'") % ident);
1268
1269 guess_branch(ident, app);
1270
1271 I(!app.opts.branchname().empty());
1272
1273 N(app.get_project().revision_is_in_branch(ident, app.opts.branchname),
1274 F("revision %s is not a member of branch %s")
1275 % ident % app.opts.branchname);
1276 }
1277 else
1278 {
1279 // use branch head revision
1280 N(!app.opts.branchname().empty(),
1281 F("use --revision or --branch to specify what to checkout"));
1282
1283 set<revision_id> heads;
1284 app.get_project().get_branch_heads(app.opts.branchname, heads);
1285 if (heads.size() > 1)
1286 {
1287 P(F("branch %s has multiple heads:") % app.opts.branchname);
1288 for (set<revision_id>::const_iterator i = heads.begin(); i != heads.end(); ++i)
1289 P(i18n_format(" %s") % describe_revision(app, *i));
1290 P(F("choose one with '%s checkout -r<id>'") % ui.prog_name);
1291 E(false, F("branch %s has multiple heads") % app.opts.branchname);
1292 }
1293 if (heads.size() > 0)
1294 ident = *(heads.begin());
1295 }
1296
1297 dir = system_path(idx(args, 0));
1298 require_path_is_directory
1299 (dir,
1300 F("import directory '%s' doesn't exists") % dir,
1301 F("import directory '%s' is a file") % dir);
1302
1303 app.create_workspace(dir);
1304
1305 try
1306 {
1307 revision_t rev;
1308 make_revision_for_workspace(ident, cset(), rev);
1309 app.work.put_work_rev(rev);
1310
1311 // prepare stuff for 'add' and so on.
1312 app.found_workspace = true; // Yup, this is cheating!
1313
1314 args_vector empty_args;
1315 options save_opts;
1316 // add --unknown
1317 save_opts.exclude_patterns = app.opts.exclude_patterns;
1318 app.opts.exclude_patterns = args_vector();
1319 app.opts.unknown = true;
1320 app.opts.recursive = true;
1321 process(app, make_command_id("workspace add"), empty_args);
1322 app.opts.recursive = false;
1323 app.opts.unknown = false;
1324 app.opts.exclude_patterns = save_opts.exclude_patterns;
1325
1326 // drop --missing
1327 save_opts.no_ignore = app.opts.no_ignore;
1328 app.opts.missing = true;
1329 process(app, make_command_id("workspace drop"), empty_args);
1330 app.opts.missing = false;
1331 app.opts.no_ignore = save_opts.no_ignore;
1332
1333 // commit
1334 if (!app.opts.dryrun)
1335 process(app, make_command_id("workspace commit"), empty_args);
1336 }
1337 catch (...)
1338 {
1339 // clean up before rethrowing
1340 delete_dir_recursive(bookkeeping_root);
1341 throw;
1342 }
1343
1344 // clean up
1345 delete_dir_recursive(bookkeeping_root);
1346}
1347
1348CMD_NO_WORKSPACE(migrate_workspace, "migrate_workspace", "", CMD_REF(tree),
1349 N_("[DIRECTORY]"),
1350 N_("Migrates a workspace directory's metadata to the latest format"),
1351 N_("If no directory is given, defaults to the current workspace."),
1352 options::opts::none)
1353{
1354 if (args.size() > 1)
1355 throw usage(execid);
1356
1357 if (args.size() == 1)
1358 go_to_workspace(system_path(idx(args, 0)));
1359
1360 app.work.migrate_ws_format();
1361}
1362
1363CMD(refresh_inodeprints, "refresh_inodeprints", "", CMD_REF(tree), "",
1364 N_("Refreshes the inodeprint cache"),
1365 "",
1366 options::opts::none)
1367{
1368 app.require_workspace();
1369 app.work.enable_inodeprints();
1370 app.work.maybe_update_inodeprints();
1371}
1372
1373
1374// Local Variables:
1375// mode: C++
1376// fill-column: 76
1377// c-file-style: "gnu"
1378// indent-tabs-mode: nil
1379// End:
1380// 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