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