monotone

monotone Mtn Source Tree

Root/automate.cc

1// Copyright (C) 2004, 2007 Nathaniel Smith <njs@pobox.com>
2// Copyright (C) 2007 - 2008 Stephen Leake <stephen_leake@stephe-leake.org>
3//
4// This program is made available under the GNU GPL version 2.0 or
5// greater. See the accompanying file COPYING for details.
6//
7// This program is distributed WITHOUT ANY WARRANTY; without even the
8// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
9// PURPOSE.
10
11#include "base.hh"
12#include <algorithm>
13#include <iterator>
14#include <sstream>
15#include <unistd.h>
16#include "vector.hh"
17
18#include <boost/bind.hpp>
19#include <boost/function.hpp>
20#include "lexical_cast.hh"
21#include <boost/tuple/tuple.hpp>
22#include <boost/shared_ptr.hpp>
23
24#include "app_state.hh"
25#include "project.hh"
26#include "basic_io.hh"
27#include "cert.hh"
28#include "cmd.hh"
29#include "commands.hh"
30#include "constants.hh"
31#include "inodeprint.hh"
32#include "keys.hh"
33#include "key_store.hh"
34#include "file_io.hh"
35#include "packet.hh"
36#include "restrictions.hh"
37#include "revision.hh"
38#include "roster.hh"
39#include "transforms.hh"
40#include "simplestring_xform.hh"
41#include "vocab.hh"
42#include "globish.hh"
43#include "charset.hh"
44#include "safe_map.hh"
45#include "work.hh"
46#include "xdelta.hh"
47#include "database.hh"
48
49using std::allocator;
50using std::basic_ios;
51using std::basic_stringbuf;
52using std::char_traits;
53using std::inserter;
54using std::make_pair;
55using std::map;
56using std::multimap;
57using std::ostream;
58using std::ostringstream;
59using std::pair;
60using std::set;
61using std::sort;
62using std::streamsize;
63using std::string;
64using std::vector;
65
66using boost::shared_ptr;
67
68
69// Name: heads
70// Arguments:
71// 1: branch name (optional, default branch is used if non-existant)
72// Added in: 0.0
73// Purpose: Prints the heads of the given branch.
74// Output format: A list of revision ids, in hexadecimal, each followed by a
75// newline. Revision ids are printed in alphabetically sorted order.
76// Error conditions: If the branch does not exist, prints nothing. (There are
77// no heads.)
78CMD_AUTOMATE(heads, N_("[BRANCH]"),
79 N_("Prints the heads of the given branch"),
80 "",
81 options::opts::none)
82{
83 N(args.size() < 2,
84 F("wrong argument count"));
85
86 database db(app);
87 project_t project(db);
88
89 branch_name branch;
90 if (args.size() == 1)
91 // branchname was explicitly given, use that
92 branch = branch_name(idx(args, 0)());
93 else
94 {
95 workspace::require_workspace(F("with no argument, this command prints the heads of the workspace's branch"));
96 branch = app.opts.branchname;
97 }
98
99 set<revision_id> heads;
100 project.get_branch_heads(branch, heads, app.opts.ignore_suspend_certs);
101 for (set<revision_id>::const_iterator i = heads.begin();
102 i != heads.end(); ++i)
103 output << *i << '\n';
104}
105
106// Name: ancestors
107// Arguments:
108// 1 or more: revision ids
109// Added in: 0.2
110// Purpose: Prints the ancestors (exclusive) of the given revisions
111// Output format: A list of revision ids, in hexadecimal, each followed by a
112// newline. Revision ids are printed in alphabetically sorted order.
113// Error conditions: If any of the revisions do not exist, prints nothing to
114// stdout, prints an error message to stderr, and exits with status 1.
115CMD_AUTOMATE(ancestors, N_("REV1 [REV2 [REV3 [...]]]"),
116 N_("Prints the ancestors of the given revisions"),
117 "",
118 options::opts::none)
119{
120 N(args.size() > 0,
121 F("wrong argument count"));
122
123 database db(app);
124
125 set<revision_id> ancestors;
126 vector<revision_id> frontier;
127 for (args_vector::const_iterator i = args.begin(); i != args.end(); ++i)
128 {
129 revision_id rid(decode_hexenc((*i)()));
130 N(db.revision_exists(rid), F("no such revision '%s'") % rid);
131 frontier.push_back(rid);
132 }
133 while (!frontier.empty())
134 {
135 revision_id rid = frontier.back();
136 frontier.pop_back();
137 if(!null_id(rid)) {
138 set<revision_id> parents;
139 db.get_revision_parents(rid, parents);
140 for (set<revision_id>::const_iterator i = parents.begin();
141 i != parents.end(); ++i)
142 {
143 if (ancestors.find(*i) == ancestors.end())
144 {
145 frontier.push_back(*i);
146 ancestors.insert(*i);
147 }
148 }
149 }
150 }
151 for (set<revision_id>::const_iterator i = ancestors.begin();
152 i != ancestors.end(); ++i)
153 if (!null_id(*i))
154 output << *i << '\n';
155}
156
157
158// Name: descendents
159// Arguments:
160// 1 or more: revision ids
161// Added in: 0.1
162// Purpose: Prints the descendents (exclusive) of the given revisions
163// Output format: A list of revision ids, in hexadecimal, each followed by a
164// newline. Revision ids are printed in alphabetically sorted order.
165// Error conditions: If any of the revisions do not exist, prints nothing to
166// stdout, prints an error message to stderr, and exits with status 1.
167CMD_AUTOMATE(descendents, N_("REV1 [REV2 [REV3 [...]]]"),
168 N_("Prints the descendents of the given revisions"),
169 "",
170 options::opts::none)
171{
172 N(args.size() > 0,
173 F("wrong argument count"));
174
175 database db(app);
176
177 set<revision_id> descendents;
178 vector<revision_id> frontier;
179 for (args_vector::const_iterator i = args.begin(); i != args.end(); ++i)
180 {
181 revision_id rid(decode_hexenc((*i)()));
182 N(db.revision_exists(rid), F("no such revision '%s'") % rid);
183 frontier.push_back(rid);
184 }
185 while (!frontier.empty())
186 {
187 revision_id rid = frontier.back();
188 frontier.pop_back();
189 set<revision_id> children;
190 db.get_revision_children(rid, children);
191 for (set<revision_id>::const_iterator i = children.begin();
192 i != children.end(); ++i)
193 {
194 if (descendents.find(*i) == descendents.end())
195 {
196 frontier.push_back(*i);
197 descendents.insert(*i);
198 }
199 }
200 }
201 for (set<revision_id>::const_iterator i = descendents.begin();
202 i != descendents.end(); ++i)
203 output << *i << '\n';
204}
205
206
207// Name: erase_ancestors
208// Arguments:
209// 0 or more: revision ids
210// Added in: 0.1
211// Purpose: Prints all arguments, except those that are an ancestor of some
212// other argument. One way to think about this is that it prints the
213// minimal elements of the given set, under the ordering imposed by the
214// "child of" relation. Another way to think of it is if the arguments were
215// a branch, then we print the heads of that branch.
216// Output format: A list of revision ids, in hexadecimal, each followed by a
217// newline. Revision ids are printed in alphabetically sorted order.
218// Error conditions: If any of the revisions do not exist, prints nothing to
219// stdout, prints an error message to stderr, and exits with status 1.
220CMD_AUTOMATE(erase_ancestors, N_("[REV1 [REV2 [REV3 [...]]]]"),
221 N_("Erases the ancestors in a list of revisions"),
222 "",
223 options::opts::none)
224{
225 database db(app);
226
227 set<revision_id> revs;
228 for (args_vector::const_iterator i = args.begin(); i != args.end(); ++i)
229 {
230 revision_id rid(decode_hexenc((*i)()));
231 N(db.revision_exists(rid), F("no such revision '%s'") % rid);
232 revs.insert(rid);
233 }
234 erase_ancestors(db, revs);
235 for (set<revision_id>::const_iterator i = revs.begin(); i != revs.end(); ++i)
236 output << *i << '\n';
237}
238
239// Name: toposort
240// Arguments:
241// 0 or more: revision ids
242// Added in: 0.1
243// Purpose: Prints all arguments, topologically sorted. I.e., if A is an
244// ancestor of B, then A will appear before B in the output list.
245// Output format: A list of revision ids, in hexadecimal, each followed by a
246// newline. Revisions are printed in topologically sorted order.
247// Error conditions: If any of the revisions do not exist, prints nothing to
248// stdout, prints an error message to stderr, and exits with status 1.
249CMD_AUTOMATE(toposort, N_("[REV1 [REV2 [REV3 [...]]]]"),
250 N_("Topologically sorts a list of revisions"),
251 "",
252 options::opts::none)
253{
254 database db(app);
255
256 set<revision_id> revs;
257 for (args_vector::const_iterator i = args.begin(); i != args.end(); ++i)
258 {
259 revision_id rid(decode_hexenc((*i)()));
260 N(db.revision_exists(rid), F("no such revision '%s'") % rid);
261 revs.insert(rid);
262 }
263 vector<revision_id> sorted;
264 toposort(db, revs, sorted);
265 for (vector<revision_id>::const_iterator i = sorted.begin();
266 i != sorted.end(); ++i)
267 output << *i << '\n';
268}
269
270// Name: ancestry_difference
271// Arguments:
272// 1: a revision id
273// 0 or more further arguments: also revision ids
274// Added in: 0.1
275// Purpose: Prints all ancestors of the first revision A, that are not also
276// ancestors of the other revision ids, the "Bs". For purposes of this
277// command, "ancestor" is an inclusive term; that is, A is an ancestor of
278// one of the Bs, it will not be printed, but otherwise, it will be; and
279// none of the Bs will ever be printed. If A is a new revision, and Bs are
280// revisions that you have processed before, then this command tells you
281// which revisions are new since then.
282// Output format: A list of revision ids, in hexadecimal, each followed by a
283// newline. Revisions are printed in topologically sorted order.
284// Error conditions: If any of the revisions do not exist, prints nothing to
285// stdout, prints an error message to stderr, and exits with status 1.
286CMD_AUTOMATE(ancestry_difference, N_("NEW_REV [OLD_REV1 [OLD_REV2 [...]]]"),
287 N_("Lists the ancestors of the first revision given, not in "
288 "the others"),
289 "",
290 options::opts::none)
291{
292 N(args.size() > 0,
293 F("wrong argument count"));
294
295 database db(app);
296
297 revision_id a;
298 set<revision_id> bs;
299 args_vector::const_iterator i = args.begin();
300 a = revision_id(decode_hexenc((*i)()));
301 N(db.revision_exists(a), F("no such revision '%s'") % a);
302 for (++i; i != args.end(); ++i)
303 {
304 revision_id b(decode_hexenc((*i)()));
305 N(db.revision_exists(b), F("no such revision '%s'") % b);
306 bs.insert(b);
307 }
308 set<revision_id> ancestors;
309 ancestry_difference(db, a, bs, ancestors);
310
311 vector<revision_id> sorted;
312 toposort(db, ancestors, sorted);
313 for (vector<revision_id>::const_iterator i = sorted.begin();
314 i != sorted.end(); ++i)
315 output << *i << '\n';
316}
317
318// Name: leaves
319// Arguments:
320// None
321// Added in: 0.1
322// Purpose: Prints the leaves of the revision graph, i.e., all revisions that
323// have no children. This is similar, but not identical to the
324// functionality of 'heads', which prints every revision in a branch, that
325// has no descendents in that branch. If every revision in the database was
326// in the same branch, then they would be identical. Generally, every leaf
327// is the head of some branch, but not every branch head is a leaf.
328// Output format: A list of revision ids, in hexadecimal, each followed by a
329// newline. Revision ids are printed in alphabetically sorted order.
330// Error conditions: None.
331CMD_AUTOMATE(leaves, "",
332 N_("Lists the leaves of the revision graph"),
333 "",
334 options::opts::none)
335{
336 N(args.size() == 0,
337 F("no arguments needed"));
338
339 database db(app);
340
341 set<revision_id> leaves;
342 db.get_leaves(leaves);
343 for (set<revision_id>::const_iterator i = leaves.begin();
344 i != leaves.end(); ++i)
345 output << *i << '\n';
346}
347
348// Name: roots
349// Arguments:
350// None
351// Added in: 4.3
352// Purpose: Prints the roots of the revision graph, i.e. all revisions that
353// have no parents.
354// Output format: A list of revision ids, in hexadecimal, each followed by a
355// newline. Revision ids are printed in alphabetically sorted order.
356// Error conditions: None.
357CMD_AUTOMATE(roots, "",
358 N_("Lists the roots of the revision graph"),
359 "",
360 options::opts::none)
361{
362 N(args.size() == 0,
363 F("no arguments needed"));
364
365 database db(app);
366
367 // the real root revisions are the children of one single imaginary root
368 // with an empty revision id
369 set<revision_id> roots;
370 revision_id nullid;
371 db.get_revision_children(nullid, roots);
372 for (set<revision_id>::const_iterator i = roots.begin();
373 i != roots.end(); ++i)
374 output << *i << '\n';
375}
376
377// Name: parents
378// Arguments:
379// 1: a revision id
380// Added in: 0.2
381// Purpose: Prints the immediate ancestors of the given revision, i.e., the
382// parents.
383// Output format: A list of revision ids, in hexadecimal, each followed by a
384// newline. Revision ids are printed in alphabetically sorted order.
385// Error conditions: If the revision does not exist, prints nothing to stdout,
386// prints an error message to stderr, and exits with status 1.
387CMD_AUTOMATE(parents, N_("REV"),
388 N_("Prints the parents of a revision"),
389 "",
390 options::opts::none)
391{
392 N(args.size() == 1,
393 F("wrong argument count"));
394
395 database db(app);
396
397 revision_id rid(decode_hexenc(idx(args, 0)()));
398 N(db.revision_exists(rid), F("no such revision '%s'") % rid);
399 set<revision_id> parents;
400 db.get_revision_parents(rid, parents);
401 for (set<revision_id>::const_iterator i = parents.begin();
402 i != parents.end(); ++i)
403 if (!null_id(*i))
404 output << *i << '\n';
405}
406
407// Name: children
408// Arguments:
409// 1: a revision id
410// Added in: 0.2
411// Purpose: Prints the immediate descendents of the given revision, i.e., the
412// children.
413// Output format: A list of revision ids, in hexadecimal, each followed by a
414// newline. Revision ids are printed in alphabetically sorted order.
415// Error conditions: If the revision does not exist, prints nothing to stdout,
416// prints an error message to stderr, and exits with status 1.
417CMD_AUTOMATE(children, N_("REV"),
418 N_("Prints the children of a revision"),
419 "",
420 options::opts::none)
421{
422 N(args.size() == 1,
423 F("wrong argument count"));
424
425 database db(app);
426
427 revision_id rid(decode_hexenc(idx(args, 0)()));
428 N(db.revision_exists(rid), F("no such revision '%s'") % rid);
429 set<revision_id> children;
430 db.get_revision_children(rid, children);
431 for (set<revision_id>::const_iterator i = children.begin();
432 i != children.end(); ++i)
433 if (!null_id(*i))
434 output << *i << '\n';
435}
436
437// Name: graph
438// Arguments:
439// None
440// Added in: 0.2
441// Purpose: Prints out the complete ancestry graph of this database.
442// Output format:
443// Each line begins with a revision id. Following this are zero or more
444// space-prefixed revision ids. Each revision id after the first is a
445// parent (in the sense of 'automate parents') of the first. For instance,
446// the following are valid lines:
447// 07804171823d963f78d6a0ff1763d694dd74ff40
448// 07804171823d963f78d6a0ff1763d694dd74ff40 79d755c197e54dd3db65751d3803833d4cbf0d01
449// 07804171823d963f78d6a0ff1763d694dd74ff40 79d755c197e54dd3db65751d3803833d4cbf0d01 a02e7a1390e3e4745c31be922f03f56450c13dce
450// The first would indicate that 07804171823d963f78d6a0ff1763d694dd74ff40
451// was a root node; the second would indicate that it had one parent, and
452// the third would indicate that it had two parents, i.e., was a merge.
453//
454// The output as a whole is alphabetically sorted; additionally, the parents
455// within each line are alphabetically sorted.
456// Error conditions: None.
457CMD_AUTOMATE(graph, "",
458 N_("Prints the complete ancestry graph"),
459 "",
460 options::opts::none)
461{
462 N(args.size() == 0,
463 F("no arguments needed"));
464
465 database db(app);
466
467 multimap<revision_id, revision_id> edges_mmap;
468 map<revision_id, set<revision_id> > child_to_parents;
469
470 db.get_revision_ancestry(edges_mmap);
471
472 for (multimap<revision_id, revision_id>::const_iterator i = edges_mmap.begin();
473 i != edges_mmap.end(); ++i)
474 {
475 if (child_to_parents.find(i->second) == child_to_parents.end())
476 child_to_parents.insert(make_pair(i->second, set<revision_id>()));
477 if (null_id(i->first))
478 continue;
479 map<revision_id, set<revision_id> >::iterator
480 j = child_to_parents.find(i->second);
481 I(j->first == i->second);
482 j->second.insert(i->first);
483 }
484
485 for (map<revision_id, set<revision_id> >::const_iterator
486 i = child_to_parents.begin();
487 i != child_to_parents.end(); ++i)
488 {
489 output << i->first;
490 for (set<revision_id>::const_iterator j = i->second.begin();
491 j != i->second.end(); ++j)
492 output << ' ' << *j;
493 output << '\n';
494 }
495}
496
497// Name: select
498// Arguments:
499// 1: selector
500// Added in: 0.2
501// Purpose: Prints all the revisions that match the given selector.
502// Output format: A list of revision ids, in hexadecimal, each followed by a
503// newline. Revision ids are printed in alphabetically sorted order.
504// Error conditions: None.
505CMD_AUTOMATE(select, N_("SELECTOR"),
506 N_("Lists the revisions that match a selector"),
507 "",
508 options::opts::none)
509{
510 N(args.size() == 1,
511 F("wrong argument count"));
512
513 database db(app);
514 project_t project(db);
515 set<revision_id> completions;
516 expand_selector(app.opts, app.lua, project, idx(args, 0)(), completions);
517
518 for (set<revision_id>::const_iterator i = completions.begin();
519 i != completions.end(); ++i)
520 output << *i << '\n';
521}
522
523struct node_info
524{
525 bool exists;
526 // true if node_id is present in corresponding roster with the inventory map file_path
527 // false if not present, or present with a different file_path
528 // rest of data in this struct is invalid if false.
529 node_id id;
530 path::status type;
531 file_id ident;
532 full_attr_map_t attrs;
533
534 node_info() : exists(false), id(the_null_node), type(path::nonexistent) {}
535};
536
537static void
538get_node_info(node_t const & node, node_info & info)
539{
540 info.exists = true;
541 info.id = node->self;
542 info.attrs = node->attrs;
543 if (is_file_t(node))
544 {
545 info.type = path::file;
546 info.ident = downcast_to_file_t(node)->content;
547 }
548 else if (is_dir_t(node))
549 info.type = path::directory;
550 else
551 I(false);
552}
553
554struct inventory_item
555{
556 // Records information about a pair of nodes with the same node_id in the
557 // old roster and new roster, and the corresponding path in the
558 // filesystem.
559 node_info old_node;
560 node_info new_node;
561 file_path old_path;
562 file_path new_path;
563
564 path::status fs_type;
565 file_id fs_ident;
566
567 inventory_item() : fs_type(path::nonexistent) {}
568};
569
570typedef std::map<file_path, inventory_item> inventory_map;
571// file_path will typically be an existing filesystem file, but in the case
572// of a dropped or rename_source file it is only in the old roster, and in
573// the case of a file added --bookkeep_only or rename_target
574// --bookkeep_only, it is only in the new roster.
575
576static void
577inventory_rosters(roster_t const & old_roster,
578 roster_t const & new_roster,
579 node_restriction const & nmask,
580 path_restriction const & pmask,
581 inventory_map & inventory)
582{
583 std::map<int, file_path> old_paths;
584 std::map<int, file_path> new_paths;
585
586 node_map const & old_nodes = old_roster.all_nodes();
587 for (node_map::const_iterator i = old_nodes.begin(); i != old_nodes.end(); ++i)
588 {
589 if (nmask.includes(old_roster, i->first))
590 {
591 file_path fp;
592 old_roster.get_name(i->first, fp);
593 if (pmask.includes(fp))
594 {
595 get_node_info(old_roster.get_node(i->first), inventory[fp].old_node);
596 old_paths[inventory[fp].old_node.id] = fp;
597 }
598 }
599 }
600
601 node_map const & new_nodes = new_roster.all_nodes();
602 for (node_map::const_iterator i = new_nodes.begin(); i != new_nodes.end(); ++i)
603 {
604 if (nmask.includes(new_roster, i->first))
605 {
606 file_path fp;
607 new_roster.get_name(i->first, fp);
608 if (pmask.includes(fp))
609 {
610 get_node_info(new_roster.get_node(i->first), inventory[fp].new_node);
611 new_paths[inventory[fp].new_node.id] = fp;
612 }
613 }
614 }
615
616 std::map<int, file_path>::iterator i;
617 for (i = old_paths.begin(); i != old_paths.end(); ++i)
618 {
619 if (new_paths.find(i->first) == new_paths.end())
620 {
621 // There is no new node available; this is either a drop or a
622 // rename to outside the current path restriction.
623
624 if (new_roster.has_node(i->first))
625 {
626 // record rename to outside restriction
627 new_roster.get_name(i->first, inventory[i->second].new_path);
628 continue;
629 }
630 else
631 // drop; no new path
632 continue;
633 }
634
635 file_path old_path(i->second);
636 file_path new_path(new_paths[i->first]);
637
638 // both paths are identical, no rename
639 if (old_path == new_path)
640 continue;
641
642 // record rename
643 inventory[new_path].old_path = old_path;
644 inventory[old_path].new_path = new_path;
645 }
646
647 // Now look for new_paths that are renames from outside the current
648 // restriction, and thus are not in old_paths.
649 // FIXME: only need this if restriction is not null
650 for (i = new_paths.begin(); i != new_paths.end(); ++i)
651 {
652 if (old_paths.find(i->first) == old_paths.end())
653 {
654 // There is no old node available; this is either added or a
655 // rename from outside the current path restriction.
656
657 if (old_roster.has_node(i->first))
658 {
659 // record rename from outside restriction
660 old_roster.get_name(i->first, inventory[i->second].old_path);
661 }
662 else
663 // added; no old path
664 continue;
665 }
666 }
667}
668
669// check if the include/exclude paths contains paths to renamed nodes
670// if yes, add the corresponding old/new name of these nodes to the
671// paths as well, so the tree walker code will correctly identify them later
672// on or skips them if they should be excluded
673static void
674inventory_determine_corresponding_paths(roster_t const & old_roster,
675 roster_t const & new_roster,
676 vector<file_path> const & includes,
677 vector<file_path> const & excludes,
678 vector<file_path> & additional_includes,
679 vector<file_path> & additional_excludes)
680{
681 // at first check the includes vector
682 for (int i=0, s=includes.size(); i<s; i++)
683 {
684 file_path fp = includes.at(i);
685
686 if (old_roster.has_node(fp))
687 {
688 node_t node = old_roster.get_node(fp);
689 if (new_roster.has_node(node->self))
690 {
691 file_path new_path;
692 new_roster.get_name(node->self, new_path);
693 if (fp != new_path &&
694 find(includes.begin(), includes.end(), new_path) == includes.end())
695 {
696 additional_includes.push_back(new_path);
697 }
698 }
699 }
700
701 if (new_roster.has_node(fp))
702 {
703 node_t node = new_roster.get_node(fp);
704 if (old_roster.has_node(node->self))
705 {
706 file_path old_path;
707 old_roster.get_name(node->self, old_path);
708 if (fp != old_path &&
709 find(includes.begin(), includes.end(), old_path) == includes.end())
710 {
711 additional_includes.push_back(old_path);
712 }
713 }
714 }
715 }
716
717 // and now the excludes vector
718 vector<file_path> new_excludes;
719 for (int i=0, s=excludes.size(); i<s; i++)
720 {
721 file_path fp = excludes.at(i);
722
723 if (old_roster.has_node(fp))
724 {
725 node_t node = old_roster.get_node(fp);
726 if (new_roster.has_node(node->self))
727 {
728 file_path new_path;
729 new_roster.get_name(node->self, new_path);
730 if (fp != new_path &&
731 find(excludes.begin(), excludes.end(), new_path) == excludes.end())
732 {
733 additional_excludes.push_back(new_path);
734 }
735 }
736 }
737
738 if (new_roster.has_node(fp))
739 {
740 node_t node = new_roster.get_node(fp);
741 if (old_roster.has_node(node->self))
742 {
743 file_path old_path;
744 old_roster.get_name(node->self, old_path);
745 if (fp != old_path &&
746 find(excludes.begin(), excludes.end(), old_path) == excludes.end())
747 {
748 additional_excludes.push_back(old_path);
749 }
750 }
751 }
752 }
753}
754
755struct inventory_itemizer : public tree_walker
756{
757 path_restriction const & mask;
758 inventory_map & inventory;
759 inodeprint_map ipm;
760 workspace & work;
761 worker_pool & pool;
762
763 inventory_itemizer(workspace & work,
764 worker_pool & pool,
765 path_restriction const & m,
766 inventory_map & i)
767 : mask(m), inventory(i), work(work), pool(pool)
768 {
769 if (work.in_inodeprints_mode())
770 {
771 data dat;
772 work.read_inodeprints(dat);
773 read_inodeprint_map(dat, ipm);
774 }
775 }
776 virtual bool visit_dir(file_path const & path);
777 virtual void visit_file(file_path const & path);
778};
779
780bool
781inventory_itemizer::visit_dir(file_path const & path)
782{
783 if(mask.includes(path))
784 {
785 inventory[path].fs_type = path::directory;
786 }
787 // don't recurse into ignored subdirectories
788 return !work.ignore_file(path);
789}
790
791void
792inventory_itemizer::visit_file(file_path const & path)
793{
794 if (mask.includes(path))
795 {
796 inventory_item & item = inventory[path];
797
798 item.fs_type = path::file;
799
800 if (item.new_node.exists)
801 {
802 if (inodeprint_unchanged(ipm, path))
803 item.fs_ident = item.old_node.ident;
804 else
805 {
806 // FIXME: pool??
807 shared_ptr<file_path> fpath(new file_path(path));
808 shared_ptr<file_id> fid(new file_id());
809 ident_existing_file(pool, fpath, fid);
810 pool.wait();
811 item.fs_ident = *fid;
812 }
813 }
814 }
815}
816
817static void
818inventory_filesystem(workspace & work,
819 path_restriction const & mask,
820 inventory_map & inventory)
821{
822 worker_pool pool;
823 inventory_itemizer itemizer(work, pool, mask, inventory);
824 file_path const root;
825 // The constructor file_path() returns ""; the root directory. walk_tree
826 // does not visit that node, so set fs_type now, if it meets the
827 // restriction.
828 if (mask.includes(root))
829 {
830 inventory[root].fs_type = path::directory;
831 }
832 walk_tree(root, itemizer);
833 pool.wait();
834}
835
836namespace
837{
838 namespace syms
839 {
840 symbol const path("path");
841 symbol const old_type("old_type");
842 symbol const new_type("new_type");
843 symbol const fs_type("fs_type");
844 symbol const old_path("old_path");
845 symbol const new_path("new_path");
846 symbol const status("status");
847 symbol const birth("birth");
848 symbol const changes("changes");
849 }
850}
851
852static void
853inventory_determine_states(workspace & work, file_path const & fs_path,
854 inventory_item const & item, roster_t const & old_roster,
855 roster_t const & new_roster, vector<string> & states)
856{
857 // if both nodes exist, the only interesting case is
858 // when the node ids aren't equal (so we have different nodes
859 // with one and the same path in the old and the new roster)
860 if (item.old_node.exists &&
861 item.new_node.exists &&
862 item.old_node.id != item.new_node.id)
863 {
864 if (new_roster.has_node(item.old_node.id))
865 states.push_back("rename_source");
866 else
867 states.push_back("dropped");
868
869 if (old_roster.has_node(item.new_node.id))
870 states.push_back("rename_target");
871 else
872 states.push_back("added");
873 }
874 // this can be either a drop or a renamed item
875 else if (item.old_node.exists &&
876 !item.new_node.exists)
877 {
878 if (new_roster.has_node(item.old_node.id))
879 states.push_back("rename_source");
880 else
881 states.push_back("dropped");
882 }
883 // this can be either an add or a renamed item
884 else if (!item.old_node.exists &&
885 item.new_node.exists)
886 {
887 if (old_roster.has_node(item.new_node.id))
888 states.push_back("rename_target");
889 else
890 states.push_back("added");
891 }
892
893 // check the state of the file system item
894 if (item.fs_type == path::nonexistent)
895 {
896 if (item.new_node.exists)
897 {
898 states.push_back("missing");
899
900 // If this node is in a directory that is ignored in .mtn-ignore,
901 // we will output this warning. Note that we don't detect a known
902 // file that is ignored but not in an ignored directory.
903 if (work.ignore_file(fs_path))
904 W(F("'%s' is both known and ignored; "
905 "it will be shown as 'missing'. Check .mtn-ignore.")
906 % fs_path);
907 }
908 }
909 else // exists on filesystem
910 {
911 if (!item.new_node.exists)
912 {
913 if (work.ignore_file(fs_path))
914 {
915 states.push_back("ignored");
916 }
917 else
918 {
919 states.push_back("unknown");
920 }
921 }
922 else if (item.new_node.type != item.fs_type)
923 {
924 states.push_back("invalid");
925 }
926 else
927 {
928 states.push_back("known");
929 }
930 }
931}
932
933static void
934inventory_determine_changes(inventory_item const & item, roster_t const & old_roster,
935 vector<string> & changes)
936{
937 // old nodes do not have any recorded content changes and attributes,
938 // so we can't print anything for them here
939 if (!item.new_node.exists)
940 return;
941
942 // this is an existing item
943 if (old_roster.has_node(item.new_node.id))
944 {
945 // check if the content has changed - this makes only sense for files
946 // for which we can get the content id of both new and old nodes.
947 if (item.new_node.type == path::file && item.fs_type != path::nonexistent)
948 {
949 file_t old_file = downcast_to_file_t(old_roster.get_node(item.new_node.id));
950
951 switch (item.old_node.type)
952 {
953 case path::file:
954 case path::nonexistent:
955 // A file can be nonexistent due to mtn drop, user delete, mtn
956 // rename, or user rename. If it was drop or delete, it would
957 // not be in the new roster, and we would not get here. So
958 // it's a rename, and we can get the content. This lets us
959 // check if a user has edited a file after renaming it.
960 if (item.fs_ident != old_file->content)
961 changes.push_back("content");
962 break;
963
964 case path::directory:
965 break;
966 }
967 }
968
969 // now look for changed attributes
970 node_t old_node = old_roster.get_node(item.new_node.id);
971 if (old_node->attrs != item.new_node.attrs)
972 changes.push_back("attrs");
973 }
974 else
975 {
976 // FIXME: paranoia: shall we I(new_roster.has_node(item.new_node.id)) here?
977
978 // this is apparently a new item, if it is a file it gets at least
979 // the "content" marker and we also check for recorded attributes
980 if (item.new_node.type == path::file)
981 changes.push_back("content");
982
983 if (!item.new_node.attrs.empty())
984 changes.push_back("attrs");
985 }
986}
987
988static revision_id
989inventory_determine_birth(inventory_item const & item,
990 roster_t const & old_roster,
991 marking_map const & old_marking)
992{
993 revision_id rid;
994 if (old_roster.has_node(item.new_node.id))
995 {
996 node_t node = old_roster.get_node(item.new_node.id);
997 marking_map::const_iterator m = old_marking.find(node->self);
998 I(m != old_marking.end());
999 marking_t mark = m->second;
1000 rid = mark.birth_revision;
1001 }
1002 return rid;
1003}
1004
1005// Name: inventory
1006// Arguments: [PATH]...
1007// Added in: 1.0
1008// Modified to basic_io in: 4.1
1009
1010// Purpose: Prints a summary of every file or directory found in the
1011// workspace or its associated base manifest.
1012
1013// See monotone.texi for output format description.
1014//
1015// Error conditions: If no workspace book keeping _MTN directory is found,
1016// prints an error message to stderr, and exits with status 1.
1017
1018CMD_AUTOMATE(inventory, N_("[PATH]..."),
1019 N_("Prints a summary of files found in the workspace"),
1020 "",
1021 options::opts::depth |
1022 options::opts::exclude |
1023 options::opts::no_ignored |
1024 options::opts::no_unknown |
1025 options::opts::no_unchanged |
1026 options::opts::no_corresponding_renames)
1027{
1028 database db(app);
1029 workspace work(app);
1030
1031 parent_map parents;
1032 work.get_parent_rosters(db, parents);
1033 // for now, until we've figured out what the format could look like
1034 // and what conceptional model we can implement
1035 // see: http://www.venge.net/mtn-wiki/MultiParentWorkspaceFallout
1036 N(parents.size() == 1,
1037 F("this command can only be used in a single-parent workspace"));
1038
1039 roster_t new_roster, old_roster = parent_roster(parents.begin());
1040 marking_map old_marking = parent_marking(parents.begin());
1041 temp_node_id_source nis;
1042
1043 work.get_current_roster_shape(db, nis, new_roster);
1044
1045 inventory_map inventory;
1046 vector<file_path> includes = args_to_paths(args);
1047 vector<file_path> excludes = args_to_paths(app.opts.exclude_patterns);
1048
1049 if (!app.opts.no_corresponding_renames)
1050 {
1051 vector<file_path> add_includes, add_excludes;
1052 inventory_determine_corresponding_paths(old_roster, new_roster,
1053 includes, excludes,
1054 add_includes, add_excludes);
1055
1056 copy(add_includes.begin(), add_includes.end(),
1057 inserter(includes, includes.end()));
1058
1059 copy(add_excludes.begin(), add_excludes.end(),
1060 inserter(excludes, excludes.end()));
1061 }
1062
1063 node_restriction nmask(work, includes, excludes, app.opts.depth, old_roster, new_roster);
1064 // skip the check of the workspace paths because some of them might
1065 // be missing and the user might want to query the recorded structure
1066 // of them anyways
1067 path_restriction pmask(work, includes, excludes, app.opts.depth, path_restriction::skip_check);
1068
1069 inventory_rosters(old_roster, new_roster, nmask, pmask, inventory);
1070 inventory_filesystem(work, pmask, inventory);
1071
1072 basic_io::printer pr;
1073
1074 for (inventory_map::const_iterator i = inventory.begin(); i != inventory.end();
1075 ++i)
1076 {
1077 file_path const & fp = i->first;
1078 inventory_item const & item = i->second;
1079
1080 //
1081 // check if we should output this element at all
1082 //
1083 vector<string> states;
1084 inventory_determine_states(work, fp, item,
1085 old_roster, new_roster, states);
1086
1087 if (find(states.begin(), states.end(), "ignored") != states.end() &&
1088 app.opts.no_ignored)
1089 continue;
1090
1091 if (find(states.begin(), states.end(), "unknown") != states.end() &&
1092 app.opts.no_unknown)
1093 continue;
1094
1095 vector<string> changes;
1096 inventory_determine_changes(item, old_roster, changes);
1097
1098 revision_id birth_revision =
1099 inventory_determine_birth(item, old_roster, old_marking);
1100
1101 bool is_tracked =
1102 find(states.begin(), states.end(), "unknown") == states.end() &&
1103 find(states.begin(), states.end(), "ignored") == states.end();
1104
1105 bool has_changed =
1106 find(states.begin(), states.end(), "rename_source") != states.end() ||
1107 find(states.begin(), states.end(), "rename_target") != states.end() ||
1108 find(states.begin(), states.end(), "added") != states.end() ||
1109 find(states.begin(), states.end(), "dropped") != states.end() ||
1110 find(states.begin(), states.end(), "missing") != states.end() ||
1111 !changes.empty();
1112
1113 if (is_tracked && !has_changed && app.opts.no_unchanged)
1114 continue;
1115
1116 //
1117 // begin building the output stanza
1118 //
1119 basic_io::stanza st;
1120 st.push_file_pair(syms::path, fp);
1121
1122 if (item.old_node.exists)
1123 {
1124 switch (item.old_node.type)
1125 {
1126 case path::file: st.push_str_pair(syms::old_type, "file"); break;
1127 case path::directory: st.push_str_pair(syms::old_type, "directory"); break;
1128 case path::nonexistent: I(false);
1129 }
1130
1131 if (item.new_path.as_internal().length() > 0)
1132 {
1133 st.push_file_pair(syms::new_path, item.new_path);
1134 }
1135 }
1136
1137 if (item.new_node.exists)
1138 {
1139 switch (item.new_node.type)
1140 {
1141 case path::file: st.push_str_pair(syms::new_type, "file"); break;
1142 case path::directory: st.push_str_pair(syms::new_type, "directory"); break;
1143 case path::nonexistent: I(false);
1144 }
1145
1146 if (item.old_path.as_internal().length() > 0)
1147 {
1148 st.push_file_pair(syms::old_path, item.old_path);
1149 }
1150 }
1151
1152 switch (item.fs_type)
1153 {
1154 case path::file: st.push_str_pair(syms::fs_type, "file"); break;
1155 case path::directory: st.push_str_pair(syms::fs_type, "directory"); break;
1156 case path::nonexistent: st.push_str_pair(syms::fs_type, "none"); break;
1157 }
1158
1159 //
1160 // finally output the previously recorded states and changes
1161 //
1162 if (!birth_revision.inner()().empty())
1163 st.push_binary_pair(syms::birth, birth_revision.inner());
1164
1165 I(!states.empty());
1166 st.push_str_multi(syms::status, states);
1167
1168 if (!changes.empty())
1169 st.push_str_multi(syms::changes, changes);
1170
1171 pr.print_stanza(st);
1172 }
1173
1174 output.write(pr.buf.data(), pr.buf.size());
1175}
1176
1177// Name: get_revision
1178// Arguments:
1179// 1: a revision id
1180// Added in: 1.0
1181// Changed in: 7.0 (REVID argument is now mandatory)
1182
1183// Purpose: Prints change information for the specified revision id.
1184// There are several changes that are described; each of these is
1185// described by a different basic_io stanza. The first string pair
1186// of each stanza indicates the type of change represented.
1187//
1188// All stanzas are formatted by basic_io. Stanzas are separated
1189// by a blank line. Values will be escaped, '\' to '\\' and
1190// '"' to '\"'.
1191//
1192// Possible values of this first value are along with an ordered list of
1193// basic_io formatted stanzas that will be provided are:
1194//
1195// 'format_version'
1196// used in case this format ever needs to change.
1197// format: ('format_version', the string "1")
1198// occurs: exactly once
1199// 'new_manifest'
1200// represents the new manifest associated with the revision.
1201// format: ('new_manifest', manifest id)
1202// occurs: exactly one
1203// 'old_revision'
1204// represents a parent revision.
1205// format: ('old_revision', revision id)
1206// occurs: either one or two times
1207// 'delete
1208// represents a file or directory that was deleted.
1209// format: ('delete', path)
1210// occurs: zero or more times
1211// 'rename'
1212// represents a file or directory that was renamed.
1213// format: ('rename, old filename), ('to', new filename)
1214// occurs: zero or more times
1215// 'add_dir'
1216// represents a directory that was added.
1217// format: ('add_dir, path)
1218// occurs: zero or more times
1219// 'add_file'
1220// represents a file that was added.
1221// format: ('add_file', path), ('content', file id)
1222// occurs: zero or more times
1223// 'patch'
1224// represents a file that was modified.
1225// format: ('patch', filename), ('from', file id), ('to', file id)
1226// occurs: zero or more times
1227// 'clear'
1228// represents an attr that was removed.
1229// format: ('clear', filename), ('attr', attr name)
1230// occurs: zero or more times
1231// 'set'
1232// represents an attr whose value was changed.
1233// format: ('set', filename), ('attr', attr name), ('value', attr value)
1234// occurs: zero or more times
1235//
1236// These stanzas will always occur in the order listed here; stanzas of
1237// the same type will be sorted by the filename they refer to.
1238// Error conditions: If the revision specified is unknown or invalid
1239// prints an error message to stderr and exits with status 1.
1240CMD_AUTOMATE(get_revision, N_("REVID"),
1241 N_("Shows change information for a revision"),
1242 "",
1243 options::opts::none)
1244{
1245 N(args.size() == 1,
1246 F("wrong argument count"));
1247
1248 database db(app);
1249
1250 revision_data dat;
1251 revision_id rid(decode_hexenc(idx(args, 0)()));
1252 N(db.revision_exists(rid),
1253 F("no revision %s found in database") % rid);
1254 db.get_revision(rid, dat);
1255
1256 L(FL("dumping revision %s") % rid);
1257 output << dat;
1258}
1259
1260// Name: get_current_revision
1261// Arguments:
1262// 1: zero or more path names
1263// Added in: 7.0
1264// Purpose: Outputs (an optionally restricted) revision based on
1265// changes in the current workspace
1266// Error conditions: If there are no changes in the current workspace or the
1267// restriction is invalid or has no recorded changes, prints an error message
1268// to stderr and exits with status 1. A workspace is required.
1269CMD_AUTOMATE(get_current_revision, N_("[PATHS ...]"),
1270 N_("Shows change information for a workspace"),
1271 "",
1272 options::opts::exclude | options::opts::depth)
1273{
1274 temp_node_id_source nis;
1275 revision_data dat;
1276 revision_id ident;
1277
1278 roster_t new_roster;
1279 parent_map old_rosters;
1280 revision_t rev;
1281 cset excluded;
1282
1283 database db(app);
1284 workspace work(app);
1285 work.get_parent_rosters(db, old_rosters);
1286 work.get_current_roster_shape(db, nis, new_roster);
1287
1288 node_restriction mask(args_to_paths(args),
1289 args_to_paths(app.opts.exclude_patterns),
1290 app.opts.depth,
1291 old_rosters, new_roster);
1292
1293 work.update_current_roster_from_filesystem(new_roster, mask);
1294
1295 make_revision(old_rosters, new_roster, rev);
1296 make_restricted_revision(old_rosters, new_roster, mask, rev,
1297 excluded, join_words(execid));
1298 rev.check_sane();
1299 N(rev.is_nontrivial(), F("no changes to commit"));
1300
1301 calculate_ident(rev, ident);
1302 write_revision(rev, dat);
1303
1304 L(FL("dumping revision %s") % ident);
1305 output << dat;
1306}
1307
1308
1309// Name: get_base_revision_id
1310// Arguments: none
1311// Added in: 2.0
1312// Purpose: Prints the revision id the current workspace is based
1313// on. This is the value stored in _MTN/revision
1314// Error conditions: If no workspace book keeping _MTN directory is found,
1315// prints an error message to stderr, and exits with status 1.
1316CMD_AUTOMATE(get_base_revision_id, "",
1317 N_("Shows the revision on which the workspace is based"),
1318 "",
1319 options::opts::none)
1320{
1321 N(args.size() == 0,
1322 F("no arguments needed"));
1323
1324 database db(app);
1325 workspace work(app);
1326
1327 parent_map parents;
1328 work.get_parent_rosters(db, parents);
1329 N(parents.size() == 1,
1330 F("this command can only be used in a single-parent workspace"));
1331
1332 output << parent_id(parents.begin()) << '\n';
1333}
1334
1335// Name: get_current_revision_id
1336// Arguments: none
1337// Added in: 2.0
1338// Purpose: Prints the revision id of the current workspace. This is the
1339// id of the revision that would be committed by an unrestricted
1340// commit calculated from _MTN/revision, _MTN/work and any edits to
1341// files in the workspace.
1342// Error conditions: If no workspace book keeping _MTN directory is found,
1343// prints an error message to stderr, and exits with status 1.
1344CMD_AUTOMATE(get_current_revision_id, "",
1345 N_("Shows the revision of the current workspace"),
1346 "",
1347 options::opts::none)
1348{
1349 N(args.size() == 0,
1350 F("no arguments needed"));
1351
1352 workspace work(app);
1353 database db(app);
1354
1355 parent_map parents;
1356 roster_t new_roster;
1357 revision_id new_revision_id;
1358 revision_t rev;
1359 temp_node_id_source nis;
1360
1361 work.get_current_roster_shape(db, nis, new_roster);
1362 work.update_current_roster_from_filesystem(new_roster);
1363
1364 work.get_parent_rosters(db, parents);
1365 make_revision(parents, new_roster, rev);
1366
1367 calculate_ident(rev, new_revision_id);
1368
1369 output << new_revision_id << '\n';
1370}
1371
1372// Name: get_manifest_of
1373// Arguments:
1374// 1: a revision id (optional, determined from the workspace if not given)
1375// Added in: 2.0
1376// Purpose: Prints the contents of the manifest associated with the
1377// given revision ID.
1378//
1379// Output format:
1380// There is one basic_io stanza for each file or directory in the
1381// manifest.
1382//
1383// All stanzas are formatted by basic_io. Stanzas are separated
1384// by a blank line. Values will be escaped, '\' to '\\' and
1385// '"' to '\"'.
1386//
1387// Possible values of this first value are along with an ordered list of
1388// basic_io formatted stanzas that will be provided are:
1389//
1390// 'format_version'
1391// used in case this format ever needs to change.
1392// format: ('format_version', the string "1")
1393// occurs: exactly once
1394// 'dir':
1395// represents a directory. The path "" (the empty string) is used
1396// to represent the root of the tree.
1397// format: ('dir', pathname)
1398// occurs: one or more times
1399// 'file':
1400// represents a file.
1401// format: ('file', pathname), ('content', file id)
1402// occurs: zero or more times
1403//
1404// In addition, 'dir' and 'file' stanzas may have attr information
1405// included. These are appended to the stanza below the basic
1406// dir/file information, with one line describing each attr. These
1407// lines take the form ('attr', attr name, attr value).
1408//
1409// Stanzas are sorted by the path string.
1410//
1411// Error conditions: If the revision ID specified is unknown or
1412// invalid prints an error message to stderr and exits with status 1.
1413CMD_AUTOMATE(get_manifest_of, N_("[REVID]"),
1414 N_("Shows the manifest associated with a revision"),
1415 "",
1416 options::opts::none)
1417{
1418 database db(app);
1419
1420 N(args.size() < 2,
1421 F("wrong argument count"));
1422
1423 manifest_data dat;
1424 manifest_id mid;
1425 roster_t new_roster;
1426
1427 if (args.size() == 0)
1428 {
1429 workspace work(app);
1430
1431 temp_node_id_source nis;
1432
1433 work.get_current_roster_shape(db, nis, new_roster);
1434 work.update_current_roster_from_filesystem(new_roster);
1435 }
1436 else
1437 {
1438 revision_id rid = revision_id(decode_hexenc(idx(args, 0)()));
1439 N(db.revision_exists(rid),
1440 F("no revision %s found in database") % rid);
1441 db.get_roster(rid, new_roster);
1442 }
1443
1444 calculate_ident(new_roster, mid);
1445 write_manifest_of_roster(new_roster, dat);
1446 L(FL("dumping manifest %s") % mid);
1447 output << dat;
1448}
1449
1450
1451// Name: packet_for_rdata
1452// Arguments:
1453// 1: a revision id
1454// Added in: 2.0
1455// Purpose: Prints the revision data in packet format
1456//
1457// Output format: revision data in "monotone read" compatible packet
1458// format
1459//
1460// Error conditions: If the revision id specified is unknown or
1461// invalid prints an error message to stderr and exits with status 1.
1462CMD_AUTOMATE(packet_for_rdata, N_("REVID"),
1463 N_("Prints the revision data in packet format"),
1464 "",
1465 options::opts::none)
1466{
1467 N(args.size() == 1,
1468 F("wrong argument count"));
1469
1470 database db(app);
1471
1472 packet_writer pw(output);
1473
1474 revision_id r_id(decode_hexenc(idx(args, 0)()));
1475 revision_data r_data;
1476
1477 N(db.revision_exists(r_id), F("no such revision '%s'") % r_id);
1478 db.get_revision(r_id, r_data);
1479 pw.consume_revision_data(r_id, r_data);
1480}
1481
1482// Name: packets_for_certs
1483// Arguments:
1484// 1: a revision id
1485// Added in: 2.0
1486// Purpose: Prints the certs associated with a revision in packet format
1487//
1488// Output format: certs in "monotone read" compatible packet format
1489//
1490// Error conditions: If the revision id specified is unknown or
1491// invalid prints an error message to stderr and exits with status 1.
1492CMD_AUTOMATE(packets_for_certs, N_("REVID"),
1493 N_("Prints the certs associated with a revision in "
1494 "packet format"),
1495 "",
1496 options::opts::none)
1497{
1498 N(args.size() == 1,
1499 F("wrong argument count"));
1500
1501 database db(app);
1502 project_t project(db);
1503 packet_writer pw(output);
1504
1505 revision_id r_id(decode_hexenc(idx(args, 0)()));
1506 vector< revision<cert> > certs;
1507
1508 N(db.revision_exists(r_id), F("no such revision '%s'") % r_id);
1509 project.get_revision_certs(r_id, certs);
1510
1511 for (vector< revision<cert> >::const_iterator i = certs.begin();
1512 i != certs.end(); i++)
1513 pw.consume_revision_cert(*i);
1514}
1515
1516// Name: packet_for_fdata
1517// Arguments:
1518// 1: a file id
1519// Added in: 2.0
1520// Purpose: Prints the file data in packet format
1521//
1522// Output format: file data in "monotone read" compatible packet format
1523//
1524// Error conditions: If the file id specified is unknown or invalid
1525// prints an error message to stderr and exits with status 1.
1526CMD_AUTOMATE(packet_for_fdata, N_("FILEID"),
1527 N_("Prints the file data in packet format"),
1528 "",
1529 options::opts::none)
1530{
1531 N(args.size() == 1,
1532 F("wrong argument count"));
1533
1534 database db(app);
1535
1536 packet_writer pw(output);
1537
1538 file_id f_id(decode_hexenc(idx(args, 0)()));
1539 file_data f_data;
1540
1541 N(db.file_version_exists(f_id), F("no such file '%s'") % f_id);
1542 db.get_file_version(f_id, f_data);
1543 pw.consume_file_data(f_id, f_data);
1544}
1545
1546// Name: packet_for_fdelta
1547// Arguments:
1548// 1: a file id
1549// 2: a file id
1550// Added in: 2.0
1551// Purpose: Prints the file delta in packet format
1552//
1553// Output format: file delta in "monotone read" compatible packet format
1554//
1555// Error conditions: If any of the file ids specified are unknown or
1556// invalid prints an error message to stderr and exits with status 1.
1557CMD_AUTOMATE(packet_for_fdelta, N_("OLD_FILE NEW_FILE"),
1558 N_("Prints the file delta in packet format"),
1559 "",
1560 options::opts::none)
1561{
1562 N(args.size() == 2,
1563 F("wrong argument count"));
1564
1565 database db(app);
1566
1567 packet_writer pw(output);
1568
1569 file_id f_old_id(decode_hexenc(idx(args, 0)()));
1570 file_id f_new_id(decode_hexenc(idx(args, 1)()));
1571 file_data f_old_data, f_new_data;
1572
1573 N(db.file_version_exists(f_old_id),
1574 F("no such revision '%s'") % f_old_id);
1575 N(db.file_version_exists(f_new_id),
1576 F("no such revision '%s'") % f_new_id);
1577 db.get_file_version(f_old_id, f_old_data);
1578 db.get_file_version(f_new_id, f_new_data);
1579 delta del;
1580 diff(f_old_data.inner(), f_new_data.inner(), del);
1581 pw.consume_file_delta(f_old_id, f_new_id, file_delta(del));
1582}
1583
1584// Name: common_ancestors
1585// Arguments:
1586// 1 or more revision ids
1587// Added in: 2.1
1588// Purpose: Prints all revisions which are ancestors of all of the
1589// revisions given as arguments.
1590// Output format: A list of revision ids, in hexadecimal, each
1591// followed by a newline. Revisions are printed in alphabetically
1592// sorted order.
1593// Error conditions: If any of the revisions do not exist, prints
1594// nothing to stdout, prints an error message to stderr, and exits
1595// with status 1.
1596CMD_AUTOMATE(common_ancestors, N_("REV1 [REV2 [REV3 [...]]]"),
1597 N_("Prints revisions that are common ancestors of a list "
1598 "of revisions"),
1599 "",
1600 options::opts::none)
1601{
1602 N(args.size() > 0,
1603 F("wrong argument count"));
1604
1605 database db(app);
1606
1607 set<revision_id> revs, common_ancestors;
1608 for (args_vector::const_iterator i = args.begin(); i != args.end(); ++i)
1609 {
1610 revision_id rid(decode_hexenc((*i)()));
1611 N(db.revision_exists(rid), F("No such revision %s") % rid);
1612 revs.insert(rid);
1613 }
1614
1615 db.get_common_ancestors(revs, common_ancestors);
1616
1617 for (set<revision_id>::const_iterator i = common_ancestors.begin();
1618 i != common_ancestors.end(); ++i)
1619 output << *i << "\n";
1620}
1621
1622// Name: branches
1623// Arguments:
1624// None
1625// Added in: 2.2
1626// Purpose:
1627// Prints all branch certs present in the revision graph, that are not
1628// excluded by the lua hook 'ignore_branch'.
1629// Output format:
1630// Zero or more lines, each the name of a branch. The lines are printed
1631// in alphabetically sorted order.
1632// Error conditions:
1633// None.
1634CMD_AUTOMATE(branches, "",
1635 N_("Prints all branch certs in the revision graph"),
1636 "",
1637 options::opts::none)
1638{
1639 N(args.size() == 0,
1640 F("no arguments needed"));
1641
1642 database db(app);
1643 project_t project(db);
1644 set<branch_name> names;
1645
1646 project.get_branch_list(names, !app.opts.ignore_suspend_certs);
1647
1648 for (set<branch_name>::const_iterator i = names.begin();
1649 i != names.end(); ++i)
1650 if (!app.lua.hook_ignore_branch(*i))
1651 output << (*i) << '\n';
1652}
1653
1654// Name: tags
1655// Arguments:
1656// A branch pattern (optional).
1657// Added in: 2.2
1658// Purpose:
1659// If a branch pattern is given, prints all tags that are attached to
1660// revisions on branches matched by the pattern; otherwise prints all tags
1661// of the revision graph.
1662//
1663// If a branch name is ignored by means of the lua hook 'ignore_branch',
1664// it is neither printed, nor can it be matched by a pattern.
1665// Output format:
1666// There is one basic_io stanza for each tag.
1667//
1668// All stanzas are formatted by basic_io. Stanzas are separated
1669// by a blank line. Values will be escaped, '\' to '\\' and
1670// '"' to '\"'.
1671//
1672// Each stanza has exactly the following four entries:
1673//
1674// 'tag'
1675// the value of the tag cert, i.e. the name of the tag
1676// 'revision'
1677// the hexadecimal id of the revision the tag is attached to
1678// 'signer'
1679// the name of the key used to sign the tag cert
1680// 'branches'
1681// a (possibly empty) list of all branches the tagged revision is on
1682//
1683// Stanzas are printed in arbitrary order.
1684// Error conditions:
1685// A run-time exception is thrown for illegal patterns.
1686CMD_AUTOMATE(tags, N_("[BRANCH_PATTERN]"),
1687 N_("Prints all tags attached to a set of branches"),
1688 "",
1689 options::opts::none)
1690{
1691 N(args.size() < 2,
1692 F("wrong argument count"));
1693
1694 database db(app);
1695 project_t project(db);
1696 globish incl("*");
1697 bool filtering(false);
1698
1699 if (args.size() == 1) {
1700 incl = globish(idx(args, 0)());
1701 filtering = true;
1702 }
1703
1704 basic_io::printer prt;
1705 basic_io::stanza stz;
1706 stz.push_str_pair(symbol("format_version"), "1");
1707 prt.print_stanza(stz);
1708
1709 set<tag_t> tags;
1710 project.get_tags(tags);
1711
1712 for (set<tag_t>::const_iterator tag = tags.begin();
1713 tag != tags.end(); ++tag)
1714 {
1715 set<branch_name> branches;
1716 project.get_revision_branches(tag->ident, branches);
1717
1718 bool show(!filtering);
1719 vector<string> branch_names;
1720
1721 for (set<branch_name>::const_iterator branch = branches.begin();
1722 branch != branches.end(); ++branch)
1723 {
1724 // FIXME: again, hook_ignore_branch should probably be in the
1725 // database context...
1726 if (app.lua.hook_ignore_branch(*branch))
1727 continue;
1728
1729 if (!show && incl.matches((*branch)()))
1730 show = true;
1731 branch_names.push_back((*branch)());
1732 }
1733
1734 if (show)
1735 {
1736 basic_io::stanza stz;
1737 stz.push_str_pair(symbol("tag"), tag->name());
1738 stz.push_binary_pair(symbol("revision"), tag->ident.inner());
1739 stz.push_str_pair(symbol("signer"), tag->key());
1740 stz.push_str_multi(symbol("branches"), branch_names);
1741 prt.print_stanza(stz);
1742 }
1743 }
1744 output.write(prt.buf.data(), prt.buf.size());
1745}
1746
1747namespace
1748{
1749 namespace syms
1750 {
1751 symbol const key("key");
1752 symbol const signature("signature");
1753 symbol const name("name");
1754 symbol const value("value");
1755 symbol const trust("trust");
1756
1757 symbol const public_hash("public_hash");
1758 symbol const private_hash("private_hash");
1759 symbol const public_location("public_location");
1760 symbol const private_location("private_location");
1761
1762 symbol const domain("domain");
1763 symbol const entry("entry");
1764 }
1765};
1766
1767// Name: genkey
1768// Arguments:
1769// 1: the key ID
1770// 2: the key passphrase
1771// Added in: 3.1
1772// Purpose: Generates a key with the given ID and passphrase
1773//
1774// Output format: a basic_io stanza for the new key, as for ls keys
1775//
1776// Sample output:
1777// name "tbrownaw@gmail.com"
1778// public_hash [475055ec71ad48f5dfaf875b0fea597b5cbbee64]
1779// private_hash [7f76dae3f91bb48f80f1871856d9d519770b7f8a]
1780// public_location "database" "keystore"
1781// private_location "keystore"
1782//
1783// Error conditions: If the passphrase is empty or the key already exists,
1784// prints an error message to stderr and exits with status 1.
1785CMD_AUTOMATE(genkey, N_("KEYID PASSPHRASE"),
1786 N_("Generates a key"),
1787 "",
1788 options::opts::none)
1789{
1790 N(args.size() == 2,
1791 F("wrong argument count"));
1792
1793 database db(app);
1794 key_store keys(app);
1795
1796 rsa_keypair_id ident;
1797 internalize_rsa_keypair_id(idx(args, 0), ident);
1798
1799 utf8 passphrase = idx(args, 1);
1800
1801 id pubhash, privhash;
1802 keys.create_key_pair(db, ident, &passphrase, &pubhash, &privhash);
1803
1804 basic_io::printer prt;
1805 basic_io::stanza stz;
1806 vector<string> publocs, privlocs;
1807 if (db.database_specified())
1808 publocs.push_back("database");
1809 publocs.push_back("keystore");
1810 privlocs.push_back("keystore");
1811
1812 stz.push_str_pair(syms::name, ident());
1813 stz.push_binary_pair(syms::public_hash, pubhash);
1814 stz.push_binary_pair(syms::private_hash, privhash);
1815 stz.push_str_multi(syms::public_location, publocs);
1816 stz.push_str_multi(syms::private_location, privlocs);
1817 prt.print_stanza(stz);
1818
1819 output.write(prt.buf.data(), prt.buf.size());
1820
1821}
1822
1823// Name: get_option
1824// Arguments:
1825// 1: an options name
1826// Added in: 3.1
1827// Purpose: Show the value of the named option in _MTN/options
1828//
1829// Output format: A string
1830//
1831// Sample output (for 'mtn automate get_option branch:
1832// net.venge.monotone
1833//
1834CMD_AUTOMATE(get_option, N_("OPTION"),
1835 N_("Shows the value of an option"),
1836 "",
1837 options::opts::none)
1838{
1839 N(args.size() == 1,
1840 F("wrong argument count"));
1841
1842 workspace work(app);
1843 work.print_ws_option(args[0], output);
1844}
1845
1846// Name: get_content_changed
1847// Arguments:
1848// 1: a revision ID
1849// 2: a file name
1850// Added in: 3.1
1851// Purpose: Returns a list of revision IDs in which the content
1852// was most recently changed, relative to the revision ID specified
1853// in argument 1. This equates to a content mark following
1854// the *-merge algorithm.
1855//
1856// Output format: Zero or more basic_io stanzas, each specifying a
1857// revision ID for which a content mark is set.
1858//
1859// Each stanza has exactly one entry:
1860//
1861// 'content_mark'
1862// the hexadecimal id of the revision the content mark is attached to
1863// Sample output (for 'mtn automate get_content_changed 3bccff99d08421df72519b61a4dded16d1139c33 ChangeLog):
1864// content_mark [276264b0b3f1e70fc1835a700e6e61bdbe4c3f2f]
1865//
1866CMD_AUTOMATE(get_content_changed, N_("REV FILE"),
1867 N_("Lists the revisions that changed the content relative "
1868 "to another revision"),
1869 "",
1870 options::opts::none)
1871{
1872 N(args.size() == 2,
1873 F("wrong argument count"));
1874
1875 database db(app);
1876
1877 roster_t new_roster;
1878 revision_id ident;
1879 marking_map mm;
1880
1881 ident = revision_id(decode_hexenc(idx(args, 0)()));
1882 N(db.revision_exists(ident),
1883 F("no revision %s found in database") % ident);
1884 db.get_roster(ident, new_roster, mm);
1885
1886 file_path path = file_path_external(idx(args,1));
1887 N(new_roster.has_node(path),
1888 F("file %s is unknown for revision %s")
1889 % path % ident);
1890
1891 node_t node = new_roster.get_node(path);
1892 marking_map::const_iterator m = mm.find(node->self);
1893 I(m != mm.end());
1894 marking_t mark = m->second;
1895
1896 basic_io::printer prt;
1897 for (set<revision_id>::const_iterator i = mark.file_content.begin();
1898 i != mark.file_content.end(); ++i)
1899 {
1900 basic_io::stanza st;
1901 st.push_binary_pair(basic_io::syms::content_mark, i->inner());
1902 prt.print_stanza(st);
1903 }
1904 output.write(prt.buf.data(), prt.buf.size());
1905}
1906
1907// Name: get_corresponding_path
1908// Arguments:
1909// 1: a source revision ID
1910// 2: a file name (in the source revision)
1911// 3: a target revision ID
1912// Added in: 3.1
1913// Purpose: Given a the file name in the source revision, a filename
1914// will if possible be returned naming the file in the target revision.
1915// This allows the same file to be matched between revisions, accounting
1916// for renames and other changes.
1917//
1918// Output format: Zero or one basic_io stanzas. Zero stanzas will be
1919// output if the file does not exist within the target revision; this is
1920// not considered an error.
1921// If the file does exist in the target revision, a single stanza with the
1922// following details is output.
1923//
1924// The stanza has exactly one entry:
1925//
1926// 'file'
1927// the file name corresponding to "file name" (arg 2) in the target revision
1928//
1929// Sample output (for automate get_corresponding_path 91f25c8ee830b11b52dd356c925161848d4274d0 foo2 dae0d8e3f944c82a9688bcd6af99f5b837b41968; see automate_get_corresponding_path test)
1930// file "foo"
1931CMD_AUTOMATE(get_corresponding_path, N_("REV1 FILE REV2"),
1932 N_("Prints the name of a file in a target revision relative "
1933 "to a given revision"),
1934 "",
1935 options::opts::none)
1936{
1937 N(args.size() == 3,
1938 F("wrong argument count"));
1939
1940 database db(app);
1941
1942 roster_t new_roster, old_roster;
1943 revision_id ident, old_ident;
1944
1945 ident = revision_id(decode_hexenc(idx(args, 0)()));
1946 N(db.revision_exists(ident),
1947 F("no revision %s found in database") % ident);
1948 db.get_roster(ident, new_roster);
1949
1950 old_ident = revision_id(decode_hexenc(idx(args, 2)()));
1951 N(db.revision_exists(old_ident),
1952 F("no revision %s found in database") % old_ident);
1953 db.get_roster(old_ident, old_roster);
1954
1955 file_path path = file_path_external(idx(args,1));
1956 N(new_roster.has_node(path),
1957 F("file %s is unknown for revision %s") % path % ident);
1958
1959 node_t node = new_roster.get_node(path);
1960 basic_io::printer prt;
1961 if (old_roster.has_node(node->self))
1962 {
1963 file_path old_path;
1964 basic_io::stanza st;
1965 old_roster.get_name(node->self, old_path);
1966 st.push_file_pair(basic_io::syms::file, old_path);
1967 prt.print_stanza(st);
1968 }
1969 output.write(prt.buf.data(), prt.buf.size());
1970}
1971
1972// Name: put_file
1973// Arguments:
1974// base FILEID (optional)
1975// file contents (binary, intended for automate stdio use)
1976// Added in: 4.1
1977// Purpose:
1978// Store a file in the database.
1979// Optionally encode it as a file_delta
1980// Output format:
1981// The ID of the new file (40 digit hex string)
1982// Error conditions:
1983// a runtime exception is thrown if base revision is not available
1984CMD_AUTOMATE(put_file, N_("[FILEID] CONTENTS"),
1985 N_("Stores a file in the database"),
1986 "",
1987 options::opts::none)
1988{
1989 N(args.size() == 1 || args.size() == 2,
1990 F("wrong argument count"));
1991
1992 database db(app);
1993
1994 file_id sha1sum;
1995 transaction_guard tr(db);
1996 if (args.size() == 1)
1997 {
1998 file_data dat(idx(args, 0)());
1999 calculate_ident(dat, sha1sum);
2000
2001 db.put_file(sha1sum, dat);
2002 }
2003 else if (args.size() == 2)
2004 {
2005 file_data dat(idx(args, 1)());
2006 calculate_ident(dat, sha1sum);
2007 file_id base_id(decode_hexenc(idx(args, 0)()));
2008 N(db.file_version_exists(base_id),
2009 F("no file version %s found in database") % base_id);
2010
2011 // put_file_version won't do anything if the target ID already exists,
2012 // but we can save the delta calculation by checking here too
2013 if (!db.file_version_exists(sha1sum))
2014 {
2015 file_data olddat;
2016 db.get_file_version(base_id, olddat);
2017 delta del;
2018 diff(olddat.inner(), dat.inner(), del);
2019
2020 db.put_file_version(base_id, sha1sum, file_delta(del));
2021 }
2022 }
2023 else I(false);
2024
2025 tr.commit();
2026 output << sha1sum << '\n';
2027}
2028
2029// Name: put_revision
2030// Arguments:
2031// revision-data
2032// Added in: 4.1
2033// Purpose:
2034// Store a revision into the database.
2035// Output format:
2036// The ID of the new revision
2037// Error conditions:
2038// none
2039CMD_AUTOMATE(put_revision, N_("REVISION-DATA"),
2040 N_("Stores a revision into the database"),
2041 "",
2042 options::opts::none)
2043{
2044 N(args.size() == 1,
2045 F("wrong argument count"));
2046
2047 database db(app);
2048
2049 revision_t rev;
2050 read_revision(revision_data(idx(args, 0)()), rev);
2051
2052 // recalculate manifest
2053 temp_node_id_source nis;
2054 rev.new_manifest = manifest_id();
2055 for (edge_map::const_iterator e = rev.edges.begin(); e != rev.edges.end(); ++e)
2056 {
2057 // calculate new manifest
2058 roster_t old_roster;
2059 if (!null_id(e->first)) db.get_roster(e->first, old_roster);
2060 roster_t new_roster = old_roster;
2061 editable_roster_base eros(new_roster, nis);
2062 e->second->apply_to(eros);
2063 if (null_id(rev.new_manifest))
2064 // first edge, initialize manifest
2065 calculate_ident(new_roster, rev.new_manifest);
2066 else
2067 // following edge, make sure that all csets end at the same manifest
2068 {
2069 manifest_id calculated;
2070 calculate_ident(new_roster, calculated);
2071 I(calculated == rev.new_manifest);
2072 }
2073 }
2074
2075 revision_id id;
2076 calculate_ident(rev, id);
2077
2078 // If the database refuses the revision, make sure this is because it's
2079 // already there.
2080 E(db.put_revision(id, rev) || db.revision_exists(id),
2081 F("missing prerequisite for revision %s") % id);
2082
2083 output << id << '\n';
2084}
2085
2086// Name: cert
2087// Arguments:
2088// revision ID
2089// certificate name
2090// certificate value
2091// Added in: 4.1
2092// Purpose:
2093// Add a revision certificate (like mtn cert).
2094// Output format:
2095// nothing
2096// Error conditions:
2097// none
2098CMD_AUTOMATE(cert, N_("REVISION-ID NAME VALUE"),
2099 N_("Adds a revision certificate"),
2100 "",
2101 options::opts::none)
2102{
2103 N(args.size() == 3,
2104 F("wrong argument count"));
2105
2106 database db(app);
2107 key_store keys(app);
2108
2109 hexenc<id> hrid(idx(args, 0)());
2110 revision_id rid(decode_hexenc(hrid()));
2111 N(db.revision_exists(rid),
2112 F("no such revision '%s'") % hrid);
2113
2114 cache_user_key(app.opts, app.lua, db, keys);
2115 put_simple_revision_cert(db, keys, rid, cert_name(idx(args, 1)()),
2116 cert_value(idx(args, 2)()));
2117}
2118
2119// Name: get_db_variables
2120// Arguments:
2121// variable domain
2122// Changes:
2123// 4.1 (added as 'db_get')
2124// 7.0 (changed to 'get_db_variables', output is now basic_io)
2125// Purpose:
2126// Retrieves db variables, optionally filtered by DOMAIN
2127// Output format:
2128// basic_io, see the mtn docs for details
2129// Error conditions:
2130// none
2131CMD_AUTOMATE(get_db_variables, N_("[DOMAIN]"),
2132 N_("Retrieve database variables"),
2133 "",
2134 options::opts::none)
2135{
2136 N(args.size() < 2,
2137 F("wrong argument count"));
2138
2139 database db(app);
2140 bool filter_by_domain = false;
2141 var_domain filter;
2142 if (args.size() == 1)
2143 {
2144 filter_by_domain = true;
2145 filter = var_domain(idx(args, 0)());
2146 }
2147
2148 map<var_key, var_value> vars;
2149 db.get_vars(vars);
2150
2151 var_domain cur_domain;
2152 basic_io::stanza st;
2153 basic_io::printer pr;
2154 bool found_something = false;
2155
2156 for (map<var_key, var_value>::const_iterator i = vars.begin();
2157 i != vars.end(); ++i)
2158 {
2159 if (filter_by_domain && !(i->first.first == filter))
2160 continue;
2161
2162 found_something = true;
2163
2164 if (cur_domain != i->first.first)
2165 {
2166 // check if we need to print a previous stanza
2167 if (!st.entries.empty())
2168 {
2169 pr.print_stanza(st);
2170 st.entries.clear();
2171 }
2172 cur_domain = i->first.first;
2173 st.push_str_pair(syms::domain, cur_domain());
2174 }
2175
2176 st.push_str_triple(syms::entry, i->first.second(), i->second());
2177 }
2178
2179 N(found_something,
2180 F("No variables found or invalid domain specified"));
2181
2182 // print the last stanza
2183 pr.print_stanza(st);
2184 output.write(pr.buf.data(), pr.buf.size());
2185}
2186
2187// Name: set_db_variable
2188// Arguments:
2189// variable domain
2190// variable name
2191// veriable value
2192// Changes:
2193// 4.1 (added as 'db_set')
2194// 7.0 (renamed to 'set_db_variable')
2195// Purpose:
2196// Set a database variable (like mtn database set)
2197// Output format:
2198// nothing
2199// Error conditions:
2200// none
2201CMD_AUTOMATE(set_db_variable, N_("DOMAIN NAME VALUE"),
2202 N_("Sets a database variable"),
2203 "",
2204 options::opts::none)
2205{
2206 N(args.size() == 3,
2207 F("wrong argument count"));
2208
2209 database db(app);
2210
2211 var_domain domain = var_domain(idx(args, 0)());
2212 utf8 name = idx(args, 1);
2213 utf8 value = idx(args, 2);
2214 var_key key(domain, var_name(name()));
2215 db.set_var(key, var_value(value()));
2216}
2217
2218// Name: drop_db_variables
2219// Arguments:
2220// variable domain
2221// variable name
2222// Changes:
2223// 7.0 (added)
2224// Purpose:
2225// Drops a database variable (like mtn unset DOMAIN NAME) or all variables
2226// within a domain
2227// Output format:
2228// none
2229// Error conditions:
2230// a runtime exception is thrown if the variable was not found
2231CMD_AUTOMATE(drop_db_variables, N_("DOMAIN [NAME]"),
2232 N_("Drops a database variable"),
2233 "",
2234 options::opts::none)
2235{
2236 N(args.size() == 1 || args.size() == 2,
2237 F("wrong argument count"));
2238
2239 database db(app);
2240
2241 var_domain domain(idx(args, 0)());
2242
2243 if (args.size() == 2)
2244 {
2245 var_name name(idx(args, 1)());
2246 var_key key(domain, name);
2247 N(db.var_exists(key),
2248 F("no var with name %s in domain %s") % name % domain);
2249 db.clear_var(key);
2250 }
2251 else
2252 {
2253 map<var_key, var_value> vars;
2254 db.get_vars(vars);
2255 bool found_something = false;
2256
2257 for (map<var_key, var_value>::const_iterator i = vars.begin();
2258 i != vars.end(); ++i)
2259 {
2260 if (i->first.first == domain)
2261 {
2262 found_something = true;
2263 db.clear_var(i->first);
2264 }
2265 }
2266
2267 N(found_something,
2268 F("no variables found in domain %s") % domain);
2269 }
2270}
2271
2272// Name: drop_db_variables
2273// Arguments:
2274// none
2275// Changes:
2276// 8.0 (added)
2277// Purpose:
2278// To show the path of the workspace root for the current directory.
2279// Output format:
2280// A path
2281// Error conditions:
2282// a runtime exception is thrown if the current directory isn't part
2283// of a workspace.
2284CMD_AUTOMATE(get_workspace_root, "",
2285 N_("Prints the workspace root for the current directory"),
2286 "",
2287 options::opts::none)
2288{
2289 workspace work(app);
2290 output << get_current_working_dir() << '\n';
2291}
2292
2293// Local Variables:
2294// mode: C++
2295// fill-column: 76
2296// c-file-style: "gnu"
2297// indent-tabs-mode: nil
2298// End:
2299// 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