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