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