~bzr-pqm/bzr/bzr.dev

644 by Martin Pool
- add aaron's pending patch for annotate
1
*** added file 'bzrlib/patches.py'
2
--- /dev/null 
3
+++ bzrlib/patches.py 
4
@@ -0,0 +1,497 @@
5
+# Copyright (C) 2004, 2005 Aaron Bentley
6
+# <aaron.bentley@utoronto.ca>
7
+#
8
+#    This program is free software; you can redistribute it and/or modify
9
+#    it under the terms of the GNU General Public License as published by
10
+#    the Free Software Foundation; either version 2 of the License, or
11
+#    (at your option) any later version.
12
+#
13
+#    This program is distributed in the hope that it will be useful,
14
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
+#    GNU General Public License for more details.
17
+#
18
+#    You should have received a copy of the GNU General Public License
19
+#    along with this program; if not, write to the Free Software
20
+#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
21
+import sys
22
+import progress
23
+class PatchSyntax(Exception):
24
+    def __init__(self, msg):
25
+        Exception.__init__(self, msg)
26
+
27
+
28
+class MalformedPatchHeader(PatchSyntax):
29
+    def __init__(self, desc, line):
30
+        self.desc = desc
31
+        self.line = line
32
+        msg = "Malformed patch header.  %s\n%s" % (self.desc, self.line)
33
+        PatchSyntax.__init__(self, msg)
34
+
35
+class MalformedHunkHeader(PatchSyntax):
36
+    def __init__(self, desc, line):
37
+        self.desc = desc
38
+        self.line = line
39
+        msg = "Malformed hunk header.  %s\n%s" % (self.desc, self.line)
40
+        PatchSyntax.__init__(self, msg)
41
+
42
+class MalformedLine(PatchSyntax):
43
+    def __init__(self, desc, line):
44
+        self.desc = desc
45
+        self.line = line
46
+        msg = "Malformed line.  %s\n%s" % (self.desc, self.line)
47
+        PatchSyntax.__init__(self, msg)
48
+
49
+def get_patch_names(iter_lines):
50
+    try:
51
+        line = iter_lines.next()
52
+        if not line.startswith("--- "):
53
+            raise MalformedPatchHeader("No orig name", line)
54
+        else:
55
+            orig_name = line[4:].rstrip("\n")
56
+    except StopIteration:
57
+        raise MalformedPatchHeader("No orig line", "")
58
+    try:
59
+        line = iter_lines.next()
60
+        if not line.startswith("+++ "):
61
+            raise PatchSyntax("No mod name")
62
+        else:
63
+            mod_name = line[4:].rstrip("\n")
64
+    except StopIteration:
65
+        raise MalformedPatchHeader("No mod line", "")
66
+    return (orig_name, mod_name)
67
+
68
+def parse_range(textrange):
69
+    """Parse a patch range, handling the "1" special-case
70
+
71
+    :param textrange: The text to parse
72
+    :type textrange: str
73
+    :return: the position and range, as a tuple
74
+    :rtype: (int, int)
75
+    """
76
+    tmp = textrange.split(',')
77
+    if len(tmp) == 1:
78
+        pos = tmp[0]
79
+        range = "1"
80
+    else:
81
+        (pos, range) = tmp
82
+    pos = int(pos)
83
+    range = int(range)
84
+    return (pos, range)
85
+
86
+ 
87
+def hunk_from_header(line):
88
+    if not line.startswith("@@") or not line.endswith("@@\n") \
89
+        or not len(line) > 4:
90
+        raise MalformedHunkHeader("Does not start and end with @@.", line)
91
+    try:
92
+        (orig, mod) = line[3:-4].split(" ")
93
+    except Exception, e:
94
+        raise MalformedHunkHeader(str(e), line)
95
+    if not orig.startswith('-') or not mod.startswith('+'):
96
+        raise MalformedHunkHeader("Positions don't start with + or -.", line)
97
+    try:
98
+        (orig_pos, orig_range) = parse_range(orig[1:])
99
+        (mod_pos, mod_range) = parse_range(mod[1:])
100
+    except Exception, e:
101
+        raise MalformedHunkHeader(str(e), line)
102
+    if mod_range < 0 or orig_range < 0:
103
+        raise MalformedHunkHeader("Hunk range is negative", line)
104
+    return Hunk(orig_pos, orig_range, mod_pos, mod_range)
105
+
106
+
107
+class HunkLine:
108
+    def __init__(self, contents):
109
+        self.contents = contents
110
+
111
+    def get_str(self, leadchar):
112
+        if self.contents == "\n" and leadchar == " " and False:
113
+            return "\n"
114
+        return leadchar + self.contents
115
+
116
+class ContextLine(HunkLine):
117
+    def __init__(self, contents):
118
+        HunkLine.__init__(self, contents)
119
+
120
+    def __str__(self):
121
+        return self.get_str(" ")
122
+
123
+
124
+class InsertLine(HunkLine):
125
+    def __init__(self, contents):
126
+        HunkLine.__init__(self, contents)
127
+
128
+    def __str__(self):
129
+        return self.get_str("+")
130
+
131
+
132
+class RemoveLine(HunkLine):
133
+    def __init__(self, contents):
134
+        HunkLine.__init__(self, contents)
135
+
136
+    def __str__(self):
137
+        return self.get_str("-")
138
+
139
+__pychecker__="no-returnvalues"
140
+def parse_line(line):
141
+    if line.startswith("\n"):
142
+        return ContextLine(line)
143
+    elif line.startswith(" "):
144
+        return ContextLine(line[1:])
145
+    elif line.startswith("+"):
146
+        return InsertLine(line[1:])
147
+    elif line.startswith("-"):
148
+        return RemoveLine(line[1:])
149
+    else:
150
+        raise MalformedLine("Unknown line type", line)
151
+__pychecker__=""
152
+
153
+
154
+class Hunk:
155
+    def __init__(self, orig_pos, orig_range, mod_pos, mod_range):
156
+        self.orig_pos = orig_pos
157
+        self.orig_range = orig_range
158
+        self.mod_pos = mod_pos
159
+        self.mod_range = mod_range
160
+        self.lines = []
161
+
162
+    def get_header(self):
163
+        return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, 
164
+                                                   self.orig_range),
165
+                                    self.range_str(self.mod_pos, 
166
+                                                   self.mod_range))
167
+
168
+    def range_str(self, pos, range):
169
+        """Return a file range, special-casing for 1-line files.
170
+
171
+        :param pos: The position in the file
172
+        :type pos: int
173
+        :range: The range in the file
174
+        :type range: int
175
+        :return: a string in the format 1,4 except when range == pos == 1
176
+        """
177
+        if range == 1:
178
+            return "%i" % pos
179
+        else:
180
+            return "%i,%i" % (pos, range)
181
+
182
+    def __str__(self):
183
+        lines = [self.get_header()]
184
+        for line in self.lines:
185
+            lines.append(str(line))
186
+        return "".join(lines)
187
+
188
+    def shift_to_mod(self, pos):
189
+        if pos < self.orig_pos-1:
190
+            return 0
191
+        elif pos > self.orig_pos+self.orig_range:
192
+            return self.mod_range - self.orig_range
193
+        else:
194
+            return self.shift_to_mod_lines(pos)
195
+
196
+    def shift_to_mod_lines(self, pos):
197
+        assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range)
198
+        position = self.orig_pos-1
199
+        shift = 0
200
+        for line in self.lines:
201
+            if isinstance(line, InsertLine):
202
+                shift += 1
203
+            elif isinstance(line, RemoveLine):
204
+                if position == pos:
205
+                    return None
206
+                shift -= 1
207
+                position += 1
208
+            elif isinstance(line, ContextLine):
209
+                position += 1
210
+            if position > pos:
211
+                break
212
+        return shift
213
+
214
+def iter_hunks(iter_lines):
215
+    hunk = None
216
+    for line in iter_lines:
217
+        if line.startswith("@@"):
218
+            if hunk is not None:
219
+                yield hunk
220
+            hunk = hunk_from_header(line)
221
+        else:
222
+            hunk.lines.append(parse_line(line))
223
+
224
+    if hunk is not None:
225
+        yield hunk
226
+
227
+class Patch:
228
+    def __init__(self, oldname, newname):
229
+        self.oldname = oldname
230
+        self.newname = newname
231
+        self.hunks = []
232
+
233
+    def __str__(self):
234
+        ret =  "--- %s\n+++ %s\n" % (self.oldname, self.newname) 
235
+        ret += "".join([str(h) for h in self.hunks])
236
+        return ret
237
+
238
+    def stats_str(self):
239
+        """Return a string of patch statistics"""
240
+        removes = 0
241
+        inserts = 0
242
+        for hunk in self.hunks:
243
+            for line in hunk.lines:
244
+                if isinstance(line, InsertLine):
245
+                     inserts+=1;
246
+                elif isinstance(line, RemoveLine):
247
+                     removes+=1;
248
+        return "%i inserts, %i removes in %i hunks" % \
249
+            (inserts, removes, len(self.hunks))
250
+
251
+    def pos_in_mod(self, position):
252
+        newpos = position
253
+        for hunk in self.hunks:
254
+            shift = hunk.shift_to_mod(position)
255
+            if shift is None:
256
+                return None
257
+            newpos += shift
258
+        return newpos
259
+            
260
+    def iter_inserted(self):
261
+        """Iteraties through inserted lines
262
+        
263
+        :return: Pair of line number, line
264
+        :rtype: iterator of (int, InsertLine)
265
+        """
266
+        for hunk in self.hunks:
267
+            pos = hunk.mod_pos - 1;
268
+            for line in hunk.lines:
269
+                if isinstance(line, InsertLine):
270
+                    yield (pos, line)
271
+                    pos += 1
272
+                if isinstance(line, ContextLine):
273
+                    pos += 1
274
+
275
+def parse_patch(iter_lines):
276
+    (orig_name, mod_name) = get_patch_names(iter_lines)
277
+    patch = Patch(orig_name, mod_name)
278
+    for hunk in iter_hunks(iter_lines):
279
+        patch.hunks.append(hunk)
280
+    return patch
281
+
282
+
283
+class AnnotateLine:
284
+    """A line associated with the log that produced it"""
285
+    def __init__(self, text, log=None):
286
+        self.text = text
287
+        self.log = log
288
+
289
+class CantGetRevisionData(Exception):
290
+    def __init__(self, revision):
291
+        Exception.__init__(self, "Can't get data for revision %s" % revision)
292
+        
293
+def annotate_file2(file_lines, anno_iter):
294
+    for result in iter_annotate_file(file_lines, anno_iter):
295
+        pass
296
+    return result
297
+
298
+        
299
+def iter_annotate_file(file_lines, anno_iter):
300
+    lines = [AnnotateLine(f) for f in file_lines]
301
+    patches = []
302
+    try:
303
+        for result in anno_iter:
304
+            if isinstance(result, progress.Progress):
305
+                yield result
306
+                continue
307
+            log, iter_inserted, patch = result
308
+            for (num, line) in iter_inserted:
309
+                old_num = num
310
+
311
+                for cur_patch in patches:
312
+                    num = cur_patch.pos_in_mod(num)
313
+                    if num == None: 
314
+                        break
315
+
316
+                if num >= len(lines):
317
+                    continue
318
+                if num is not None and lines[num].log is None:
319
+                    lines[num].log = log
320
+            patches=[patch]+patches
321
+    except CantGetRevisionData:
322
+        pass
323
+    yield lines
324
+
325
+
326
+def difference_index(atext, btext):
327
+    """Find the indext of the first character that differs betweeen two texts
328
+
329
+    :param atext: The first text
330
+    :type atext: str
331
+    :param btext: The second text
332
+    :type str: str
333
+    :return: The index, or None if there are no differences within the range
334
+    :rtype: int or NoneType
335
+    """
336
+    length = len(atext)
337
+    if len(btext) < length:
338
+        length = len(btext)
339
+    for i in range(length):
340
+        if atext[i] != btext[i]:
341
+            return i;
342
+    return None
343
+
344
+
345
+def test():
346
+    import unittest
347
+    class PatchesTester(unittest.TestCase):
348
+        def testValidPatchHeader(self):
349
+            """Parse a valid patch header"""
350
+            lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n')
351
+            (orig, mod) = get_patch_names(lines.__iter__())
352
+            assert(orig == "orig/commands.py")
353
+            assert(mod == "mod/dommands.py")
354
+
355
+        def testInvalidPatchHeader(self):
356
+            """Parse an invalid patch header"""
357
+            lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n')
358
+            self.assertRaises(MalformedPatchHeader, get_patch_names,
359
+                              lines.__iter__())
360
+
361
+        def testValidHunkHeader(self):
362
+            """Parse a valid hunk header"""
363
+            header = "@@ -34,11 +50,6 @@\n"
364
+            hunk = hunk_from_header(header);
365
+            assert (hunk.orig_pos == 34)
366
+            assert (hunk.orig_range == 11)
367
+            assert (hunk.mod_pos == 50)
368
+            assert (hunk.mod_range == 6)
369
+            assert (str(hunk) == header)
370
+
371
+        def testValidHunkHeader2(self):
372
+            """Parse a tricky, valid hunk header"""
373
+            header = "@@ -1 +0,0 @@\n"
374
+            hunk = hunk_from_header(header);
375
+            assert (hunk.orig_pos == 1)
376
+            assert (hunk.orig_range == 1)
377
+            assert (hunk.mod_pos == 0)
378
+            assert (hunk.mod_range == 0)
379
+            assert (str(hunk) == header)
380
+
381
+        def makeMalformed(self, header):
382
+            self.assertRaises(MalformedHunkHeader, hunk_from_header, header)
383
+
384
+        def testInvalidHeader(self):
385
+            """Parse an invalid hunk header"""
386
+            self.makeMalformed(" -34,11 +50,6 \n")
387
+            self.makeMalformed("@@ +50,6 -34,11 @@\n")
388
+            self.makeMalformed("@@ -34,11 +50,6 @@")
389
+            self.makeMalformed("@@ -34.5,11 +50,6 @@\n")
390
+            self.makeMalformed("@@-34,11 +50,6@@\n")
391
+            self.makeMalformed("@@ 34,11 50,6 @@\n")
392
+            self.makeMalformed("@@ -34,11 @@\n")
393
+            self.makeMalformed("@@ -34,11 +50,6.5 @@\n")
394
+            self.makeMalformed("@@ -34,11 +50,-6 @@\n")
395
+
396
+        def lineThing(self,text, type):
397
+            line = parse_line(text)
398
+            assert(isinstance(line, type))
399
+            assert(str(line)==text)
400
+
401
+        def makeMalformedLine(self, text):
402
+            self.assertRaises(MalformedLine, parse_line, text)
403
+
404
+        def testValidLine(self):
405
+            """Parse a valid hunk line"""
406
+            self.lineThing(" hello\n", ContextLine)
407
+            self.lineThing("+hello\n", InsertLine)
408
+            self.lineThing("-hello\n", RemoveLine)
409
+        
410
+        def testMalformedLine(self):
411
+            """Parse invalid valid hunk lines"""
412
+            self.makeMalformedLine("hello\n")
413
+        
414
+        def compare_parsed(self, patchtext):
415
+            lines = patchtext.splitlines(True)
416
+            patch = parse_patch(lines.__iter__())
417
+            pstr = str(patch)
418
+            i = difference_index(patchtext, pstr)
419
+            if i is not None:
420
+                print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i])
421
+            assert (patchtext == str(patch))
422
+
423
+        def testAll(self):
424
+            """Test parsing a whole patch"""
425
+            patchtext = """--- orig/commands.py
426
++++ mod/commands.py
427
+@@ -1337,7 +1337,8 @@
428
+ 
429
+     def set_title(self, command=None):
430
+         try:
431
+-            version = self.tree.tree_version.nonarch
432
++            version = pylon.alias_or_version(self.tree.tree_version, self.tree,
433
++                                             full=False)
434
+         except:
435
+             version = "[no version]"
436
+         if command is None:
437
+@@ -1983,7 +1984,11 @@
438
+                                          version)
439
+         if len(new_merges) > 0:
440
+             if cmdutil.prompt("Log for merge"):
441
+-                mergestuff = cmdutil.log_for_merge(tree, comp_version)
442
++                if cmdutil.prompt("changelog for merge"):
443
++                    mergestuff = "Patches applied:\\n"
444
++                    mergestuff += pylon.changelog_for_merge(new_merges)
445
++                else:
446
++                    mergestuff = cmdutil.log_for_merge(tree, comp_version)
447
+                 log.description += mergestuff
448
+         log.save()
449
+     try:
450
+"""
451
+            self.compare_parsed(patchtext)
452
+
453
+        def testInit(self):
454
+            """Handle patches missing half the position, range tuple"""
455
+            patchtext = \
456
+"""--- orig/__init__.py
457
++++ mod/__init__.py
458
+@@ -1 +1,2 @@
459
+ __docformat__ = "restructuredtext en"
460
++__doc__ = An alternate Arch commandline interface"""
461
+            self.compare_parsed(patchtext)
462
+            
463
+
464
+
465
+        def testLineLookup(self):
466
+            """Make sure we can accurately look up mod line from orig"""
467
+            patch = parse_patch(open("testdata/diff"))
468
+            orig = list(open("testdata/orig"))
469
+            mod = list(open("testdata/mod"))
470
+            removals = []
471
+            for i in range(len(orig)):
472
+                mod_pos = patch.pos_in_mod(i)
473
+                if mod_pos is None:
474
+                    removals.append(orig[i])
475
+                    continue
476
+                assert(mod[mod_pos]==orig[i])
477
+            rem_iter = removals.__iter__()
478
+            for hunk in patch.hunks:
479
+                for line in hunk.lines:
480
+                    if isinstance(line, RemoveLine):
481
+                        next = rem_iter.next()
482
+                        if line.contents != next:
483
+                            sys.stdout.write(" orig:%spatch:%s" % (next,
484
+                                             line.contents))
485
+                        assert(line.contents == next)
486
+            self.assertRaises(StopIteration, rem_iter.next)
487
+
488
+        def testFirstLineRenumber(self):
489
+            """Make sure we handle lines at the beginning of the hunk"""
490
+            patch = parse_patch(open("testdata/insert_top.patch"))
491
+            assert (patch.pos_in_mod(0)==1)
492
+    
493
+            
494
+    patchesTestSuite = unittest.makeSuite(PatchesTester,'test')
495
+    runner = unittest.TextTestRunner(verbosity=0)
496
+    return runner.run(patchesTestSuite)
497
+    
498
+
499
+if __name__ == "__main__":
500
+    test()
501
+# arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683
502
503
*** added directory 'testdata'
504
*** added file 'testdata/diff'
505
--- /dev/null 
506
+++ testdata/diff 
507
@@ -0,0 +1,1154 @@
508
+--- orig/commands.py
509
++++ mod/commands.py
510
+@@ -19,25 +19,31 @@
511
+ import arch
512
+ import arch.util
513
+ import arch.arch
514
++
515
++import pylon.errors
516
++from pylon.errors import *
517
++from pylon import errors
518
++from pylon import util
519
++from pylon import arch_core
520
++from pylon import arch_compound
521
++from pylon import ancillary
522
++from pylon import misc
523
++from pylon import paths 
524
++
525
+ import abacmds
526
+ import cmdutil
527
+ import shutil
528
+ import os
529
+ import options
530
+-import paths 
531
+ import time
532
+ import cmd
533
+ import readline
534
+ import re
535
+ import string
536
+-import arch_core
537
+-from errors import *
538
+-import errors
539
+ import terminal
540
+-import ancillary
541
+-import misc
542
+ import email
543
+ import smtplib
544
++import textwrap
545
+ 
546
+ __docformat__ = "restructuredtext"
547
+ __doc__ = "Implementation of user (sub) commands"
548
+@@ -257,7 +263,7 @@
549
+ 
550
+         tree=arch.tree_root()
551
+         if len(args) == 0:
552
+-            a_spec = cmdutil.comp_revision(tree)
553
++            a_spec = ancillary.comp_revision(tree)
554
+         else:
555
+             a_spec = cmdutil.determine_revision_tree(tree, args[0])
556
+         cmdutil.ensure_archive_registered(a_spec.archive)
557
+@@ -284,7 +290,7 @@
558
+             changeset=options.changeset
559
+             tmpdir = None
560
+         else:
561
+-            tmpdir=cmdutil.tmpdir()
562
++            tmpdir=util.tmpdir()
563
+             changeset=tmpdir+"/changeset"
564
+         try:
565
+             delta=arch.iter_delta(a_spec, b_spec, changeset)
566
+@@ -304,14 +310,14 @@
567
+             if status > 1:
568
+                 return
569
+             if (options.perform_diff):
570
+-                chan = cmdutil.ChangesetMunger(changeset)
571
++                chan = arch_compound.ChangesetMunger(changeset)
572
+                 chan.read_indices()
573
+-                if isinstance(b_spec, arch.Revision):
574
+-                    b_dir = b_spec.library_find()
575
+-                else:
576
+-                    b_dir = b_spec
577
+-                a_dir = a_spec.library_find()
578
+                 if options.diffopts is not None:
579
++                    if isinstance(b_spec, arch.Revision):
580
++                        b_dir = b_spec.library_find()
581
++                    else:
582
++                        b_dir = b_spec
583
++                    a_dir = a_spec.library_find()
584
+                     diffopts = options.diffopts.split()
585
+                     cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir)
586
+                 else:
587
+@@ -517,7 +523,7 @@
588
+         except arch.errors.TreeRootError, e:
589
+             print e
590
+             return
591
+-        from_revision=cmdutil.tree_latest(tree)
592
++        from_revision = arch_compound.tree_latest(tree)
593
+         if from_revision==to_revision:
594
+             print "Tree is already up to date with:\n"+str(to_revision)+"."
595
+             return
596
+@@ -592,6 +598,9 @@
597
+ 
598
+         if len(args) == 0:
599
+             args = None
600
++        if options.version is None:
601
++            return options, tree.tree_version, args
602
++
603
+         revision=cmdutil.determine_revision_arch(tree, options.version)
604
+         return options, revision.get_version(), args
605
+ 
606
+@@ -601,11 +610,16 @@
607
+         """
608
+         tree=arch.tree_root()
609
+         options, version, files = self.parse_commandline(cmdargs, tree)
610
++        ancestor = None
611
+         if options.__dict__.has_key("base") and options.base:
612
+             base = cmdutil.determine_revision_tree(tree, options.base)
613
++            ancestor = base
614
+         else:
615
+-            base = cmdutil.submit_revision(tree)
616
+-        
617
++            base = ancillary.submit_revision(tree)
618
++            ancestor = base
619
++        if ancestor is None:
620
++            ancestor = arch_compound.tree_latest(tree, version)
621
++
622
+         writeversion=version
623
+         archive=version.archive
624
+         source=cmdutil.get_mirror_source(archive)
625
+@@ -625,18 +639,26 @@
626
+         try:
627
+             last_revision=tree.iter_logs(version, True).next().revision
628
+         except StopIteration, e:
629
+-            if cmdutil.prompt("Import from commit"):
630
+-                return do_import(version)
631
+-            else:
632
+-                raise NoVersionLogs(version)
633
+-        if last_revision!=version.iter_revisions(True).next():
634
++            last_revision = None
635
++            if ancestor is None:
636
++                if cmdutil.prompt("Import from commit"):
637
++                    return do_import(version)
638
++                else:
639
++                    raise NoVersionLogs(version)
640
++        try:
641
++            arch_last_revision = version.iter_revisions(True).next()
642
++        except StopIteration, e:
643
++            arch_last_revision = None
644
++ 
645
++        if last_revision != arch_last_revision:
646
++            print "Tree is not up to date with %s" % str(version)
647
+             if not cmdutil.prompt("Out of date"):
648
+                 raise OutOfDate
649
+             else:
650
+                 allow_old=True
651
+ 
652
+         try:
653
+-            if not cmdutil.has_changed(version):
654
++            if not cmdutil.has_changed(ancestor):
655
+                 if not cmdutil.prompt("Empty commit"):
656
+                     raise EmptyCommit
657
+         except arch.util.ExecProblem, e:
658
+@@ -645,15 +667,15 @@
659
+                 raise MissingID(e)
660
+             else:
661
+                 raise
662
+-        log = tree.log_message(create=False)
663
++        log = tree.log_message(create=False, version=version)
664
+         if log is None:
665
+             try:
666
+                 if cmdutil.prompt("Create log"):
667
+-                    edit_log(tree)
668
++                    edit_log(tree, version)
669
+ 
670
+             except cmdutil.NoEditorSpecified, e:
671
+                 raise CommandFailed(e)
672
+-            log = tree.log_message(create=False)
673
++            log = tree.log_message(create=False, version=version)
674
+         if log is None: 
675
+             raise NoLogMessage
676
+         if log["Summary"] is None or len(log["Summary"].strip()) == 0:
677
+@@ -837,23 +859,24 @@
678
+             if spec is not None:
679
+                 revision = cmdutil.determine_revision_tree(tree, spec)
680
+             else:
681
+-                revision = cmdutil.comp_revision(tree)
682
++                revision = ancillary.comp_revision(tree)
683
+         except cmdutil.CantDetermineRevision, e:
684
+             raise CommandFailedWrapper(e)
685
+         munger = None
686
+ 
687
+         if options.file_contents or options.file_perms or options.deletions\
688
+             or options.additions or options.renames or options.hunk_prompt:
689
+-            munger = cmdutil.MungeOpts()
690
+-            munger.hunk_prompt = options.hunk_prompt
691
++            munger = arch_compound.MungeOpts()
692
++            munger.set_hunk_prompt(cmdutil.colorize, cmdutil.user_hunk_confirm,
693
++                                   options.hunk_prompt)
694
+ 
695
+         if len(args) > 0 or options.logs or options.pattern_files or \
696
+             options.control:
697
+             if munger is None:
698
+-                munger = cmdutil.MungeOpts(True)
699
++                munger = cmdutil.arch_compound.MungeOpts(True)
700
+                 munger.all_types(True)
701
+         if len(args) > 0:
702
+-            t_cwd = cmdutil.tree_cwd(tree)
703
++            t_cwd = arch_compound.tree_cwd(tree)
704
+             for name in args:
705
+                 if len(t_cwd) > 0:
706
+                     t_cwd += "/"
707
+@@ -878,7 +901,7 @@
708
+         if options.pattern_files:
709
+             munger.add_keep_pattern(options.pattern_files)
710
+                 
711
+-        for line in cmdutil.revert(tree, revision, munger, 
712
++        for line in arch_compound.revert(tree, revision, munger, 
713
+                                    not options.no_output):
714
+             cmdutil.colorize(line)
715
+ 
716
+@@ -1042,18 +1065,13 @@
717
+         help_tree_spec()
718
+         return
719
+ 
720
+-def require_version_exists(version, spec):
721
+-    if not version.exists():
722
+-        raise cmdutil.CantDetermineVersion(spec, 
723
+-                                           "The version %s does not exist." \
724
+-                                           % version)
725
+-
726
+ class Revisions(BaseCommand):
727
+     """
728
+     Print a revision name based on a revision specifier
729
+     """
730
+     def __init__(self):
731
+         self.description="Lists revisions"
732
++        self.cl_revisions = []
733
+     
734
+     def do_command(self, cmdargs):
735
+         """
736
+@@ -1066,224 +1084,68 @@
737
+             self.tree = arch.tree_root()
738
+         except arch.errors.TreeRootError:
739
+             self.tree = None
740
++        if options.type == "default":
741
++            options.type = "archive"
742
+         try:
743
+-            iter = self.get_iterator(options.type, args, options.reverse, 
744
+-                                     options.modified)
745
++            iter = cmdutil.revision_iterator(self.tree, options.type, args, 
746
++                                             options.reverse, options.modified,
747
++                                             options.shallow)
748
+         except cmdutil.CantDetermineRevision, e:
749
+             raise CommandFailedWrapper(e)
750
+-
751
++        except cmdutil.CantDetermineVersion, e:
752
++            raise CommandFailedWrapper(e)
753
+         if options.skip is not None:
754
+             iter = cmdutil.iter_skip(iter, int(options.skip))
755
+ 
756
+-        for revision in iter:
757
+-            log = None
758
+-            if isinstance(revision, arch.Patchlog):
759
+-                log = revision
760
+-                revision=revision.revision
761
+-            print options.display(revision)
762
+-            if log is None and (options.summary or options.creator or 
763
+-                                options.date or options.merges):
764
+-                log = revision.patchlog
765
+-            if options.creator:
766
+-                print "    %s" % log.creator
767
+-            if options.date:
768
+-                print "    %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date)
769
+-            if options.summary:
770
+-                print "    %s" % log.summary
771
+-            if options.merges:
772
+-                showed_title = False
773
+-                for revision in log.merged_patches:
774
+-                    if not showed_title:
775
+-                        print "    Merged:"
776
+-                        showed_title = True
777
+-                    print "    %s" % revision
778
+-
779
+-    def get_iterator(self, type, args, reverse, modified):
780
+-        if len(args) > 0:
781
+-            spec = args[0]
782
+-        else:
783
+-            spec = None
784
+-        if modified is not None:
785
+-            iter = cmdutil.modified_iter(modified, self.tree)
786
+-            if reverse:
787
+-                return iter
788
+-            else:
789
+-                return cmdutil.iter_reverse(iter)
790
+-        elif type == "archive":
791
+-            if spec is None:
792
+-                if self.tree is None:
793
+-                    raise cmdutil.CantDetermineRevision("", 
794
+-                                                        "Not in a project tree")
795
+-                version = cmdutil.determine_version_tree(spec, self.tree)
796
+-            else:
797
+-                version = cmdutil.determine_version_arch(spec, self.tree)
798
+-                cmdutil.ensure_archive_registered(version.archive)
799
+-                require_version_exists(version, spec)
800
+-            return version.iter_revisions(reverse)
801
+-        elif type == "cacherevs":
802
+-            if spec is None:
803
+-                if self.tree is None:
804
+-                    raise cmdutil.CantDetermineRevision("", 
805
+-                                                        "Not in a project tree")
806
+-                version = cmdutil.determine_version_tree(spec, self.tree)
807
+-            else:
808
+-                version = cmdutil.determine_version_arch(spec, self.tree)
809
+-                cmdutil.ensure_archive_registered(version.archive)
810
+-                require_version_exists(version, spec)
811
+-            return cmdutil.iter_cacherevs(version, reverse)
812
+-        elif type == "library":
813
+-            if spec is None:
814
+-                if self.tree is None:
815
+-                    raise cmdutil.CantDetermineRevision("", 
816
+-                                                        "Not in a project tree")
817
+-                version = cmdutil.determine_version_tree(spec, self.tree)
818
+-            else:
819
+-                version = cmdutil.determine_version_arch(spec, self.tree)
820
+-            return version.iter_library_revisions(reverse)
821
+-        elif type == "logs":
822
+-            if self.tree is None:
823
+-                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
824
+-            return self.tree.iter_logs(cmdutil.determine_version_tree(spec, \
825
+-                                  self.tree), reverse)
826
+-        elif type == "missing" or type == "skip-present":
827
+-            if self.tree is None:
828
+-                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
829
+-            skip = (type == "skip-present")
830
+-            version = cmdutil.determine_version_tree(spec, self.tree)
831
+-            cmdutil.ensure_archive_registered(version.archive)
832
+-            require_version_exists(version, spec)
833
+-            return cmdutil.iter_missing(self.tree, version, reverse,
834
+-                                        skip_present=skip)
835
+-
836
+-        elif type == "present":
837
+-            if self.tree is None:
838
+-                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
839
+-            version = cmdutil.determine_version_tree(spec, self.tree)
840
+-            cmdutil.ensure_archive_registered(version.archive)
841
+-            require_version_exists(version, spec)
842
+-            return cmdutil.iter_present(self.tree, version, reverse)
843
+-
844
+-        elif type == "new-merges" or type == "direct-merges":
845
+-            if self.tree is None:
846
+-                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
847
+-            version = cmdutil.determine_version_tree(spec, self.tree)
848
+-            cmdutil.ensure_archive_registered(version.archive)
849
+-            require_version_exists(version, spec)
850
+-            iter = cmdutil.iter_new_merges(self.tree, version, reverse)
851
+-            if type == "new-merges":
852
+-                return iter
853
+-            elif type == "direct-merges":
854
+-                return cmdutil.direct_merges(iter)
855
+-
856
+-        elif type == "missing-from":
857
+-            if self.tree is None:
858
+-                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
859
+-            revision = cmdutil.determine_revision_tree(self.tree, spec)
860
+-            libtree = cmdutil.find_or_make_local_revision(revision)
861
+-            return cmdutil.iter_missing(libtree, self.tree.tree_version,
862
+-                                        reverse)
863
+-
864
+-        elif type == "partner-missing":
865
+-            return cmdutil.iter_partner_missing(self.tree, reverse)
866
+-
867
+-        elif type == "ancestry":
868
+-            revision = cmdutil.determine_revision_tree(self.tree, spec)
869
+-            iter = cmdutil._iter_ancestry(self.tree, revision)
870
+-            if reverse:
871
+-                return iter
872
+-            else:
873
+-                return cmdutil.iter_reverse(iter)
874
+-
875
+-        elif type == "dependencies" or type == "non-dependencies":
876
+-            nondeps = (type == "non-dependencies")
877
+-            revision = cmdutil.determine_revision_tree(self.tree, spec)
878
+-            anc_iter = cmdutil._iter_ancestry(self.tree, revision)
879
+-            iter_depends = cmdutil.iter_depends(anc_iter, nondeps)
880
+-            if reverse:
881
+-                return iter_depends
882
+-            else:
883
+-                return cmdutil.iter_reverse(iter_depends)
884
+-        elif type == "micro":
885
+-            return cmdutil.iter_micro(self.tree)
886
+-
887
+-    
888
++        try:
889
++            for revision in iter:
890
++                log = None
891
++                if isinstance(revision, arch.Patchlog):
892
++                    log = revision
893
++                    revision=revision.revision
894
++                out = options.display(revision)
895
++                if out is not None:
896
++                    print out
897
++                if log is None and (options.summary or options.creator or 
898
++                                    options.date or options.merges):
899
++                    log = revision.patchlog
900
++                if options.creator:
901
++                    print "    %s" % log.creator
902
++                if options.date:
903
++                    print "    %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date)
904
++                if options.summary:
905
++                    print "    %s" % log.summary
906
++                if options.merges:
907
++                    showed_title = False
908
++                    for revision in log.merged_patches:
909
++                        if not showed_title:
910
++                            print "    Merged:"
911
++                            showed_title = True
912
++                        print "    %s" % revision
913
++            if len(self.cl_revisions) > 0:
914
++                print pylon.changelog_for_merge(self.cl_revisions)
915
++        except pylon.errors.TreeRootNone:
916
++            raise CommandFailedWrapper(
917
++                Exception("This option can only be used in a project tree."))
918
++
919
++    def changelog_append(self, revision):
920
++        if isinstance(revision, arch.Revision):
921
++            revision=arch.Patchlog(revision)
922
++        self.cl_revisions.append(revision)
923
++   
924
+     def get_parser(self):
925
+         """
926
+         Returns the options parser to use for the "revision" command.
927
+ 
928
+         :rtype: cmdutil.CmdOptionParser
929
+         """
930
+-        parser=cmdutil.CmdOptionParser("fai revisions [revision]")
931
++        parser=cmdutil.CmdOptionParser("fai revisions [version/revision]")
932
+         select = cmdutil.OptionGroup(parser, "Selection options",
933
+                           "Control which revisions are listed.  These options"
934
+                           " are mutually exclusive.  If more than one is"
935
+                           " specified, the last is used.")
936
+-        select.add_option("", "--archive", action="store_const", 
937
+-                          const="archive", dest="type", default="archive",
938
+-                          help="List all revisions in the archive")
939
+-        select.add_option("", "--cacherevs", action="store_const", 
940
+-                          const="cacherevs", dest="type",
941
+-                          help="List all revisions stored in the archive as "
942
+-                          "complete copies")
943
+-        select.add_option("", "--logs", action="store_const", 
944
+-                          const="logs", dest="type",
945
+-                          help="List revisions that have a patchlog in the "
946
+-                          "tree")
947
+-        select.add_option("", "--missing", action="store_const", 
948
+-                          const="missing", dest="type",
949
+-                          help="List revisions from the specified version that"
950
+-                          " have no patchlog in the tree")
951
+-        select.add_option("", "--skip-present", action="store_const", 
952
+-                          const="skip-present", dest="type",
953
+-                          help="List revisions from the specified version that"
954
+-                          " have no patchlogs at all in the tree")
955
+-        select.add_option("", "--present", action="store_const", 
956
+-                          const="present", dest="type",
957
+-                          help="List revisions from the specified version that"
958
+-                          " have no patchlog in the tree, but can't be merged")
959
+-        select.add_option("", "--missing-from", action="store_const", 
960
+-                          const="missing-from", dest="type",
961
+-                          help="List revisions from the specified revision "
962
+-                          "that have no patchlog for the tree version")
963
+-        select.add_option("", "--partner-missing", action="store_const", 
964
+-                          const="partner-missing", dest="type",
965
+-                          help="List revisions in partner versions that are"
966
+-                          " missing")
967
+-        select.add_option("", "--new-merges", action="store_const", 
968
+-                          const="new-merges", dest="type",
969
+-                          help="List revisions that have had patchlogs added"
970
+-                          " to the tree since the last commit")
971
+-        select.add_option("", "--direct-merges", action="store_const", 
972
+-                          const="direct-merges", dest="type",
973
+-                          help="List revisions that have been directly added"
974
+-                          " to tree since the last commit ")
975
+-        select.add_option("", "--library", action="store_const", 
976
+-                          const="library", dest="type",
977
+-                          help="List revisions in the revision library")
978
+-        select.add_option("", "--ancestry", action="store_const", 
979
+-                          const="ancestry", dest="type",
980
+-                          help="List revisions that are ancestors of the "
981
+-                          "current tree version")
982
+-
983
+-        select.add_option("", "--dependencies", action="store_const", 
984
+-                          const="dependencies", dest="type",
985
+-                          help="List revisions that the given revision "
986
+-                          "depends on")
987
+-
988
+-        select.add_option("", "--non-dependencies", action="store_const", 
989
+-                          const="non-dependencies", dest="type",
990
+-                          help="List revisions that the given revision "
991
+-                          "does not depend on")
992
+-
993
+-        select.add_option("--micro", action="store_const", 
994
+-                          const="micro", dest="type",
995
+-                          help="List partner revisions aimed for this "
996
+-                          "micro-branch")
997
+-
998
+-        select.add_option("", "--modified", dest="modified", 
999
+-                          help="List tree ancestor revisions that modified a "
1000
+-                          "given file", metavar="FILE[:LINE]")
1001
+ 
1002
++        cmdutil.add_revision_iter_options(select)
1003
+         parser.add_option("", "--skip", dest="skip", 
1004
+                           help="Skip revisions.  Positive numbers skip from "
1005
+                           "beginning, negative skip from end.",
1006
+@@ -1312,6 +1174,9 @@
1007
+         format.add_option("--cacherev", action="store_const", 
1008
+                          const=paths.determine_cacherev_path, dest="display",
1009
+                          help="Show location of cacherev file")
1010
++        format.add_option("--changelog", action="store_const", 
1011
++                         const=self.changelog_append, dest="display",
1012
++                         help="Show location of cacherev file")
1013
+         parser.add_option_group(format)
1014
+         display = cmdutil.OptionGroup(parser, "Display format options",
1015
+                           "These control the display of data")
1016
+@@ -1448,6 +1313,7 @@
1017
+         if os.access(self.history_file, os.R_OK) and \
1018
+             os.path.isfile(self.history_file):
1019
+             readline.read_history_file(self.history_file)
1020
++        self.cwd = os.getcwd()
1021
+ 
1022
+     def write_history(self):
1023
+         readline.write_history_file(self.history_file)
1024
+@@ -1470,16 +1336,21 @@
1025
+     def set_prompt(self):
1026
+         if self.tree is not None:
1027
+             try:
1028
+-                version = " "+self.tree.tree_version.nonarch
1029
++                prompt = pylon.alias_or_version(self.tree.tree_version, 
1030
++                                                self.tree, 
1031
++                                                full=False)
1032
++                if prompt is not None:
1033
++                    prompt = " " + prompt
1034
+             except:
1035
+-                version = ""
1036
++                prompt = ""
1037
+         else:
1038
+-            version = ""
1039
+-        self.prompt = "Fai%s> " % version
1040
++            prompt = ""
1041
++        self.prompt = "Fai%s> " % prompt
1042
+ 
1043
+     def set_title(self, command=None):
1044
+         try:
1045
+-            version = self.tree.tree_version.nonarch
1046
++            version = pylon.alias_or_version(self.tree.tree_version, self.tree, 
1047
++                                             full=False)
1048
+         except:
1049
+             version = "[no version]"
1050
+         if command is None:
1051
+@@ -1489,8 +1360,15 @@
1052
+     def do_cd(self, line):
1053
+         if line == "":
1054
+             line = "~"
1055
++        line = os.path.expanduser(line)
1056
++        if os.path.isabs(line):
1057
++            newcwd = line
1058
++        else:
1059
++            newcwd = self.cwd+'/'+line
1060
++        newcwd = os.path.normpath(newcwd)
1061
+         try:
1062
+-            os.chdir(os.path.expanduser(line))
1063
++            os.chdir(newcwd)
1064
++            self.cwd = newcwd
1065
+         except Exception, e:
1066
+             print e
1067
+         try:
1068
+@@ -1523,7 +1401,7 @@
1069
+             except cmdutil.CantDetermineRevision, e:
1070
+                 print e
1071
+             except Exception, e:
1072
+-                print "Unhandled error:\n%s" % cmdutil.exception_str(e)
1073
++                print "Unhandled error:\n%s" % errors.exception_str(e)
1074
+ 
1075
+         elif suggestions.has_key(args[0]):
1076
+             print suggestions[args[0]]
1077
+@@ -1574,7 +1452,7 @@
1078
+                 arg = line.split()[-1]
1079
+             else:
1080
+                 arg = ""
1081
+-            iter = iter_munged_completions(iter, arg, text)
1082
++            iter = cmdutil.iter_munged_completions(iter, arg, text)
1083
+         except Exception, e:
1084
+             print e
1085
+         return list(iter)
1086
+@@ -1604,10 +1482,11 @@
1087
+                 else:
1088
+                     arg = ""
1089
+                 if arg.startswith("-"):
1090
+-                    return list(iter_munged_completions(iter, arg, text))
1091
++                    return list(cmdutil.iter_munged_completions(iter, arg, 
1092
++                                                                text))
1093
+                 else:
1094
+-                    return list(iter_munged_completions(
1095
+-                        iter_file_completions(arg), arg, text))
1096
++                    return list(cmdutil.iter_munged_completions(
1097
++                        cmdutil.iter_file_completions(arg), arg, text))
1098
+ 
1099
+ 
1100
+             elif cmd == "cd":
1101
+@@ -1615,13 +1494,13 @@
1102
+                     arg = args.split()[-1]
1103
+                 else:
1104
+                     arg = ""
1105
+-                iter = iter_dir_completions(arg)
1106
+-                iter = iter_munged_completions(iter, arg, text)
1107
++                iter = cmdutil.iter_dir_completions(arg)
1108
++                iter = cmdutil.iter_munged_completions(iter, arg, text)
1109
+                 return list(iter)
1110
+             elif len(args)>0:
1111
+                 arg = args.split()[-1]
1112
+-                return list(iter_munged_completions(iter_file_completions(arg),
1113
+-                                                    arg, text))
1114
++                iter = cmdutil.iter_file_completions(arg)
1115
++                return list(cmdutil.iter_munged_completions(iter, arg, text))
1116
+             else:
1117
+                 return self.completenames(text, line, begidx, endidx)
1118
+         except Exception, e:
1119
+@@ -1636,44 +1515,8 @@
1120
+             yield entry
1121
+ 
1122
+ 
1123
+-def iter_file_completions(arg, only_dirs = False):
1124
+-    """Generate an iterator that iterates through filename completions.
1125
+-
1126
+-    :param arg: The filename fragment to match
1127
+-    :type arg: str
1128
+-    :param only_dirs: If true, match only directories
1129
+-    :type only_dirs: bool
1130
+-    """
1131
+-    cwd = os.getcwd()
1132
+-    if cwd != "/":
1133
+-        extras = [".", ".."]
1134
+-    else:
1135
+-        extras = []
1136
+-    (dir, file) = os.path.split(arg)
1137
+-    if dir != "":
1138
+-        listingdir = os.path.expanduser(dir)
1139
+-    else:
1140
+-        listingdir = cwd
1141
+-    for file in cmdutil.iter_combine([os.listdir(listingdir), extras]):
1142
+-        if dir != "":
1143
+-            userfile = dir+'/'+file
1144
+-        else:
1145
+-            userfile = file
1146
+-        if userfile.startswith(arg):
1147
+-            if os.path.isdir(listingdir+'/'+file):
1148
+-                userfile+='/'
1149
+-                yield userfile
1150
+-            elif not only_dirs:
1151
+-                yield userfile
1152
+-
1153
+-def iter_munged_completions(iter, arg, text):
1154
+-    for completion in iter:
1155
+-        completion = str(completion)
1156
+-        if completion.startswith(arg):
1157
+-            yield completion[len(arg)-len(text):]
1158
+-
1159
+ def iter_source_file_completions(tree, arg):
1160
+-    treepath = cmdutil.tree_cwd(tree)
1161
++    treepath = arch_compound.tree_cwd(tree)
1162
+     if len(treepath) > 0:
1163
+         dirs = [treepath]
1164
+     else:
1165
+@@ -1701,7 +1544,7 @@
1166
+     :return: An iterator of all matching untagged files
1167
+     :rtype: iterator of str
1168
+     """
1169
+-    treepath = cmdutil.tree_cwd(tree)
1170
++    treepath = arch_compound.tree_cwd(tree)
1171
+     if len(treepath) > 0:
1172
+         dirs = [treepath]
1173
+     else:
1174
+@@ -1743,8 +1586,8 @@
1175
+     :param arg: The prefix to match
1176
+     :type arg: str
1177
+     """
1178
+-    treepath = cmdutil.tree_cwd(tree)
1179
+-    tmpdir = cmdutil.tmpdir()
1180
++    treepath = arch_compound.tree_cwd(tree)
1181
++    tmpdir = util.tmpdir()
1182
+     changeset = tmpdir+"/changeset"
1183
+     completions = []
1184
+     revision = cmdutil.determine_revision_tree(tree)
1185
+@@ -1756,14 +1599,6 @@
1186
+     shutil.rmtree(tmpdir)
1187
+     return completions
1188
+ 
1189
+-def iter_dir_completions(arg):
1190
+-    """Generate an iterator that iterates through directory name completions.
1191
+-
1192
+-    :param arg: The directory name fragment to match
1193
+-    :type arg: str
1194
+-    """
1195
+-    return iter_file_completions(arg, True)
1196
+-
1197
+ class Shell(BaseCommand):
1198
+     def __init__(self):
1199
+         self.description = "Runs Fai as a shell"
1200
+@@ -1795,7 +1630,11 @@
1201
+         parser=self.get_parser()
1202
+         (options, args) = parser.parse_args(cmdargs)
1203
+ 
1204
+-        tree = arch.tree_root()
1205
++        try:
1206
++            tree = arch.tree_root()
1207
++        except arch.errors.TreeRootError, e:
1208
++            raise pylon.errors.CommandFailedWrapper(e)
1209
++            
1210
+ 
1211
+         if (len(args) == 0) == (options.untagged == False):
1212
+             raise cmdutil.GetHelp
1213
+@@ -1809,13 +1648,22 @@
1214
+         if options.id_type == "tagline":
1215
+             if method != "tagline":
1216
+                 if not cmdutil.prompt("Tagline in other tree"):
1217
+-                    if method == "explicit":
1218
+-                        options.id_type == explicit
1219
++                    if method == "explicit" or method == "implicit":
1220
++                        options.id_type == method
1221
+                     else:
1222
+                         print "add-id not supported for \"%s\" tagging method"\
1223
+                             % method 
1224
+                         return
1225
+         
1226
++        elif options.id_type == "implicit":
1227
++            if method != "implicit":
1228
++                if not cmdutil.prompt("Implicit in other tree"):
1229
++                    if method == "explicit" or method == "tagline":
1230
++                        options.id_type == method
1231
++                    else:
1232
++                        print "add-id not supported for \"%s\" tagging method"\
1233
++                            % method 
1234
++                        return
1235
+         elif options.id_type == "explicit":
1236
+             if method != "tagline" and method != explicit:
1237
+                 if not prompt("Explicit in other tree"):
1238
+@@ -1824,7 +1672,8 @@
1239
+                     return
1240
+         
1241
+         if options.id_type == "auto":
1242
+-            if method != "tagline" and method != "explicit":
1243
++            if method != "tagline" and method != "explicit" \
1244
++                and method !="implicit":
1245
+                 print "add-id not supported for \"%s\" tagging method" % method
1246
+                 return
1247
+             else:
1248
+@@ -1852,10 +1701,12 @@
1249
+             previous_files.extend(files)
1250
+             if id_type == "explicit":
1251
+                 cmdutil.add_id(files)
1252
+-            elif id_type == "tagline":
1253
++            elif id_type == "tagline" or id_type == "implicit":
1254
+                 for file in files:
1255
+                     try:
1256
+-                        cmdutil.add_tagline_or_explicit_id(file)
1257
++                        implicit = (id_type == "implicit")
1258
++                        cmdutil.add_tagline_or_explicit_id(file, False,
1259
++                                                           implicit)
1260
+                     except cmdutil.AlreadyTagged:
1261
+                         print "\"%s\" already has a tagline." % file
1262
+                     except cmdutil.NoCommentSyntax:
1263
+@@ -1888,6 +1739,9 @@
1264
+         parser.add_option("--tagline", action="store_const", 
1265
+                          const="tagline", dest="id_type", 
1266
+                          help="Use a tagline id")
1267
++        parser.add_option("--implicit", action="store_const", 
1268
++                         const="implicit", dest="id_type", 
1269
++                         help="Use an implicit id (deprecated)")
1270
+         parser.add_option("--untagged", action="store_true", 
1271
+                          dest="untagged", default=False, 
1272
+                          help="tag all untagged files")
1273
+@@ -1926,27 +1780,7 @@
1274
+     def get_completer(self, arg, index):
1275
+         if self.tree is None:
1276
+             raise arch.errors.TreeRootError
1277
+-        completions = list(ancillary.iter_partners(self.tree, 
1278
+-                                                   self.tree.tree_version))
1279
+-        if len(completions) == 0:
1280
+-            completions = list(self.tree.iter_log_versions())
1281
+-
1282
+-        aliases = []
1283
+-        try:
1284
+-            for completion in completions:
1285
+-                alias = ancillary.compact_alias(str(completion), self.tree)
1286
+-                if alias:
1287
+-                    aliases.extend(alias)
1288
+-
1289
+-            for completion in completions:
1290
+-                if completion.archive == self.tree.tree_version.archive:
1291
+-                    aliases.append(completion.nonarch)
1292
+-
1293
+-        except Exception, e:
1294
+-            print e
1295
+-            
1296
+-        completions.extend(aliases)
1297
+-        return completions
1298
++        return cmdutil.merge_completions(self.tree, arg, index)
1299
+ 
1300
+     def do_command(self, cmdargs):
1301
+         """
1302
+@@ -1961,7 +1795,7 @@
1303
+         
1304
+         if self.tree is None:
1305
+             raise arch.errors.TreeRootError(os.getcwd())
1306
+-        if cmdutil.has_changed(self.tree.tree_version):
1307
++        if cmdutil.has_changed(ancillary.comp_revision(self.tree)):
1308
+             raise UncommittedChanges(self.tree)
1309
+ 
1310
+         if len(args) > 0:
1311
+@@ -2027,14 +1861,14 @@
1312
+         :type other_revision: `arch.Revision`
1313
+         :return: 0 if the merge was skipped, 1 if it was applied
1314
+         """
1315
+-        other_tree = cmdutil.find_or_make_local_revision(other_revision)
1316
++        other_tree = arch_compound.find_or_make_local_revision(other_revision)
1317
+         try:
1318
+             if action == "native-merge":
1319
+-                ancestor = cmdutil.merge_ancestor2(self.tree, other_tree, 
1320
+-                                                   other_revision)
1321
++                ancestor = arch_compound.merge_ancestor2(self.tree, other_tree, 
1322
++                                                         other_revision)
1323
+             elif action == "update":
1324
+-                ancestor = cmdutil.tree_latest(self.tree, 
1325
+-                                               other_revision.version)
1326
++                ancestor = arch_compound.tree_latest(self.tree, 
1327
++                                                     other_revision.version)
1328
+         except CantDetermineRevision, e:
1329
+             raise CommandFailedWrapper(e)
1330
+         cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor))
1331
+@@ -2104,7 +1938,10 @@
1332
+         if self.tree is None:
1333
+             raise arch.errors.TreeRootError
1334
+ 
1335
+-        edit_log(self.tree)
1336
++        try:
1337
++            edit_log(self.tree, self.tree.tree_version)
1338
++        except pylon.errors.NoEditorSpecified, e:
1339
++            raise pylon.errors.CommandFailedWrapper(e)
1340
+ 
1341
+     def get_parser(self):
1342
+         """
1343
+@@ -2132,7 +1969,7 @@
1344
+         """
1345
+         return
1346
+ 
1347
+-def edit_log(tree):
1348
++def edit_log(tree, version):
1349
+     """Makes and edits the log for a tree.  Does all kinds of fancy things
1350
+     like log templates and merge summaries and log-for-merge
1351
+     
1352
+@@ -2141,28 +1978,29 @@
1353
+     """
1354
+     #ensure we have an editor before preparing the log
1355
+     cmdutil.find_editor()
1356
+-    log = tree.log_message(create=False)
1357
++    log = tree.log_message(create=False, version=version)
1358
+     log_is_new = False
1359
+     if log is None or cmdutil.prompt("Overwrite log"):
1360
+         if log is not None:
1361
+            os.remove(log.name)
1362
+-        log = tree.log_message(create=True)
1363
++        log = tree.log_message(create=True, version=version)
1364
+         log_is_new = True
1365
+         tmplog = log.name
1366
+-        template = tree+"/{arch}/=log-template"
1367
+-        if not os.path.exists(template):
1368
+-            template = os.path.expanduser("~/.arch-params/=log-template")
1369
+-            if not os.path.exists(template):
1370
+-                template = None
1371
++        template = pylon.log_template_path(tree)
1372
+         if template:
1373
+             shutil.copyfile(template, tmplog)
1374
+-        
1375
+-        new_merges = list(cmdutil.iter_new_merges(tree, 
1376
+-                                                  tree.tree_version))
1377
+-        log["Summary"] = merge_summary(new_merges, tree.tree_version)
1378
++        comp_version = ancillary.comp_revision(tree).version
1379
++        new_merges = cmdutil.iter_new_merges(tree, comp_version)
1380
++        new_merges = cmdutil.direct_merges(new_merges)
1381
++        log["Summary"] = pylon.merge_summary(new_merges, 
1382
++                                         version)
1383
+         if len(new_merges) > 0:   
1384
+             if cmdutil.prompt("Log for merge"):
1385
+-                mergestuff = cmdutil.log_for_merge(tree)
1386
++                if cmdutil.prompt("changelog for merge"):
1387
++                    mergestuff = "Patches applied:\n"
1388
++                    mergestuff += pylon.changelog_for_merge(new_merges)
1389
++                else:
1390
++                    mergestuff = cmdutil.log_for_merge(tree, comp_version)
1391
+                 log.description += mergestuff
1392
+         log.save()
1393
+     try:
1394
+@@ -2172,29 +2010,6 @@
1395
+             os.remove(log.name)
1396
+         raise
1397
+ 
1398
+-def merge_summary(new_merges, tree_version):
1399
+-    if len(new_merges) == 0:
1400
+-        return ""
1401
+-    if len(new_merges) == 1:
1402
+-        summary = new_merges[0].summary
1403
+-    else:
1404
+-        summary = "Merge"
1405
+-
1406
+-    credits = []
1407
+-    for merge in new_merges:
1408
+-        if arch.my_id() != merge.creator:
1409
+-            name = re.sub("<.*>", "", merge.creator).rstrip(" ");
1410
+-            if not name in credits:
1411
+-                credits.append(name)
1412
+-        else:
1413
+-            version = merge.revision.version
1414
+-            if version.archive == tree_version.archive:
1415
+-                if not version.nonarch in credits:
1416
+-                    credits.append(version.nonarch)
1417
+-            elif not str(version) in credits:
1418
+-                credits.append(str(version))
1419
+-
1420
+-    return ("%s (%s)") % (summary, ", ".join(credits))
1421
+ 
1422
+ class MirrorArchive(BaseCommand):
1423
+     """
1424
+@@ -2268,31 +2083,73 @@
1425
+ 
1426
+ Use "alias" to list available (user and automatic) aliases."""
1427
+ 
1428
++auto_alias = [
1429
++"acur", 
1430
++"The latest revision in the archive of the tree-version.  You can specify \
1431
++a different version like so: acur:foo--bar--0 (aliases can be used)",
1432
++"tcur",
1433
++"""(tree current) The latest revision in the tree of the tree-version. \
1434
++You can specify a different version like so: tcur:foo--bar--0 (aliases can be \
1435
++used).""",
1436
++"tprev" , 
1437
++"""(tree previous) The previous revision in the tree of the tree-version.  To \
1438
++specify an older revision, use a number, e.g. "tprev:4" """,
1439
++"tanc" , 
1440
++"""(tree ancestor) The ancestor revision of the tree To specify an older \
1441
++revision, use a number, e.g. "tanc:4".""",
1442
++"tdate" , 
1443
++"""(tree date) The latest revision from a given date, e.g. "tdate:July 6".""",
1444
++"tmod" , 
1445
++""" (tree modified) The latest revision to modify a given file, e.g. \
1446
++"tmod:engine.cpp" or "tmod:engine.cpp:16".""",
1447
++"ttag" , 
1448
++"""(tree tag) The revision that was tagged into the current tree revision, \
1449
++according to the tree""",
1450
++"tagcur", 
1451
++"""(tag current) The latest revision of the version that the current tree \
1452
++was tagged from.""",
1453
++"mergeanc" , 
1454
++"""The common ancestor of the current tree and the specified revision. \
1455
++Defaults to the first partner-version's latest revision or to tagcur.""",
1456
++]
1457
++
1458
++
1459
++def is_auto_alias(name):
1460
++    """Determine whether a name is an auto alias name
1461
++
1462
++    :param name: the name to check
1463
++    :type name: str
1464
++    :return: True if the name is an auto alias, false if not
1465
++    :rtype: bool
1466
++    """
1467
++    return name in [f for (f, v) in pylon.util.iter_pairs(auto_alias)]
1468
++
1469
++
1470
++def display_def(iter, wrap = 80):
1471
++    """Display a list of definitions
1472
++
1473
++    :param iter: iter of name, definition pairs
1474
++    :type iter: iter of (str, str)
1475
++    :param wrap: The width for text wrapping
1476
++    :type wrap: int
1477
++    """
1478
++    vals = list(iter)
1479
++    maxlen = 0
1480
++    for (key, value) in vals:
1481
++        if len(key) > maxlen:
1482
++            maxlen = len(key)
1483
++    for (key, value) in vals:
1484
++        tw=textwrap.TextWrapper(width=wrap, 
1485
++                                initial_indent=key.rjust(maxlen)+" : ",
1486
++                                subsequent_indent="".rjust(maxlen+3))
1487
++        print tw.fill(value)
1488
++
1489
++
1490
+ def help_aliases(tree):
1491
+-    print """Auto-generated aliases
1492
+- acur : The latest revision in the archive of the tree-version.  You can specfy
1493
+-        a different version like so: acur:foo--bar--0 (aliases can be used)
1494
+- tcur : (tree current) The latest revision in the tree of the tree-version.
1495
+-        You can specify a different version like so: tcur:foo--bar--0 (aliases
1496
+-        can be used).
1497
+-tprev : (tree previous) The previous revision in the tree of the tree-version.
1498
+-        To specify an older revision, use a number, e.g. "tprev:4"
1499
+- tanc : (tree ancestor) The ancestor revision of the tree
1500
+-        To specify an older revision, use a number, e.g. "tanc:4"
1501
+-tdate : (tree date) The latest revision from a given date (e.g. "tdate:July 6")
1502
+- tmod : (tree modified) The latest revision to modify a given file 
1503
+-        (e.g. "tmod:engine.cpp" or "tmod:engine.cpp:16")
1504
+- ttag : (tree tag) The revision that was tagged into the current tree revision,
1505
+-        according to the tree.
1506
+-tagcur: (tag current) The latest revision of the version that the current tree
1507
+-        was tagged from.
1508
+-mergeanc : The common ancestor of the current tree and the specified revision.
1509
+-        Defaults to the first partner-version's latest revision or to tagcur.
1510
+-   """
1511
++    print """Auto-generated aliases"""
1512
++    display_def(pylon.util.iter_pairs(auto_alias))
1513
+     print "User aliases"
1514
+-    for parts in ancillary.iter_all_alias(tree):
1515
+-        print parts[0].rjust(10)+" : "+parts[1]
1516
+-
1517
++    display_def(ancillary.iter_all_alias(tree))
1518
+ 
1519
+ class Inventory(BaseCommand):
1520
+     """List the status of files in the tree"""
1521
+@@ -2428,6 +2285,11 @@
1522
+         except cmdutil.ForbiddenAliasSyntax, e:
1523
+             raise CommandFailedWrapper(e)
1524
+ 
1525
++    def no_prefix(self, alias):
1526
++        if alias.startswith("^"):
1527
++            alias = alias[1:]
1528
++        return alias
1529
++        
1530
+     def arg_dispatch(self, args, options):
1531
+         """Add, modify, or list aliases, depending on number of arguments
1532
+ 
1533
+@@ -2438,15 +2300,20 @@
1534
+         if len(args) == 0:
1535
+             help_aliases(self.tree)
1536
+             return
1537
+-        elif len(args) == 1:
1538
+-            self.print_alias(args[0])
1539
+-        elif (len(args)) == 2:
1540
+-            self.add(args[0], args[1], options)
1541
+         else:
1542
+-            raise cmdutil.GetHelp
1543
++            alias = self.no_prefix(args[0])
1544
++            if len(args) == 1:
1545
++                self.print_alias(alias)
1546
++            elif (len(args)) == 2:
1547
++                self.add(alias, args[1], options)
1548
++            else:
1549
++                raise cmdutil.GetHelp
1550
+ 
1551
+     def print_alias(self, alias):
1552
+         answer = None
1553
++        if is_auto_alias(alias):
1554
++            raise pylon.errors.IsAutoAlias(alias, "\"%s\" is an auto alias."
1555
++                "  Use \"revision\" to expand auto aliases." % alias)
1556
+         for pair in ancillary.iter_all_alias(self.tree):
1557
+             if pair[0] == alias:
1558
+                 answer = pair[1]
1559
+@@ -2464,6 +2331,8 @@
1560
+         :type expansion: str
1561
+         :param options: The commandline options
1562
+         """
1563
++        if is_auto_alias(alias):
1564
++            raise IsAutoAlias(alias)
1565
+         newlist = ""
1566
+         written = False
1567
+         new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, 
1568
+@@ -2490,14 +2359,17 @@
1569
+         deleted = False
1570
+         if len(args) != 1:
1571
+             raise cmdutil.GetHelp
1572
++        alias = self.no_prefix(args[0])
1573
++        if is_auto_alias(alias):
1574
++            raise IsAutoAlias(alias)
1575
+         newlist = ""
1576
+         for pair in self.get_iterator(options):
1577
+-            if pair[0] != args[0]:
1578
++            if pair[0] != alias:
1579
+                 newlist+="%s=%s\n" % (pair[0], pair[1])
1580
+             else:
1581
+                 deleted = True
1582
+         if not deleted:
1583
+-            raise errors.NoSuchAlias(args[0])
1584
++            raise errors.NoSuchAlias(alias)
1585
+         self.write_aliases(newlist, options)
1586
+ 
1587
+     def get_alias_file(self, options):
1588
+@@ -2526,7 +2398,7 @@
1589
+         :param options: The commandline options
1590
+         """
1591
+         filename = os.path.expanduser(self.get_alias_file(options))
1592
+-        file = cmdutil.NewFileVersion(filename)
1593
++        file = util.NewFileVersion(filename)
1594
+         file.write(newlist)
1595
+         file.commit()
1596
+ 
1597
+@@ -2588,10 +2460,13 @@
1598
+         :param cmdargs: The commandline arguments
1599
+         :type cmdargs: list of str
1600
+         """
1601
+-        cmdutil.find_editor()
1602
+         parser = self.get_parser()
1603
+         (options, args) = parser.parse_args(cmdargs)
1604
+         try:
1605
++            cmdutil.find_editor()
1606
++        except pylon.errors.NoEditorSpecified, e:
1607
++            raise pylon.errors.CommandFailedWrapper(e)
1608
++        try:
1609
+             self.tree=arch.tree_root()
1610
+         except:
1611
+             self.tree=None
1612
+@@ -2655,7 +2530,7 @@
1613
+             target_revision = cmdutil.determine_revision_arch(self.tree, 
1614
+                                                               args[0])
1615
+         else:
1616
+-            target_revision = cmdutil.tree_latest(self.tree)
1617
++            target_revision = arch_compound.tree_latest(self.tree)
1618
+         if len(args) > 1:
1619
+             merges = [ arch.Patchlog(cmdutil.determine_revision_arch(
1620
+                        self.tree, f)) for f in args[1:] ]
1621
+@@ -2711,7 +2586,7 @@
1622
+ 
1623
+         :param message: The message to send
1624
+         :type message: `email.Message`"""
1625
+-        server = smtplib.SMTP()
1626
++        server = smtplib.SMTP("localhost")
1627
+         server.sendmail(message['From'], message['To'], message.as_string())
1628
+         server.quit()
1629
+ 
1630
+@@ -2763,6 +2638,22 @@
1631
+ 'alias' : Alias,
1632
+ 'request-merge': RequestMerge,
1633
+ }
1634
++
1635
++def my_import(mod_name):
1636
++    module = __import__(mod_name)
1637
++    components = mod_name.split('.')
1638
++    for comp in components[1:]:
1639
++        module = getattr(module, comp)
1640
++    return module
1641
++
1642
++def plugin(mod_name):
1643
++    module = my_import(mod_name)
1644
++    module.add_command(commands)
1645
++
1646
++for file in os.listdir(sys.path[0]+"/command"):
1647
++    if len(file) > 3 and file[-3:] == ".py" and file != "__init__.py":
1648
++        plugin("command."+file[:-3])
1649
++
1650
+ suggestions = {
1651
+ 'apply-delta' : "Try \"apply-changes\".",
1652
+ 'delta' : "To compare two revisions, use \"changes\".",
1653
+@@ -2784,6 +2675,7 @@
1654
+ 'tagline' : "Use add-id.  It uses taglines in tagline trees",
1655
+ 'emlog' : "Use elog.  It automatically adds log-for-merge text, if any",
1656
+ 'library-revisions' : "Use revisions --library",
1657
+-'file-revert' : "Use revert FILE"
1658
++'file-revert' : "Use revert FILE",
1659
++'join-branch' : "Use replay --logs-only"
1660
+ }
1661
+ # arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7
1662
1663
*** added file 'testdata/insert_top.patch'
1664
--- /dev/null 
1665
+++ testdata/insert_top.patch 
1666
@@ -0,0 +1,7 @@
1667
+--- orig/pylon/patches.py
1668
++++ mod/pylon/patches.py
1669
+@@ -1,3 +1,4 @@
1670
++#test
1671
+ import util
1672
+ import sys
1673
+ class PatchSyntax(Exception):
1674
1675
*** added file 'testdata/mod'
1676
--- /dev/null 
1677
+++ testdata/mod 
1678
@@ -0,0 +1,2681 @@
1679
+# Copyright (C) 2004 Aaron Bentley
1680
+# <aaron.bentley@utoronto.ca>
1681
+#
1682
+#    This program is free software; you can redistribute it and/or modify
1683
+#    it under the terms of the GNU General Public License as published by
1684
+#    the Free Software Foundation; either version 2 of the License, or
1685
+#    (at your option) any later version.
1686
+#
1687
+#    This program is distributed in the hope that it will be useful,
1688
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
1689
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
1690
+#    GNU General Public License for more details.
1691
+#
1692
+#    You should have received a copy of the GNU General Public License
1693
+#    along with this program; if not, write to the Free Software
1694
+#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
1695
+
1696
+import sys
1697
+import arch
1698
+import arch.util
1699
+import arch.arch
1700
+
1701
+import pylon.errors
1702
+from pylon.errors import *
1703
+from pylon import errors
1704
+from pylon import util
1705
+from pylon import arch_core
1706
+from pylon import arch_compound
1707
+from pylon import ancillary
1708
+from pylon import misc
1709
+from pylon import paths 
1710
+
1711
+import abacmds
1712
+import cmdutil
1713
+import shutil
1714
+import os
1715
+import options
1716
+import time
1717
+import cmd
1718
+import readline
1719
+import re
1720
+import string
1721
+import terminal
1722
+import email
1723
+import smtplib
1724
+import textwrap
1725
+
1726
+__docformat__ = "restructuredtext"
1727
+__doc__ = "Implementation of user (sub) commands"
1728
+commands = {}
1729
+
1730
+def find_command(cmd):
1731
+    """
1732
+    Return an instance of a command type.  Return None if the type isn't
1733
+    registered.
1734
+
1735
+    :param cmd: the name of the command to look for
1736
+    :type cmd: the type of the command
1737
+    """
1738
+    if commands.has_key(cmd):
1739
+        return commands[cmd]()
1740
+    else:
1741
+        return None
1742
+
1743
+class BaseCommand:
1744
+    def __call__(self, cmdline):
1745
+        try:
1746
+            self.do_command(cmdline.split())
1747
+        except cmdutil.GetHelp, e:
1748
+            self.help()
1749
+        except Exception, e:
1750
+            print e
1751
+
1752
+    def get_completer(index):
1753
+        return None
1754
+
1755
+    def complete(self, args, text):
1756
+        """
1757
+        Returns a list of possible completions for the given text.
1758
+
1759
+        :param args: The complete list of arguments
1760
+        :type args: List of str
1761
+        :param text: text to complete (may be shorter than args[-1])
1762
+        :type text: str
1763
+        :rtype: list of str
1764
+        """
1765
+        matches = []
1766
+        candidates = None
1767
+
1768
+        if len(args) > 0: 
1769
+            realtext = args[-1]
1770
+        else:
1771
+            realtext = ""
1772
+
1773
+        try:
1774
+            parser=self.get_parser()
1775
+            if realtext.startswith('-'):
1776
+                candidates = parser.iter_options()
1777
+            else:
1778
+                (options, parsed_args) = parser.parse_args(args)
1779
+
1780
+                if len (parsed_args) > 0:
1781
+                    candidates = self.get_completer(parsed_args[-1], len(parsed_args) -1)
1782
+                else:
1783
+                    candidates = self.get_completer("", 0)
1784
+        except:
1785
+            pass
1786
+        if candidates is None:
1787
+            return
1788
+        for candidate in candidates:
1789
+            candidate = str(candidate)
1790
+            if candidate.startswith(realtext):
1791
+                matches.append(candidate[len(realtext)- len(text):])
1792
+        return matches
1793
+
1794
+
1795
+class Help(BaseCommand):
1796
+    """
1797
+    Lists commands, prints help messages.
1798
+    """
1799
+    def __init__(self):
1800
+        self.description="Prints help mesages"
1801
+        self.parser = None
1802
+
1803
+    def do_command(self, cmdargs):
1804
+        """
1805
+        Prints a help message.
1806
+        """
1807
+        options, args = self.get_parser().parse_args(cmdargs)
1808
+        if len(args) > 1:
1809
+            raise cmdutil.GetHelp
1810
+
1811
+        if options.native or options.suggestions or options.external:
1812
+            native = options.native
1813
+            suggestions = options.suggestions
1814
+            external = options.external
1815
+        else:
1816
+            native = True
1817
+            suggestions = False
1818
+            external = True
1819
+        
1820
+        if len(args) == 0:
1821
+            self.list_commands(native, suggestions, external)
1822
+            return
1823
+        elif len(args) == 1:
1824
+            command_help(args[0])
1825
+            return
1826
+
1827
+    def help(self):
1828
+        self.get_parser().print_help()
1829
+        print """
1830
+If no command is specified, commands are listed.  If a command is
1831
+specified, help for that command is listed.
1832
+        """
1833
+
1834
+    def get_parser(self):
1835
+        """
1836
+        Returns the options parser to use for the "revision" command.
1837
+
1838
+        :rtype: cmdutil.CmdOptionParser
1839
+        """
1840
+        if self.parser is not None:
1841
+            return self.parser
1842
+        parser=cmdutil.CmdOptionParser("fai help [command]")
1843
+        parser.add_option("-n", "--native", action="store_true", 
1844
+                         dest="native", help="Show native commands")
1845
+        parser.add_option("-e", "--external", action="store_true", 
1846
+                         dest="external", help="Show external commands")
1847
+        parser.add_option("-s", "--suggest", action="store_true", 
1848
+                         dest="suggestions", help="Show suggestions")
1849
+        self.parser = parser
1850
+        return parser 
1851
+      
1852
+    def list_commands(self, native=True, suggest=False, external=True):
1853
+        """
1854
+        Lists supported commands.
1855
+
1856
+        :param native: list native, python-based commands
1857
+        :type native: bool
1858
+        :param external: list external aba-style commands
1859
+        :type external: bool
1860
+        """
1861
+        if native:
1862
+            print "Native Fai commands"
1863
+            keys=commands.keys()
1864
+            keys.sort()
1865
+            for k in keys:
1866
+                space=""
1867
+                for i in range(28-len(k)):
1868
+                    space+=" "
1869
+                print space+k+" : "+commands[k]().description
1870
+            print
1871
+        if suggest:
1872
+            print "Unavailable commands and suggested alternatives"
1873
+            key_list = suggestions.keys()
1874
+            key_list.sort()
1875
+            for key in key_list:
1876
+                print "%28s : %s" % (key, suggestions[key])
1877
+            print
1878
+        if external:
1879
+            fake_aba = abacmds.AbaCmds()
1880
+            if (fake_aba.abadir == ""):
1881
+                return
1882
+            print "External commands"
1883
+            fake_aba.list_commands()
1884
+            print
1885
+        if not suggest:
1886
+            print "Use help --suggest to list alternatives to tla and aba"\
1887
+                " commands."
1888
+        if options.tla_fallthrough and (native or external):
1889
+            print "Fai also supports tla commands."
1890
+
1891
+def command_help(cmd):
1892
+    """
1893
+    Prints help for a command.
1894
+
1895
+    :param cmd: The name of the command to print help for
1896
+    :type cmd: str
1897
+    """
1898
+    fake_aba = abacmds.AbaCmds()
1899
+    cmdobj = find_command(cmd)
1900
+    if cmdobj != None:
1901
+        cmdobj.help()
1902
+    elif suggestions.has_key(cmd):
1903
+        print "Not available\n" + suggestions[cmd]
1904
+    else:
1905
+        abacmd = fake_aba.is_command(cmd)
1906
+        if abacmd:
1907
+            abacmd.help()
1908
+        else:
1909
+            print "No help is available for \""+cmd+"\". Maybe try \"tla "+cmd+" -H\"?"
1910
+
1911
+
1912
+
1913
+class Changes(BaseCommand):
1914
+    """
1915
+    the "changes" command: lists differences between trees/revisions:
1916
+    """
1917
+    
1918
+    def __init__(self):
1919
+        self.description="Lists what files have changed in the project tree"
1920
+
1921
+    def get_completer(self, arg, index):
1922
+        if index > 1:
1923
+            return None
1924
+        try:
1925
+            tree = arch.tree_root()
1926
+        except:
1927
+            tree = None
1928
+        return cmdutil.iter_revision_completions(arg, tree)
1929
+    
1930
+    def parse_commandline(self, cmdline):
1931
+        """
1932
+        Parse commandline arguments.  Raises cmdutil.GetHelp if help is needed.
1933
+        
1934
+        :param cmdline: A list of arguments to parse
1935
+        :rtype: (options, Revision, Revision/WorkingTree)
1936
+        """
1937
+        parser=self.get_parser()
1938
+        (options, args) = parser.parse_args(cmdline)
1939
+        if len(args) > 2:
1940
+            raise cmdutil.GetHelp
1941
+
1942
+        tree=arch.tree_root()
1943
+        if len(args) == 0:
1944
+            a_spec = ancillary.comp_revision(tree)
1945
+        else:
1946
+            a_spec = cmdutil.determine_revision_tree(tree, args[0])
1947
+        cmdutil.ensure_archive_registered(a_spec.archive)
1948
+        if len(args) == 2:
1949
+            b_spec = cmdutil.determine_revision_tree(tree, args[1])
1950
+            cmdutil.ensure_archive_registered(b_spec.archive)
1951
+        else:
1952
+            b_spec=tree
1953
+        return options, a_spec, b_spec
1954
+
1955
+    def do_command(self, cmdargs):
1956
+        """
1957
+        Master function that perfoms the "changes" command.
1958
+        """
1959
+        try:
1960
+            options, a_spec, b_spec = self.parse_commandline(cmdargs);
1961
+        except cmdutil.CantDetermineRevision, e:
1962
+            print e
1963
+            return
1964
+        except arch.errors.TreeRootError, e:
1965
+            print e
1966
+            return
1967
+        if options.changeset:
1968
+            changeset=options.changeset
1969
+            tmpdir = None
1970
+        else:
1971
+            tmpdir=util.tmpdir()
1972
+            changeset=tmpdir+"/changeset"
1973
+        try:
1974
+            delta=arch.iter_delta(a_spec, b_spec, changeset)
1975
+            try:
1976
+                for line in delta:
1977
+                    if cmdutil.chattermatch(line, "changeset:"):
1978
+                        pass
1979
+                    else:
1980
+                        cmdutil.colorize(line, options.suppress_chatter)
1981
+            except arch.util.ExecProblem, e:
1982
+                if e.proc.error and e.proc.error.startswith(
1983
+                    "missing explicit id for file"):
1984
+                    raise MissingID(e)
1985
+                else:
1986
+                    raise
1987
+            status=delta.status
1988
+            if status > 1:
1989
+                return
1990
+            if (options.perform_diff):
1991
+                chan = arch_compound.ChangesetMunger(changeset)
1992
+                chan.read_indices()
1993
+                if options.diffopts is not None:
1994
+                    if isinstance(b_spec, arch.Revision):
1995
+                        b_dir = b_spec.library_find()
1996
+                    else:
1997
+                        b_dir = b_spec
1998
+                    a_dir = a_spec.library_find()
1999
+                    diffopts = options.diffopts.split()
2000
+                    cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir)
2001
+                else:
2002
+                    cmdutil.show_diffs(delta.changeset)
2003
+        finally:
2004
+            if tmpdir and (os.access(tmpdir, os.X_OK)):
2005
+                shutil.rmtree(tmpdir)
2006
+
2007
+    def get_parser(self):
2008
+        """
2009
+        Returns the options parser to use for the "changes" command.
2010
+
2011
+        :rtype: cmdutil.CmdOptionParser
2012
+        """
2013
+        parser=cmdutil.CmdOptionParser("fai changes [options] [revision]"
2014
+                                       " [revision]")
2015
+        parser.add_option("-d", "--diff", action="store_true", 
2016
+                          dest="perform_diff", default=False, 
2017
+                          help="Show diffs in summary")
2018
+        parser.add_option("-c", "--changeset", dest="changeset", 
2019
+                          help="Store a changeset in the given directory", 
2020
+                          metavar="DIRECTORY")
2021
+        parser.add_option("-s", "--silent", action="store_true", 
2022
+                          dest="suppress_chatter", default=False, 
2023
+                          help="Suppress chatter messages")
2024
+        parser.add_option("--diffopts", dest="diffopts", 
2025
+                          help="Use the specified diff options", 
2026
+                          metavar="OPTIONS")
2027
+
2028
+        return parser
2029
+
2030
+    def help(self, parser=None):
2031
+        """
2032
+        Prints a help message.
2033
+
2034
+        :param parser: If supplied, the parser to use for generating help.  If \
2035
+        not supplied, it is retrieved.
2036
+        :type parser: cmdutil.CmdOptionParser
2037
+        """
2038
+        if parser is None:
2039
+            parser=self.get_parser()
2040
+        parser.print_help()
2041
+        print """
2042
+Performs source-tree comparisons
2043
+
2044
+If no revision is specified, the current project tree is compared to the
2045
+last-committed revision.  If one revision is specified, the current project
2046
+tree is compared to that revision.  If two revisions are specified, they are
2047
+compared to each other.
2048
+        """
2049
+        help_tree_spec() 
2050
+        return
2051
+
2052
+
2053
+class ApplyChanges(BaseCommand):
2054
+    """
2055
+    Apply differences between two revisions to a tree
2056
+    """
2057
+    
2058
+    def __init__(self):
2059
+        self.description="Applies changes to a project tree"
2060
+    
2061
+    def get_completer(self, arg, index):
2062
+        if index > 1:
2063
+            return None
2064
+        try:
2065
+            tree = arch.tree_root()
2066
+        except:
2067
+            tree = None
2068
+        return cmdutil.iter_revision_completions(arg, tree)
2069
+
2070
+    def parse_commandline(self, cmdline, tree):
2071
+        """
2072
+        Parse commandline arguments.  Raises cmdutil.GetHelp if help is needed.
2073
+        
2074
+        :param cmdline: A list of arguments to parse
2075
+        :rtype: (options, Revision, Revision/WorkingTree)
2076
+        """
2077
+        parser=self.get_parser()
2078
+        (options, args) = parser.parse_args(cmdline)
2079
+        if len(args) != 2:
2080
+            raise cmdutil.GetHelp
2081
+
2082
+        a_spec = cmdutil.determine_revision_tree(tree, args[0])
2083
+        cmdutil.ensure_archive_registered(a_spec.archive)
2084
+        b_spec = cmdutil.determine_revision_tree(tree, args[1])
2085
+        cmdutil.ensure_archive_registered(b_spec.archive)
2086
+        return options, a_spec, b_spec
2087
+
2088
+    def do_command(self, cmdargs):
2089
+        """
2090
+        Master function that performs "apply-changes".
2091
+        """
2092
+        try:
2093
+            tree = arch.tree_root()
2094
+            options, a_spec, b_spec = self.parse_commandline(cmdargs, tree);
2095
+        except cmdutil.CantDetermineRevision, e:
2096
+            print e
2097
+            return
2098
+        except arch.errors.TreeRootError, e:
2099
+            print e
2100
+            return
2101
+        delta=cmdutil.apply_delta(a_spec, b_spec, tree)
2102
+        for line in cmdutil.iter_apply_delta_filter(delta):
2103
+            cmdutil.colorize(line, options.suppress_chatter)
2104
+
2105
+    def get_parser(self):
2106
+        """
2107
+        Returns the options parser to use for the "apply-changes" command.
2108
+
2109
+        :rtype: cmdutil.CmdOptionParser
2110
+        """
2111
+        parser=cmdutil.CmdOptionParser("fai apply-changes [options] revision"
2112
+                                       " revision")
2113
+        parser.add_option("-d", "--diff", action="store_true", 
2114
+                          dest="perform_diff", default=False, 
2115
+                          help="Show diffs in summary")
2116
+        parser.add_option("-c", "--changeset", dest="changeset", 
2117
+                          help="Store a changeset in the given directory", 
2118
+                          metavar="DIRECTORY")
2119
+        parser.add_option("-s", "--silent", action="store_true", 
2120
+                          dest="suppress_chatter", default=False, 
2121
+                          help="Suppress chatter messages")
2122
+        return parser
2123
+
2124
+    def help(self, parser=None):
2125
+        """
2126
+        Prints a help message.
2127
+
2128
+        :param parser: If supplied, the parser to use for generating help.  If \
2129
+        not supplied, it is retrieved.
2130
+        :type parser: cmdutil.CmdOptionParser
2131
+        """
2132
+        if parser is None:
2133
+            parser=self.get_parser()
2134
+        parser.print_help()
2135
+        print """
2136
+Applies changes to a project tree
2137
+
2138
+Compares two revisions and applies the difference between them to the current
2139
+tree.
2140
+        """
2141
+        help_tree_spec() 
2142
+        return
2143
+
2144
+class Update(BaseCommand):
2145
+    """
2146
+    Updates a project tree to a given revision, preserving un-committed hanges. 
2147
+    """
2148
+    
2149
+    def __init__(self):
2150
+        self.description="Apply the latest changes to the current directory"
2151
+
2152
+    def get_completer(self, arg, index):
2153
+        if index > 0:
2154
+            return None
2155
+        try:
2156
+            tree = arch.tree_root()
2157
+        except:
2158
+            tree = None
2159
+        return cmdutil.iter_revision_completions(arg, tree)
2160
+    
2161
+    def parse_commandline(self, cmdline, tree):
2162
+        """
2163
+        Parse commandline arguments.  Raises cmdutil.GetHelp if help is needed.
2164
+        
2165
+        :param cmdline: A list of arguments to parse
2166
+        :rtype: (options, Revision, Revision/WorkingTree)
2167
+        """
2168
+        parser=self.get_parser()
2169
+        (options, args) = parser.parse_args(cmdline)
2170
+        if len(args) > 2:
2171
+            raise cmdutil.GetHelp
2172
+
2173
+        spec=None
2174
+        if len(args)>0:
2175
+            spec=args[0]
2176
+        revision=cmdutil.determine_revision_arch(tree, spec)
2177
+        cmdutil.ensure_archive_registered(revision.archive)
2178
+
2179
+        mirror_source = cmdutil.get_mirror_source(revision.archive)
2180
+        if mirror_source != None:
2181
+            if cmdutil.prompt("Mirror update"):
2182
+                cmd=cmdutil.mirror_archive(mirror_source, 
2183
+                    revision.archive, arch.NameParser(revision).get_package_version())
2184
+                for line in arch.chatter_classifier(cmd):
2185
+                    cmdutil.colorize(line, options.suppress_chatter)
2186
+
2187
+                revision=cmdutil.determine_revision_arch(tree, spec)
2188
+
2189
+        return options, revision 
2190
+
2191
+    def do_command(self, cmdargs):
2192
+        """
2193
+        Master function that perfoms the "update" command.
2194
+        """
2195
+        tree=arch.tree_root()
2196
+        try:
2197
+            options, to_revision = self.parse_commandline(cmdargs, tree);
2198
+        except cmdutil.CantDetermineRevision, e:
2199
+            print e
2200
+            return
2201
+        except arch.errors.TreeRootError, e:
2202
+            print e
2203
+            return
2204
+        from_revision = arch_compound.tree_latest(tree)
2205
+        if from_revision==to_revision:
2206
+            print "Tree is already up to date with:\n"+str(to_revision)+"."
2207
+            return
2208
+        cmdutil.ensure_archive_registered(from_revision.archive)
2209
+        cmd=cmdutil.apply_delta(from_revision, to_revision, tree,
2210
+            options.patch_forward)
2211
+        for line in cmdutil.iter_apply_delta_filter(cmd):
2212
+            cmdutil.colorize(line)
2213
+        if to_revision.version != tree.tree_version:
2214
+            if cmdutil.prompt("Update version"):
2215
+                tree.tree_version = to_revision.version
2216
+
2217
+    def get_parser(self):
2218
+        """
2219
+        Returns the options parser to use for the "update" command.
2220
+
2221
+        :rtype: cmdutil.CmdOptionParser
2222
+        """
2223
+        parser=cmdutil.CmdOptionParser("fai update [options]"
2224
+                                       " [revision/version]")
2225
+        parser.add_option("-f", "--forward", action="store_true", 
2226
+                          dest="patch_forward", default=False, 
2227
+                          help="pass the --forward option to 'patch'")
2228
+        parser.add_option("-s", "--silent", action="store_true", 
2229
+                          dest="suppress_chatter", default=False, 
2230
+                          help="Suppress chatter messages")
2231
+        return parser
2232
+
2233
+    def help(self, parser=None):
2234
+        """
2235
+        Prints a help message.
2236
+
2237
+        :param parser: If supplied, the parser to use for generating help.  If \
2238
+        not supplied, it is retrieved.
2239
+        :type parser: cmdutil.CmdOptionParser
2240
+        """
2241
+        if parser is None:
2242
+            parser=self.get_parser()
2243
+        parser.print_help()
2244
+        print """
2245
+Updates a working tree to the current archive revision
2246
+
2247
+If a revision or version is specified, that is used instead 
2248
+        """
2249
+        help_tree_spec() 
2250
+        return
2251
+
2252
+
2253
+class Commit(BaseCommand):
2254
+    """
2255
+    Create a revision based on the changes in the current tree.
2256
+    """
2257
+    
2258
+    def __init__(self):
2259
+        self.description="Write local changes to the archive"
2260
+
2261
+    def get_completer(self, arg, index):
2262
+        if arg is None:
2263
+            arg = ""
2264
+        return iter_modified_file_completions(arch.tree_root(), arg)
2265
+#        return iter_source_file_completions(arch.tree_root(), arg)
2266
+    
2267
+    def parse_commandline(self, cmdline, tree):
2268
+        """
2269
+        Parse commandline arguments.  Raise cmtutil.GetHelp if help is needed.
2270
+        
2271
+        :param cmdline: A list of arguments to parse
2272
+        :rtype: (options, Revision, Revision/WorkingTree)
2273
+        """
2274
+        parser=self.get_parser()
2275
+        (options, args) = parser.parse_args(cmdline)
2276
+
2277
+        if len(args) == 0:
2278
+            args = None
2279
+        if options.version is None:
2280
+            return options, tree.tree_version, args
2281
+
2282
+        revision=cmdutil.determine_revision_arch(tree, options.version)
2283
+        return options, revision.get_version(), args
2284
+
2285
+    def do_command(self, cmdargs):
2286
+        """
2287
+        Master function that perfoms the "commit" command.
2288
+        """
2289
+        tree=arch.tree_root()
2290
+        options, version, files = self.parse_commandline(cmdargs, tree)
2291
+        ancestor = None
2292
+        if options.__dict__.has_key("base") and options.base:
2293
+            base = cmdutil.determine_revision_tree(tree, options.base)
2294
+            ancestor = base
2295
+        else:
2296
+            base = ancillary.submit_revision(tree)
2297
+            ancestor = base
2298
+        if ancestor is None:
2299
+            ancestor = arch_compound.tree_latest(tree, version)
2300
+
2301
+        writeversion=version
2302
+        archive=version.archive
2303
+        source=cmdutil.get_mirror_source(archive)
2304
+        allow_old=False
2305
+        writethrough="implicit"
2306
+
2307
+        if source!=None:
2308
+            if writethrough=="explicit" and \
2309
+                cmdutil.prompt("Writethrough"):
2310
+                writeversion=arch.Version(str(source)+"/"+str(version.get_nonarch()))
2311
+            elif writethrough=="none":
2312
+                raise CommitToMirror(archive)
2313
+
2314
+        elif archive.is_mirror:
2315
+            raise CommitToMirror(archive)
2316
+
2317
+        try:
2318
+            last_revision=tree.iter_logs(version, True).next().revision
2319
+        except StopIteration, e:
2320
+            last_revision = None
2321
+            if ancestor is None:
2322
+                if cmdutil.prompt("Import from commit"):
2323
+                    return do_import(version)
2324
+                else:
2325
+                    raise NoVersionLogs(version)
2326
+        try:
2327
+            arch_last_revision = version.iter_revisions(True).next()
2328
+        except StopIteration, e:
2329
+            arch_last_revision = None
2330
+ 
2331
+        if last_revision != arch_last_revision:
2332
+            print "Tree is not up to date with %s" % str(version)
2333
+            if not cmdutil.prompt("Out of date"):
2334
+                raise OutOfDate
2335
+            else:
2336
+                allow_old=True
2337
+
2338
+        try:
2339
+            if not cmdutil.has_changed(ancestor):
2340
+                if not cmdutil.prompt("Empty commit"):
2341
+                    raise EmptyCommit
2342
+        except arch.util.ExecProblem, e:
2343
+            if e.proc.error and e.proc.error.startswith(
2344
+                "missing explicit id for file"):
2345
+                raise MissingID(e)
2346
+            else:
2347
+                raise
2348
+        log = tree.log_message(create=False, version=version)
2349
+        if log is None:
2350
+            try:
2351
+                if cmdutil.prompt("Create log"):
2352
+                    edit_log(tree, version)
2353
+
2354
+            except cmdutil.NoEditorSpecified, e:
2355
+                raise CommandFailed(e)
2356
+            log = tree.log_message(create=False, version=version)
2357
+        if log is None: 
2358
+            raise NoLogMessage
2359
+        if log["Summary"] is None or len(log["Summary"].strip()) == 0:
2360
+            if not cmdutil.prompt("Omit log summary"):
2361
+                raise errors.NoLogSummary
2362
+        try:
2363
+            for line in tree.iter_commit(version, seal=options.seal_version,
2364
+                base=base, out_of_date_ok=allow_old, file_list=files):
2365
+                cmdutil.colorize(line, options.suppress_chatter)
2366
+
2367
+        except arch.util.ExecProblem, e:
2368
+            if e.proc.error and e.proc.error.startswith(
2369
+                "These files violate naming conventions:"):
2370
+                raise LintFailure(e.proc.error)
2371
+            else:
2372
+                raise
2373
+
2374
+    def get_parser(self):
2375
+        """
2376
+        Returns the options parser to use for the "commit" command.
2377
+
2378
+        :rtype: cmdutil.CmdOptionParser
2379
+        """
2380
+
2381
+        parser=cmdutil.CmdOptionParser("fai commit [options] [file1]"
2382
+                                       " [file2...]")
2383
+        parser.add_option("--seal", action="store_true", 
2384
+                          dest="seal_version", default=False, 
2385
+                          help="seal this version")
2386
+        parser.add_option("-v", "--version", dest="version", 
2387
+                          help="Use the specified version", 
2388
+                          metavar="VERSION")
2389
+        parser.add_option("-s", "--silent", action="store_true", 
2390
+                          dest="suppress_chatter", default=False, 
2391
+                          help="Suppress chatter messages")
2392
+        if cmdutil.supports_switch("commit", "--base"):
2393
+            parser.add_option("--base", dest="base", help="", 
2394
+                              metavar="REVISION")
2395
+        return parser
2396
+
2397
+    def help(self, parser=None):
2398
+        """
2399
+        Prints a help message.
2400
+
2401
+        :param parser: If supplied, the parser to use for generating help.  If \
2402
+        not supplied, it is retrieved.
2403
+        :type parser: cmdutil.CmdOptionParser
2404
+        """
2405
+        if parser is None:
2406
+            parser=self.get_parser()
2407
+        parser.print_help()
2408
+        print """
2409
+Updates a working tree to the current archive revision
2410
+
2411
+If a version is specified, that is used instead 
2412
+        """
2413
+#        help_tree_spec() 
2414
+        return
2415
+
2416
+
2417
+
2418
+class CatLog(BaseCommand):
2419
+    """
2420
+    Print the log of a given file (from current tree)
2421
+    """
2422
+    def __init__(self):
2423
+        self.description="Prints the patch log for a revision"
2424
+
2425
+    def get_completer(self, arg, index):
2426
+        if index > 0:
2427
+            return None
2428
+        try:
2429
+            tree = arch.tree_root()
2430
+        except:
2431
+            tree = None
2432
+        return cmdutil.iter_revision_completions(arg, tree)
2433
+
2434
+    def do_command(self, cmdargs):
2435
+        """
2436
+        Master function that perfoms the "cat-log" command.
2437
+        """
2438
+        parser=self.get_parser()
2439
+        (options, args) = parser.parse_args(cmdargs)
2440
+        try:
2441
+            tree = arch.tree_root()
2442
+        except arch.errors.TreeRootError, e:
2443
+            tree = None
2444
+        spec=None
2445
+        if len(args) > 0:
2446
+            spec=args[0]
2447
+        if len(args) > 1:
2448
+            raise cmdutil.GetHelp()
2449
+        try:
2450
+            if tree:
2451
+                revision = cmdutil.determine_revision_tree(tree, spec)
2452
+            else:
2453
+                revision = cmdutil.determine_revision_arch(tree, spec)
2454
+        except cmdutil.CantDetermineRevision, e:
2455
+            raise CommandFailedWrapper(e)
2456
+        log = None
2457
+        
2458
+        use_tree = (options.source == "tree" or \
2459
+            (options.source == "any" and tree))
2460
+        use_arch = (options.source == "archive" or options.source == "any")
2461
+        
2462
+        log = None
2463
+        if use_tree:
2464
+            for log in tree.iter_logs(revision.get_version()):
2465
+                if log.revision == revision:
2466
+                    break
2467
+                else:
2468
+                    log = None
2469
+        if log is None and use_arch:
2470
+            cmdutil.ensure_revision_exists(revision)
2471
+            log = arch.Patchlog(revision)
2472
+        if log is not None:
2473
+            for item in log.items():
2474
+                print "%s: %s" % item
2475
+            print log.description
2476
+
2477
+    def get_parser(self):
2478
+        """
2479
+        Returns the options parser to use for the "cat-log" command.
2480
+
2481
+        :rtype: cmdutil.CmdOptionParser
2482
+        """
2483
+        parser=cmdutil.CmdOptionParser("fai cat-log [revision]")
2484
+        parser.add_option("--archive", action="store_const", dest="source",
2485
+                          const="archive", default="any",
2486
+                          help="Always get the log from the archive")
2487
+        parser.add_option("--tree", action="store_const", dest="source",
2488
+                          const="tree", help="Always get the log from the tree")
2489
+        return parser 
2490
+
2491
+    def help(self, parser=None):
2492
+        """
2493
+        Prints a help message.
2494
+
2495
+        :param parser: If supplied, the parser to use for generating help.  If \
2496
+        not supplied, it is retrieved.
2497
+        :type parser: cmdutil.CmdOptionParser
2498
+        """
2499
+        if parser==None:
2500
+            parser=self.get_parser()
2501
+        parser.print_help()
2502
+        print """
2503
+Prints the log for the specified revision
2504
+        """
2505
+        help_tree_spec()
2506
+        return
2507
+
2508
+class Revert(BaseCommand):
2509
+    """ Reverts a tree (or aspects of it) to a revision
2510
+    """
2511
+    def __init__(self):
2512
+        self.description="Reverts a tree (or aspects of it) to a revision "
2513
+
2514
+    def get_completer(self, arg, index):
2515
+        if index > 0:
2516
+            return None
2517
+        try:
2518
+            tree = arch.tree_root()
2519
+        except:
2520
+            tree = None
2521
+        return iter_modified_file_completions(tree, arg)
2522
+
2523
+    def do_command(self, cmdargs):
2524
+        """
2525
+        Master function that perfoms the "revert" command.
2526
+        """
2527
+        parser=self.get_parser()
2528
+        (options, args) = parser.parse_args(cmdargs)
2529
+        try:
2530
+            tree = arch.tree_root()
2531
+        except arch.errors.TreeRootError, e:
2532
+            raise CommandFailed(e)
2533
+        spec=None
2534
+        if options.revision is not None:
2535
+            spec=options.revision
2536
+        try:
2537
+            if spec is not None:
2538
+                revision = cmdutil.determine_revision_tree(tree, spec)
2539
+            else:
2540
+                revision = ancillary.comp_revision(tree)
2541
+        except cmdutil.CantDetermineRevision, e:
2542
+            raise CommandFailedWrapper(e)
2543
+        munger = None
2544
+
2545
+        if options.file_contents or options.file_perms or options.deletions\
2546
+            or options.additions or options.renames or options.hunk_prompt:
2547
+            munger = arch_compound.MungeOpts()
2548
+            munger.set_hunk_prompt(cmdutil.colorize, cmdutil.user_hunk_confirm,
2549
+                                   options.hunk_prompt)
2550
+
2551
+        if len(args) > 0 or options.logs or options.pattern_files or \
2552
+            options.control:
2553
+            if munger is None:
2554
+                munger = cmdutil.arch_compound.MungeOpts(True)
2555
+                munger.all_types(True)
2556
+        if len(args) > 0:
2557
+            t_cwd = arch_compound.tree_cwd(tree)
2558
+            for name in args:
2559
+                if len(t_cwd) > 0:
2560
+                    t_cwd += "/"
2561
+                name = "./" + t_cwd + name
2562
+                munger.add_keep_file(name);
2563
+
2564
+        if options.file_perms:
2565
+            munger.file_perms = True
2566
+        if options.file_contents:
2567
+            munger.file_contents = True
2568
+        if options.deletions:
2569
+            munger.deletions = True
2570
+        if options.additions:
2571
+            munger.additions = True
2572
+        if options.renames:
2573
+            munger.renames = True
2574
+        if options.logs:
2575
+            munger.add_keep_pattern('^\./\{arch\}/[^=].*')
2576
+        if options.control:
2577
+            munger.add_keep_pattern("/\.arch-ids|^\./\{arch\}|"\
2578
+                                    "/\.arch-inventory$")
2579
+        if options.pattern_files:
2580
+            munger.add_keep_pattern(options.pattern_files)
2581
+                
2582
+        for line in arch_compound.revert(tree, revision, munger, 
2583
+                                   not options.no_output):
2584
+            cmdutil.colorize(line)
2585
+
2586
+
2587
+    def get_parser(self):
2588
+        """
2589
+        Returns the options parser to use for the "cat-log" command.
2590
+
2591
+        :rtype: cmdutil.CmdOptionParser
2592
+        """
2593
+        parser=cmdutil.CmdOptionParser("fai revert [options] [FILE...]")
2594
+        parser.add_option("", "--contents", action="store_true", 
2595
+                          dest="file_contents", 
2596
+                          help="Revert file content changes")
2597
+        parser.add_option("", "--permissions", action="store_true", 
2598
+                          dest="file_perms", 
2599
+                          help="Revert file permissions changes")
2600
+        parser.add_option("", "--deletions", action="store_true", 
2601
+                          dest="deletions", 
2602
+                          help="Restore deleted files")
2603
+        parser.add_option("", "--additions", action="store_true", 
2604
+                          dest="additions", 
2605
+                          help="Remove added files")
2606
+        parser.add_option("", "--renames", action="store_true", 
2607
+                          dest="renames", 
2608
+                          help="Revert file names")
2609
+        parser.add_option("--hunks", action="store_true", 
2610
+                          dest="hunk_prompt", default=False,
2611
+                          help="Prompt which hunks to revert")
2612
+        parser.add_option("--pattern-files", dest="pattern_files", 
2613
+                          help="Revert files that match this pattern", 
2614
+                          metavar="REGEX")
2615
+        parser.add_option("--logs", action="store_true", 
2616
+                          dest="logs", default=False,
2617
+                          help="Revert only logs")
2618
+        parser.add_option("--control-files", action="store_true", 
2619
+                          dest="control", default=False,
2620
+                          help="Revert logs and other control files")
2621
+        parser.add_option("-n", "--no-output", action="store_true", 
2622
+                          dest="no_output", 
2623
+                          help="Don't keep an undo changeset")
2624
+        parser.add_option("--revision", dest="revision", 
2625
+                          help="Revert to the specified revision", 
2626
+                          metavar="REVISION")
2627
+        return parser 
2628
+
2629
+    def help(self, parser=None):
2630
+        """
2631
+        Prints a help message.
2632
+
2633
+        :param parser: If supplied, the parser to use for generating help.  If \
2634
+        not supplied, it is retrieved.
2635
+        :type parser: cmdutil.CmdOptionParser
2636
+        """
2637
+        if parser==None:
2638
+            parser=self.get_parser()
2639
+        parser.print_help()
2640
+        print """
2641
+Reverts changes in the current working tree.  If no flags are specified, all
2642
+types of changes are reverted.  Otherwise, only selected types of changes are
2643
+reverted.  
2644
+
2645
+If a revision is specified on the commandline, differences between the current
2646
+tree and that revision are reverted.  If a version is specified, the current
2647
+tree is used to determine the revision.
2648
+
2649
+If files are specified, only those files listed will have any changes applied.
2650
+To specify a renamed file, you can use either the old or new name. (or both!)
2651
+
2652
+Unless "-n" is specified, reversions can be undone with "redo".
2653
+        """
2654
+        return
2655
+
2656
+class Revision(BaseCommand):
2657
+    """
2658
+    Print a revision name based on a revision specifier
2659
+    """
2660
+    def __init__(self):
2661
+        self.description="Prints the name of a revision"
2662
+
2663
+    def get_completer(self, arg, index):
2664
+        if index > 0:
2665
+            return None
2666
+        try:
2667
+            tree = arch.tree_root()
2668
+        except:
2669
+            tree = None
2670
+        return cmdutil.iter_revision_completions(arg, tree)
2671
+
2672
+    def do_command(self, cmdargs):
2673
+        """
2674
+        Master function that perfoms the "revision" command.
2675
+        """
2676
+        parser=self.get_parser()
2677
+        (options, args) = parser.parse_args(cmdargs)
2678
+
2679
+        try:
2680
+            tree = arch.tree_root()
2681
+        except arch.errors.TreeRootError:
2682
+            tree = None
2683
+
2684
+        spec=None
2685
+        if len(args) > 0:
2686
+            spec=args[0]
2687
+        if len(args) > 1:
2688
+            raise cmdutil.GetHelp
2689
+        try:
2690
+            if tree:
2691
+                revision = cmdutil.determine_revision_tree(tree, spec)
2692
+            else:
2693
+                revision = cmdutil.determine_revision_arch(tree, spec)
2694
+        except cmdutil.CantDetermineRevision, e:
2695
+            print str(e)
2696
+            return
2697
+        print options.display(revision)
2698
+
2699
+    def get_parser(self):
2700
+        """
2701
+        Returns the options parser to use for the "revision" command.
2702
+
2703
+        :rtype: cmdutil.CmdOptionParser
2704
+        """
2705
+        parser=cmdutil.CmdOptionParser("fai revision [revision]")
2706
+        parser.add_option("", "--location", action="store_const", 
2707
+                         const=paths.determine_path, dest="display", 
2708
+                         help="Show location instead of name", default=str)
2709
+        parser.add_option("--import", action="store_const", 
2710
+                         const=paths.determine_import_path, dest="display",  
2711
+                         help="Show location of import file")
2712
+        parser.add_option("--log", action="store_const", 
2713
+                         const=paths.determine_log_path, dest="display", 
2714
+                         help="Show location of log file")
2715
+        parser.add_option("--patch", action="store_const", 
2716
+                         dest="display", const=paths.determine_patch_path,
2717
+                         help="Show location of patchfile")
2718
+        parser.add_option("--continuation", action="store_const", 
2719
+                         const=paths.determine_continuation_path, 
2720
+                         dest="display",
2721
+                         help="Show location of continuation file")
2722
+        parser.add_option("--cacherev", action="store_const", 
2723
+                         const=paths.determine_cacherev_path, dest="display",
2724
+                         help="Show location of cacherev file")
2725
+        return parser 
2726
+
2727
+    def help(self, parser=None):
2728
+        """
2729
+        Prints a help message.
2730
+
2731
+        :param parser: If supplied, the parser to use for generating help.  If \
2732
+        not supplied, it is retrieved.
2733
+        :type parser: cmdutil.CmdOptionParser
2734
+        """
2735
+        if parser==None:
2736
+            parser=self.get_parser()
2737
+        parser.print_help()
2738
+        print """
2739
+Expands aliases and prints the name of the specified revision.  Instead of
2740
+the name, several options can be used to print locations.  If more than one is
2741
+specified, the last one is used.
2742
+        """
2743
+        help_tree_spec()
2744
+        return
2745
+
2746
+class Revisions(BaseCommand):
2747
+    """
2748
+    Print a revision name based on a revision specifier
2749
+    """
2750
+    def __init__(self):
2751
+        self.description="Lists revisions"
2752
+        self.cl_revisions = []
2753
+    
2754
+    def do_command(self, cmdargs):
2755
+        """
2756
+        Master function that perfoms the "revision" command.
2757
+        """
2758
+        (options, args) = self.get_parser().parse_args(cmdargs)
2759
+        if len(args) > 1:
2760
+            raise cmdutil.GetHelp
2761
+        try:
2762
+            self.tree = arch.tree_root()
2763
+        except arch.errors.TreeRootError:
2764
+            self.tree = None
2765
+        if options.type == "default":
2766
+            options.type = "archive"
2767
+        try:
2768
+            iter = cmdutil.revision_iterator(self.tree, options.type, args, 
2769
+                                             options.reverse, options.modified,
2770
+                                             options.shallow)
2771
+        except cmdutil.CantDetermineRevision, e:
2772
+            raise CommandFailedWrapper(e)
2773
+        except cmdutil.CantDetermineVersion, e:
2774
+            raise CommandFailedWrapper(e)
2775
+        if options.skip is not None:
2776
+            iter = cmdutil.iter_skip(iter, int(options.skip))
2777
+
2778
+        try:
2779
+            for revision in iter:
2780
+                log = None
2781
+                if isinstance(revision, arch.Patchlog):
2782
+                    log = revision
2783
+                    revision=revision.revision
2784
+                out = options.display(revision)
2785
+                if out is not None:
2786
+                    print out
2787
+                if log is None and (options.summary or options.creator or 
2788
+                                    options.date or options.merges):
2789
+                    log = revision.patchlog
2790
+                if options.creator:
2791
+                    print "    %s" % log.creator
2792
+                if options.date:
2793
+                    print "    %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date)
2794
+                if options.summary:
2795
+                    print "    %s" % log.summary
2796
+                if options.merges:
2797
+                    showed_title = False
2798
+                    for revision in log.merged_patches:
2799
+                        if not showed_title:
2800
+                            print "    Merged:"
2801
+                            showed_title = True
2802
+                        print "    %s" % revision
2803
+            if len(self.cl_revisions) > 0:
2804
+                print pylon.changelog_for_merge(self.cl_revisions)
2805
+        except pylon.errors.TreeRootNone:
2806
+            raise CommandFailedWrapper(
2807
+                Exception("This option can only be used in a project tree."))
2808
+
2809
+    def changelog_append(self, revision):
2810
+        if isinstance(revision, arch.Revision):
2811
+            revision=arch.Patchlog(revision)
2812
+        self.cl_revisions.append(revision)
2813
+   
2814
+    def get_parser(self):
2815
+        """
2816
+        Returns the options parser to use for the "revision" command.
2817
+
2818
+        :rtype: cmdutil.CmdOptionParser
2819
+        """
2820
+        parser=cmdutil.CmdOptionParser("fai revisions [version/revision]")
2821
+        select = cmdutil.OptionGroup(parser, "Selection options",
2822
+                          "Control which revisions are listed.  These options"
2823
+                          " are mutually exclusive.  If more than one is"
2824
+                          " specified, the last is used.")
2825
+
2826
+        cmdutil.add_revision_iter_options(select)
2827
+        parser.add_option("", "--skip", dest="skip", 
2828
+                          help="Skip revisions.  Positive numbers skip from "
2829
+                          "beginning, negative skip from end.",
2830
+                          metavar="NUMBER")
2831
+
2832
+        parser.add_option_group(select)
2833
+
2834
+        format = cmdutil.OptionGroup(parser, "Revision format options",
2835
+                          "These control the appearance of listed revisions")
2836
+        format.add_option("", "--location", action="store_const", 
2837
+                         const=paths.determine_path, dest="display", 
2838
+                         help="Show location instead of name", default=str)
2839
+        format.add_option("--import", action="store_const", 
2840
+                         const=paths.determine_import_path, dest="display",  
2841
+                         help="Show location of import file")
2842
+        format.add_option("--log", action="store_const", 
2843
+                         const=paths.determine_log_path, dest="display", 
2844
+                         help="Show location of log file")
2845
+        format.add_option("--patch", action="store_const", 
2846
+                         dest="display", const=paths.determine_patch_path,
2847
+                         help="Show location of patchfile")
2848
+        format.add_option("--continuation", action="store_const", 
2849
+                         const=paths.determine_continuation_path, 
2850
+                         dest="display",
2851
+                         help="Show location of continuation file")
2852
+        format.add_option("--cacherev", action="store_const", 
2853
+                         const=paths.determine_cacherev_path, dest="display",
2854
+                         help="Show location of cacherev file")
2855
+        format.add_option("--changelog", action="store_const", 
2856
+                         const=self.changelog_append, dest="display",
2857
+                         help="Show location of cacherev file")
2858
+        parser.add_option_group(format)
2859
+        display = cmdutil.OptionGroup(parser, "Display format options",
2860
+                          "These control the display of data")
2861
+        display.add_option("-r", "--reverse", action="store_true", 
2862
+                          dest="reverse", help="Sort from newest to oldest")
2863
+        display.add_option("-s", "--summary", action="store_true", 
2864
+                          dest="summary", help="Show patchlog summary")
2865
+        display.add_option("-D", "--date", action="store_true", 
2866
+                          dest="date", help="Show patchlog date")
2867
+        display.add_option("-c", "--creator", action="store_true", 
2868
+                          dest="creator", help="Show the id that committed the"
2869
+                          " revision")
2870
+        display.add_option("-m", "--merges", action="store_true", 
2871
+                          dest="merges", help="Show the revisions that were"
2872
+                          " merged")
2873
+        parser.add_option_group(display)
2874
+        return parser 
2875
+    def help(self, parser=None):
2876
+        """Attempt to explain the revisions command
2877
+        
2878
+        :param parser: If supplied, used to determine options
2879
+        """
2880
+        if parser==None:
2881
+            parser=self.get_parser()
2882
+        parser.print_help()
2883
+        print """List revisions.
2884
+        """
2885
+        help_tree_spec()
2886
+
2887
+
2888
+class Get(BaseCommand):
2889
+    """
2890
+    Retrieve a revision from the archive
2891
+    """
2892
+    def __init__(self):
2893
+        self.description="Retrieve a revision from the archive"
2894
+        self.parser=self.get_parser()
2895
+
2896
+
2897
+    def get_completer(self, arg, index):
2898
+        if index > 0:
2899
+            return None
2900
+        try:
2901
+            tree = arch.tree_root()
2902
+        except:
2903
+            tree = None
2904
+        return cmdutil.iter_revision_completions(arg, tree)
2905
+
2906
+
2907
+    def do_command(self, cmdargs):
2908
+        """
2909
+        Master function that perfoms the "get" command.
2910
+        """
2911
+        (options, args) = self.parser.parse_args(cmdargs)
2912
+        if len(args) < 1:
2913
+            return self.help()            
2914
+        try:
2915
+            tree = arch.tree_root()
2916
+        except arch.errors.TreeRootError:
2917
+            tree = None
2918
+        
2919
+        arch_loc = None
2920
+        try:
2921
+            revision, arch_loc = paths.full_path_decode(args[0])
2922
+        except Exception, e:
2923
+            revision = cmdutil.determine_revision_arch(tree, args[0], 
2924
+                check_existence=False, allow_package=True)
2925
+        if len(args) > 1:
2926
+            directory = args[1]
2927
+        else:
2928
+            directory = str(revision.nonarch)
2929
+        if os.path.exists(directory):
2930
+            raise DirectoryExists(directory)
2931
+        cmdutil.ensure_archive_registered(revision.archive, arch_loc)
2932
+        try:
2933
+            cmdutil.ensure_revision_exists(revision)
2934
+        except cmdutil.NoSuchRevision, e:
2935
+            raise CommandFailedWrapper(e)
2936
+
2937
+        link = cmdutil.prompt ("get link")
2938
+        for line in cmdutil.iter_get(revision, directory, link,
2939
+                                     options.no_pristine,
2940
+                                     options.no_greedy_add):
2941
+            cmdutil.colorize(line)
2942
+
2943
+    def get_parser(self):
2944
+        """
2945
+        Returns the options parser to use for the "get" command.
2946
+
2947
+        :rtype: cmdutil.CmdOptionParser
2948
+        """
2949
+        parser=cmdutil.CmdOptionParser("fai get revision [dir]")
2950
+        parser.add_option("--no-pristine", action="store_true", 
2951
+                         dest="no_pristine", 
2952
+                         help="Do not make pristine copy for reference")
2953
+        parser.add_option("--no-greedy-add", action="store_true", 
2954
+                         dest="no_greedy_add", 
2955
+                         help="Never add to greedy libraries")
2956
+
2957
+        return parser 
2958
+
2959
+    def help(self, parser=None):
2960
+        """
2961
+        Prints a help message.
2962
+
2963
+        :param parser: If supplied, the parser to use for generating help.  If \
2964
+        not supplied, it is retrieved.
2965
+        :type parser: cmdutil.CmdOptionParser
2966
+        """
2967
+        if parser==None:
2968
+            parser=self.get_parser()
2969
+        parser.print_help()
2970
+        print """
2971
+Expands aliases and constructs a project tree for a revision.  If the optional
2972
+"dir" argument is provided, the project tree will be stored in this directory.
2973
+        """
2974
+        help_tree_spec()
2975
+        return
2976
+
2977
+class PromptCmd(cmd.Cmd):
2978
+    def __init__(self):
2979
+        cmd.Cmd.__init__(self)
2980
+        self.prompt = "Fai> "
2981
+        try:
2982
+            self.tree = arch.tree_root()
2983
+        except:
2984
+            self.tree = None
2985
+        self.set_title()
2986
+        self.set_prompt()
2987
+        self.fake_aba = abacmds.AbaCmds()
2988
+        self.identchars += '-'
2989
+        self.history_file = os.path.expanduser("~/.fai-history")
2990
+        readline.set_completer_delims(string.whitespace)
2991
+        if os.access(self.history_file, os.R_OK) and \
2992
+            os.path.isfile(self.history_file):
2993
+            readline.read_history_file(self.history_file)
2994
+        self.cwd = os.getcwd()
2995
+
2996
+    def write_history(self):
2997
+        readline.write_history_file(self.history_file)
2998
+
2999
+    def do_quit(self, args):
3000
+        self.write_history()
3001
+        sys.exit(0)
3002
+
3003
+    def do_exit(self, args):
3004
+        self.do_quit(args)
3005
+
3006
+    def do_EOF(self, args):
3007
+        print
3008
+        self.do_quit(args)
3009
+
3010
+    def postcmd(self, line, bar):
3011
+        self.set_title()
3012
+        self.set_prompt()
3013
+
3014
+    def set_prompt(self):
3015
+        if self.tree is not None:
3016
+            try:
3017
+                prompt = pylon.alias_or_version(self.tree.tree_version, 
3018
+                                                self.tree, 
3019
+                                                full=False)
3020
+                if prompt is not None:
3021
+                    prompt = " " + prompt
3022
+            except:
3023
+                prompt = ""
3024
+        else:
3025
+            prompt = ""
3026
+        self.prompt = "Fai%s> " % prompt
3027
+
3028
+    def set_title(self, command=None):
3029
+        try:
3030
+            version = pylon.alias_or_version(self.tree.tree_version, self.tree, 
3031
+                                             full=False)
3032
+        except:
3033
+            version = "[no version]"
3034
+        if command is None:
3035
+            command = ""
3036
+        sys.stdout.write(terminal.term_title("Fai %s %s" % (command, version)))
3037
+
3038
+    def do_cd(self, line):
3039
+        if line == "":
3040
+            line = "~"
3041
+        line = os.path.expanduser(line)
3042
+        if os.path.isabs(line):
3043
+            newcwd = line
3044
+        else:
3045
+            newcwd = self.cwd+'/'+line
3046
+        newcwd = os.path.normpath(newcwd)
3047
+        try:
3048
+            os.chdir(newcwd)
3049
+            self.cwd = newcwd
3050
+        except Exception, e:
3051
+            print e
3052
+        try:
3053
+            self.tree = arch.tree_root()
3054
+        except:
3055
+            self.tree = None
3056
+
3057
+    def do_help(self, line):
3058
+        Help()(line)
3059
+
3060
+    def default(self, line):
3061
+        args = line.split()
3062
+        if find_command(args[0]):
3063
+            try:
3064
+                find_command(args[0]).do_command(args[1:])
3065
+            except cmdutil.BadCommandOption, e:
3066
+                print e
3067
+            except cmdutil.GetHelp, e:
3068
+                find_command(args[0]).help()
3069
+            except CommandFailed, e:
3070
+                print e
3071
+            except arch.errors.ArchiveNotRegistered, e:
3072
+                print e
3073
+            except KeyboardInterrupt, e:
3074
+                print "Interrupted"
3075
+            except arch.util.ExecProblem, e:
3076
+                print e.proc.error.rstrip('\n')
3077
+            except cmdutil.CantDetermineVersion, e:
3078
+                print e
3079
+            except cmdutil.CantDetermineRevision, e:
3080
+                print e
3081
+            except Exception, e:
3082
+                print "Unhandled error:\n%s" % errors.exception_str(e)
3083
+
3084
+        elif suggestions.has_key(args[0]):
3085
+            print suggestions[args[0]]
3086
+
3087
+        elif self.fake_aba.is_command(args[0]):
3088
+            tree = None
3089
+            try:
3090
+                tree = arch.tree_root()
3091
+            except arch.errors.TreeRootError:
3092
+                pass
3093
+            cmd = self.fake_aba.is_command(args[0])
3094
+            try:
3095
+                cmd.run(cmdutil.expand_prefix_alias(args[1:], tree))
3096
+            except KeyboardInterrupt, e:
3097
+                print "Interrupted"
3098
+
3099
+        elif options.tla_fallthrough and args[0] != "rm" and \
3100
+            cmdutil.is_tla_command(args[0]):
3101
+            try:
3102
+                tree = None
3103
+                try:
3104
+                    tree = arch.tree_root()
3105
+                except arch.errors.TreeRootError:
3106
+                    pass
3107
+                args = cmdutil.expand_prefix_alias(args, tree)
3108
+                arch.util.exec_safe('tla', args, stderr=sys.stderr,
3109
+                expected=(0, 1))
3110
+            except arch.util.ExecProblem, e:
3111
+                pass
3112
+            except KeyboardInterrupt, e:
3113
+                print "Interrupted"
3114
+        else:
3115
+            try:
3116
+                try:
3117
+                    tree = arch.tree_root()
3118
+                except arch.errors.TreeRootError:
3119
+                    tree = None
3120
+                args=line.split()
3121
+                os.system(" ".join(cmdutil.expand_prefix_alias(args, tree)))
3122
+            except KeyboardInterrupt, e:
3123
+                print "Interrupted"
3124
+
3125
+    def completenames(self, text, line, begidx, endidx):
3126
+        completions = []
3127
+        iter = iter_command_names(self.fake_aba)
3128
+        try:
3129
+            if len(line) > 0:
3130
+                arg = line.split()[-1]
3131
+            else:
3132
+                arg = ""
3133
+            iter = cmdutil.iter_munged_completions(iter, arg, text)
3134
+        except Exception, e:
3135
+            print e
3136
+        return list(iter)
3137
+
3138
+    def completedefault(self, text, line, begidx, endidx):
3139
+        """Perform completion for native commands.
3140
+        
3141
+        :param text: The text to complete
3142
+        :type text: str
3143
+        :param line: The entire line to complete
3144
+        :type line: str
3145
+        :param begidx: The start of the text in the line
3146
+        :type begidx: int
3147
+        :param endidx: The end of the text in the line
3148
+        :type endidx: int
3149
+        """
3150
+        try:
3151
+            (cmd, args, foo) = self.parseline(line)
3152
+            command_obj=find_command(cmd)
3153
+            if command_obj is not None:
3154
+                return command_obj.complete(args.split(), text)
3155
+            elif not self.fake_aba.is_command(cmd) and \
3156
+                cmdutil.is_tla_command(cmd):
3157
+                iter = cmdutil.iter_supported_switches(cmd)
3158
+                if len(args) > 0:
3159
+                    arg = args.split()[-1]
3160
+                else:
3161
+                    arg = ""
3162
+                if arg.startswith("-"):
3163
+                    return list(cmdutil.iter_munged_completions(iter, arg, 
3164
+                                                                text))
3165
+                else:
3166
+                    return list(cmdutil.iter_munged_completions(
3167
+                        cmdutil.iter_file_completions(arg), arg, text))
3168
+
3169
+
3170
+            elif cmd == "cd":
3171
+                if len(args) > 0:
3172
+                    arg = args.split()[-1]
3173
+                else:
3174
+                    arg = ""
3175
+                iter = cmdutil.iter_dir_completions(arg)
3176
+                iter = cmdutil.iter_munged_completions(iter, arg, text)
3177
+                return list(iter)
3178
+            elif len(args)>0:
3179
+                arg = args.split()[-1]
3180
+                iter = cmdutil.iter_file_completions(arg)
3181
+                return list(cmdutil.iter_munged_completions(iter, arg, text))
3182
+            else:
3183
+                return self.completenames(text, line, begidx, endidx)
3184
+        except Exception, e:
3185
+            print e
3186
+
3187
+
3188
+def iter_command_names(fake_aba):
3189
+    for entry in cmdutil.iter_combine([commands.iterkeys(), 
3190
+                                     fake_aba.get_commands(), 
3191
+                                     cmdutil.iter_tla_commands(False)]):
3192
+        if not suggestions.has_key(str(entry)):
3193
+            yield entry
3194
+
3195
+
3196
+def iter_source_file_completions(tree, arg):
3197
+    treepath = arch_compound.tree_cwd(tree)
3198
+    if len(treepath) > 0:
3199
+        dirs = [treepath]
3200
+    else:
3201
+        dirs = None
3202
+    for file in tree.iter_inventory(dirs, source=True, both=True):
3203
+        file = file_completion_match(file, treepath, arg)
3204
+        if file is not None:
3205
+            yield file
3206
+
3207
+
3208
+def iter_untagged(tree, dirs):
3209
+    for file in arch_core.iter_inventory_filter(tree, dirs, tagged=False, 
3210
+                                                categories=arch_core.non_root,
3211
+                                                control_files=True):
3212
+        yield file.name 
3213
+
3214
+
3215
+def iter_untagged_completions(tree, arg):
3216
+    """Generate an iterator for all visible untagged files that match arg.
3217
+
3218
+    :param tree: The tree to look for untagged files in
3219
+    :type tree: `arch.WorkingTree`
3220
+    :param arg: The argument to match
3221
+    :type arg: str
3222
+    :return: An iterator of all matching untagged files
3223
+    :rtype: iterator of str
3224
+    """
3225
+    treepath = arch_compound.tree_cwd(tree)
3226
+    if len(treepath) > 0:
3227
+        dirs = [treepath]
3228
+    else:
3229
+        dirs = None
3230
+
3231
+    for file in iter_untagged(tree, dirs):
3232
+        file = file_completion_match(file, treepath, arg)
3233
+        if file is not None:
3234
+            yield file
3235
+
3236
+
3237
+def file_completion_match(file, treepath, arg):
3238
+    """Determines whether a file within an arch tree matches the argument.
3239
+
3240
+    :param file: The rooted filename
3241
+    :type file: str
3242
+    :param treepath: The path to the cwd within the tree
3243
+    :type treepath: str
3244
+    :param arg: The prefix to match
3245
+    :return: The completion name, or None if not a match
3246
+    :rtype: str
3247
+    """
3248
+    if not file.startswith(treepath):
3249
+        return None
3250
+    if treepath != "":
3251
+        file = file[len(treepath)+1:]
3252
+
3253
+    if not file.startswith(arg):
3254
+        return None 
3255
+    if os.path.isdir(file):
3256
+        file += '/'
3257
+    return file
3258
+
3259
+def iter_modified_file_completions(tree, arg):
3260
+    """Returns a list of modified files that match the specified prefix.
3261
+
3262
+    :param tree: The current tree
3263
+    :type tree: `arch.WorkingTree`
3264
+    :param arg: The prefix to match
3265
+    :type arg: str
3266
+    """
3267
+    treepath = arch_compound.tree_cwd(tree)
3268
+    tmpdir = util.tmpdir()
3269
+    changeset = tmpdir+"/changeset"
3270
+    completions = []
3271
+    revision = cmdutil.determine_revision_tree(tree)
3272
+    for line in arch.iter_delta(revision, tree, changeset):
3273
+        if isinstance(line, arch.FileModification):
3274
+            file = file_completion_match(line.name[1:], treepath, arg)
3275
+            if file is not None:
3276
+                completions.append(file)
3277
+    shutil.rmtree(tmpdir)
3278
+    return completions
3279
+
3280
+class Shell(BaseCommand):
3281
+    def __init__(self):
3282
+        self.description = "Runs Fai as a shell"
3283
+
3284
+    def do_command(self, cmdargs):
3285
+        if len(cmdargs)!=0:
3286
+            raise cmdutil.GetHelp
3287
+        prompt = PromptCmd()
3288
+        try:
3289
+            prompt.cmdloop()
3290
+        finally:
3291
+            prompt.write_history()
3292
+
3293
+class AddID(BaseCommand):
3294
+    """
3295
+    Adds an inventory id for the given file
3296
+    """
3297
+    def __init__(self):
3298
+        self.description="Add an inventory id for a given file"
3299
+
3300
+    def get_completer(self, arg, index):
3301
+        tree = arch.tree_root()
3302
+        return iter_untagged_completions(tree, arg)
3303
+
3304
+    def do_command(self, cmdargs):
3305
+        """
3306
+        Master function that perfoms the "revision" command.
3307
+        """
3308
+        parser=self.get_parser()
3309
+        (options, args) = parser.parse_args(cmdargs)
3310
+
3311
+        try:
3312
+            tree = arch.tree_root()
3313
+        except arch.errors.TreeRootError, e:
3314
+            raise pylon.errors.CommandFailedWrapper(e)
3315
+            
3316
+
3317
+        if (len(args) == 0) == (options.untagged == False):
3318
+            raise cmdutil.GetHelp
3319
+
3320
+       #if options.id and len(args) != 1:
3321
+       #    print "If --id is specified, only one file can be named."
3322
+       #    return
3323
+        
3324
+        method = tree.tagging_method
3325
+        
3326
+        if options.id_type == "tagline":
3327
+            if method != "tagline":
3328
+                if not cmdutil.prompt("Tagline in other tree"):
3329
+                    if method == "explicit" or method == "implicit":
3330
+                        options.id_type == method
3331
+                    else:
3332
+                        print "add-id not supported for \"%s\" tagging method"\
3333
+                            % method 
3334
+                        return
3335
+        
3336
+        elif options.id_type == "implicit":
3337
+            if method != "implicit":
3338
+                if not cmdutil.prompt("Implicit in other tree"):
3339
+                    if method == "explicit" or method == "tagline":
3340
+                        options.id_type == method
3341
+                    else:
3342
+                        print "add-id not supported for \"%s\" tagging method"\
3343
+                            % method 
3344
+                        return
3345
+        elif options.id_type == "explicit":
3346
+            if method != "tagline" and method != explicit:
3347
+                if not prompt("Explicit in other tree"):
3348
+                    print "add-id not supported for \"%s\" tagging method" % \
3349
+                        method
3350
+                    return
3351
+        
3352
+        if options.id_type == "auto":
3353
+            if method != "tagline" and method != "explicit" \
3354
+                and method !="implicit":
3355
+                print "add-id not supported for \"%s\" tagging method" % method
3356
+                return
3357
+            else:
3358
+                options.id_type = method
3359
+        if options.untagged:
3360
+            args = None
3361
+        self.add_ids(tree, options.id_type, args)
3362
+
3363
+    def add_ids(self, tree, id_type, files=()):
3364
+        """Add inventory ids to files.
3365
+        
3366
+        :param tree: the tree the files are in
3367
+        :type tree: `arch.WorkingTree`
3368
+        :param id_type: the type of id to add: "explicit" or "tagline"
3369
+        :type id_type: str
3370
+        :param files: The list of files to add.  If None do all untagged.
3371
+        :type files: tuple of str
3372
+        """
3373
+
3374
+        untagged = (files is None)
3375
+        if untagged:
3376
+            files = list(iter_untagged(tree, None))
3377
+        previous_files = []
3378
+        while len(files) > 0:
3379
+            previous_files.extend(files)
3380
+            if id_type == "explicit":
3381
+                cmdutil.add_id(files)
3382
+            elif id_type == "tagline" or id_type == "implicit":
3383
+                for file in files:
3384
+                    try:
3385
+                        implicit = (id_type == "implicit")
3386
+                        cmdutil.add_tagline_or_explicit_id(file, False,
3387
+                                                           implicit)
3388
+                    except cmdutil.AlreadyTagged:
3389
+                        print "\"%s\" already has a tagline." % file
3390
+                    except cmdutil.NoCommentSyntax:
3391
+                        pass
3392
+            #do inventory after tagging until no untagged files are encountered
3393
+            if untagged:
3394
+                files = []
3395
+                for file in iter_untagged(tree, None):
3396
+                    if not file in previous_files:
3397
+                        files.append(file)
3398
+
3399
+            else:
3400
+                break
3401
+
3402
+    def get_parser(self):
3403
+        """
3404
+        Returns the options parser to use for the "revision" command.
3405
+
3406
+        :rtype: cmdutil.CmdOptionParser
3407
+        """
3408
+        parser=cmdutil.CmdOptionParser("fai add-id file1 [file2] [file3]...")
3409
+# ddaa suggests removing this to promote GUIDs.  Let's see who squalks.
3410
+#        parser.add_option("-i", "--id", dest="id", 
3411
+#                         help="Specify id for a single file", default=None)
3412
+        parser.add_option("--tltl", action="store_true", 
3413
+                         dest="lord_style",  help="Use Tom Lord's style of id.")
3414
+        parser.add_option("--explicit", action="store_const", 
3415
+                         const="explicit", dest="id_type", 
3416
+                         help="Use an explicit id", default="auto")
3417
+        parser.add_option("--tagline", action="store_const", 
3418
+                         const="tagline", dest="id_type", 
3419
+                         help="Use a tagline id")
3420
+        parser.add_option("--implicit", action="store_const", 
3421
+                         const="implicit", dest="id_type", 
3422
+                         help="Use an implicit id (deprecated)")
3423
+        parser.add_option("--untagged", action="store_true", 
3424
+                         dest="untagged", default=False, 
3425
+                         help="tag all untagged files")
3426
+        return parser 
3427
+
3428
+    def help(self, parser=None):
3429
+        """
3430
+        Prints a help message.
3431
+
3432
+        :param parser: If supplied, the parser to use for generating help.  If \
3433
+        not supplied, it is retrieved.
3434
+        :type parser: cmdutil.CmdOptionParser
3435
+        """
3436
+        if parser==None:
3437
+            parser=self.get_parser()
3438
+        parser.print_help()
3439
+        print """
3440
+Adds an inventory to the specified file(s) and directories.  If --untagged is
3441
+specified, adds inventory to all untagged files and directories.
3442
+        """
3443
+        return
3444
+
3445
+
3446
+class Merge(BaseCommand):
3447
+    """
3448
+    Merges changes from other versions into the current tree
3449
+    """
3450
+    def __init__(self):
3451
+        self.description="Merges changes from other versions"
3452
+        try:
3453
+            self.tree = arch.tree_root()
3454
+        except:
3455
+            self.tree = None
3456
+
3457
+
3458
+    def get_completer(self, arg, index):
3459
+        if self.tree is None:
3460
+            raise arch.errors.TreeRootError
3461
+        return cmdutil.merge_completions(self.tree, arg, index)
3462
+
3463
+    def do_command(self, cmdargs):
3464
+        """
3465
+        Master function that perfoms the "merge" command.
3466
+        """
3467
+        parser=self.get_parser()
3468
+        (options, args) = parser.parse_args(cmdargs)
3469
+        if options.diff3:
3470
+            action="star-merge"
3471
+        else:
3472
+            action = options.action
3473
+        
3474
+        if self.tree is None:
3475
+            raise arch.errors.TreeRootError(os.getcwd())
3476
+        if cmdutil.has_changed(ancillary.comp_revision(self.tree)):
3477
+            raise UncommittedChanges(self.tree)
3478
+
3479
+        if len(args) > 0:
3480
+            revisions = []
3481
+            for arg in args:
3482
+                revisions.append(cmdutil.determine_revision_arch(self.tree, 
3483
+                                                                 arg))
3484
+            source = "from commandline"
3485
+        else:
3486
+            revisions = ancillary.iter_partner_revisions(self.tree, 
3487
+                                                         self.tree.tree_version)
3488
+            source = "from partner version"
3489
+        revisions = misc.rewind_iterator(revisions)
3490
+        try:
3491
+            revisions.next()
3492
+            revisions.rewind()
3493
+        except StopIteration, e:
3494
+            revision = cmdutil.tag_cur(self.tree)
3495
+            if revision is None:
3496
+                raise CantDetermineRevision("", "No version specified, no "
3497
+                                            "partner-versions, and no tag"
3498
+                                            " source")
3499
+            revisions = [revision]
3500
+            source = "from tag source"
3501
+        for revision in revisions:
3502
+            cmdutil.ensure_archive_registered(revision.archive)
3503
+            cmdutil.colorize(arch.Chatter("* Merging %s [%s]" % 
3504
+                             (revision, source)))
3505
+            if action=="native-merge" or action=="update":
3506
+                if self.native_merge(revision, action) == 0:
3507
+                    continue
3508
+            elif action=="star-merge":
3509
+                try: 
3510
+                    self.star_merge(revision, options.diff3)
3511
+                except errors.MergeProblem, e:
3512
+                    break
3513
+            if cmdutil.has_changed(self.tree.tree_version):
3514
+                break
3515
+
3516
+    def star_merge(self, revision, diff3):
3517
+        """Perform a star-merge on the current tree.
3518
+        
3519
+        :param revision: The revision to use for the merge
3520
+        :type revision: `arch.Revision`
3521
+        :param diff3: If true, do a diff3 merge
3522
+        :type diff3: bool
3523
+        """
3524
+        try:
3525
+            for line in self.tree.iter_star_merge(revision, diff3=diff3):
3526
+                cmdutil.colorize(line)
3527
+        except arch.util.ExecProblem, e:
3528
+            if e.proc.status is not None and e.proc.status == 1:
3529
+                if e.proc.error:
3530
+                    print e.proc.error
3531
+                raise MergeProblem
3532
+            else:
3533
+                raise
3534
+
3535
+    def native_merge(self, other_revision, action):
3536
+        """Perform a native-merge on the current tree.
3537
+        
3538
+        :param other_revision: The revision to use for the merge
3539
+        :type other_revision: `arch.Revision`
3540
+        :return: 0 if the merge was skipped, 1 if it was applied
3541
+        """
3542
+        other_tree = arch_compound.find_or_make_local_revision(other_revision)
3543
+        try:
3544
+            if action == "native-merge":
3545
+                ancestor = arch_compound.merge_ancestor2(self.tree, other_tree, 
3546
+                                                         other_revision)
3547
+            elif action == "update":
3548
+                ancestor = arch_compound.tree_latest(self.tree, 
3549
+                                                     other_revision.version)
3550
+        except CantDetermineRevision, e:
3551
+            raise CommandFailedWrapper(e)
3552
+        cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor))
3553
+        if (ancestor == other_revision):
3554
+            cmdutil.colorize(arch.Chatter("* Skipping redundant merge" 
3555
+                                          % ancestor))
3556
+            return 0
3557
+        delta = cmdutil.apply_delta(ancestor, other_tree, self.tree)    
3558
+        for line in cmdutil.iter_apply_delta_filter(delta):
3559
+            cmdutil.colorize(line)
3560
+        return 1
3561
+
3562
+
3563
+
3564
+    def get_parser(self):
3565
+        """
3566
+        Returns the options parser to use for the "merge" command.
3567
+
3568
+        :rtype: cmdutil.CmdOptionParser
3569
+        """
3570
+        parser=cmdutil.CmdOptionParser("fai merge [VERSION]")
3571
+        parser.add_option("-s", "--star-merge", action="store_const",
3572
+                          dest="action", help="Use star-merge",
3573
+                          const="star-merge", default="native-merge")
3574
+        parser.add_option("--update", action="store_const",
3575
+                          dest="action", help="Use update picker",
3576
+                          const="update")
3577
+        parser.add_option("--diff3", action="store_true", 
3578
+                         dest="diff3",  
3579
+                         help="Use diff3 for merge (implies star-merge)")
3580
+        return parser 
3581
+
3582
+    def help(self, parser=None):
3583
+        """
3584
+        Prints a help message.
3585
+
3586
+        :param parser: If supplied, the parser to use for generating help.  If \
3587
+        not supplied, it is retrieved.
3588
+        :type parser: cmdutil.CmdOptionParser
3589
+        """
3590
+        if parser==None:
3591
+            parser=self.get_parser()
3592
+        parser.print_help()
3593
+        print """
3594
+Performs a merge operation using the specified version.
3595
+        """
3596
+        return
3597
+
3598
+class ELog(BaseCommand):
3599
+    """
3600
+    Produces a raw patchlog and invokes the user's editor
3601
+    """
3602
+    def __init__(self):
3603
+        self.description="Edit a patchlog to commit"
3604
+        try:
3605
+            self.tree = arch.tree_root()
3606
+        except:
3607
+            self.tree = None
3608
+
3609
+
3610
+    def do_command(self, cmdargs):
3611
+        """
3612
+        Master function that perfoms the "elog" command.
3613
+        """
3614
+        parser=self.get_parser()
3615
+        (options, args) = parser.parse_args(cmdargs)
3616
+        if self.tree is None:
3617
+            raise arch.errors.TreeRootError
3618
+
3619
+        try:
3620
+            edit_log(self.tree, self.tree.tree_version)
3621
+        except pylon.errors.NoEditorSpecified, e:
3622
+            raise pylon.errors.CommandFailedWrapper(e)
3623
+
3624
+    def get_parser(self):
3625
+        """
3626
+        Returns the options parser to use for the "merge" command.
3627
+
3628
+        :rtype: cmdutil.CmdOptionParser
3629
+        """
3630
+        parser=cmdutil.CmdOptionParser("fai elog")
3631
+        return parser 
3632
+
3633
+
3634
+    def help(self, parser=None):
3635
+        """
3636
+        Invokes $EDITOR to produce a log for committing.
3637
+
3638
+        :param parser: If supplied, the parser to use for generating help.  If \
3639
+        not supplied, it is retrieved.
3640
+        :type parser: cmdutil.CmdOptionParser
3641
+        """
3642
+        if parser==None:
3643
+            parser=self.get_parser()
3644
+        parser.print_help()
3645
+        print """
3646
+Invokes $EDITOR to produce a log for committing.
3647
+        """
3648
+        return
3649
+
3650
+def edit_log(tree, version):
3651
+    """Makes and edits the log for a tree.  Does all kinds of fancy things
3652
+    like log templates and merge summaries and log-for-merge
3653
+    
3654
+    :param tree: The tree to edit the log for
3655
+    :type tree: `arch.WorkingTree`
3656
+    """
3657
+    #ensure we have an editor before preparing the log
3658
+    cmdutil.find_editor()
3659
+    log = tree.log_message(create=False, version=version)
3660
+    log_is_new = False
3661
+    if log is None or cmdutil.prompt("Overwrite log"):
3662
+        if log is not None:
3663
+           os.remove(log.name)
3664
+        log = tree.log_message(create=True, version=version)
3665
+        log_is_new = True
3666
+        tmplog = log.name
3667
+        template = pylon.log_template_path(tree)
3668
+        if template:
3669
+            shutil.copyfile(template, tmplog)
3670
+        comp_version = ancillary.comp_revision(tree).version
3671
+        new_merges = cmdutil.iter_new_merges(tree, comp_version)
3672
+        new_merges = cmdutil.direct_merges(new_merges)
3673
+        log["Summary"] = pylon.merge_summary(new_merges, 
3674
+                                         version)
3675
+        if len(new_merges) > 0:   
3676
+            if cmdutil.prompt("Log for merge"):
3677
+                if cmdutil.prompt("changelog for merge"):
3678
+                    mergestuff = "Patches applied:\n"
3679
+                    mergestuff += pylon.changelog_for_merge(new_merges)
3680
+                else:
3681
+                    mergestuff = cmdutil.log_for_merge(tree, comp_version)
3682
+                log.description += mergestuff
3683
+        log.save()
3684
+    try:
3685
+        cmdutil.invoke_editor(log.name)
3686
+    except:
3687
+        if log_is_new:
3688
+            os.remove(log.name)
3689
+        raise
3690
+
3691
+
3692
+class MirrorArchive(BaseCommand):
3693
+    """
3694
+    Updates a mirror from an archive
3695
+    """
3696
+    def __init__(self):
3697
+        self.description="Update a mirror from an archive"
3698
+
3699
+    def do_command(self, cmdargs):
3700
+        """
3701
+        Master function that perfoms the "revision" command.
3702
+        """
3703
+
3704
+        parser=self.get_parser()
3705
+        (options, args) = parser.parse_args(cmdargs)
3706
+        if len(args) > 1:
3707
+            raise GetHelp
3708
+        try:
3709
+            tree = arch.tree_root()
3710
+        except:
3711
+            tree = None
3712
+
3713
+        if len(args) == 0:
3714
+            if tree is not None:
3715
+                name = tree.tree_version()
3716
+        else:
3717
+            name = cmdutil.expand_alias(args[0], tree)
3718
+            name = arch.NameParser(name)
3719
+
3720
+        to_arch = name.get_archive()
3721
+        from_arch = cmdutil.get_mirror_source(arch.Archive(to_arch))
3722
+        limit = name.get_nonarch()
3723
+
3724
+        iter = arch_core.mirror_archive(from_arch,to_arch, limit)
3725
+        for line in arch.chatter_classifier(iter):
3726
+            cmdutil.colorize(line)
3727
+
3728
+    def get_parser(self):
3729
+        """
3730
+        Returns the options parser to use for the "revision" command.
3731
+
3732
+        :rtype: cmdutil.CmdOptionParser
3733
+        """
3734
+        parser=cmdutil.CmdOptionParser("fai mirror-archive ARCHIVE")
3735
+        return parser 
3736
+
3737
+    def help(self, parser=None):
3738
+        """
3739
+        Prints a help message.
3740
+
3741
+        :param parser: If supplied, the parser to use for generating help.  If \
3742
+        not supplied, it is retrieved.
3743
+        :type parser: cmdutil.CmdOptionParser
3744
+        """
3745
+        if parser==None:
3746
+            parser=self.get_parser()
3747
+        parser.print_help()
3748
+        print """
3749
+Updates a mirror from an archive.  If a branch, package, or version is
3750
+supplied, only changes under it are mirrored.
3751
+        """
3752
+        return
3753
+
3754
+def help_tree_spec():
3755
+    print """Specifying revisions (default: tree)
3756
+Revisions may be specified by alias, revision, version or patchlevel.
3757
+Revisions or versions may be fully qualified.  Unqualified revisions, versions, 
3758
+or patchlevels use the archive of the current project tree.  Versions will
3759
+use the latest patchlevel in the tree.  Patchlevels will use the current tree-
3760
+version.
3761
+
3762
+Use "alias" to list available (user and automatic) aliases."""
3763
+
3764
+auto_alias = [
3765
+"acur", 
3766
+"The latest revision in the archive of the tree-version.  You can specify \
3767
+a different version like so: acur:foo--bar--0 (aliases can be used)",
3768
+"tcur",
3769
+"""(tree current) The latest revision in the tree of the tree-version. \
3770
+You can specify a different version like so: tcur:foo--bar--0 (aliases can be \
3771
+used).""",
3772
+"tprev" , 
3773
+"""(tree previous) The previous revision in the tree of the tree-version.  To \
3774
+specify an older revision, use a number, e.g. "tprev:4" """,
3775
+"tanc" , 
3776
+"""(tree ancestor) The ancestor revision of the tree To specify an older \
3777
+revision, use a number, e.g. "tanc:4".""",
3778
+"tdate" , 
3779
+"""(tree date) The latest revision from a given date, e.g. "tdate:July 6".""",
3780
+"tmod" , 
3781
+""" (tree modified) The latest revision to modify a given file, e.g. \
3782
+"tmod:engine.cpp" or "tmod:engine.cpp:16".""",
3783
+"ttag" , 
3784
+"""(tree tag) The revision that was tagged into the current tree revision, \
3785
+according to the tree""",
3786
+"tagcur", 
3787
+"""(tag current) The latest revision of the version that the current tree \
3788
+was tagged from.""",
3789
+"mergeanc" , 
3790
+"""The common ancestor of the current tree and the specified revision. \
3791
+Defaults to the first partner-version's latest revision or to tagcur.""",
3792
+]
3793
+
3794
+
3795
+def is_auto_alias(name):
3796
+    """Determine whether a name is an auto alias name
3797
+
3798
+    :param name: the name to check
3799
+    :type name: str
3800
+    :return: True if the name is an auto alias, false if not
3801
+    :rtype: bool
3802
+    """
3803
+    return name in [f for (f, v) in pylon.util.iter_pairs(auto_alias)]
3804
+
3805
+
3806
+def display_def(iter, wrap = 80):
3807
+    """Display a list of definitions
3808
+
3809
+    :param iter: iter of name, definition pairs
3810
+    :type iter: iter of (str, str)
3811
+    :param wrap: The width for text wrapping
3812
+    :type wrap: int
3813
+    """
3814
+    vals = list(iter)
3815
+    maxlen = 0
3816
+    for (key, value) in vals:
3817
+        if len(key) > maxlen:
3818
+            maxlen = len(key)
3819
+    for (key, value) in vals:
3820
+        tw=textwrap.TextWrapper(width=wrap, 
3821
+                                initial_indent=key.rjust(maxlen)+" : ",
3822
+                                subsequent_indent="".rjust(maxlen+3))
3823
+        print tw.fill(value)
3824
+
3825
+
3826
+def help_aliases(tree):
3827
+    print """Auto-generated aliases"""
3828
+    display_def(pylon.util.iter_pairs(auto_alias))
3829
+    print "User aliases"
3830
+    display_def(ancillary.iter_all_alias(tree))
3831
+
3832
+class Inventory(BaseCommand):
3833
+    """List the status of files in the tree"""
3834
+    def __init__(self):
3835
+        self.description=self.__doc__
3836
+
3837
+    def do_command(self, cmdargs):
3838
+        """
3839
+        Master function that perfoms the "revision" command.
3840
+        """
3841
+
3842
+        parser=self.get_parser()
3843
+        (options, args) = parser.parse_args(cmdargs)
3844
+        tree = arch.tree_root()
3845
+        categories = []
3846
+
3847
+        if (options.source):
3848
+            categories.append(arch_core.SourceFile)
3849
+        if (options.precious):
3850
+            categories.append(arch_core.PreciousFile)
3851
+        if (options.backup):
3852
+            categories.append(arch_core.BackupFile)
3853
+        if (options.junk):
3854
+            categories.append(arch_core.JunkFile)
3855
+
3856
+        if len(categories) == 1:
3857
+            show_leading = False
3858
+        else:
3859
+            show_leading = True
3860
+
3861
+        if len(categories) == 0:
3862
+            categories = None
3863
+
3864
+        if options.untagged:
3865
+            categories = arch_core.non_root
3866
+            show_leading = False
3867
+            tagged = False
3868
+        else:
3869
+            tagged = None
3870
+        
3871
+        for file in arch_core.iter_inventory_filter(tree, None, 
3872
+            control_files=options.control_files, 
3873
+            categories = categories, tagged=tagged):
3874
+            print arch_core.file_line(file, 
3875
+                                      category = show_leading, 
3876
+                                      untagged = show_leading,
3877
+                                      id = options.ids)
3878
+
3879
+    def get_parser(self):
3880
+        """
3881
+        Returns the options parser to use for the "revision" command.
3882
+
3883
+        :rtype: cmdutil.CmdOptionParser
3884
+        """
3885
+        parser=cmdutil.CmdOptionParser("fai inventory [options]")
3886
+        parser.add_option("--ids", action="store_true", dest="ids", 
3887
+                          help="Show file ids")
3888
+        parser.add_option("--control", action="store_true", 
3889
+                          dest="control_files", help="include control files")
3890
+        parser.add_option("--source", action="store_true", dest="source",
3891
+                          help="List source files")
3892
+        parser.add_option("--backup", action="store_true", dest="backup",
3893
+                          help="List backup files")
3894
+        parser.add_option("--precious", action="store_true", dest="precious",
3895
+                          help="List precious files")
3896
+        parser.add_option("--junk", action="store_true", dest="junk",
3897
+                          help="List junk files")
3898
+        parser.add_option("--unrecognized", action="store_true", 
3899
+                          dest="unrecognized", help="List unrecognized files")
3900
+        parser.add_option("--untagged", action="store_true", 
3901
+                          dest="untagged", help="List only untagged files")
3902
+        return parser 
3903
+
3904
+    def help(self, parser=None):
3905
+        """
3906
+        Prints a help message.
3907
+
3908
+        :param parser: If supplied, the parser to use for generating help.  If \
3909
+        not supplied, it is retrieved.
3910
+        :type parser: cmdutil.CmdOptionParser
3911
+        """
3912
+        if parser==None:
3913
+            parser=self.get_parser()
3914
+        parser.print_help()
3915
+        print """
3916
+Lists the status of files in the archive:
3917
+S source
3918
+P precious
3919
+B backup
3920
+J junk
3921
+U unrecognized
3922
+T tree root
3923
+? untagged-source
3924
+Leading letter are not displayed if only one kind of file is shown
3925
+        """
3926
+        return
3927
+
3928
+
3929
+class Alias(BaseCommand):
3930
+    """List or adjust aliases"""
3931
+    def __init__(self):
3932
+        self.description=self.__doc__
3933
+
3934
+    def get_completer(self, arg, index):
3935
+        if index > 2:
3936
+            return ()
3937
+        try:
3938
+            self.tree = arch.tree_root()
3939
+        except:
3940
+            self.tree = None
3941
+
3942
+        if index == 0:
3943
+            return [part[0]+" " for part in ancillary.iter_all_alias(self.tree)]
3944
+        elif index == 1:
3945
+            return cmdutil.iter_revision_completions(arg, self.tree)
3946
+
3947
+
3948
+    def do_command(self, cmdargs):
3949
+        """
3950
+        Master function that perfoms the "revision" command.
3951
+        """
3952
+
3953
+        parser=self.get_parser()
3954
+        (options, args) = parser.parse_args(cmdargs)
3955
+        try:
3956
+            self.tree =  arch.tree_root()
3957
+        except:
3958
+            self.tree = None
3959
+
3960
+
3961
+        try:
3962
+            options.action(args, options)
3963
+        except cmdutil.ForbiddenAliasSyntax, e:
3964
+            raise CommandFailedWrapper(e)
3965
+
3966
+    def no_prefix(self, alias):
3967
+        if alias.startswith("^"):
3968
+            alias = alias[1:]
3969
+        return alias
3970
+        
3971
+    def arg_dispatch(self, args, options):
3972
+        """Add, modify, or list aliases, depending on number of arguments
3973
+
3974
+        :param args: The list of commandline arguments
3975
+        :type args: list of str
3976
+        :param options: The commandline options
3977
+        """
3978
+        if len(args) == 0:
3979
+            help_aliases(self.tree)
3980
+            return
3981
+        else:
3982
+            alias = self.no_prefix(args[0])
3983
+            if len(args) == 1:
3984
+                self.print_alias(alias)
3985
+            elif (len(args)) == 2:
3986
+                self.add(alias, args[1], options)
3987
+            else:
3988
+                raise cmdutil.GetHelp
3989
+
3990
+    def print_alias(self, alias):
3991
+        answer = None
3992
+        if is_auto_alias(alias):
3993
+            raise pylon.errors.IsAutoAlias(alias, "\"%s\" is an auto alias."
3994
+                "  Use \"revision\" to expand auto aliases." % alias)
3995
+        for pair in ancillary.iter_all_alias(self.tree):
3996
+            if pair[0] == alias:
3997
+                answer = pair[1]
3998
+        if answer is not None:
3999
+            print answer
4000
+        else:
4001
+            print "The alias %s is not assigned." % alias
4002
+
4003
+    def add(self, alias, expansion, options):
4004
+        """Add or modify aliases
4005
+
4006
+        :param alias: The alias name to create/modify
4007
+        :type alias: str
4008
+        :param expansion: The expansion to assign to the alias name
4009
+        :type expansion: str
4010
+        :param options: The commandline options
4011
+        """
4012
+        if is_auto_alias(alias):
4013
+            raise IsAutoAlias(alias)
4014
+        newlist = ""
4015
+        written = False
4016
+        new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, 
4017
+            self.tree))
4018
+        ancillary.check_alias(new_line.rstrip("\n"), [alias, expansion])
4019
+
4020
+        for pair in self.get_iterator(options):
4021
+            if pair[0] != alias:
4022
+                newlist+="%s=%s\n" % (pair[0], pair[1])
4023
+            elif not written:
4024
+                newlist+=new_line
4025
+                written = True
4026
+        if not written:
4027
+            newlist+=new_line
4028
+        self.write_aliases(newlist, options)
4029
+            
4030
+    def delete(self, args, options):
4031
+        """Delete the specified alias
4032
+
4033
+        :param args: The list of arguments
4034
+        :type args: list of str
4035
+        :param options: The commandline options
4036
+        """
4037
+        deleted = False
4038
+        if len(args) != 1:
4039
+            raise cmdutil.GetHelp
4040
+        alias = self.no_prefix(args[0])
4041
+        if is_auto_alias(alias):
4042
+            raise IsAutoAlias(alias)
4043
+        newlist = ""
4044
+        for pair in self.get_iterator(options):
4045
+            if pair[0] != alias:
4046
+                newlist+="%s=%s\n" % (pair[0], pair[1])
4047
+            else:
4048
+                deleted = True
4049
+        if not deleted:
4050
+            raise errors.NoSuchAlias(alias)
4051
+        self.write_aliases(newlist, options)
4052
+
4053
+    def get_alias_file(self, options):
4054
+        """Return the name of the alias file to use
4055
+
4056
+        :param options: The commandline options
4057
+        """
4058
+        if options.tree:
4059
+            if self.tree is None:
4060
+                self.tree == arch.tree_root()
4061
+            return str(self.tree)+"/{arch}/+aliases"
4062
+        else:
4063
+            return "~/.aba/aliases"
4064
+
4065
+    def get_iterator(self, options):
4066
+        """Return the alias iterator to use
4067
+
4068
+        :param options: The commandline options
4069
+        """
4070
+        return ancillary.iter_alias(self.get_alias_file(options))
4071
+
4072
+    def write_aliases(self, newlist, options):
4073
+        """Safely rewrite the alias file
4074
+        :param newlist: The new list of aliases
4075
+        :type newlist: str
4076
+        :param options: The commandline options
4077
+        """
4078
+        filename = os.path.expanduser(self.get_alias_file(options))
4079
+        file = util.NewFileVersion(filename)
4080
+        file.write(newlist)
4081
+        file.commit()
4082
+
4083
+
4084
+    def get_parser(self):
4085
+        """
4086
+        Returns the options parser to use for the "alias" command.
4087
+
4088
+        :rtype: cmdutil.CmdOptionParser
4089
+        """
4090
+        parser=cmdutil.CmdOptionParser("fai alias [ALIAS] [NAME]")
4091
+        parser.add_option("-d", "--delete", action="store_const", dest="action",
4092
+                          const=self.delete, default=self.arg_dispatch, 
4093
+                          help="Delete an alias")
4094
+        parser.add_option("--tree", action="store_true", dest="tree", 
4095
+                          help="Create a per-tree alias", default=False)
4096
+        return parser 
4097
+
4098
+    def help(self, parser=None):
4099
+        """
4100
+        Prints a help message.
4101
+
4102
+        :param parser: If supplied, the parser to use for generating help.  If \
4103
+        not supplied, it is retrieved.
4104
+        :type parser: cmdutil.CmdOptionParser
4105
+        """
4106
+        if parser==None:
4107
+            parser=self.get_parser()
4108
+        parser.print_help()
4109
+        print """
4110
+Lists current aliases or modifies the list of aliases.
4111
+
4112
+If no arguments are supplied, aliases will be listed.  If two arguments are
4113
+supplied, the specified alias will be created or modified.  If -d or --delete
4114
+is supplied, the specified alias will be deleted.
4115
+
4116
+You can create aliases that refer to any fully-qualified part of the
4117
+Arch namespace, e.g. 
4118
+archive, 
4119
+archive/category, 
4120
+archive/category--branch, 
4121
+archive/category--branch--version (my favourite)
4122
+archive/category--branch--version--patchlevel
4123
+
4124
+Aliases can be used automatically by native commands.  To use them
4125
+with external or tla commands, prefix them with ^ (you can do this
4126
+with native commands, too).
4127
+"""
4128
+
4129
+
4130
+class RequestMerge(BaseCommand):
4131
+    """Submit a merge request to Bug Goo"""
4132
+    def __init__(self):
4133
+        self.description=self.__doc__
4134
+
4135
+    def do_command(self, cmdargs):
4136
+        """Submit a merge request
4137
+
4138
+        :param cmdargs: The commandline arguments
4139
+        :type cmdargs: list of str
4140
+        """
4141
+        parser = self.get_parser()
4142
+        (options, args) = parser.parse_args(cmdargs)
4143
+        try:
4144
+            cmdutil.find_editor()
4145
+        except pylon.errors.NoEditorSpecified, e:
4146
+            raise pylon.errors.CommandFailedWrapper(e)
4147
+        try:
4148
+            self.tree=arch.tree_root()
4149
+        except:
4150
+            self.tree=None
4151
+        base, revisions = self.revision_specs(args)
4152
+        message = self.make_headers(base, revisions)
4153
+        message += self.make_summary(revisions)
4154
+        path = self.edit_message(message)
4155
+        message = self.tidy_message(path)
4156
+        if cmdutil.prompt("Send merge"):
4157
+            self.send_message(message)
4158
+            print "Merge request sent"
4159
+
4160
+    def make_headers(self, base, revisions):
4161
+        """Produce email and Bug Goo header strings
4162
+
4163
+        :param base: The base revision to apply merges to
4164
+        :type base: `arch.Revision`
4165
+        :param revisions: The revisions to replay into the base
4166
+        :type revisions: list of `arch.Patchlog`
4167
+        :return: The headers
4168
+        :rtype: str
4169
+        """
4170
+        headers = "To: gnu-arch-users@gnu.org\n"
4171
+        headers += "From: %s\n" % options.fromaddr
4172
+        if len(revisions) == 1:
4173
+            headers += "Subject: [MERGE REQUEST] %s\n" % revisions[0].summary
4174
+        else:
4175
+            headers += "Subject: [MERGE REQUEST]\n"
4176
+        headers += "\n"
4177
+        headers += "Base-Revision: %s\n" % base
4178
+        for revision in revisions:
4179
+            headers += "Revision: %s\n" % revision.revision
4180
+        headers += "Bug: \n\n"
4181
+        return headers
4182
+
4183
+    def make_summary(self, logs):
4184
+        """Generate a summary of merges
4185
+
4186
+        :param logs: the patchlogs that were directly added by the merges
4187
+        :type logs: list of `arch.Patchlog`
4188
+        :return: the summary
4189
+        :rtype: str
4190
+        """ 
4191
+        summary = ""
4192
+        for log in logs:
4193
+            summary+=str(log.revision)+"\n"
4194
+            summary+=log.summary+"\n"
4195
+            if log.description.strip():
4196
+                summary+=log.description.strip('\n')+"\n\n"
4197
+        return summary
4198
+
4199
+    def revision_specs(self, args):
4200
+        """Determine the base and merge revisions from tree and arguments.
4201
+
4202
+        :param args: The parsed arguments
4203
+        :type args: list of str
4204
+        :return: The base revision and merge revisions 
4205
+        :rtype: `arch.Revision`, list of `arch.Patchlog`
4206
+        """
4207
+        if len(args) > 0:
4208
+            target_revision = cmdutil.determine_revision_arch(self.tree, 
4209
+                                                              args[0])
4210
+        else:
4211
+            target_revision = arch_compound.tree_latest(self.tree)
4212
+        if len(args) > 1:
4213
+            merges = [ arch.Patchlog(cmdutil.determine_revision_arch(
4214
+                       self.tree, f)) for f in args[1:] ]
4215
+        else:
4216
+            if self.tree is None:
4217
+                raise CantDetermineRevision("", "Not in a project tree")
4218
+            merge_iter = cmdutil.iter_new_merges(self.tree, 
4219
+                                                 target_revision.version, 
4220
+                                                 False)
4221
+            merges = [f for f in cmdutil.direct_merges(merge_iter)]
4222
+        return (target_revision, merges)
4223
+
4224
+    def edit_message(self, message):
4225
+        """Edit an email message in the user's standard editor
4226
+
4227
+        :param message: The message to edit
4228
+        :type message: str
4229
+        :return: the path of the edited message
4230
+        :rtype: str
4231
+        """
4232
+        if self.tree is None:
4233
+            path = os.get_cwd()
4234
+        else:
4235
+            path = self.tree
4236
+        path += "/,merge-request"
4237
+        file = open(path, 'w')
4238
+        file.write(message)
4239
+        file.flush()
4240
+        cmdutil.invoke_editor(path)
4241
+        return path
4242
+
4243
+    def tidy_message(self, path):
4244
+        """Validate and clean up message.
4245
+
4246
+        :param path: The path to the message to clean up
4247
+        :type path: str
4248
+        :return: The parsed message
4249
+        :rtype: `email.Message`
4250
+        """
4251
+        mail = email.message_from_file(open(path))
4252
+        if mail["Subject"].strip() == "[MERGE REQUEST]":
4253
+            raise BlandSubject
4254
+        
4255
+        request = email.message_from_string(mail.get_payload())
4256
+        if request.has_key("Bug"):
4257
+            if request["Bug"].strip()=="":
4258
+                del request["Bug"]
4259
+        mail.set_payload(request.as_string())
4260
+        return mail
4261
+
4262
+    def send_message(self, message):
4263
+        """Send a message, using its headers to address it.
4264
+
4265
+        :param message: The message to send
4266
+        :type message: `email.Message`"""
4267
+        server = smtplib.SMTP("localhost")
4268
+        server.sendmail(message['From'], message['To'], message.as_string())
4269
+        server.quit()
4270
+
4271
+    def help(self, parser=None):
4272
+        """Print a usage message
4273
+
4274
+        :param parser: The options parser to use
4275
+        :type parser: `cmdutil.CmdOptionParser`
4276
+        """
4277
+        if parser is None:
4278
+            parser = self.get_parser()
4279
+        parser.print_help()
4280
+        print """
4281
+Sends a merge request formatted for Bug Goo.  Intended use: get the tree
4282
+you'd like to merge into.  Apply the merges you want.  Invoke request-merge.
4283
+The merge request will open in your $EDITOR.
4284
+
4285
+When no TARGET is specified, it uses the current tree revision.  When
4286
+no MERGE is specified, it uses the direct merges (as in "revisions
4287
+--direct-merges").  But you can specify just the TARGET, or all the MERGE
4288
+revisions.
4289
+"""
4290
+
4291
+    def get_parser(self):
4292
+        """Produce a commandline parser for this command.
4293
+
4294
+        :rtype: `cmdutil.CmdOptionParser`
4295
+        """
4296
+        parser=cmdutil.CmdOptionParser("request-merge [TARGET] [MERGE1...]")
4297
+        return parser
4298
+
4299
+commands = { 
4300
+'changes' : Changes,
4301
+'help' : Help,
4302
+'update': Update,
4303
+'apply-changes':ApplyChanges,
4304
+'cat-log': CatLog,
4305
+'commit': Commit,
4306
+'revision': Revision,
4307
+'revisions': Revisions,
4308
+'get': Get,
4309
+'revert': Revert,
4310
+'shell': Shell,
4311
+'add-id': AddID,
4312
+'merge': Merge,
4313
+'elog': ELog,
4314
+'mirror-archive': MirrorArchive,
4315
+'ninventory': Inventory,
4316
+'alias' : Alias,
4317
+'request-merge': RequestMerge,
4318
+}
4319
+
4320
+def my_import(mod_name):
4321
+    module = __import__(mod_name)
4322
+    components = mod_name.split('.')
4323
+    for comp in components[1:]:
4324
+        module = getattr(module, comp)
4325
+    return module
4326
+
4327
+def plugin(mod_name):
4328
+    module = my_import(mod_name)
4329
+    module.add_command(commands)
4330
+
4331
+for file in os.listdir(sys.path[0]+"/command"):
4332
+    if len(file) > 3 and file[-3:] == ".py" and file != "__init__.py":
4333
+        plugin("command."+file[:-3])
4334
+
4335
+suggestions = {
4336
+'apply-delta' : "Try \"apply-changes\".",
4337
+'delta' : "To compare two revisions, use \"changes\".",
4338
+'diff-rev' : "To compare two revisions, use \"changes\".",
4339
+'undo' : "To undo local changes, use \"revert\".",
4340
+'undelete' : "To undo only deletions, use \"revert --deletions\"",
4341
+'missing-from' : "Try \"revisions --missing-from\".",
4342
+'missing' : "Try \"revisions --missing\".",
4343
+'missing-merge' : "Try \"revisions --partner-missing\".",
4344
+'new-merges' : "Try \"revisions --new-merges\".",
4345
+'cachedrevs' : "Try \"revisions --cacherevs\". (no 'd')",
4346
+'logs' : "Try \"revisions --logs\"",
4347
+'tree-source' : "Use the \"^ttag\" alias (\"revision ^ttag\")",
4348
+'latest-revision' : "Use the \"^acur\" alias (\"revision ^acur\")",
4349
+'change-version' : "Try \"update REVISION\"",
4350
+'tree-revision' : "Use the \"^tcur\" alias (\"revision ^tcur\")",
4351
+'rev-depends' : "Use revisions --dependencies",
4352
+'auto-get' : "Plain get will do archive lookups",
4353
+'tagline' : "Use add-id.  It uses taglines in tagline trees",
4354
+'emlog' : "Use elog.  It automatically adds log-for-merge text, if any",
4355
+'library-revisions' : "Use revisions --library",
4356
+'file-revert' : "Use revert FILE",
4357
+'join-branch' : "Use replay --logs-only"
4358
+}
4359
+# arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7
4360
4361
*** added file 'testdata/orig'
4362
--- /dev/null 
4363
+++ testdata/orig 
4364
@@ -0,0 +1,2789 @@
4365
+# Copyright (C) 2004 Aaron Bentley
4366
+# <aaron.bentley@utoronto.ca>
4367
+#
4368
+#    This program is free software; you can redistribute it and/or modify
4369
+#    it under the terms of the GNU General Public License as published by
4370
+#    the Free Software Foundation; either version 2 of the License, or
4371
+#    (at your option) any later version.
4372
+#
4373
+#    This program is distributed in the hope that it will be useful,
4374
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
4375
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
4376
+#    GNU General Public License for more details.
4377
+#
4378
+#    You should have received a copy of the GNU General Public License
4379
+#    along with this program; if not, write to the Free Software
4380
+#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
4381
+
4382
+import sys
4383
+import arch
4384
+import arch.util
4385
+import arch.arch
4386
+import abacmds
4387
+import cmdutil
4388
+import shutil
4389
+import os
4390
+import options
4391
+import paths 
4392
+import time
4393
+import cmd
4394
+import readline
4395
+import re
4396
+import string
4397
+import arch_core
4398
+from errors import *
4399
+import errors
4400
+import terminal
4401
+import ancillary
4402
+import misc
4403
+import email
4404
+import smtplib
4405
+
4406
+__docformat__ = "restructuredtext"
4407
+__doc__ = "Implementation of user (sub) commands"
4408
+commands = {}
4409
+
4410
+def find_command(cmd):
4411
+    """
4412
+    Return an instance of a command type.  Return None if the type isn't
4413
+    registered.
4414
+
4415
+    :param cmd: the name of the command to look for
4416
+    :type cmd: the type of the command
4417
+    """
4418
+    if commands.has_key(cmd):
4419
+        return commands[cmd]()
4420
+    else:
4421
+        return None
4422
+
4423
+class BaseCommand:
4424
+    def __call__(self, cmdline):
4425
+        try:
4426
+            self.do_command(cmdline.split())
4427
+        except cmdutil.GetHelp, e:
4428
+            self.help()
4429
+        except Exception, e:
4430
+            print e
4431
+
4432
+    def get_completer(index):
4433
+        return None
4434
+
4435
+    def complete(self, args, text):
4436
+        """
4437
+        Returns a list of possible completions for the given text.
4438
+
4439
+        :param args: The complete list of arguments
4440
+        :type args: List of str
4441
+        :param text: text to complete (may be shorter than args[-1])
4442
+        :type text: str
4443
+        :rtype: list of str
4444
+        """
4445
+        matches = []
4446
+        candidates = None
4447
+
4448
+        if len(args) > 0: 
4449
+            realtext = args[-1]
4450
+        else:
4451
+            realtext = ""
4452
+
4453
+        try:
4454
+            parser=self.get_parser()
4455
+            if realtext.startswith('-'):
4456
+                candidates = parser.iter_options()
4457
+            else:
4458
+                (options, parsed_args) = parser.parse_args(args)
4459
+
4460
+                if len (parsed_args) > 0:
4461
+                    candidates = self.get_completer(parsed_args[-1], len(parsed_args) -1)
4462
+                else:
4463
+                    candidates = self.get_completer("", 0)
4464
+        except:
4465
+            pass
4466
+        if candidates is None:
4467
+            return
4468
+        for candidate in candidates:
4469
+            candidate = str(candidate)
4470
+            if candidate.startswith(realtext):
4471
+                matches.append(candidate[len(realtext)- len(text):])
4472
+        return matches
4473
+
4474
+
4475
+class Help(BaseCommand):
4476
+    """
4477
+    Lists commands, prints help messages.
4478
+    """
4479
+    def __init__(self):
4480
+        self.description="Prints help mesages"
4481
+        self.parser = None
4482
+
4483
+    def do_command(self, cmdargs):
4484
+        """
4485
+        Prints a help message.
4486
+        """
4487
+        options, args = self.get_parser().parse_args(cmdargs)
4488
+        if len(args) > 1:
4489
+            raise cmdutil.GetHelp
4490
+
4491
+        if options.native or options.suggestions or options.external:
4492
+            native = options.native
4493
+            suggestions = options.suggestions
4494
+            external = options.external
4495
+        else:
4496
+            native = True
4497
+            suggestions = False
4498
+            external = True
4499
+        
4500
+        if len(args) == 0:
4501
+            self.list_commands(native, suggestions, external)
4502
+            return
4503
+        elif len(args) == 1:
4504
+            command_help(args[0])
4505
+            return
4506
+
4507
+    def help(self):
4508
+        self.get_parser().print_help()
4509
+        print """
4510
+If no command is specified, commands are listed.  If a command is
4511
+specified, help for that command is listed.
4512
+        """
4513
+
4514
+    def get_parser(self):
4515
+        """
4516
+        Returns the options parser to use for the "revision" command.
4517
+
4518
+        :rtype: cmdutil.CmdOptionParser
4519
+        """
4520
+        if self.parser is not None:
4521
+            return self.parser
4522
+        parser=cmdutil.CmdOptionParser("fai help [command]")
4523
+        parser.add_option("-n", "--native", action="store_true", 
4524
+                         dest="native", help="Show native commands")
4525
+        parser.add_option("-e", "--external", action="store_true", 
4526
+                         dest="external", help="Show external commands")
4527
+        parser.add_option("-s", "--suggest", action="store_true", 
4528
+                         dest="suggestions", help="Show suggestions")
4529
+        self.parser = parser
4530
+        return parser 
4531
+      
4532
+    def list_commands(self, native=True, suggest=False, external=True):
4533
+        """
4534
+        Lists supported commands.
4535
+
4536
+        :param native: list native, python-based commands
4537
+        :type native: bool
4538
+        :param external: list external aba-style commands
4539
+        :type external: bool
4540
+        """
4541
+        if native:
4542
+            print "Native Fai commands"
4543
+            keys=commands.keys()
4544
+            keys.sort()
4545
+            for k in keys:
4546
+                space=""
4547
+                for i in range(28-len(k)):
4548
+                    space+=" "
4549
+                print space+k+" : "+commands[k]().description
4550
+            print
4551
+        if suggest:
4552
+            print "Unavailable commands and suggested alternatives"
4553
+            key_list = suggestions.keys()
4554
+            key_list.sort()
4555
+            for key in key_list:
4556
+                print "%28s : %s" % (key, suggestions[key])
4557
+            print
4558
+        if external:
4559
+            fake_aba = abacmds.AbaCmds()
4560
+            if (fake_aba.abadir == ""):
4561
+                return
4562
+            print "External commands"
4563
+            fake_aba.list_commands()
4564
+            print
4565
+        if not suggest:
4566
+            print "Use help --suggest to list alternatives to tla and aba"\
4567
+                " commands."
4568
+        if options.tla_fallthrough and (native or external):
4569
+            print "Fai also supports tla commands."
4570
+
4571
+def command_help(cmd):
4572
+    """
4573
+    Prints help for a command.
4574
+
4575
+    :param cmd: The name of the command to print help for
4576
+    :type cmd: str
4577
+    """
4578
+    fake_aba = abacmds.AbaCmds()
4579
+    cmdobj = find_command(cmd)
4580
+    if cmdobj != None:
4581
+        cmdobj.help()
4582
+    elif suggestions.has_key(cmd):
4583
+        print "Not available\n" + suggestions[cmd]
4584
+    else:
4585
+        abacmd = fake_aba.is_command(cmd)
4586
+        if abacmd:
4587
+            abacmd.help()
4588
+        else:
4589
+            print "No help is available for \""+cmd+"\". Maybe try \"tla "+cmd+" -H\"?"
4590
+
4591
+
4592
+
4593
+class Changes(BaseCommand):
4594
+    """
4595
+    the "changes" command: lists differences between trees/revisions:
4596
+    """
4597
+    
4598
+    def __init__(self):
4599
+        self.description="Lists what files have changed in the project tree"
4600
+
4601
+    def get_completer(self, arg, index):
4602
+        if index > 1:
4603
+            return None
4604
+        try:
4605
+            tree = arch.tree_root()
4606
+        except:
4607
+            tree = None
4608
+        return cmdutil.iter_revision_completions(arg, tree)
4609
+    
4610
+    def parse_commandline(self, cmdline):
4611
+        """
4612
+        Parse commandline arguments.  Raises cmdutil.GetHelp if help is needed.
4613
+        
4614
+        :param cmdline: A list of arguments to parse
4615
+        :rtype: (options, Revision, Revision/WorkingTree)
4616
+        """
4617
+        parser=self.get_parser()
4618
+        (options, args) = parser.parse_args(cmdline)
4619
+        if len(args) > 2:
4620
+            raise cmdutil.GetHelp
4621
+
4622
+        tree=arch.tree_root()
4623
+        if len(args) == 0:
4624
+            a_spec = cmdutil.comp_revision(tree)
4625
+        else:
4626
+            a_spec = cmdutil.determine_revision_tree(tree, args[0])
4627
+        cmdutil.ensure_archive_registered(a_spec.archive)
4628
+        if len(args) == 2:
4629
+            b_spec = cmdutil.determine_revision_tree(tree, args[1])
4630
+            cmdutil.ensure_archive_registered(b_spec.archive)
4631
+        else:
4632
+            b_spec=tree
4633
+        return options, a_spec, b_spec
4634
+
4635
+    def do_command(self, cmdargs):
4636
+        """
4637
+        Master function that perfoms the "changes" command.
4638
+        """
4639
+        try:
4640
+            options, a_spec, b_spec = self.parse_commandline(cmdargs);
4641
+        except cmdutil.CantDetermineRevision, e:
4642
+            print e
4643
+            return
4644
+        except arch.errors.TreeRootError, e:
4645
+            print e
4646
+            return
4647
+        if options.changeset:
4648
+            changeset=options.changeset
4649
+            tmpdir = None
4650
+        else:
4651
+            tmpdir=cmdutil.tmpdir()
4652
+            changeset=tmpdir+"/changeset"
4653
+        try:
4654
+            delta=arch.iter_delta(a_spec, b_spec, changeset)
4655
+            try:
4656
+                for line in delta:
4657
+                    if cmdutil.chattermatch(line, "changeset:"):
4658
+                        pass
4659
+                    else:
4660
+                        cmdutil.colorize(line, options.suppress_chatter)
4661
+            except arch.util.ExecProblem, e:
4662
+                if e.proc.error and e.proc.error.startswith(
4663
+                    "missing explicit id for file"):
4664
+                    raise MissingID(e)
4665
+                else:
4666
+                    raise
4667
+            status=delta.status
4668
+            if status > 1:
4669
+                return
4670
+            if (options.perform_diff):
4671
+                chan = cmdutil.ChangesetMunger(changeset)
4672
+                chan.read_indices()
4673
+                if isinstance(b_spec, arch.Revision):
4674
+                    b_dir = b_spec.library_find()
4675
+                else:
4676
+                    b_dir = b_spec
4677
+                a_dir = a_spec.library_find()
4678
+                if options.diffopts is not None:
4679
+                    diffopts = options.diffopts.split()
4680
+                    cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir)
4681
+                else:
4682
+                    cmdutil.show_diffs(delta.changeset)
4683
+        finally:
4684
+            if tmpdir and (os.access(tmpdir, os.X_OK)):
4685
+                shutil.rmtree(tmpdir)
4686
+
4687
+    def get_parser(self):
4688
+        """
4689
+        Returns the options parser to use for the "changes" command.
4690
+
4691
+        :rtype: cmdutil.CmdOptionParser
4692
+        """
4693
+        parser=cmdutil.CmdOptionParser("fai changes [options] [revision]"
4694
+                                       " [revision]")
4695
+        parser.add_option("-d", "--diff", action="store_true", 
4696
+                          dest="perform_diff", default=False, 
4697
+                          help="Show diffs in summary")
4698
+        parser.add_option("-c", "--changeset", dest="changeset", 
4699
+                          help="Store a changeset in the given directory", 
4700
+                          metavar="DIRECTORY")
4701
+        parser.add_option("-s", "--silent", action="store_true", 
4702
+                          dest="suppress_chatter", default=False, 
4703
+                          help="Suppress chatter messages")
4704
+        parser.add_option("--diffopts", dest="diffopts", 
4705
+                          help="Use the specified diff options", 
4706
+                          metavar="OPTIONS")
4707
+
4708
+        return parser
4709
+
4710
+    def help(self, parser=None):
4711
+        """
4712
+        Prints a help message.
4713
+
4714
+        :param parser: If supplied, the parser to use for generating help.  If \
4715
+        not supplied, it is retrieved.
4716
+        :type parser: cmdutil.CmdOptionParser
4717
+        """
4718
+        if parser is None:
4719
+            parser=self.get_parser()
4720
+        parser.print_help()
4721
+        print """
4722
+Performs source-tree comparisons
4723
+
4724
+If no revision is specified, the current project tree is compared to the
4725
+last-committed revision.  If one revision is specified, the current project
4726
+tree is compared to that revision.  If two revisions are specified, they are
4727
+compared to each other.
4728
+        """
4729
+        help_tree_spec() 
4730
+        return
4731
+
4732
+
4733
+class ApplyChanges(BaseCommand):
4734
+    """
4735
+    Apply differences between two revisions to a tree
4736
+    """
4737
+    
4738
+    def __init__(self):
4739
+        self.description="Applies changes to a project tree"
4740
+    
4741
+    def get_completer(self, arg, index):
4742
+        if index > 1:
4743
+            return None
4744
+        try:
4745
+            tree = arch.tree_root()
4746
+        except:
4747
+            tree = None
4748
+        return cmdutil.iter_revision_completions(arg, tree)
4749
+
4750
+    def parse_commandline(self, cmdline, tree):
4751
+        """
4752
+        Parse commandline arguments.  Raises cmdutil.GetHelp if help is needed.
4753
+        
4754
+        :param cmdline: A list of arguments to parse
4755
+        :rtype: (options, Revision, Revision/WorkingTree)
4756
+        """
4757
+        parser=self.get_parser()
4758
+        (options, args) = parser.parse_args(cmdline)
4759
+        if len(args) != 2:
4760
+            raise cmdutil.GetHelp
4761
+
4762
+        a_spec = cmdutil.determine_revision_tree(tree, args[0])
4763
+        cmdutil.ensure_archive_registered(a_spec.archive)
4764
+        b_spec = cmdutil.determine_revision_tree(tree, args[1])
4765
+        cmdutil.ensure_archive_registered(b_spec.archive)
4766
+        return options, a_spec, b_spec
4767
+
4768
+    def do_command(self, cmdargs):
4769
+        """
4770
+        Master function that performs "apply-changes".
4771
+        """
4772
+        try:
4773
+            tree = arch.tree_root()
4774
+            options, a_spec, b_spec = self.parse_commandline(cmdargs, tree);
4775
+        except cmdutil.CantDetermineRevision, e:
4776
+            print e
4777
+            return
4778
+        except arch.errors.TreeRootError, e:
4779
+            print e
4780
+            return
4781
+        delta=cmdutil.apply_delta(a_spec, b_spec, tree)
4782
+        for line in cmdutil.iter_apply_delta_filter(delta):
4783
+            cmdutil.colorize(line, options.suppress_chatter)
4784
+
4785
+    def get_parser(self):
4786
+        """
4787
+        Returns the options parser to use for the "apply-changes" command.
4788
+
4789
+        :rtype: cmdutil.CmdOptionParser
4790
+        """
4791
+        parser=cmdutil.CmdOptionParser("fai apply-changes [options] revision"
4792
+                                       " revision")
4793
+        parser.add_option("-d", "--diff", action="store_true", 
4794
+                          dest="perform_diff", default=False, 
4795
+                          help="Show diffs in summary")
4796
+        parser.add_option("-c", "--changeset", dest="changeset", 
4797
+                          help="Store a changeset in the given directory", 
4798
+                          metavar="DIRECTORY")
4799
+        parser.add_option("-s", "--silent", action="store_true", 
4800
+                          dest="suppress_chatter", default=False, 
4801
+                          help="Suppress chatter messages")
4802
+        return parser
4803
+
4804
+    def help(self, parser=None):
4805
+        """
4806
+        Prints a help message.
4807
+
4808
+        :param parser: If supplied, the parser to use for generating help.  If \
4809
+        not supplied, it is retrieved.
4810
+        :type parser: cmdutil.CmdOptionParser
4811
+        """
4812
+        if parser is None:
4813
+            parser=self.get_parser()
4814
+        parser.print_help()
4815
+        print """
4816
+Applies changes to a project tree
4817
+
4818
+Compares two revisions and applies the difference between them to the current
4819
+tree.
4820
+        """
4821
+        help_tree_spec() 
4822
+        return
4823
+
4824
+class Update(BaseCommand):
4825
+    """
4826
+    Updates a project tree to a given revision, preserving un-committed hanges. 
4827
+    """
4828
+    
4829
+    def __init__(self):
4830
+        self.description="Apply the latest changes to the current directory"
4831
+
4832
+    def get_completer(self, arg, index):
4833
+        if index > 0:
4834
+            return None
4835
+        try:
4836
+            tree = arch.tree_root()
4837
+        except:
4838
+            tree = None
4839
+        return cmdutil.iter_revision_completions(arg, tree)
4840
+    
4841
+    def parse_commandline(self, cmdline, tree):
4842
+        """
4843
+        Parse commandline arguments.  Raises cmdutil.GetHelp if help is needed.
4844
+        
4845
+        :param cmdline: A list of arguments to parse
4846
+        :rtype: (options, Revision, Revision/WorkingTree)
4847
+        """
4848
+        parser=self.get_parser()
4849
+        (options, args) = parser.parse_args(cmdline)
4850
+        if len(args) > 2:
4851
+            raise cmdutil.GetHelp
4852
+
4853
+        spec=None
4854
+        if len(args)>0:
4855
+            spec=args[0]
4856
+        revision=cmdutil.determine_revision_arch(tree, spec)
4857
+        cmdutil.ensure_archive_registered(revision.archive)
4858
+
4859
+        mirror_source = cmdutil.get_mirror_source(revision.archive)
4860
+        if mirror_source != None:
4861
+            if cmdutil.prompt("Mirror update"):
4862
+                cmd=cmdutil.mirror_archive(mirror_source, 
4863
+                    revision.archive, arch.NameParser(revision).get_package_version())
4864
+                for line in arch.chatter_classifier(cmd):
4865
+                    cmdutil.colorize(line, options.suppress_chatter)
4866
+
4867
+                revision=cmdutil.determine_revision_arch(tree, spec)
4868
+
4869
+        return options, revision 
4870
+
4871
+    def do_command(self, cmdargs):
4872
+        """
4873
+        Master function that perfoms the "update" command.
4874
+        """
4875
+        tree=arch.tree_root()
4876
+        try:
4877
+            options, to_revision = self.parse_commandline(cmdargs, tree);
4878
+        except cmdutil.CantDetermineRevision, e:
4879
+            print e
4880
+            return
4881
+        except arch.errors.TreeRootError, e:
4882
+            print e
4883
+            return
4884
+        from_revision=cmdutil.tree_latest(tree)
4885
+        if from_revision==to_revision:
4886
+            print "Tree is already up to date with:\n"+str(to_revision)+"."
4887
+            return
4888
+        cmdutil.ensure_archive_registered(from_revision.archive)
4889
+        cmd=cmdutil.apply_delta(from_revision, to_revision, tree,
4890
+            options.patch_forward)
4891
+        for line in cmdutil.iter_apply_delta_filter(cmd):
4892
+            cmdutil.colorize(line)
4893
+        if to_revision.version != tree.tree_version:
4894
+            if cmdutil.prompt("Update version"):
4895
+                tree.tree_version = to_revision.version
4896
+
4897
+    def get_parser(self):
4898
+        """
4899
+        Returns the options parser to use for the "update" command.
4900
+
4901
+        :rtype: cmdutil.CmdOptionParser
4902
+        """
4903
+        parser=cmdutil.CmdOptionParser("fai update [options]"
4904
+                                       " [revision/version]")
4905
+        parser.add_option("-f", "--forward", action="store_true", 
4906
+                          dest="patch_forward", default=False, 
4907
+                          help="pass the --forward option to 'patch'")
4908
+        parser.add_option("-s", "--silent", action="store_true", 
4909
+                          dest="suppress_chatter", default=False, 
4910
+                          help="Suppress chatter messages")
4911
+        return parser
4912
+
4913
+    def help(self, parser=None):
4914
+        """
4915
+        Prints a help message.
4916
+
4917
+        :param parser: If supplied, the parser to use for generating help.  If \
4918
+        not supplied, it is retrieved.
4919
+        :type parser: cmdutil.CmdOptionParser
4920
+        """
4921
+        if parser is None:
4922
+            parser=self.get_parser()
4923
+        parser.print_help()
4924
+        print """
4925
+Updates a working tree to the current archive revision
4926
+
4927
+If a revision or version is specified, that is used instead 
4928
+        """
4929
+        help_tree_spec() 
4930
+        return
4931
+
4932
+
4933
+class Commit(BaseCommand):
4934
+    """
4935
+    Create a revision based on the changes in the current tree.
4936
+    """
4937
+    
4938
+    def __init__(self):
4939
+        self.description="Write local changes to the archive"
4940
+
4941
+    def get_completer(self, arg, index):
4942
+        if arg is None:
4943
+            arg = ""
4944
+        return iter_modified_file_completions(arch.tree_root(), arg)
4945
+#        return iter_source_file_completions(arch.tree_root(), arg)
4946
+    
4947
+    def parse_commandline(self, cmdline, tree):
4948
+        """
4949
+        Parse commandline arguments.  Raise cmtutil.GetHelp if help is needed.
4950
+        
4951
+        :param cmdline: A list of arguments to parse
4952
+        :rtype: (options, Revision, Revision/WorkingTree)
4953
+        """
4954
+        parser=self.get_parser()
4955
+        (options, args) = parser.parse_args(cmdline)
4956
+
4957
+        if len(args) == 0:
4958
+            args = None
4959
+        revision=cmdutil.determine_revision_arch(tree, options.version)
4960
+        return options, revision.get_version(), args
4961
+
4962
+    def do_command(self, cmdargs):
4963
+        """
4964
+        Master function that perfoms the "commit" command.
4965
+        """
4966
+        tree=arch.tree_root()
4967
+        options, version, files = self.parse_commandline(cmdargs, tree)
4968
+        if options.__dict__.has_key("base") and options.base:
4969
+            base = cmdutil.determine_revision_tree(tree, options.base)
4970
+        else:
4971
+            base = cmdutil.submit_revision(tree)
4972
+        
4973
+        writeversion=version
4974
+        archive=version.archive
4975
+        source=cmdutil.get_mirror_source(archive)
4976
+        allow_old=False
4977
+        writethrough="implicit"
4978
+
4979
+        if source!=None:
4980
+            if writethrough=="explicit" and \
4981
+                cmdutil.prompt("Writethrough"):
4982
+                writeversion=arch.Version(str(source)+"/"+str(version.get_nonarch()))
4983
+            elif writethrough=="none":
4984
+                raise CommitToMirror(archive)
4985
+
4986
+        elif archive.is_mirror:
4987
+            raise CommitToMirror(archive)
4988
+
4989
+        try:
4990
+            last_revision=tree.iter_logs(version, True).next().revision
4991
+        except StopIteration, e:
4992
+            if cmdutil.prompt("Import from commit"):
4993
+                return do_import(version)
4994
+            else:
4995
+                raise NoVersionLogs(version)
4996
+        if last_revision!=version.iter_revisions(True).next():
4997
+            if not cmdutil.prompt("Out of date"):
4998
+                raise OutOfDate
4999
+            else:
5000
+                allow_old=True
5001
+
5002
+        try:
5003
+            if not cmdutil.has_changed(version):
5004
+                if not cmdutil.prompt("Empty commit"):
5005
+                    raise EmptyCommit
5006
+        except arch.util.ExecProblem, e:
5007
+            if e.proc.error and e.proc.error.startswith(
5008
+                "missing explicit id for file"):
5009
+                raise MissingID(e)
5010
+            else:
5011
+                raise
5012
+        log = tree.log_message(create=False)
5013
+        if log is None:
5014
+            try:
5015
+                if cmdutil.prompt("Create log"):
5016
+                    edit_log(tree)
5017
+
5018
+            except cmdutil.NoEditorSpecified, e:
5019
+                raise CommandFailed(e)
5020
+            log = tree.log_message(create=False)
5021
+        if log is None: 
5022
+            raise NoLogMessage
5023
+        if log["Summary"] is None or len(log["Summary"].strip()) == 0:
5024
+            if not cmdutil.prompt("Omit log summary"):
5025
+                raise errors.NoLogSummary
5026
+        try:
5027
+            for line in tree.iter_commit(version, seal=options.seal_version,
5028
+                base=base, out_of_date_ok=allow_old, file_list=files):
5029
+                cmdutil.colorize(line, options.suppress_chatter)
5030
+
5031
+        except arch.util.ExecProblem, e:
5032
+            if e.proc.error and e.proc.error.startswith(
5033
+                "These files violate naming conventions:"):
5034
+                raise LintFailure(e.proc.error)
5035
+            else:
5036
+                raise
5037
+
5038
+    def get_parser(self):
5039
+        """
5040
+        Returns the options parser to use for the "commit" command.
5041
+
5042
+        :rtype: cmdutil.CmdOptionParser
5043
+        """
5044
+
5045
+        parser=cmdutil.CmdOptionParser("fai commit [options] [file1]"
5046
+                                       " [file2...]")
5047
+        parser.add_option("--seal", action="store_true", 
5048
+                          dest="seal_version", default=False, 
5049
+                          help="seal this version")
5050
+        parser.add_option("-v", "--version", dest="version", 
5051
+                          help="Use the specified version", 
5052
+                          metavar="VERSION")
5053
+        parser.add_option("-s", "--silent", action="store_true", 
5054
+                          dest="suppress_chatter", default=False, 
5055
+                          help="Suppress chatter messages")
5056
+        if cmdutil.supports_switch("commit", "--base"):
5057
+            parser.add_option("--base", dest="base", help="", 
5058
+                              metavar="REVISION")
5059
+        return parser
5060
+
5061
+    def help(self, parser=None):
5062
+        """
5063
+        Prints a help message.
5064
+
5065
+        :param parser: If supplied, the parser to use for generating help.  If \
5066
+        not supplied, it is retrieved.
5067
+        :type parser: cmdutil.CmdOptionParser
5068
+        """
5069
+        if parser is None:
5070
+            parser=self.get_parser()
5071
+        parser.print_help()
5072
+        print """
5073
+Updates a working tree to the current archive revision
5074
+
5075
+If a version is specified, that is used instead 
5076
+        """
5077
+#        help_tree_spec() 
5078
+        return
5079
+
5080
+
5081
+
5082
+class CatLog(BaseCommand):
5083
+    """
5084
+    Print the log of a given file (from current tree)
5085
+    """
5086
+    def __init__(self):
5087
+        self.description="Prints the patch log for a revision"
5088
+
5089
+    def get_completer(self, arg, index):
5090
+        if index > 0:
5091
+            return None
5092
+        try:
5093
+            tree = arch.tree_root()
5094
+        except:
5095
+            tree = None
5096
+        return cmdutil.iter_revision_completions(arg, tree)
5097
+
5098
+    def do_command(self, cmdargs):
5099
+        """
5100
+        Master function that perfoms the "cat-log" command.
5101
+        """
5102
+        parser=self.get_parser()
5103
+        (options, args) = parser.parse_args(cmdargs)
5104
+        try:
5105
+            tree = arch.tree_root()
5106
+        except arch.errors.TreeRootError, e:
5107
+            tree = None
5108
+        spec=None
5109
+        if len(args) > 0:
5110
+            spec=args[0]
5111
+        if len(args) > 1:
5112
+            raise cmdutil.GetHelp()
5113
+        try:
5114
+            if tree:
5115
+                revision = cmdutil.determine_revision_tree(tree, spec)
5116
+            else:
5117
+                revision = cmdutil.determine_revision_arch(tree, spec)
5118
+        except cmdutil.CantDetermineRevision, e:
5119
+            raise CommandFailedWrapper(e)
5120
+        log = None
5121
+        
5122
+        use_tree = (options.source == "tree" or \
5123
+            (options.source == "any" and tree))
5124
+        use_arch = (options.source == "archive" or options.source == "any")
5125
+        
5126
+        log = None
5127
+        if use_tree:
5128
+            for log in tree.iter_logs(revision.get_version()):
5129
+                if log.revision == revision:
5130
+                    break
5131
+                else:
5132
+                    log = None
5133
+        if log is None and use_arch:
5134
+            cmdutil.ensure_revision_exists(revision)
5135
+            log = arch.Patchlog(revision)
5136
+        if log is not None:
5137
+            for item in log.items():
5138
+                print "%s: %s" % item
5139
+            print log.description
5140
+
5141
+    def get_parser(self):
5142
+        """
5143
+        Returns the options parser to use for the "cat-log" command.
5144
+
5145
+        :rtype: cmdutil.CmdOptionParser
5146
+        """
5147
+        parser=cmdutil.CmdOptionParser("fai cat-log [revision]")
5148
+        parser.add_option("--archive", action="store_const", dest="source",
5149
+                          const="archive", default="any",
5150
+                          help="Always get the log from the archive")
5151
+        parser.add_option("--tree", action="store_const", dest="source",
5152
+                          const="tree", help="Always get the log from the tree")
5153
+        return parser 
5154
+
5155
+    def help(self, parser=None):
5156
+        """
5157
+        Prints a help message.
5158
+
5159
+        :param parser: If supplied, the parser to use for generating help.  If \
5160
+        not supplied, it is retrieved.
5161
+        :type parser: cmdutil.CmdOptionParser
5162
+        """
5163
+        if parser==None:
5164
+            parser=self.get_parser()
5165
+        parser.print_help()
5166
+        print """
5167
+Prints the log for the specified revision
5168
+        """
5169
+        help_tree_spec()
5170
+        return
5171
+
5172
+class Revert(BaseCommand):
5173
+    """ Reverts a tree (or aspects of it) to a revision
5174
+    """
5175
+    def __init__(self):
5176
+        self.description="Reverts a tree (or aspects of it) to a revision "
5177
+
5178
+    def get_completer(self, arg, index):
5179
+        if index > 0:
5180
+            return None
5181
+        try:
5182
+            tree = arch.tree_root()
5183
+        except:
5184
+            tree = None
5185
+        return iter_modified_file_completions(tree, arg)
5186
+
5187
+    def do_command(self, cmdargs):
5188
+        """
5189
+        Master function that perfoms the "revert" command.
5190
+        """
5191
+        parser=self.get_parser()
5192
+        (options, args) = parser.parse_args(cmdargs)
5193
+        try:
5194
+            tree = arch.tree_root()
5195
+        except arch.errors.TreeRootError, e:
5196
+            raise CommandFailed(e)
5197
+        spec=None
5198
+        if options.revision is not None:
5199
+            spec=options.revision
5200
+        try:
5201
+            if spec is not None:
5202
+                revision = cmdutil.determine_revision_tree(tree, spec)
5203
+            else:
5204
+                revision = cmdutil.comp_revision(tree)
5205
+        except cmdutil.CantDetermineRevision, e:
5206
+            raise CommandFailedWrapper(e)
5207
+        munger = None
5208
+
5209
+        if options.file_contents or options.file_perms or options.deletions\
5210
+            or options.additions or options.renames or options.hunk_prompt:
5211
+            munger = cmdutil.MungeOpts()
5212
+            munger.hunk_prompt = options.hunk_prompt
5213
+
5214
+        if len(args) > 0 or options.logs or options.pattern_files or \
5215
+            options.control:
5216
+            if munger is None:
5217
+                munger = cmdutil.MungeOpts(True)
5218
+                munger.all_types(True)
5219
+        if len(args) > 0:
5220
+            t_cwd = cmdutil.tree_cwd(tree)
5221
+            for name in args:
5222
+                if len(t_cwd) > 0:
5223
+                    t_cwd += "/"
5224
+                name = "./" + t_cwd + name
5225
+                munger.add_keep_file(name);
5226
+
5227
+        if options.file_perms:
5228
+            munger.file_perms = True
5229
+        if options.file_contents:
5230
+            munger.file_contents = True
5231
+        if options.deletions:
5232
+            munger.deletions = True
5233
+        if options.additions:
5234
+            munger.additions = True
5235
+        if options.renames:
5236
+            munger.renames = True
5237
+        if options.logs:
5238
+            munger.add_keep_pattern('^\./\{arch\}/[^=].*')
5239
+        if options.control:
5240
+            munger.add_keep_pattern("/\.arch-ids|^\./\{arch\}|"\
5241
+                                    "/\.arch-inventory$")
5242
+        if options.pattern_files:
5243
+            munger.add_keep_pattern(options.pattern_files)
5244
+                
5245
+        for line in cmdutil.revert(tree, revision, munger, 
5246
+                                   not options.no_output):
5247
+            cmdutil.colorize(line)
5248
+
5249
+
5250
+    def get_parser(self):
5251
+        """
5252
+        Returns the options parser to use for the "cat-log" command.
5253
+
5254
+        :rtype: cmdutil.CmdOptionParser
5255
+        """
5256
+        parser=cmdutil.CmdOptionParser("fai revert [options] [FILE...]")
5257
+        parser.add_option("", "--contents", action="store_true", 
5258
+                          dest="file_contents", 
5259
+                          help="Revert file content changes")
5260
+        parser.add_option("", "--permissions", action="store_true", 
5261
+                          dest="file_perms", 
5262
+                          help="Revert file permissions changes")
5263
+        parser.add_option("", "--deletions", action="store_true", 
5264
+                          dest="deletions", 
5265
+                          help="Restore deleted files")
5266
+        parser.add_option("", "--additions", action="store_true", 
5267
+                          dest="additions", 
5268
+                          help="Remove added files")
5269
+        parser.add_option("", "--renames", action="store_true", 
5270
+                          dest="renames", 
5271
+                          help="Revert file names")
5272
+        parser.add_option("--hunks", action="store_true", 
5273
+                          dest="hunk_prompt", default=False,
5274
+                          help="Prompt which hunks to revert")
5275
+        parser.add_option("--pattern-files", dest="pattern_files", 
5276
+                          help="Revert files that match this pattern", 
5277
+                          metavar="REGEX")
5278
+        parser.add_option("--logs", action="store_true", 
5279
+                          dest="logs", default=False,
5280
+                          help="Revert only logs")
5281
+        parser.add_option("--control-files", action="store_true", 
5282
+                          dest="control", default=False,
5283
+                          help="Revert logs and other control files")
5284
+        parser.add_option("-n", "--no-output", action="store_true", 
5285
+                          dest="no_output", 
5286
+                          help="Don't keep an undo changeset")
5287
+        parser.add_option("--revision", dest="revision", 
5288
+                          help="Revert to the specified revision", 
5289
+                          metavar="REVISION")
5290
+        return parser 
5291
+
5292
+    def help(self, parser=None):
5293
+        """
5294
+        Prints a help message.
5295
+
5296
+        :param parser: If supplied, the parser to use for generating help.  If \
5297
+        not supplied, it is retrieved.
5298
+        :type parser: cmdutil.CmdOptionParser
5299
+        """
5300
+        if parser==None:
5301
+            parser=self.get_parser()
5302
+        parser.print_help()
5303
+        print """
5304
+Reverts changes in the current working tree.  If no flags are specified, all
5305
+types of changes are reverted.  Otherwise, only selected types of changes are
5306
+reverted.  
5307
+
5308
+If a revision is specified on the commandline, differences between the current
5309
+tree and that revision are reverted.  If a version is specified, the current
5310
+tree is used to determine the revision.
5311
+
5312
+If files are specified, only those files listed will have any changes applied.
5313
+To specify a renamed file, you can use either the old or new name. (or both!)
5314
+
5315
+Unless "-n" is specified, reversions can be undone with "redo".
5316
+        """
5317
+        return
5318
+
5319
+class Revision(BaseCommand):
5320
+    """
5321
+    Print a revision name based on a revision specifier
5322
+    """
5323
+    def __init__(self):
5324
+        self.description="Prints the name of a revision"
5325
+
5326
+    def get_completer(self, arg, index):
5327
+        if index > 0:
5328
+            return None
5329
+        try:
5330
+            tree = arch.tree_root()
5331
+        except:
5332
+            tree = None
5333
+        return cmdutil.iter_revision_completions(arg, tree)
5334
+
5335
+    def do_command(self, cmdargs):
5336
+        """
5337
+        Master function that perfoms the "revision" command.
5338
+        """
5339
+        parser=self.get_parser()
5340
+        (options, args) = parser.parse_args(cmdargs)
5341
+
5342
+        try:
5343
+            tree = arch.tree_root()
5344
+        except arch.errors.TreeRootError:
5345
+            tree = None
5346
+
5347
+        spec=None
5348
+        if len(args) > 0:
5349
+            spec=args[0]
5350
+        if len(args) > 1:
5351
+            raise cmdutil.GetHelp
5352
+        try:
5353
+            if tree:
5354
+                revision = cmdutil.determine_revision_tree(tree, spec)
5355
+            else:
5356
+                revision = cmdutil.determine_revision_arch(tree, spec)
5357
+        except cmdutil.CantDetermineRevision, e:
5358
+            print str(e)
5359
+            return
5360
+        print options.display(revision)
5361
+
5362
+    def get_parser(self):
5363
+        """
5364
+        Returns the options parser to use for the "revision" command.
5365
+
5366
+        :rtype: cmdutil.CmdOptionParser
5367
+        """
5368
+        parser=cmdutil.CmdOptionParser("fai revision [revision]")
5369
+        parser.add_option("", "--location", action="store_const", 
5370
+                         const=paths.determine_path, dest="display", 
5371
+                         help="Show location instead of name", default=str)
5372
+        parser.add_option("--import", action="store_const", 
5373
+                         const=paths.determine_import_path, dest="display",  
5374
+                         help="Show location of import file")
5375
+        parser.add_option("--log", action="store_const", 
5376
+                         const=paths.determine_log_path, dest="display", 
5377
+                         help="Show location of log file")
5378
+        parser.add_option("--patch", action="store_const", 
5379
+                         dest="display", const=paths.determine_patch_path,
5380
+                         help="Show location of patchfile")
5381
+        parser.add_option("--continuation", action="store_const", 
5382
+                         const=paths.determine_continuation_path, 
5383
+                         dest="display",
5384
+                         help="Show location of continuation file")
5385
+        parser.add_option("--cacherev", action="store_const", 
5386
+                         const=paths.determine_cacherev_path, dest="display",
5387
+                         help="Show location of cacherev file")
5388
+        return parser 
5389
+
5390
+    def help(self, parser=None):
5391
+        """
5392
+        Prints a help message.
5393
+
5394
+        :param parser: If supplied, the parser to use for generating help.  If \
5395
+        not supplied, it is retrieved.
5396
+        :type parser: cmdutil.CmdOptionParser
5397
+        """
5398
+        if parser==None:
5399
+            parser=self.get_parser()
5400
+        parser.print_help()
5401
+        print """
5402
+Expands aliases and prints the name of the specified revision.  Instead of
5403
+the name, several options can be used to print locations.  If more than one is
5404
+specified, the last one is used.
5405
+        """
5406
+        help_tree_spec()
5407
+        return
5408
+
5409
+def require_version_exists(version, spec):
5410
+    if not version.exists():
5411
+        raise cmdutil.CantDetermineVersion(spec, 
5412
+                                           "The version %s does not exist." \
5413
+                                           % version)
5414
+
5415
+class Revisions(BaseCommand):
5416
+    """
5417
+    Print a revision name based on a revision specifier
5418
+    """
5419
+    def __init__(self):
5420
+        self.description="Lists revisions"
5421
+    
5422
+    def do_command(self, cmdargs):
5423
+        """
5424
+        Master function that perfoms the "revision" command.
5425
+        """
5426
+        (options, args) = self.get_parser().parse_args(cmdargs)
5427
+        if len(args) > 1:
5428
+            raise cmdutil.GetHelp
5429
+        try:
5430
+            self.tree = arch.tree_root()
5431
+        except arch.errors.TreeRootError:
5432
+            self.tree = None
5433
+        try:
5434
+            iter = self.get_iterator(options.type, args, options.reverse, 
5435
+                                     options.modified)
5436
+        except cmdutil.CantDetermineRevision, e:
5437
+            raise CommandFailedWrapper(e)
5438
+
5439
+        if options.skip is not None:
5440
+            iter = cmdutil.iter_skip(iter, int(options.skip))
5441
+
5442
+        for revision in iter:
5443
+            log = None
5444
+            if isinstance(revision, arch.Patchlog):
5445
+                log = revision
5446
+                revision=revision.revision
5447
+            print options.display(revision)
5448
+            if log is None and (options.summary or options.creator or 
5449
+                                options.date or options.merges):
5450
+                log = revision.patchlog
5451
+            if options.creator:
5452
+                print "    %s" % log.creator
5453
+            if options.date:
5454
+                print "    %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date)
5455
+            if options.summary:
5456
+                print "    %s" % log.summary
5457
+            if options.merges:
5458
+                showed_title = False
5459
+                for revision in log.merged_patches:
5460
+                    if not showed_title:
5461
+                        print "    Merged:"
5462
+                        showed_title = True
5463
+                    print "    %s" % revision
5464
+
5465
+    def get_iterator(self, type, args, reverse, modified):
5466
+        if len(args) > 0:
5467
+            spec = args[0]
5468
+        else:
5469
+            spec = None
5470
+        if modified is not None:
5471
+            iter = cmdutil.modified_iter(modified, self.tree)
5472
+            if reverse:
5473
+                return iter
5474
+            else:
5475
+                return cmdutil.iter_reverse(iter)
5476
+        elif type == "archive":
5477
+            if spec is None:
5478
+                if self.tree is None:
5479
+                    raise cmdutil.CantDetermineRevision("", 
5480
+                                                        "Not in a project tree")
5481
+                version = cmdutil.determine_version_tree(spec, self.tree)
5482
+            else:
5483
+                version = cmdutil.determine_version_arch(spec, self.tree)
5484
+                cmdutil.ensure_archive_registered(version.archive)
5485
+                require_version_exists(version, spec)
5486
+            return version.iter_revisions(reverse)
5487
+        elif type == "cacherevs":
5488
+            if spec is None:
5489
+                if self.tree is None:
5490
+                    raise cmdutil.CantDetermineRevision("", 
5491
+                                                        "Not in a project tree")
5492
+                version = cmdutil.determine_version_tree(spec, self.tree)
5493
+            else:
5494
+                version = cmdutil.determine_version_arch(spec, self.tree)
5495
+                cmdutil.ensure_archive_registered(version.archive)
5496
+                require_version_exists(version, spec)
5497
+            return cmdutil.iter_cacherevs(version, reverse)
5498
+        elif type == "library":
5499
+            if spec is None:
5500
+                if self.tree is None:
5501
+                    raise cmdutil.CantDetermineRevision("", 
5502
+                                                        "Not in a project tree")
5503
+                version = cmdutil.determine_version_tree(spec, self.tree)
5504
+            else:
5505
+                version = cmdutil.determine_version_arch(spec, self.tree)
5506
+            return version.iter_library_revisions(reverse)
5507
+        elif type == "logs":
5508
+            if self.tree is None:
5509
+                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
5510
+            return self.tree.iter_logs(cmdutil.determine_version_tree(spec, \
5511
+                                  self.tree), reverse)
5512
+        elif type == "missing" or type == "skip-present":
5513
+            if self.tree is None:
5514
+                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
5515
+            skip = (type == "skip-present")
5516
+            version = cmdutil.determine_version_tree(spec, self.tree)
5517
+            cmdutil.ensure_archive_registered(version.archive)
5518
+            require_version_exists(version, spec)
5519
+            return cmdutil.iter_missing(self.tree, version, reverse,
5520
+                                        skip_present=skip)
5521
+
5522
+        elif type == "present":
5523
+            if self.tree is None:
5524
+                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
5525
+            version = cmdutil.determine_version_tree(spec, self.tree)
5526
+            cmdutil.ensure_archive_registered(version.archive)
5527
+            require_version_exists(version, spec)
5528
+            return cmdutil.iter_present(self.tree, version, reverse)
5529
+
5530
+        elif type == "new-merges" or type == "direct-merges":
5531
+            if self.tree is None:
5532
+                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
5533
+            version = cmdutil.determine_version_tree(spec, self.tree)
5534
+            cmdutil.ensure_archive_registered(version.archive)
5535
+            require_version_exists(version, spec)
5536
+            iter = cmdutil.iter_new_merges(self.tree, version, reverse)
5537
+            if type == "new-merges":
5538
+                return iter
5539
+            elif type == "direct-merges":
5540
+                return cmdutil.direct_merges(iter)
5541
+
5542
+        elif type == "missing-from":
5543
+            if self.tree is None:
5544
+                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
5545
+            revision = cmdutil.determine_revision_tree(self.tree, spec)
5546
+            libtree = cmdutil.find_or_make_local_revision(revision)
5547
+            return cmdutil.iter_missing(libtree, self.tree.tree_version,
5548
+                                        reverse)
5549
+
5550
+        elif type == "partner-missing":
5551
+            return cmdutil.iter_partner_missing(self.tree, reverse)
5552
+
5553
+        elif type == "ancestry":
5554
+            revision = cmdutil.determine_revision_tree(self.tree, spec)
5555
+            iter = cmdutil._iter_ancestry(self.tree, revision)
5556
+            if reverse:
5557
+                return iter
5558
+            else:
5559
+                return cmdutil.iter_reverse(iter)
5560
+
5561
+        elif type == "dependencies" or type == "non-dependencies":
5562
+            nondeps = (type == "non-dependencies")
5563
+            revision = cmdutil.determine_revision_tree(self.tree, spec)
5564
+            anc_iter = cmdutil._iter_ancestry(self.tree, revision)
5565
+            iter_depends = cmdutil.iter_depends(anc_iter, nondeps)
5566
+            if reverse:
5567
+                return iter_depends
5568
+            else:
5569
+                return cmdutil.iter_reverse(iter_depends)
5570
+        elif type == "micro":
5571
+            return cmdutil.iter_micro(self.tree)
5572
+
5573
+    
5574
+    def get_parser(self):
5575
+        """
5576
+        Returns the options parser to use for the "revision" command.
5577
+
5578
+        :rtype: cmdutil.CmdOptionParser
5579
+        """
5580
+        parser=cmdutil.CmdOptionParser("fai revisions [revision]")
5581
+        select = cmdutil.OptionGroup(parser, "Selection options",
5582
+                          "Control which revisions are listed.  These options"
5583
+                          " are mutually exclusive.  If more than one is"
5584
+                          " specified, the last is used.")
5585
+        select.add_option("", "--archive", action="store_const", 
5586
+                          const="archive", dest="type", default="archive",
5587
+                          help="List all revisions in the archive")
5588
+        select.add_option("", "--cacherevs", action="store_const", 
5589
+                          const="cacherevs", dest="type",
5590
+                          help="List all revisions stored in the archive as "
5591
+                          "complete copies")
5592
+        select.add_option("", "--logs", action="store_const", 
5593
+                          const="logs", dest="type",
5594
+                          help="List revisions that have a patchlog in the "
5595
+                          "tree")
5596
+        select.add_option("", "--missing", action="store_const", 
5597
+                          const="missing", dest="type",
5598
+                          help="List revisions from the specified version that"
5599
+                          " have no patchlog in the tree")
5600
+        select.add_option("", "--skip-present", action="store_const", 
5601
+                          const="skip-present", dest="type",
5602
+                          help="List revisions from the specified version that"
5603
+                          " have no patchlogs at all in the tree")
5604
+        select.add_option("", "--present", action="store_const", 
5605
+                          const="present", dest="type",
5606
+                          help="List revisions from the specified version that"
5607
+                          " have no patchlog in the tree, but can't be merged")
5608
+        select.add_option("", "--missing-from", action="store_const", 
5609
+                          const="missing-from", dest="type",
5610
+                          help="List revisions from the specified revision "
5611
+                          "that have no patchlog for the tree version")
5612
+        select.add_option("", "--partner-missing", action="store_const", 
5613
+                          const="partner-missing", dest="type",
5614
+                          help="List revisions in partner versions that are"
5615
+                          " missing")
5616
+        select.add_option("", "--new-merges", action="store_const", 
5617
+                          const="new-merges", dest="type",
5618
+                          help="List revisions that have had patchlogs added"
5619
+                          " to the tree since the last commit")
5620
+        select.add_option("", "--direct-merges", action="store_const", 
5621
+                          const="direct-merges", dest="type",
5622
+                          help="List revisions that have been directly added"
5623
+                          " to tree since the last commit ")
5624
+        select.add_option("", "--library", action="store_const", 
5625
+                          const="library", dest="type",
5626
+                          help="List revisions in the revision library")
5627
+        select.add_option("", "--ancestry", action="store_const", 
5628
+                          const="ancestry", dest="type",
5629
+                          help="List revisions that are ancestors of the "
5630
+                          "current tree version")
5631
+
5632
+        select.add_option("", "--dependencies", action="store_const", 
5633
+                          const="dependencies", dest="type",
5634
+                          help="List revisions that the given revision "
5635
+                          "depends on")
5636
+
5637
+        select.add_option("", "--non-dependencies", action="store_const", 
5638
+                          const="non-dependencies", dest="type",
5639
+                          help="List revisions that the given revision "
5640
+                          "does not depend on")
5641
+
5642
+        select.add_option("--micro", action="store_const", 
5643
+                          const="micro", dest="type",
5644
+                          help="List partner revisions aimed for this "
5645
+                          "micro-branch")
5646
+
5647
+        select.add_option("", "--modified", dest="modified", 
5648
+                          help="List tree ancestor revisions that modified a "
5649
+                          "given file", metavar="FILE[:LINE]")
5650
+
5651
+        parser.add_option("", "--skip", dest="skip", 
5652
+                          help="Skip revisions.  Positive numbers skip from "
5653
+                          "beginning, negative skip from end.",
5654
+                          metavar="NUMBER")
5655
+
5656
+        parser.add_option_group(select)
5657
+
5658
+        format = cmdutil.OptionGroup(parser, "Revision format options",
5659
+                          "These control the appearance of listed revisions")
5660
+        format.add_option("", "--location", action="store_const", 
5661
+                         const=paths.determine_path, dest="display", 
5662
+                         help="Show location instead of name", default=str)
5663
+        format.add_option("--import", action="store_const", 
5664
+                         const=paths.determine_import_path, dest="display",  
5665
+                         help="Show location of import file")
5666
+        format.add_option("--log", action="store_const", 
5667
+                         const=paths.determine_log_path, dest="display", 
5668
+                         help="Show location of log file")
5669
+        format.add_option("--patch", action="store_const", 
5670
+                         dest="display", const=paths.determine_patch_path,
5671
+                         help="Show location of patchfile")
5672
+        format.add_option("--continuation", action="store_const", 
5673
+                         const=paths.determine_continuation_path, 
5674
+                         dest="display",
5675
+                         help="Show location of continuation file")
5676
+        format.add_option("--cacherev", action="store_const", 
5677
+                         const=paths.determine_cacherev_path, dest="display",
5678
+                         help="Show location of cacherev file")
5679
+        parser.add_option_group(format)
5680
+        display = cmdutil.OptionGroup(parser, "Display format options",
5681
+                          "These control the display of data")
5682
+        display.add_option("-r", "--reverse", action="store_true", 
5683
+                          dest="reverse", help="Sort from newest to oldest")
5684
+        display.add_option("-s", "--summary", action="store_true", 
5685
+                          dest="summary", help="Show patchlog summary")
5686
+        display.add_option("-D", "--date", action="store_true", 
5687
+                          dest="date", help="Show patchlog date")
5688
+        display.add_option("-c", "--creator", action="store_true", 
5689
+                          dest="creator", help="Show the id that committed the"
5690
+                          " revision")
5691
+        display.add_option("-m", "--merges", action="store_true", 
5692
+                          dest="merges", help="Show the revisions that were"
5693
+                          " merged")
5694
+        parser.add_option_group(display)
5695
+        return parser 
5696
+    def help(self, parser=None):
5697
+        """Attempt to explain the revisions command
5698
+        
5699
+        :param parser: If supplied, used to determine options
5700
+        """
5701
+        if parser==None:
5702
+            parser=self.get_parser()
5703
+        parser.print_help()
5704
+        print """List revisions.
5705
+        """
5706
+        help_tree_spec()
5707
+
5708
+
5709
+class Get(BaseCommand):
5710
+    """
5711
+    Retrieve a revision from the archive
5712
+    """
5713
+    def __init__(self):
5714
+        self.description="Retrieve a revision from the archive"
5715
+        self.parser=self.get_parser()
5716
+
5717
+
5718
+    def get_completer(self, arg, index):
5719
+        if index > 0:
5720
+            return None
5721
+        try:
5722
+            tree = arch.tree_root()
5723
+        except:
5724
+            tree = None
5725
+        return cmdutil.iter_revision_completions(arg, tree)
5726
+
5727
+
5728
+    def do_command(self, cmdargs):
5729
+        """
5730
+        Master function that perfoms the "get" command.
5731
+        """
5732
+        (options, args) = self.parser.parse_args(cmdargs)
5733
+        if len(args) < 1:
5734
+            return self.help()            
5735
+        try:
5736
+            tree = arch.tree_root()
5737
+        except arch.errors.TreeRootError:
5738
+            tree = None
5739
+        
5740
+        arch_loc = None
5741
+        try:
5742
+            revision, arch_loc = paths.full_path_decode(args[0])
5743
+        except Exception, e:
5744
+            revision = cmdutil.determine_revision_arch(tree, args[0], 
5745
+                check_existence=False, allow_package=True)
5746
+        if len(args) > 1:
5747
+            directory = args[1]
5748
+        else:
5749
+            directory = str(revision.nonarch)
5750
+        if os.path.exists(directory):
5751
+            raise DirectoryExists(directory)
5752
+        cmdutil.ensure_archive_registered(revision.archive, arch_loc)
5753
+        try:
5754
+            cmdutil.ensure_revision_exists(revision)
5755
+        except cmdutil.NoSuchRevision, e:
5756
+            raise CommandFailedWrapper(e)
5757
+
5758
+        link = cmdutil.prompt ("get link")
5759
+        for line in cmdutil.iter_get(revision, directory, link,
5760
+                                     options.no_pristine,
5761
+                                     options.no_greedy_add):
5762
+            cmdutil.colorize(line)
5763
+
5764
+    def get_parser(self):
5765
+        """
5766
+        Returns the options parser to use for the "get" command.
5767
+
5768
+        :rtype: cmdutil.CmdOptionParser
5769
+        """
5770
+        parser=cmdutil.CmdOptionParser("fai get revision [dir]")
5771
+        parser.add_option("--no-pristine", action="store_true", 
5772
+                         dest="no_pristine", 
5773
+                         help="Do not make pristine copy for reference")
5774
+        parser.add_option("--no-greedy-add", action="store_true", 
5775
+                         dest="no_greedy_add", 
5776
+                         help="Never add to greedy libraries")
5777
+
5778
+        return parser 
5779
+
5780
+    def help(self, parser=None):
5781
+        """
5782
+        Prints a help message.
5783
+
5784
+        :param parser: If supplied, the parser to use for generating help.  If \
5785
+        not supplied, it is retrieved.
5786
+        :type parser: cmdutil.CmdOptionParser
5787
+        """
5788
+        if parser==None:
5789
+            parser=self.get_parser()
5790
+        parser.print_help()
5791
+        print """
5792
+Expands aliases and constructs a project tree for a revision.  If the optional
5793
+"dir" argument is provided, the project tree will be stored in this directory.
5794
+        """
5795
+        help_tree_spec()
5796
+        return
5797
+
5798
+class PromptCmd(cmd.Cmd):
5799
+    def __init__(self):
5800
+        cmd.Cmd.__init__(self)
5801
+        self.prompt = "Fai> "
5802
+        try:
5803
+            self.tree = arch.tree_root()
5804
+        except:
5805
+            self.tree = None
5806
+        self.set_title()
5807
+        self.set_prompt()
5808
+        self.fake_aba = abacmds.AbaCmds()
5809
+        self.identchars += '-'
5810
+        self.history_file = os.path.expanduser("~/.fai-history")
5811
+        readline.set_completer_delims(string.whitespace)
5812
+        if os.access(self.history_file, os.R_OK) and \
5813
+            os.path.isfile(self.history_file):
5814
+            readline.read_history_file(self.history_file)
5815
+
5816
+    def write_history(self):
5817
+        readline.write_history_file(self.history_file)
5818
+
5819
+    def do_quit(self, args):
5820
+        self.write_history()
5821
+        sys.exit(0)
5822
+
5823
+    def do_exit(self, args):
5824
+        self.do_quit(args)
5825
+
5826
+    def do_EOF(self, args):
5827
+        print
5828
+        self.do_quit(args)
5829
+
5830
+    def postcmd(self, line, bar):
5831
+        self.set_title()
5832
+        self.set_prompt()
5833
+
5834
+    def set_prompt(self):
5835
+        if self.tree is not None:
5836
+            try:
5837
+                version = " "+self.tree.tree_version.nonarch
5838
+            except:
5839
+                version = ""
5840
+        else:
5841
+            version = ""
5842
+        self.prompt = "Fai%s> " % version
5843
+
5844
+    def set_title(self, command=None):
5845
+        try:
5846
+            version = self.tree.tree_version.nonarch
5847
+        except:
5848
+            version = "[no version]"
5849
+        if command is None:
5850
+            command = ""
5851
+        sys.stdout.write(terminal.term_title("Fai %s %s" % (command, version)))
5852
+
5853
+    def do_cd(self, line):
5854
+        if line == "":
5855
+            line = "~"
5856
+        try:
5857
+            os.chdir(os.path.expanduser(line))
5858
+        except Exception, e:
5859
+            print e
5860
+        try:
5861
+            self.tree = arch.tree_root()
5862
+        except:
5863
+            self.tree = None
5864
+
5865
+    def do_help(self, line):
5866
+        Help()(line)
5867
+
5868
+    def default(self, line):
5869
+        args = line.split()
5870
+        if find_command(args[0]):
5871
+            try:
5872
+                find_command(args[0]).do_command(args[1:])
5873
+            except cmdutil.BadCommandOption, e:
5874
+                print e
5875
+            except cmdutil.GetHelp, e:
5876
+                find_command(args[0]).help()
5877
+            except CommandFailed, e:
5878
+                print e
5879
+            except arch.errors.ArchiveNotRegistered, e:
5880
+                print e
5881
+            except KeyboardInterrupt, e:
5882
+                print "Interrupted"
5883
+            except arch.util.ExecProblem, e:
5884
+                print e.proc.error.rstrip('\n')
5885
+            except cmdutil.CantDetermineVersion, e:
5886
+                print e
5887
+            except cmdutil.CantDetermineRevision, e:
5888
+                print e
5889
+            except Exception, e:
5890
+                print "Unhandled error:\n%s" % cmdutil.exception_str(e)
5891
+
5892
+        elif suggestions.has_key(args[0]):
5893
+            print suggestions[args[0]]
5894
+
5895
+        elif self.fake_aba.is_command(args[0]):
5896
+            tree = None
5897
+            try:
5898
+                tree = arch.tree_root()
5899
+            except arch.errors.TreeRootError:
5900
+                pass
5901
+            cmd = self.fake_aba.is_command(args[0])
5902
+            try:
5903
+                cmd.run(cmdutil.expand_prefix_alias(args[1:], tree))
5904
+            except KeyboardInterrupt, e:
5905
+                print "Interrupted"
5906
+
5907
+        elif options.tla_fallthrough and args[0] != "rm" and \
5908
+            cmdutil.is_tla_command(args[0]):
5909
+            try:
5910
+                tree = None
5911
+                try:
5912
+                    tree = arch.tree_root()
5913
+                except arch.errors.TreeRootError:
5914
+                    pass
5915
+                args = cmdutil.expand_prefix_alias(args, tree)
5916
+                arch.util.exec_safe('tla', args, stderr=sys.stderr,
5917
+                expected=(0, 1))
5918
+            except arch.util.ExecProblem, e:
5919
+                pass
5920
+            except KeyboardInterrupt, e:
5921
+                print "Interrupted"
5922
+        else:
5923
+            try:
5924
+                try:
5925
+                    tree = arch.tree_root()
5926
+                except arch.errors.TreeRootError:
5927
+                    tree = None
5928
+                args=line.split()
5929
+                os.system(" ".join(cmdutil.expand_prefix_alias(args, tree)))
5930
+            except KeyboardInterrupt, e:
5931
+                print "Interrupted"
5932
+
5933
+    def completenames(self, text, line, begidx, endidx):
5934
+        completions = []
5935
+        iter = iter_command_names(self.fake_aba)
5936
+        try:
5937
+            if len(line) > 0:
5938
+                arg = line.split()[-1]
5939
+            else:
5940
+                arg = ""
5941
+            iter = iter_munged_completions(iter, arg, text)
5942
+        except Exception, e:
5943
+            print e
5944
+        return list(iter)
5945
+
5946
+    def completedefault(self, text, line, begidx, endidx):
5947
+        """Perform completion for native commands.
5948
+        
5949
+        :param text: The text to complete
5950
+        :type text: str
5951
+        :param line: The entire line to complete
5952
+        :type line: str
5953
+        :param begidx: The start of the text in the line
5954
+        :type begidx: int
5955
+        :param endidx: The end of the text in the line
5956
+        :type endidx: int
5957
+        """
5958
+        try:
5959
+            (cmd, args, foo) = self.parseline(line)
5960
+            command_obj=find_command(cmd)
5961
+            if command_obj is not None:
5962
+                return command_obj.complete(args.split(), text)
5963
+            elif not self.fake_aba.is_command(cmd) and \
5964
+                cmdutil.is_tla_command(cmd):
5965
+                iter = cmdutil.iter_supported_switches(cmd)
5966
+                if len(args) > 0:
5967
+                    arg = args.split()[-1]
5968
+                else:
5969
+                    arg = ""
5970
+                if arg.startswith("-"):
5971
+                    return list(iter_munged_completions(iter, arg, text))
5972
+                else:
5973
+                    return list(iter_munged_completions(
5974
+                        iter_file_completions(arg), arg, text))
5975
+
5976
+
5977
+            elif cmd == "cd":
5978
+                if len(args) > 0:
5979
+                    arg = args.split()[-1]
5980
+                else:
5981
+                    arg = ""
5982
+                iter = iter_dir_completions(arg)
5983
+                iter = iter_munged_completions(iter, arg, text)
5984
+                return list(iter)
5985
+            elif len(args)>0:
5986
+                arg = args.split()[-1]
5987
+                return list(iter_munged_completions(iter_file_completions(arg),
5988
+                                                    arg, text))
5989
+            else:
5990
+                return self.completenames(text, line, begidx, endidx)
5991
+        except Exception, e:
5992
+            print e
5993
+
5994
+
5995
+def iter_command_names(fake_aba):
5996
+    for entry in cmdutil.iter_combine([commands.iterkeys(), 
5997
+                                     fake_aba.get_commands(), 
5998
+                                     cmdutil.iter_tla_commands(False)]):
5999
+        if not suggestions.has_key(str(entry)):
6000
+            yield entry
6001
+
6002
+
6003
+def iter_file_completions(arg, only_dirs = False):
6004
+    """Generate an iterator that iterates through filename completions.
6005
+
6006
+    :param arg: The filename fragment to match
6007
+    :type arg: str
6008
+    :param only_dirs: If true, match only directories
6009
+    :type only_dirs: bool
6010
+    """
6011
+    cwd = os.getcwd()
6012
+    if cwd != "/":
6013
+        extras = [".", ".."]
6014
+    else:
6015
+        extras = []
6016
+    (dir, file) = os.path.split(arg)
6017
+    if dir != "":
6018
+        listingdir = os.path.expanduser(dir)
6019
+    else:
6020
+        listingdir = cwd
6021
+    for file in cmdutil.iter_combine([os.listdir(listingdir), extras]):
6022
+        if dir != "":
6023
+            userfile = dir+'/'+file
6024
+        else:
6025
+            userfile = file
6026
+        if userfile.startswith(arg):
6027
+            if os.path.isdir(listingdir+'/'+file):
6028
+                userfile+='/'
6029
+                yield userfile
6030
+            elif not only_dirs:
6031
+                yield userfile
6032
+
6033
+def iter_munged_completions(iter, arg, text):
6034
+    for completion in iter:
6035
+        completion = str(completion)
6036
+        if completion.startswith(arg):
6037
+            yield completion[len(arg)-len(text):]
6038
+
6039
+def iter_source_file_completions(tree, arg):
6040
+    treepath = cmdutil.tree_cwd(tree)
6041
+    if len(treepath) > 0:
6042
+        dirs = [treepath]
6043
+    else:
6044
+        dirs = None
6045
+    for file in tree.iter_inventory(dirs, source=True, both=True):
6046
+        file = file_completion_match(file, treepath, arg)
6047
+        if file is not None:
6048
+            yield file
6049
+
6050
+
6051
+def iter_untagged(tree, dirs):
6052
+    for file in arch_core.iter_inventory_filter(tree, dirs, tagged=False, 
6053
+                                                categories=arch_core.non_root,
6054
+                                                control_files=True):
6055
+        yield file.name 
6056
+
6057
+
6058
+def iter_untagged_completions(tree, arg):
6059
+    """Generate an iterator for all visible untagged files that match arg.
6060
+
6061
+    :param tree: The tree to look for untagged files in
6062
+    :type tree: `arch.WorkingTree`
6063
+    :param arg: The argument to match
6064
+    :type arg: str
6065
+    :return: An iterator of all matching untagged files
6066
+    :rtype: iterator of str
6067
+    """
6068
+    treepath = cmdutil.tree_cwd(tree)
6069
+    if len(treepath) > 0:
6070
+        dirs = [treepath]
6071
+    else:
6072
+        dirs = None
6073
+
6074
+    for file in iter_untagged(tree, dirs):
6075
+        file = file_completion_match(file, treepath, arg)
6076
+        if file is not None:
6077
+            yield file
6078
+
6079
+
6080
+def file_completion_match(file, treepath, arg):
6081
+    """Determines whether a file within an arch tree matches the argument.
6082
+
6083
+    :param file: The rooted filename
6084
+    :type file: str
6085
+    :param treepath: The path to the cwd within the tree
6086
+    :type treepath: str
6087
+    :param arg: The prefix to match
6088
+    :return: The completion name, or None if not a match
6089
+    :rtype: str
6090
+    """
6091
+    if not file.startswith(treepath):
6092
+        return None
6093
+    if treepath != "":
6094
+        file = file[len(treepath)+1:]
6095
+
6096
+    if not file.startswith(arg):
6097
+        return None 
6098
+    if os.path.isdir(file):
6099
+        file += '/'
6100
+    return file
6101
+
6102
+def iter_modified_file_completions(tree, arg):
6103
+    """Returns a list of modified files that match the specified prefix.
6104
+
6105
+    :param tree: The current tree
6106
+    :type tree: `arch.WorkingTree`
6107
+    :param arg: The prefix to match
6108
+    :type arg: str
6109
+    """
6110
+    treepath = cmdutil.tree_cwd(tree)
6111
+    tmpdir = cmdutil.tmpdir()
6112
+    changeset = tmpdir+"/changeset"
6113
+    completions = []
6114
+    revision = cmdutil.determine_revision_tree(tree)
6115
+    for line in arch.iter_delta(revision, tree, changeset):
6116
+        if isinstance(line, arch.FileModification):
6117
+            file = file_completion_match(line.name[1:], treepath, arg)
6118
+            if file is not None:
6119
+                completions.append(file)
6120
+    shutil.rmtree(tmpdir)
6121
+    return completions
6122
+
6123
+def iter_dir_completions(arg):
6124
+    """Generate an iterator that iterates through directory name completions.
6125
+
6126
+    :param arg: The directory name fragment to match
6127
+    :type arg: str
6128
+    """
6129
+    return iter_file_completions(arg, True)
6130
+
6131
+class Shell(BaseCommand):
6132
+    def __init__(self):
6133
+        self.description = "Runs Fai as a shell"
6134
+
6135
+    def do_command(self, cmdargs):
6136
+        if len(cmdargs)!=0:
6137
+            raise cmdutil.GetHelp
6138
+        prompt = PromptCmd()
6139
+        try:
6140
+            prompt.cmdloop()
6141
+        finally:
6142
+            prompt.write_history()
6143
+
6144
+class AddID(BaseCommand):
6145
+    """
6146
+    Adds an inventory id for the given file
6147
+    """
6148
+    def __init__(self):
6149
+        self.description="Add an inventory id for a given file"
6150
+
6151
+    def get_completer(self, arg, index):
6152
+        tree = arch.tree_root()
6153
+        return iter_untagged_completions(tree, arg)
6154
+
6155
+    def do_command(self, cmdargs):
6156
+        """
6157
+        Master function that perfoms the "revision" command.
6158
+        """
6159
+        parser=self.get_parser()
6160
+        (options, args) = parser.parse_args(cmdargs)
6161
+
6162
+        tree = arch.tree_root()
6163
+
6164
+        if (len(args) == 0) == (options.untagged == False):
6165
+            raise cmdutil.GetHelp
6166
+
6167
+       #if options.id and len(args) != 1:
6168
+       #    print "If --id is specified, only one file can be named."
6169
+       #    return
6170
+        
6171
+        method = tree.tagging_method
6172
+        
6173
+        if options.id_type == "tagline":
6174
+            if method != "tagline":
6175
+                if not cmdutil.prompt("Tagline in other tree"):
6176
+                    if method == "explicit":
6177
+                        options.id_type == explicit
6178
+                    else:
6179
+                        print "add-id not supported for \"%s\" tagging method"\
6180
+                            % method 
6181
+                        return
6182
+        
6183
+        elif options.id_type == "explicit":
6184
+            if method != "tagline" and method != explicit:
6185
+                if not prompt("Explicit in other tree"):
6186
+                    print "add-id not supported for \"%s\" tagging method" % \
6187
+                        method
6188
+                    return
6189
+        
6190
+        if options.id_type == "auto":
6191
+            if method != "tagline" and method != "explicit":
6192
+                print "add-id not supported for \"%s\" tagging method" % method
6193
+                return
6194
+            else:
6195
+                options.id_type = method
6196
+        if options.untagged:
6197
+            args = None
6198
+        self.add_ids(tree, options.id_type, args)
6199
+
6200
+    def add_ids(self, tree, id_type, files=()):
6201
+        """Add inventory ids to files.
6202
+        
6203
+        :param tree: the tree the files are in
6204
+        :type tree: `arch.WorkingTree`
6205
+        :param id_type: the type of id to add: "explicit" or "tagline"
6206
+        :type id_type: str
6207
+        :param files: The list of files to add.  If None do all untagged.
6208
+        :type files: tuple of str
6209
+        """
6210
+
6211
+        untagged = (files is None)
6212
+        if untagged:
6213
+            files = list(iter_untagged(tree, None))
6214
+        previous_files = []
6215
+        while len(files) > 0:
6216
+            previous_files.extend(files)
6217
+            if id_type == "explicit":
6218
+                cmdutil.add_id(files)
6219
+            elif id_type == "tagline":
6220
+                for file in files:
6221
+                    try:
6222
+                        cmdutil.add_tagline_or_explicit_id(file)
6223
+                    except cmdutil.AlreadyTagged:
6224
+                        print "\"%s\" already has a tagline." % file
6225
+                    except cmdutil.NoCommentSyntax:
6226
+                        pass
6227
+            #do inventory after tagging until no untagged files are encountered
6228
+            if untagged:
6229
+                files = []
6230
+                for file in iter_untagged(tree, None):
6231
+                    if not file in previous_files:
6232
+                        files.append(file)
6233
+
6234
+            else:
6235
+                break
6236
+
6237
+    def get_parser(self):
6238
+        """
6239
+        Returns the options parser to use for the "revision" command.
6240
+
6241
+        :rtype: cmdutil.CmdOptionParser
6242
+        """
6243
+        parser=cmdutil.CmdOptionParser("fai add-id file1 [file2] [file3]...")
6244
+# ddaa suggests removing this to promote GUIDs.  Let's see who squalks.
6245
+#        parser.add_option("-i", "--id", dest="id", 
6246
+#                         help="Specify id for a single file", default=None)
6247
+        parser.add_option("--tltl", action="store_true", 
6248
+                         dest="lord_style",  help="Use Tom Lord's style of id.")
6249
+        parser.add_option("--explicit", action="store_const", 
6250
+                         const="explicit", dest="id_type", 
6251
+                         help="Use an explicit id", default="auto")
6252
+        parser.add_option("--tagline", action="store_const", 
6253
+                         const="tagline", dest="id_type", 
6254
+                         help="Use a tagline id")
6255
+        parser.add_option("--untagged", action="store_true", 
6256
+                         dest="untagged", default=False, 
6257
+                         help="tag all untagged files")
6258
+        return parser 
6259
+
6260
+    def help(self, parser=None):
6261
+        """
6262
+        Prints a help message.
6263
+
6264
+        :param parser: If supplied, the parser to use for generating help.  If \
6265
+        not supplied, it is retrieved.
6266
+        :type parser: cmdutil.CmdOptionParser
6267
+        """
6268
+        if parser==None:
6269
+            parser=self.get_parser()
6270
+        parser.print_help()
6271
+        print """
6272
+Adds an inventory to the specified file(s) and directories.  If --untagged is
6273
+specified, adds inventory to all untagged files and directories.
6274
+        """
6275
+        return
6276
+
6277
+
6278
+class Merge(BaseCommand):
6279
+    """
6280
+    Merges changes from other versions into the current tree
6281
+    """
6282
+    def __init__(self):
6283
+        self.description="Merges changes from other versions"
6284
+        try:
6285
+            self.tree = arch.tree_root()
6286
+        except:
6287
+            self.tree = None
6288
+
6289
+
6290
+    def get_completer(self, arg, index):
6291
+        if self.tree is None:
6292
+            raise arch.errors.TreeRootError
6293
+        completions = list(ancillary.iter_partners(self.tree, 
6294
+                                                   self.tree.tree_version))
6295
+        if len(completions) == 0:
6296
+            completions = list(self.tree.iter_log_versions())
6297
+
6298
+        aliases = []
6299
+        try:
6300
+            for completion in completions:
6301
+                alias = ancillary.compact_alias(str(completion), self.tree)
6302
+                if alias:
6303
+                    aliases.extend(alias)
6304
+
6305
+            for completion in completions:
6306
+                if completion.archive == self.tree.tree_version.archive:
6307
+                    aliases.append(completion.nonarch)
6308
+
6309
+        except Exception, e:
6310
+            print e
6311
+            
6312
+        completions.extend(aliases)
6313
+        return completions
6314
+
6315
+    def do_command(self, cmdargs):
6316
+        """
6317
+        Master function that perfoms the "merge" command.
6318
+        """
6319
+        parser=self.get_parser()
6320
+        (options, args) = parser.parse_args(cmdargs)
6321
+        if options.diff3:
6322
+            action="star-merge"
6323
+        else:
6324
+            action = options.action
6325
+        
6326
+        if self.tree is None:
6327
+            raise arch.errors.TreeRootError(os.getcwd())
6328
+        if cmdutil.has_changed(self.tree.tree_version):
6329
+            raise UncommittedChanges(self.tree)
6330
+
6331
+        if len(args) > 0:
6332
+            revisions = []
6333
+            for arg in args:
6334
+                revisions.append(cmdutil.determine_revision_arch(self.tree, 
6335
+                                                                 arg))
6336
+            source = "from commandline"
6337
+        else:
6338
+            revisions = ancillary.iter_partner_revisions(self.tree, 
6339
+                                                         self.tree.tree_version)
6340
+            source = "from partner version"
6341
+        revisions = misc.rewind_iterator(revisions)
6342
+        try:
6343
+            revisions.next()
6344
+            revisions.rewind()
6345
+        except StopIteration, e:
6346
+            revision = cmdutil.tag_cur(self.tree)
6347
+            if revision is None:
6348
+                raise CantDetermineRevision("", "No version specified, no "
6349
+                                            "partner-versions, and no tag"
6350
+                                            " source")
6351
+            revisions = [revision]
6352
+            source = "from tag source"
6353
+        for revision in revisions:
6354
+            cmdutil.ensure_archive_registered(revision.archive)
6355
+            cmdutil.colorize(arch.Chatter("* Merging %s [%s]" % 
6356
+                             (revision, source)))
6357
+            if action=="native-merge" or action=="update":
6358
+                if self.native_merge(revision, action) == 0:
6359
+                    continue
6360
+            elif action=="star-merge":
6361
+                try: 
6362
+                    self.star_merge(revision, options.diff3)
6363
+                except errors.MergeProblem, e:
6364
+                    break
6365
+            if cmdutil.has_changed(self.tree.tree_version):
6366
+                break
6367
+
6368
+    def star_merge(self, revision, diff3):
6369
+        """Perform a star-merge on the current tree.
6370
+        
6371
+        :param revision: The revision to use for the merge
6372
+        :type revision: `arch.Revision`
6373
+        :param diff3: If true, do a diff3 merge
6374
+        :type diff3: bool
6375
+        """
6376
+        try:
6377
+            for line in self.tree.iter_star_merge(revision, diff3=diff3):
6378
+                cmdutil.colorize(line)
6379
+        except arch.util.ExecProblem, e:
6380
+            if e.proc.status is not None and e.proc.status == 1:
6381
+                if e.proc.error:
6382
+                    print e.proc.error
6383
+                raise MergeProblem
6384
+            else:
6385
+                raise
6386
+
6387
+    def native_merge(self, other_revision, action):
6388
+        """Perform a native-merge on the current tree.
6389
+        
6390
+        :param other_revision: The revision to use for the merge
6391
+        :type other_revision: `arch.Revision`
6392
+        :return: 0 if the merge was skipped, 1 if it was applied
6393
+        """
6394
+        other_tree = cmdutil.find_or_make_local_revision(other_revision)
6395
+        try:
6396
+            if action == "native-merge":
6397
+                ancestor = cmdutil.merge_ancestor2(self.tree, other_tree, 
6398
+                                                   other_revision)
6399
+            elif action == "update":
6400
+                ancestor = cmdutil.tree_latest(self.tree, 
6401
+                                               other_revision.version)
6402
+        except CantDetermineRevision, e:
6403
+            raise CommandFailedWrapper(e)
6404
+        cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor))
6405
+        if (ancestor == other_revision):
6406
+            cmdutil.colorize(arch.Chatter("* Skipping redundant merge" 
6407
+                                          % ancestor))
6408
+            return 0
6409
+        delta = cmdutil.apply_delta(ancestor, other_tree, self.tree)    
6410
+        for line in cmdutil.iter_apply_delta_filter(delta):
6411
+            cmdutil.colorize(line)
6412
+        return 1
6413
+
6414
+
6415
+
6416
+    def get_parser(self):
6417
+        """
6418
+        Returns the options parser to use for the "merge" command.
6419
+
6420
+        :rtype: cmdutil.CmdOptionParser
6421
+        """
6422
+        parser=cmdutil.CmdOptionParser("fai merge [VERSION]")
6423
+        parser.add_option("-s", "--star-merge", action="store_const",
6424
+                          dest="action", help="Use star-merge",
6425
+                          const="star-merge", default="native-merge")
6426
+        parser.add_option("--update", action="store_const",
6427
+                          dest="action", help="Use update picker",
6428
+                          const="update")
6429
+        parser.add_option("--diff3", action="store_true", 
6430
+                         dest="diff3",  
6431
+                         help="Use diff3 for merge (implies star-merge)")
6432
+        return parser 
6433
+
6434
+    def help(self, parser=None):
6435
+        """
6436
+        Prints a help message.
6437
+
6438
+        :param parser: If supplied, the parser to use for generating help.  If \
6439
+        not supplied, it is retrieved.
6440
+        :type parser: cmdutil.CmdOptionParser
6441
+        """
6442
+        if parser==None:
6443
+            parser=self.get_parser()
6444
+        parser.print_help()
6445
+        print """
6446
+Performs a merge operation using the specified version.
6447
+        """
6448
+        return
6449
+
6450
+class ELog(BaseCommand):
6451
+    """
6452
+    Produces a raw patchlog and invokes the user's editor
6453
+    """
6454
+    def __init__(self):
6455
+        self.description="Edit a patchlog to commit"
6456
+        try:
6457
+            self.tree = arch.tree_root()
6458
+        except:
6459
+            self.tree = None
6460
+
6461
+
6462
+    def do_command(self, cmdargs):
6463
+        """
6464
+        Master function that perfoms the "elog" command.
6465
+        """
6466
+        parser=self.get_parser()
6467
+        (options, args) = parser.parse_args(cmdargs)
6468
+        if self.tree is None:
6469
+            raise arch.errors.TreeRootError
6470
+
6471
+        edit_log(self.tree)
6472
+
6473
+    def get_parser(self):
6474
+        """
6475
+        Returns the options parser to use for the "merge" command.
6476
+
6477
+        :rtype: cmdutil.CmdOptionParser
6478
+        """
6479
+        parser=cmdutil.CmdOptionParser("fai elog")
6480
+        return parser 
6481
+
6482
+
6483
+    def help(self, parser=None):
6484
+        """
6485
+        Invokes $EDITOR to produce a log for committing.
6486
+
6487
+        :param parser: If supplied, the parser to use for generating help.  If \
6488
+        not supplied, it is retrieved.
6489
+        :type parser: cmdutil.CmdOptionParser
6490
+        """
6491
+        if parser==None:
6492
+            parser=self.get_parser()
6493
+        parser.print_help()
6494
+        print """
6495
+Invokes $EDITOR to produce a log for committing.
6496
+        """
6497
+        return
6498
+
6499
+def edit_log(tree):
6500
+    """Makes and edits the log for a tree.  Does all kinds of fancy things
6501
+    like log templates and merge summaries and log-for-merge
6502
+    
6503
+    :param tree: The tree to edit the log for
6504
+    :type tree: `arch.WorkingTree`
6505
+    """
6506
+    #ensure we have an editor before preparing the log
6507
+    cmdutil.find_editor()
6508
+    log = tree.log_message(create=False)
6509
+    log_is_new = False
6510
+    if log is None or cmdutil.prompt("Overwrite log"):
6511
+        if log is not None:
6512
+           os.remove(log.name)
6513
+        log = tree.log_message(create=True)
6514
+        log_is_new = True
6515
+        tmplog = log.name
6516
+        template = tree+"/{arch}/=log-template"
6517
+        if not os.path.exists(template):
6518
+            template = os.path.expanduser("~/.arch-params/=log-template")
6519
+            if not os.path.exists(template):
6520
+                template = None
6521
+        if template:
6522
+            shutil.copyfile(template, tmplog)
6523
+        
6524
+        new_merges = list(cmdutil.iter_new_merges(tree, 
6525
+                                                  tree.tree_version))
6526
+        log["Summary"] = merge_summary(new_merges, tree.tree_version)
6527
+        if len(new_merges) > 0:   
6528
+            if cmdutil.prompt("Log for merge"):
6529
+                mergestuff = cmdutil.log_for_merge(tree)
6530
+                log.description += mergestuff
6531
+        log.save()
6532
+    try:
6533
+        cmdutil.invoke_editor(log.name)
6534
+    except:
6535
+        if log_is_new:
6536
+            os.remove(log.name)
6537
+        raise
6538
+
6539
+def merge_summary(new_merges, tree_version):
6540
+    if len(new_merges) == 0:
6541
+        return ""
6542
+    if len(new_merges) == 1:
6543
+        summary = new_merges[0].summary
6544
+    else:
6545
+        summary = "Merge"
6546
+
6547
+    credits = []
6548
+    for merge in new_merges:
6549
+        if arch.my_id() != merge.creator:
6550
+            name = re.sub("<.*>", "", merge.creator).rstrip(" ");
6551
+            if not name in credits:
6552
+                credits.append(name)
6553
+        else:
6554
+            version = merge.revision.version
6555
+            if version.archive == tree_version.archive:
6556
+                if not version.nonarch in credits:
6557
+                    credits.append(version.nonarch)
6558
+            elif not str(version) in credits:
6559
+                credits.append(str(version))
6560
+
6561
+    return ("%s (%s)") % (summary, ", ".join(credits))
6562
+
6563
+class MirrorArchive(BaseCommand):
6564
+    """
6565
+    Updates a mirror from an archive
6566
+    """
6567
+    def __init__(self):
6568
+        self.description="Update a mirror from an archive"
6569
+
6570
+    def do_command(self, cmdargs):
6571
+        """
6572
+        Master function that perfoms the "revision" command.
6573
+        """
6574
+
6575
+        parser=self.get_parser()
6576
+        (options, args) = parser.parse_args(cmdargs)
6577
+        if len(args) > 1:
6578
+            raise GetHelp
6579
+        try:
6580
+            tree = arch.tree_root()
6581
+        except:
6582
+            tree = None
6583
+
6584
+        if len(args) == 0:
6585
+            if tree is not None:
6586
+                name = tree.tree_version()
6587
+        else:
6588
+            name = cmdutil.expand_alias(args[0], tree)
6589
+            name = arch.NameParser(name)
6590
+
6591
+        to_arch = name.get_archive()
6592
+        from_arch = cmdutil.get_mirror_source(arch.Archive(to_arch))
6593
+        limit = name.get_nonarch()
6594
+
6595
+        iter = arch_core.mirror_archive(from_arch,to_arch, limit)
6596
+        for line in arch.chatter_classifier(iter):
6597
+            cmdutil.colorize(line)
6598
+
6599
+    def get_parser(self):
6600
+        """
6601
+        Returns the options parser to use for the "revision" command.
6602
+
6603
+        :rtype: cmdutil.CmdOptionParser
6604
+        """
6605
+        parser=cmdutil.CmdOptionParser("fai mirror-archive ARCHIVE")
6606
+        return parser 
6607
+
6608
+    def help(self, parser=None):
6609
+        """
6610
+        Prints a help message.
6611
+
6612
+        :param parser: If supplied, the parser to use for generating help.  If \
6613
+        not supplied, it is retrieved.
6614
+        :type parser: cmdutil.CmdOptionParser
6615
+        """
6616
+        if parser==None:
6617
+            parser=self.get_parser()
6618
+        parser.print_help()
6619
+        print """
6620
+Updates a mirror from an archive.  If a branch, package, or version is
6621
+supplied, only changes under it are mirrored.
6622
+        """
6623
+        return
6624
+
6625
+def help_tree_spec():
6626
+    print """Specifying revisions (default: tree)
6627
+Revisions may be specified by alias, revision, version or patchlevel.
6628
+Revisions or versions may be fully qualified.  Unqualified revisions, versions, 
6629
+or patchlevels use the archive of the current project tree.  Versions will
6630
+use the latest patchlevel in the tree.  Patchlevels will use the current tree-
6631
+version.
6632
+
6633
+Use "alias" to list available (user and automatic) aliases."""
6634
+
6635
+def help_aliases(tree):
6636
+    print """Auto-generated aliases
6637
+ acur : The latest revision in the archive of the tree-version.  You can specfy
6638
+        a different version like so: acur:foo--bar--0 (aliases can be used)
6639
+ tcur : (tree current) The latest revision in the tree of the tree-version.
6640
+        You can specify a different version like so: tcur:foo--bar--0 (aliases
6641
+        can be used).
6642
+tprev : (tree previous) The previous revision in the tree of the tree-version.
6643
+        To specify an older revision, use a number, e.g. "tprev:4"
6644
+ tanc : (tree ancestor) The ancestor revision of the tree
6645
+        To specify an older revision, use a number, e.g. "tanc:4"
6646
+tdate : (tree date) The latest revision from a given date (e.g. "tdate:July 6")
6647
+ tmod : (tree modified) The latest revision to modify a given file 
6648
+        (e.g. "tmod:engine.cpp" or "tmod:engine.cpp:16")
6649
+ ttag : (tree tag) The revision that was tagged into the current tree revision,
6650
+        according to the tree.
6651
+tagcur: (tag current) The latest revision of the version that the current tree
6652
+        was tagged from.
6653
+mergeanc : The common ancestor of the current tree and the specified revision.
6654
+        Defaults to the first partner-version's latest revision or to tagcur.
6655
+   """
6656
+    print "User aliases"
6657
+    for parts in ancillary.iter_all_alias(tree):
6658
+        print parts[0].rjust(10)+" : "+parts[1]
6659
+
6660
+
6661
+class Inventory(BaseCommand):
6662
+    """List the status of files in the tree"""
6663
+    def __init__(self):
6664
+        self.description=self.__doc__
6665
+
6666
+    def do_command(self, cmdargs):
6667
+        """
6668
+        Master function that perfoms the "revision" command.
6669
+        """
6670
+
6671
+        parser=self.get_parser()
6672
+        (options, args) = parser.parse_args(cmdargs)
6673
+        tree = arch.tree_root()
6674
+        categories = []
6675
+
6676
+        if (options.source):
6677
+            categories.append(arch_core.SourceFile)
6678
+        if (options.precious):
6679
+            categories.append(arch_core.PreciousFile)
6680
+        if (options.backup):
6681
+            categories.append(arch_core.BackupFile)
6682
+        if (options.junk):
6683
+            categories.append(arch_core.JunkFile)
6684
+
6685
+        if len(categories) == 1:
6686
+            show_leading = False
6687
+        else:
6688
+            show_leading = True
6689
+
6690
+        if len(categories) == 0:
6691
+            categories = None
6692
+
6693
+        if options.untagged:
6694
+            categories = arch_core.non_root
6695
+            show_leading = False
6696
+            tagged = False
6697
+        else:
6698
+            tagged = None
6699
+        
6700
+        for file in arch_core.iter_inventory_filter(tree, None, 
6701
+            control_files=options.control_files, 
6702
+            categories = categories, tagged=tagged):
6703
+            print arch_core.file_line(file, 
6704
+                                      category = show_leading, 
6705
+                                      untagged = show_leading,
6706
+                                      id = options.ids)
6707
+
6708
+    def get_parser(self):
6709
+        """
6710
+        Returns the options parser to use for the "revision" command.
6711
+
6712
+        :rtype: cmdutil.CmdOptionParser
6713
+        """
6714
+        parser=cmdutil.CmdOptionParser("fai inventory [options]")
6715
+        parser.add_option("--ids", action="store_true", dest="ids", 
6716
+                          help="Show file ids")
6717
+        parser.add_option("--control", action="store_true", 
6718
+                          dest="control_files", help="include control files")
6719
+        parser.add_option("--source", action="store_true", dest="source",
6720
+                          help="List source files")
6721
+        parser.add_option("--backup", action="store_true", dest="backup",
6722
+                          help="List backup files")
6723
+        parser.add_option("--precious", action="store_true", dest="precious",
6724
+                          help="List precious files")
6725
+        parser.add_option("--junk", action="store_true", dest="junk",
6726
+                          help="List junk files")
6727
+        parser.add_option("--unrecognized", action="store_true", 
6728
+                          dest="unrecognized", help="List unrecognized files")
6729
+        parser.add_option("--untagged", action="store_true", 
6730
+                          dest="untagged", help="List only untagged files")
6731
+        return parser 
6732
+
6733
+    def help(self, parser=None):
6734
+        """
6735
+        Prints a help message.
6736
+
6737
+        :param parser: If supplied, the parser to use for generating help.  If \
6738
+        not supplied, it is retrieved.
6739
+        :type parser: cmdutil.CmdOptionParser
6740
+        """
6741
+        if parser==None:
6742
+            parser=self.get_parser()
6743
+        parser.print_help()
6744
+        print """
6745
+Lists the status of files in the archive:
6746
+S source
6747
+P precious
6748
+B backup
6749
+J junk
6750
+U unrecognized
6751
+T tree root
6752
+? untagged-source
6753
+Leading letter are not displayed if only one kind of file is shown
6754
+        """
6755
+        return
6756
+
6757
+
6758
+class Alias(BaseCommand):
6759
+    """List or adjust aliases"""
6760
+    def __init__(self):
6761
+        self.description=self.__doc__
6762
+
6763
+    def get_completer(self, arg, index):
6764
+        if index > 2:
6765
+            return ()
6766
+        try:
6767
+            self.tree = arch.tree_root()
6768
+        except:
6769
+            self.tree = None
6770
+
6771
+        if index == 0:
6772
+            return [part[0]+" " for part in ancillary.iter_all_alias(self.tree)]
6773
+        elif index == 1:
6774
+            return cmdutil.iter_revision_completions(arg, self.tree)
6775
+
6776
+
6777
+    def do_command(self, cmdargs):
6778
+        """
6779
+        Master function that perfoms the "revision" command.
6780
+        """
6781
+
6782
+        parser=self.get_parser()
6783
+        (options, args) = parser.parse_args(cmdargs)
6784
+        try:
6785
+            self.tree =  arch.tree_root()
6786
+        except:
6787
+            self.tree = None
6788
+
6789
+
6790
+        try:
6791
+            options.action(args, options)
6792
+        except cmdutil.ForbiddenAliasSyntax, e:
6793
+            raise CommandFailedWrapper(e)
6794
+
6795
+    def arg_dispatch(self, args, options):
6796
+        """Add, modify, or list aliases, depending on number of arguments
6797
+
6798
+        :param args: The list of commandline arguments
6799
+        :type args: list of str
6800
+        :param options: The commandline options
6801
+        """
6802
+        if len(args) == 0:
6803
+            help_aliases(self.tree)
6804
+            return
6805
+        elif len(args) == 1:
6806
+            self.print_alias(args[0])
6807
+        elif (len(args)) == 2:
6808
+            self.add(args[0], args[1], options)
6809
+        else:
6810
+            raise cmdutil.GetHelp
6811
+
6812
+    def print_alias(self, alias):
6813
+        answer = None
6814
+        for pair in ancillary.iter_all_alias(self.tree):
6815
+            if pair[0] == alias:
6816
+                answer = pair[1]
6817
+        if answer is not None:
6818
+            print answer
6819
+        else:
6820
+            print "The alias %s is not assigned." % alias
6821
+
6822
+    def add(self, alias, expansion, options):
6823
+        """Add or modify aliases
6824
+
6825
+        :param alias: The alias name to create/modify
6826
+        :type alias: str
6827
+        :param expansion: The expansion to assign to the alias name
6828
+        :type expansion: str
6829
+        :param options: The commandline options
6830
+        """
6831
+        newlist = ""
6832
+        written = False
6833
+        new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, 
6834
+            self.tree))
6835
+        ancillary.check_alias(new_line.rstrip("\n"), [alias, expansion])
6836
+
6837
+        for pair in self.get_iterator(options):
6838
+            if pair[0] != alias:
6839
+                newlist+="%s=%s\n" % (pair[0], pair[1])
6840
+            elif not written:
6841
+                newlist+=new_line
6842
+                written = True
6843
+        if not written:
6844
+            newlist+=new_line
6845
+        self.write_aliases(newlist, options)
6846
+            
6847
+    def delete(self, args, options):
6848
+        """Delete the specified alias
6849
+
6850
+        :param args: The list of arguments
6851
+        :type args: list of str
6852
+        :param options: The commandline options
6853
+        """
6854
+        deleted = False
6855
+        if len(args) != 1:
6856
+            raise cmdutil.GetHelp
6857
+        newlist = ""
6858
+        for pair in self.get_iterator(options):
6859
+            if pair[0] != args[0]:
6860
+                newlist+="%s=%s\n" % (pair[0], pair[1])
6861
+            else:
6862
+                deleted = True
6863
+        if not deleted:
6864
+            raise errors.NoSuchAlias(args[0])
6865
+        self.write_aliases(newlist, options)
6866
+
6867
+    def get_alias_file(self, options):
6868
+        """Return the name of the alias file to use
6869
+
6870
+        :param options: The commandline options
6871
+        """
6872
+        if options.tree:
6873
+            if self.tree is None:
6874
+                self.tree == arch.tree_root()
6875
+            return str(self.tree)+"/{arch}/+aliases"
6876
+        else:
6877
+            return "~/.aba/aliases"
6878
+
6879
+    def get_iterator(self, options):
6880
+        """Return the alias iterator to use
6881
+
6882
+        :param options: The commandline options
6883
+        """
6884
+        return ancillary.iter_alias(self.get_alias_file(options))
6885
+
6886
+    def write_aliases(self, newlist, options):
6887
+        """Safely rewrite the alias file
6888
+        :param newlist: The new list of aliases
6889
+        :type newlist: str
6890
+        :param options: The commandline options
6891
+        """
6892
+        filename = os.path.expanduser(self.get_alias_file(options))
6893
+        file = cmdutil.NewFileVersion(filename)
6894
+        file.write(newlist)
6895
+        file.commit()
6896
+
6897
+
6898
+    def get_parser(self):
6899
+        """
6900
+        Returns the options parser to use for the "alias" command.
6901
+
6902
+        :rtype: cmdutil.CmdOptionParser
6903
+        """
6904
+        parser=cmdutil.CmdOptionParser("fai alias [ALIAS] [NAME]")
6905
+        parser.add_option("-d", "--delete", action="store_const", dest="action",
6906
+                          const=self.delete, default=self.arg_dispatch, 
6907
+                          help="Delete an alias")
6908
+        parser.add_option("--tree", action="store_true", dest="tree", 
6909
+                          help="Create a per-tree alias", default=False)
6910
+        return parser 
6911
+
6912
+    def help(self, parser=None):
6913
+        """
6914
+        Prints a help message.
6915
+
6916
+        :param parser: If supplied, the parser to use for generating help.  If \
6917
+        not supplied, it is retrieved.
6918
+        :type parser: cmdutil.CmdOptionParser
6919
+        """
6920
+        if parser==None:
6921
+            parser=self.get_parser()
6922
+        parser.print_help()
6923
+        print """
6924
+Lists current aliases or modifies the list of aliases.
6925
+
6926
+If no arguments are supplied, aliases will be listed.  If two arguments are
6927
+supplied, the specified alias will be created or modified.  If -d or --delete
6928
+is supplied, the specified alias will be deleted.
6929
+
6930
+You can create aliases that refer to any fully-qualified part of the
6931
+Arch namespace, e.g. 
6932
+archive, 
6933
+archive/category, 
6934
+archive/category--branch, 
6935
+archive/category--branch--version (my favourite)
6936
+archive/category--branch--version--patchlevel
6937
+
6938
+Aliases can be used automatically by native commands.  To use them
6939
+with external or tla commands, prefix them with ^ (you can do this
6940
+with native commands, too).
6941
+"""
6942
+
6943
+
6944
+class RequestMerge(BaseCommand):
6945
+    """Submit a merge request to Bug Goo"""
6946
+    def __init__(self):
6947
+        self.description=self.__doc__
6948
+
6949
+    def do_command(self, cmdargs):
6950
+        """Submit a merge request
6951
+
6952
+        :param cmdargs: The commandline arguments
6953
+        :type cmdargs: list of str
6954
+        """
6955
+        cmdutil.find_editor()
6956
+        parser = self.get_parser()
6957
+        (options, args) = parser.parse_args(cmdargs)
6958
+        try:
6959
+            self.tree=arch.tree_root()
6960
+        except:
6961
+            self.tree=None
6962
+        base, revisions = self.revision_specs(args)
6963
+        message = self.make_headers(base, revisions)
6964
+        message += self.make_summary(revisions)
6965
+        path = self.edit_message(message)
6966
+        message = self.tidy_message(path)
6967
+        if cmdutil.prompt("Send merge"):
6968
+            self.send_message(message)
6969
+            print "Merge request sent"
6970
+
6971
+    def make_headers(self, base, revisions):
6972
+        """Produce email and Bug Goo header strings
6973
+
6974
+        :param base: The base revision to apply merges to
6975
+        :type base: `arch.Revision`
6976
+        :param revisions: The revisions to replay into the base
6977
+        :type revisions: list of `arch.Patchlog`
6978
+        :return: The headers
6979
+        :rtype: str
6980
+        """
6981
+        headers = "To: gnu-arch-users@gnu.org\n"
6982
+        headers += "From: %s\n" % options.fromaddr
6983
+        if len(revisions) == 1:
6984
+            headers += "Subject: [MERGE REQUEST] %s\n" % revisions[0].summary
6985
+        else:
6986
+            headers += "Subject: [MERGE REQUEST]\n"
6987
+        headers += "\n"
6988
+        headers += "Base-Revision: %s\n" % base
6989
+        for revision in revisions:
6990
+            headers += "Revision: %s\n" % revision.revision
6991
+        headers += "Bug: \n\n"
6992
+        return headers
6993
+
6994
+    def make_summary(self, logs):
6995
+        """Generate a summary of merges
6996
+
6997
+        :param logs: the patchlogs that were directly added by the merges
6998
+        :type logs: list of `arch.Patchlog`
6999
+        :return: the summary
7000
+        :rtype: str
7001
+        """ 
7002
+        summary = ""
7003
+        for log in logs:
7004
+            summary+=str(log.revision)+"\n"
7005
+            summary+=log.summary+"\n"
7006
+            if log.description.strip():
7007
+                summary+=log.description.strip('\n')+"\n\n"
7008
+        return summary
7009
+
7010
+    def revision_specs(self, args):
7011
+        """Determine the base and merge revisions from tree and arguments.
7012
+
7013
+        :param args: The parsed arguments
7014
+        :type args: list of str
7015
+        :return: The base revision and merge revisions 
7016
+        :rtype: `arch.Revision`, list of `arch.Patchlog`
7017
+        """
7018
+        if len(args) > 0:
7019
+            target_revision = cmdutil.determine_revision_arch(self.tree, 
7020
+                                                              args[0])
7021
+        else:
7022
+            target_revision = cmdutil.tree_latest(self.tree)
7023
+        if len(args) > 1:
7024
+            merges = [ arch.Patchlog(cmdutil.determine_revision_arch(
7025
+                       self.tree, f)) for f in args[1:] ]
7026
+        else:
7027
+            if self.tree is None:
7028
+                raise CantDetermineRevision("", "Not in a project tree")
7029
+            merge_iter = cmdutil.iter_new_merges(self.tree, 
7030
+                                                 target_revision.version, 
7031
+                                                 False)
7032
+            merges = [f for f in cmdutil.direct_merges(merge_iter)]
7033
+        return (target_revision, merges)
7034
+
7035
+    def edit_message(self, message):
7036
+        """Edit an email message in the user's standard editor
7037
+
7038
+        :param message: The message to edit
7039
+        :type message: str
7040
+        :return: the path of the edited message
7041
+        :rtype: str
7042
+        """
7043
+        if self.tree is None:
7044
+            path = os.get_cwd()
7045
+        else:
7046
+            path = self.tree
7047
+        path += "/,merge-request"
7048
+        file = open(path, 'w')
7049
+        file.write(message)
7050
+        file.flush()
7051
+        cmdutil.invoke_editor(path)
7052
+        return path
7053
+
7054
+    def tidy_message(self, path):
7055
+        """Validate and clean up message.
7056
+
7057
+        :param path: The path to the message to clean up
7058
+        :type path: str
7059
+        :return: The parsed message
7060
+        :rtype: `email.Message`
7061
+        """
7062
+        mail = email.message_from_file(open(path))
7063
+        if mail["Subject"].strip() == "[MERGE REQUEST]":
7064
+            raise BlandSubject
7065
+        
7066
+        request = email.message_from_string(mail.get_payload())
7067
+        if request.has_key("Bug"):
7068
+            if request["Bug"].strip()=="":
7069
+                del request["Bug"]
7070
+        mail.set_payload(request.as_string())
7071
+        return mail
7072
+
7073
+    def send_message(self, message):
7074
+        """Send a message, using its headers to address it.
7075
+
7076
+        :param message: The message to send
7077
+        :type message: `email.Message`"""
7078
+        server = smtplib.SMTP()
7079
+        server.sendmail(message['From'], message['To'], message.as_string())
7080
+        server.quit()
7081
+
7082
+    def help(self, parser=None):
7083
+        """Print a usage message
7084
+
7085
+        :param parser: The options parser to use
7086
+        :type parser: `cmdutil.CmdOptionParser`
7087
+        """
7088
+        if parser is None:
7089
+            parser = self.get_parser()
7090
+        parser.print_help()
7091
+        print """
7092
+Sends a merge request formatted for Bug Goo.  Intended use: get the tree
7093
+you'd like to merge into.  Apply the merges you want.  Invoke request-merge.
7094
+The merge request will open in your $EDITOR.
7095
+
7096
+When no TARGET is specified, it uses the current tree revision.  When
7097
+no MERGE is specified, it uses the direct merges (as in "revisions
7098
+--direct-merges").  But you can specify just the TARGET, or all the MERGE
7099
+revisions.
7100
+"""
7101
+
7102
+    def get_parser(self):
7103
+        """Produce a commandline parser for this command.
7104
+
7105
+        :rtype: `cmdutil.CmdOptionParser`
7106
+        """
7107
+        parser=cmdutil.CmdOptionParser("request-merge [TARGET] [MERGE1...]")
7108
+        return parser
7109
+
7110
+commands = { 
7111
+'changes' : Changes,
7112
+'help' : Help,
7113
+'update': Update,
7114
+'apply-changes':ApplyChanges,
7115
+'cat-log': CatLog,
7116
+'commit': Commit,
7117
+'revision': Revision,
7118
+'revisions': Revisions,
7119
+'get': Get,
7120
+'revert': Revert,
7121
+'shell': Shell,
7122
+'add-id': AddID,
7123
+'merge': Merge,
7124
+'elog': ELog,
7125
+'mirror-archive': MirrorArchive,
7126
+'ninventory': Inventory,
7127
+'alias' : Alias,
7128
+'request-merge': RequestMerge,
7129
+}
7130
+suggestions = {
7131
+'apply-delta' : "Try \"apply-changes\".",
7132
+'delta' : "To compare two revisions, use \"changes\".",
7133
+'diff-rev' : "To compare two revisions, use \"changes\".",
7134
+'undo' : "To undo local changes, use \"revert\".",
7135
+'undelete' : "To undo only deletions, use \"revert --deletions\"",
7136
+'missing-from' : "Try \"revisions --missing-from\".",
7137
+'missing' : "Try \"revisions --missing\".",
7138
+'missing-merge' : "Try \"revisions --partner-missing\".",
7139
+'new-merges' : "Try \"revisions --new-merges\".",
7140
+'cachedrevs' : "Try \"revisions --cacherevs\". (no 'd')",
7141
+'logs' : "Try \"revisions --logs\"",
7142
+'tree-source' : "Use the \"^ttag\" alias (\"revision ^ttag\")",
7143
+'latest-revision' : "Use the \"^acur\" alias (\"revision ^acur\")",
7144
+'change-version' : "Try \"update REVISION\"",
7145
+'tree-revision' : "Use the \"^tcur\" alias (\"revision ^tcur\")",
7146
+'rev-depends' : "Use revisions --dependencies",
7147
+'auto-get' : "Plain get will do archive lookups",
7148
+'tagline' : "Use add-id.  It uses taglines in tagline trees",
7149
+'emlog' : "Use elog.  It automatically adds log-for-merge text, if any",
7150
+'library-revisions' : "Use revisions --library",
7151
+'file-revert' : "Use revert FILE"
7152
+}
7153
+# arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7
7154
7155
*** modified file 'bzrlib/branch.py'
7156
--- bzrlib/branch.py 
7157
+++ bzrlib/branch.py 
7158
@@ -31,6 +31,8 @@
7159
 from revision import Revision
7160
 from errors import bailout, BzrError
7161
 from textui import show_status
7162
+import patches
7163
+from bzrlib import progress
7164
 
7165
 BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
7166
 ## TODO: Maybe include checks for common corruption of newlines, etc?
7167
@@ -802,3 +804,36 @@
7168
 
7169
     s = hexlify(rand_bytes(8))
7170
     return '-'.join((name, compact_date(time.time()), s))
7171
+
7172
+
7173
+def iter_anno_data(branch, file_id):
7174
+    later_revision = branch.revno()
7175
+    q = range(branch.revno())
7176
+    q.reverse()
7177
+    later_text_id = branch.basis_tree().inventory[file_id].text_id
7178
+    i = 0
7179
+    for revno in q:
7180
+        i += 1
7181
+        cur_tree = branch.revision_tree(branch.lookup_revision(revno))
7182
+        if file_id not in cur_tree.inventory:
7183
+            text_id = None
7184
+        else:
7185
+            text_id = cur_tree.inventory[file_id].text_id
7186
+        if text_id != later_text_id:
7187
+            patch = get_patch(branch, revno, later_revision, file_id)
7188
+            yield revno, patch.iter_inserted(), patch
7189
+            later_revision = revno
7190
+            later_text_id = text_id
7191
+        yield progress.Progress("revisions", i)
7192
+
7193
+def get_patch(branch, old_revno, new_revno, file_id):
7194
+    old_tree = branch.revision_tree(branch.lookup_revision(old_revno))
7195
+    new_tree = branch.revision_tree(branch.lookup_revision(new_revno))
7196
+    if file_id in old_tree.inventory:
7197
+        old_file = old_tree.get_file(file_id).readlines()
7198
+    else:
7199
+        old_file = []
7200
+    ud = difflib.unified_diff(old_file, new_tree.get_file(file_id).readlines())
7201
+    return patches.parse_patch(ud)
7202
+
7203
+
7204
7205
*** modified file 'bzrlib/commands.py'
7206
--- bzrlib/commands.py 
7207
+++ bzrlib/commands.py 
7208
@@ -27,6 +27,9 @@
7209
 from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \
7210
      format_date
7211
 from bzrlib import merge
7212
+from bzrlib.branch import iter_anno_data
7213
+from bzrlib import patches
7214
+from bzrlib import progress
7215
 
7216
 
7217
 def _squish_command_name(cmd):
7218
@@ -882,7 +885,15 @@
7219
                 print '%3d FAILED!' % mf
7220
             else:
7221
                 print
7222
-
7223
+        result = bzrlib.patches.test()
7224
+        resultFailed = len(result.errors) + len(result.failures)
7225
+        print '%-40s %3d tests' % ('bzrlib.patches', result.testsRun),
7226
+        if resultFailed:
7227
+            print '%3d FAILED!' % resultFailed
7228
+        else:
7229
+            print
7230
+        tests += result.testsRun
7231
+        failures += resultFailed
7232
         print '%-40s %3d tests' % ('total', tests),
7233
         if failures:
7234
             print '%3d FAILED!' % failures
7235
@@ -897,6 +908,27 @@
7236
     """Show version of bzr"""
7237
     def run(self):
7238
         show_version()
7239
+
7240
+class cmd_annotate(Command):
7241
+    """Show which revision added each line in a file"""
7242
+
7243
+    takes_args = ['filename']
7244
+    def run(self, filename):
7245
+        branch = (Branch(filename))
7246
+        file_id = branch.working_tree().path2id(filename)
7247
+        lines = branch.basis_tree().get_file(file_id)
7248
+        total = branch.revno()
7249
+        anno_d_iter = iter_anno_data(branch, file_id)
7250
+        for result in patches.iter_annotate_file(lines, anno_d_iter):
7251
+            if isinstance(result, progress.Progress):
7252
+                result.total = total
7253
+                progress.progress_bar(result)
7254
+            else:
7255
+                progress.clear_progress_bar()
7256
+                anno_lines = result
7257
+        for line in anno_lines:
7258
+            sys.stdout.write("%4s:%s" % (str(line.log), line.text))
7259
+
7260
 
7261
 def show_version():
7262
     print "bzr (bazaar-ng) %s" % bzrlib.__version__
7263