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