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