~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to patches/annotate4.patch

  • Committer: Martin Pool
  • Date: 2005-06-10 06:32:17 UTC
  • Revision ID: mbp@sourcefrog.net-20050610063217-1acbbd123b761fea
- add updated annotate from aaron

Show diffs side-by-side

added added

removed removed

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