~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to fai/commands.py

  • Committer: Robert Collins
  • Date: 2005-09-13 15:11:39 UTC
  • mto: (147.2.6) (364.1.3 bzrtools)
  • mto: This revision was merged to the branch mainline in revision 324.
  • Revision ID: robertc@robertcollins.net-20050913151139-9ac920fc9d7bda31
TODOification

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2004 Aaron Bentley
 
2
# <aaron.bentley@utoronto.ca>
 
3
#
 
4
#    This program is free software; you can redistribute it and/or modify
 
5
#    it under the terms of the GNU General Public License as published by
 
6
#    the Free Software Foundation; either version 2 of the License, or
 
7
#    (at your option) any later version.
 
8
#
 
9
#    This program is distributed in the hope that it will be useful,
 
10
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
#    GNU General Public License for more details.
 
13
#
 
14
#    You should have received a copy of the GNU General Public License
 
15
#    along with this program; if not, write to the Free Software
 
16
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
17
 
 
18
import sys
 
19
import arch
 
20
import arch.util
 
21
import arch.errors
 
22
 
 
23
import pylon
 
24
import pylon.errors
 
25
from pylon.errors import *
 
26
from pylon import errors
 
27
import pylon.util
 
28
 
 
29
import abacmds
 
30
import cmdutil
 
31
import shutil
 
32
import os
 
33
import options
 
34
import time
 
35
import cmd
 
36
import readline
 
37
import re
 
38
import string
 
39
import terminal
 
40
import email
 
41
import smtplib
 
42
import textwrap
 
43
 
 
44
__docformat__ = "restructuredtext"
 
45
__doc__ = "Implementation of user (sub) commands"
 
46
commands = {}
 
47
 
 
48
def find_command(cmd):
 
49
    """
 
50
    Return an instance of a command type.  Return None if the type isn't
 
51
    registered.
 
52
 
 
53
    :param cmd: the name of the command to look for
 
54
    :type cmd: the type of the command
 
55
    """
 
56
    if commands.has_key(cmd):
 
57
        return commands[cmd]()
 
58
    else:
 
59
        return None
 
60
 
 
61
class BaseCommand:
 
62
    def __call__(self, cmdline):
 
63
        try:
 
64
            self.do_command(cmdline.split())
 
65
        except cmdutil.GetHelp, e:
 
66
            self.help()
 
67
        except Exception, e:
 
68
            print e
 
69
 
 
70
    def get_completer(index):
 
71
        return None
 
72
 
 
73
    def complete(self, args, text):
 
74
        """
 
75
        Returns a list of possible completions for the given text.
 
76
 
 
77
        :param args: The complete list of arguments
 
78
        :type args: List of str
 
79
        :param text: text to complete (may be shorter than args[-1])
 
80
        :type text: str
 
81
        :rtype: list of str
 
82
        """
 
83
        matches = []
 
84
        candidates = None
 
85
 
 
86
        if len(args) > 0: 
 
87
            realtext = args[-1]
 
88
        else:
 
89
            realtext = ""
 
90
 
 
91
        try:
 
92
            parser=self.get_parser()
 
93
            if realtext.startswith('-'):
 
94
                candidates = parser.iter_options()
 
95
            else:
 
96
                (options, parsed_args) = parser.parse_args(args)
 
97
 
 
98
                if len (parsed_args) > 0:
 
99
                    candidates = self.get_completer(parsed_args[-1], len(parsed_args) -1)
 
100
                else:
 
101
                    candidates = self.get_completer("", 0)
 
102
        except:
 
103
            pass
 
104
        if candidates is None:
 
105
            return
 
106
        for candidate in candidates:
 
107
            candidate = str(candidate)
 
108
            if candidate.startswith(realtext):
 
109
                matches.append(candidate[len(realtext)- len(text):])
 
110
        return matches
 
111
 
 
112
 
 
113
class Help(BaseCommand):
 
114
    """
 
115
    Lists commands, prints help messages.
 
116
    """
 
117
    def __init__(self):
 
118
        self.description="Prints help mesages"
 
119
        self.parser = None
 
120
 
 
121
    def do_command(self, cmdargs):
 
122
        """
 
123
        Prints a help message.
 
124
        """
 
125
        options, args = self.get_parser().parse_args(cmdargs)
 
126
        if len(args) > 1:
 
127
            raise cmdutil.GetHelp
 
128
 
 
129
        if options.native or options.suggestions or options.external:
 
130
            native = options.native
 
131
            suggestions = options.suggestions
 
132
            external = options.external
 
133
        else:
 
134
            native = True
 
135
            suggestions = False
 
136
            external = True
 
137
        
 
138
        if len(args) == 0:
 
139
            self.list_commands(native, suggestions, external)
 
140
            return
 
141
        elif len(args) == 1:
 
142
            command_help(args[0])
 
143
            return
 
144
 
 
145
    def help(self):
 
146
        self.get_parser().print_help()
 
147
        print """
 
148
If no command is specified, commands are listed.  If a command is
 
149
specified, help for that command is listed.
 
150
        """
 
151
 
 
152
    def get_parser(self):
 
153
        """
 
154
        Returns the options parser to use for the "revision" command.
 
155
 
 
156
        :rtype: cmdutil.CmdOptionParser
 
157
        """
 
158
        if self.parser is not None:
 
159
            return self.parser
 
160
        parser=cmdutil.CmdOptionParser("fai help [command]")
 
161
        parser.add_option("-n", "--native", action="store_true", 
 
162
                         dest="native", help="Show native commands")
 
163
        parser.add_option("-e", "--external", action="store_true", 
 
164
                         dest="external", help="Show external commands")
 
165
        parser.add_option("-s", "--suggest", action="store_true", 
 
166
                         dest="suggestions", help="Show suggestions")
 
167
        self.parser = parser
 
168
        return parser 
 
169
      
 
170
    def list_commands(self, native=True, suggest=False, external=True):
 
171
        """
 
172
        Lists supported commands.
 
173
 
 
174
        :param native: list native, python-based commands
 
175
        :type native: bool
 
176
        :param external: list external aba-style commands
 
177
        :type external: bool
 
178
        """
 
179
        if native:
 
180
            print "Native Fai commands"
 
181
            keys=commands.keys()
 
182
            keys.sort()
 
183
            for k in keys:
 
184
                space=""
 
185
                for i in range(28-len(k)):
 
186
                    space+=" "
 
187
                print space+k+" : "+commands[k]().description
 
188
            print
 
189
        if suggest:
 
190
            print "Unavailable commands and suggested alternatives"
 
191
            key_list = suggestions.keys()
 
192
            key_list.sort()
 
193
            for key in key_list:
 
194
                print "%28s : %s" % (key, suggestions[key])
 
195
            print
 
196
        if external:
 
197
            fake_aba = abacmds.AbaCmds()
 
198
            if (fake_aba.abadir == ""):
 
199
                return
 
200
            print "External commands"
 
201
            fake_aba.list_commands()
 
202
            print
 
203
        if not suggest:
 
204
            print "Use help --suggest to list alternatives to tla and aba"\
 
205
                " commands."
 
206
        if options.tla_fallthrough and (native or external):
 
207
            print "Fai also supports tla commands."
 
208
 
 
209
def command_help(cmd):
 
210
    """
 
211
    Prints help for a command.
 
212
 
 
213
    :param cmd: The name of the command to print help for
 
214
    :type cmd: str
 
215
    """
 
216
    fake_aba = abacmds.AbaCmds()
 
217
    cmdobj = find_command(cmd)
 
218
    if cmdobj != None:
 
219
        cmdobj.help()
 
220
    elif suggestions.has_key(cmd):
 
221
        print "Not available\n" + suggestions[cmd]
 
222
    else:
 
223
        abacmd = fake_aba.is_command(cmd)
 
224
        if abacmd:
 
225
            abacmd.help()
 
226
        else:
 
227
            print "No help is available for \""+cmd+"\". Maybe try \"tla "+cmd+" -H\"?"
 
228
 
 
229
 
 
230
 
 
231
class Changes(BaseCommand):
 
232
    """
 
233
    the "changes" command: lists differences between trees/revisions:
 
234
    """
 
235
    
 
236
    def __init__(self):
 
237
        self.description="Lists what files have changed in the project tree"
 
238
 
 
239
    def get_completer(self, arg, index):
 
240
        if index > 1:
 
241
            return None
 
242
        try:
 
243
            tree = arch.tree_root(".")
 
244
        except:
 
245
            tree = None
 
246
        return cmdutil.iter_revision_completions(arg, tree)
 
247
    
 
248
    def parse_commandline(self, cmdline):
 
249
        """
 
250
        Parse commandline arguments.  Raises cmdutil.GetHelp if help is needed.
 
251
        
 
252
        :param cmdline: A list of arguments to parse
 
253
        :rtype: (options, Revision, Revision/WorkingTree)
 
254
        """
 
255
        parser=self.get_parser()
 
256
        (options, args) = parser.parse_args(cmdline)
 
257
        if len(args) > 2:
 
258
            raise cmdutil.GetHelp
 
259
 
 
260
        tree=arch.tree_root(".")
 
261
        if len(args) == 0:
 
262
            a_spec = pylon.comp_revision(tree)
 
263
        else:
 
264
            a_spec = cmdutil.determine_revision_tree(tree, args[0])
 
265
        cmdutil.ensure_archive_registered(a_spec.archive)
 
266
        if len(args) == 2:
 
267
            b_spec = cmdutil.determine_revision_tree(tree, args[1])
 
268
            cmdutil.ensure_archive_registered(b_spec.archive)
 
269
        else:
 
270
            b_spec=tree
 
271
        return options, a_spec, b_spec
 
272
 
 
273
    def do_command(self, cmdargs):
 
274
        """
 
275
        Master function that perfoms the "changes" command.
 
276
        """
 
277
        try:
 
278
            options, a_spec, b_spec = self.parse_commandline(cmdargs);
 
279
        except cmdutil.CantDetermineRevision, e:
 
280
            print e
 
281
            return
 
282
        except arch.errors.TreeRootError, e:
 
283
            print e
 
284
            return
 
285
        if options.changeset:
 
286
            changeset=options.changeset
 
287
            tmpdir = None
 
288
        else:
 
289
            tmpdir=pylon.util.tmpdir()
 
290
            changeset=tmpdir+"/changeset"
 
291
        try:
 
292
            delta=arch.iter_delta(a_spec, b_spec, changeset)
 
293
            try:
 
294
                for line in delta:
 
295
                    if pylon.chattermatch(line, "changeset:"):
 
296
                        pass
 
297
                    else:
 
298
                        cmdutil.colorize(line, options.suppress_chatter)
 
299
            except arch.util.ExecProblem, e:
 
300
                if e.proc.error and e.proc.error.startswith(
 
301
                    "missing explicit id for file"):
 
302
                    raise MissingID(e)
 
303
                else:
 
304
                    raise
 
305
            if (options.perform_diff):
 
306
                chan = pylon.ChangesetMunger(changeset)
 
307
                chan.read_indices()
 
308
                if options.diffopts is not None:
 
309
                    if isinstance(b_spec, arch.Revision):
 
310
                        b_dir = b_spec.library_find()
 
311
                    else:
 
312
                        b_dir = b_spec
 
313
                    a_dir = a_spec.library_find()
 
314
                    diffopts = options.diffopts.split()
 
315
                    cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir)
 
316
                else:
 
317
                    cmdutil.show_diffs(delta.changeset)
 
318
        finally:
 
319
            if tmpdir and (os.access(tmpdir, os.X_OK)):
 
320
                shutil.rmtree(tmpdir)
 
321
 
 
322
    def get_parser(self):
 
323
        """
 
324
        Returns the options parser to use for the "changes" command.
 
325
 
 
326
        :rtype: cmdutil.CmdOptionParser
 
327
        """
 
328
        parser=cmdutil.CmdOptionParser("fai changes [options] [revision]"
 
329
                                       " [revision]")
 
330
        parser.add_option("-d", "--diff", action="store_true", 
 
331
                          dest="perform_diff", default=False, 
 
332
                          help="Show diffs in summary")
 
333
        parser.add_option("-c", "--changeset", dest="changeset", 
 
334
                          help="Store a changeset in the given directory", 
 
335
                          metavar="DIRECTORY")
 
336
        parser.add_option("-s", "--silent", action="store_true", 
 
337
                          dest="suppress_chatter", default=False, 
 
338
                          help="Suppress chatter messages")
 
339
        parser.add_option("--diffopts", dest="diffopts", 
 
340
                          help="Use the specified diff options", 
 
341
                          metavar="OPTIONS")
 
342
 
 
343
        return parser
 
344
 
 
345
    def help(self, parser=None):
 
346
        """
 
347
        Prints a help message.
 
348
 
 
349
        :param parser: If supplied, the parser to use for generating help.  If \
 
350
        not supplied, it is retrieved.
 
351
        :type parser: cmdutil.CmdOptionParser
 
352
        """
 
353
        if parser is None:
 
354
            parser=self.get_parser()
 
355
        parser.print_help()
 
356
        print """
 
357
Performs source-tree comparisons
 
358
 
 
359
If no revision is specified, the current project tree is compared to the
 
360
last-committed revision.  If one revision is specified, the current project
 
361
tree is compared to that revision.  If two revisions are specified, they are
 
362
compared to each other.
 
363
        """
 
364
        help_tree_spec() 
 
365
        return
 
366
 
 
367
 
 
368
class ApplyChanges(BaseCommand):
 
369
    """
 
370
    Apply differences between two revisions to a tree
 
371
    """
 
372
    
 
373
    def __init__(self):
 
374
        self.description="Applies changes to a project tree"
 
375
    
 
376
    def get_completer(self, arg, index):
 
377
        if index > 1:
 
378
            return None
 
379
        try:
 
380
            tree = arch.tree_root(".")
 
381
        except:
 
382
            tree = None
 
383
        return cmdutil.iter_revision_completions(arg, tree)
 
384
 
 
385
    def parse_commandline(self, cmdline, tree):
 
386
        """
 
387
        Parse commandline arguments.  Raises cmdutil.GetHelp if help is needed.
 
388
        
 
389
        :param cmdline: A list of arguments to parse
 
390
        :rtype: (options, Revision, Revision/WorkingTree)
 
391
        """
 
392
        parser=self.get_parser()
 
393
        (options, args) = parser.parse_args(cmdline)
 
394
        if len(args) != 2:
 
395
            raise cmdutil.GetHelp
 
396
 
 
397
        a_spec = cmdutil.determine_revision_tree(tree, args[0])
 
398
        cmdutil.ensure_archive_registered(a_spec.archive)
 
399
        b_spec = cmdutil.determine_revision_tree(tree, args[1])
 
400
        cmdutil.ensure_archive_registered(b_spec.archive)
 
401
        return options, a_spec, b_spec
 
402
 
 
403
    def do_command(self, cmdargs):
 
404
        """
 
405
        Master function that performs "apply-changes".
 
406
        """
 
407
        try:
 
408
            tree = arch.tree_root(".")
 
409
            options, a_spec, b_spec = self.parse_commandline(cmdargs, tree);
 
410
        except cmdutil.CantDetermineRevision, e:
 
411
            print e
 
412
            return
 
413
        except arch.errors.TreeRootError, e:
 
414
            print e
 
415
            return
 
416
        if options.diff3:
 
417
            delta=pylon.apply_delta_custom(a_spec, b_spec, tree, 
 
418
                apply_type=pylon.Diff3Handler)
 
419
        else:
 
420
            delta=cmdutil.apply_delta(a_spec, b_spec, tree)
 
421
        for line in cmdutil.iter_apply_delta_filter(delta):
 
422
            cmdutil.colorize(line, options.suppress_chatter)
 
423
 
 
424
    def get_parser(self):
 
425
        """
 
426
        Returns the options parser to use for the "apply-changes" command.
 
427
 
 
428
        :rtype: cmdutil.CmdOptionParser
 
429
        """
 
430
        parser=cmdutil.CmdOptionParser("fai apply-changes [options] revision"
 
431
                                       " revision")
 
432
        parser.add_option("-d", "--diff", action="store_true", 
 
433
                          dest="perform_diff", default=False, 
 
434
                          help="Show diffs in summary")
 
435
        parser.add_option("-c", "--changeset", dest="changeset", 
 
436
                          help="Store a changeset in the given directory", 
 
437
                          metavar="DIRECTORY")
 
438
        parser.add_option("-s", "--silent", action="store_true", 
 
439
                          dest="suppress_chatter", default=False, 
 
440
                          help="Suppress chatter messages")
 
441
        parser.add_option("-t", "--three-way", action="store_true", 
 
442
                          dest="diff3", default=False, 
 
443
                          help="[experimental] Use diff3 to merge files")
 
444
        return parser
 
445
 
 
446
    def help(self, parser=None):
 
447
        """
 
448
        Prints a help message.
 
449
 
 
450
        :param parser: If supplied, the parser to use for generating help.  If \
 
451
        not supplied, it is retrieved.
 
452
        :type parser: cmdutil.CmdOptionParser
 
453
        """
 
454
        if parser is None:
 
455
            parser=self.get_parser()
 
456
        parser.print_help()
 
457
        print """
 
458
Applies changes to a project tree
 
459
 
 
460
Compares two revisions and applies the difference between them to the current
 
461
tree.
 
462
        """
 
463
        help_tree_spec() 
 
464
        return
 
465
 
 
466
class Update(BaseCommand):
 
467
    """
 
468
    Updates a project tree to a given revision, preserving un-committed hanges. 
 
469
    """
 
470
    
 
471
    def __init__(self):
 
472
        self.description="Apply the latest changes to the current directory"
 
473
 
 
474
    def get_completer(self, arg, index):
 
475
        if index > 0:
 
476
            return None
 
477
        try:
 
478
            tree = arch.tree_root(".")
 
479
        except:
 
480
            tree = None
 
481
        return cmdutil.iter_revision_completions(arg, tree)
 
482
    
 
483
    def parse_commandline(self, cmdline, tree):
 
484
        """
 
485
        Parse commandline arguments.  Raises cmdutil.GetHelp if help is needed.
 
486
        
 
487
        :param cmdline: A list of arguments to parse
 
488
        :rtype: (options, Revision, Revision/WorkingTree)
 
489
        """
 
490
        parser=self.get_parser()
 
491
        (options, args) = parser.parse_args(cmdline)
 
492
        if len(args) > 2:
 
493
            raise cmdutil.GetHelp
 
494
 
 
495
        spec=None
 
496
        if len(args)>0:
 
497
            spec=args[0]
 
498
        revision=cmdutil.determine_revision_arch(tree, spec)
 
499
        cmdutil.ensure_archive_registered(revision.archive)
 
500
 
 
501
        mirror_source = cmdutil.get_mirror_source(revision.archive)
 
502
        if mirror_source != None:
 
503
            if cmdutil.prompt("Mirror update"):
 
504
                cmd=pylon.mirror_archive(mirror_source, 
 
505
                    revision.archive, arch.NameParser(revision).get_package_version())
 
506
                for line in arch.classify_chatter(cmd):
 
507
                    cmdutil.colorize(line, options.suppress_chatter)
 
508
 
 
509
                revision=cmdutil.determine_revision_arch(tree, spec)
 
510
 
 
511
        return options, revision 
 
512
 
 
513
    def do_command(self, cmdargs):
 
514
        """
 
515
        Master function that perfoms the "update" command.
 
516
        """
 
517
        tree=arch.tree_root(".")
 
518
        try:
 
519
            options, to_revision = self.parse_commandline(cmdargs, tree);
 
520
        except cmdutil.CantDetermineRevision, e:
 
521
            print e
 
522
            return
 
523
        except arch.errors.TreeRootError, e:
 
524
            print e
 
525
            return
 
526
        from_revision = pylon.tree_latest(tree)
 
527
        if from_revision==to_revision:
 
528
            print "Tree is already up to date with:\n"+str(to_revision)+"."
 
529
            return
 
530
        cmdutil.ensure_archive_registered(from_revision.archive)
 
531
        cmd=cmdutil.apply_delta(from_revision, to_revision, tree,
 
532
            options.patch_forward)
 
533
        for line in cmdutil.iter_apply_delta_filter(cmd):
 
534
            cmdutil.colorize(line)
 
535
        if to_revision.version != tree.tree_version:
 
536
            if cmdutil.prompt("Update version"):
 
537
                tree.tree_version = to_revision.version
 
538
 
 
539
    def get_parser(self):
 
540
        """
 
541
        Returns the options parser to use for the "update" command.
 
542
 
 
543
        :rtype: cmdutil.CmdOptionParser
 
544
        """
 
545
        parser=cmdutil.CmdOptionParser("fai update [options]"
 
546
                                       " [revision/version]")
 
547
        parser.add_option("-f", "--forward", action="store_true", 
 
548
                          dest="patch_forward", default=False, 
 
549
                          help="pass the --forward option to 'patch'")
 
550
        parser.add_option("-s", "--silent", action="store_true", 
 
551
                          dest="suppress_chatter", default=False, 
 
552
                          help="Suppress chatter messages")
 
553
        return parser
 
554
 
 
555
    def help(self, parser=None):
 
556
        """
 
557
        Prints a help message.
 
558
 
 
559
        :param parser: If supplied, the parser to use for generating help.  If \
 
560
        not supplied, it is retrieved.
 
561
        :type parser: cmdutil.CmdOptionParser
 
562
        """
 
563
        if parser is None:
 
564
            parser=self.get_parser()
 
565
        parser.print_help()
 
566
        print """
 
567
Updates a working tree to the current archive revision
 
568
 
 
569
If a revision or version is specified, that is used instead 
 
570
        """
 
571
        help_tree_spec() 
 
572
        return
 
573
 
 
574
 
 
575
class Commit(BaseCommand):
 
576
    """
 
577
    Create a revision based on the changes in the current tree.
 
578
    """
 
579
    
 
580
    def __init__(self):
 
581
        self.description="Write local changes to the archive"
 
582
 
 
583
    def get_completer(self, arg, index):
 
584
        if arg is None:
 
585
            arg = ""
 
586
        return iter_modified_file_completions(arch.tree_root("."), arg)
 
587
#        return iter_source_file_completions(arch.tree_root("."), arg)
 
588
    
 
589
    def parse_commandline(self, cmdline, tree):
 
590
        """
 
591
        Parse commandline arguments.  Raise cmtutil.GetHelp if help is needed.
 
592
        
 
593
        :param cmdline: A list of arguments to parse
 
594
        :rtype: (options, Revision, Revision/WorkingTree)
 
595
        """
 
596
        parser=self.get_parser()
 
597
        (options, files) = parser.parse_args(cmdline)
 
598
 
 
599
        if len(files) == 0:
 
600
            files = None
 
601
        else:
 
602
            files = [pylon.tree_file_path(tree, f) for f in files]
 
603
        return options, tree.tree_version, files
 
604
 
 
605
 
 
606
    def do_command(self, cmdargs):
 
607
        """
 
608
        Master function that perfoms the "commit" command.
 
609
        """
 
610
        tree=arch.tree_root(".")
 
611
        options, version, files = self.parse_commandline(cmdargs, tree)
 
612
        ancestor = None
 
613
        if options.__dict__.has_key("base") and options.base:
 
614
            base = cmdutil.determine_revision_tree(tree, options.base)
 
615
            ancestor = base
 
616
        else:
 
617
            base = pylon.submit_revision(tree)
 
618
            ancestor = base
 
619
        if ancestor is None:
 
620
            ancestor = pylon.tree_latest(tree, version)
 
621
 
 
622
        writeversion=version
 
623
        archive=version.archive
 
624
        source=cmdutil.get_mirror_source(archive)
 
625
        allow_old=False
 
626
        writethrough="implicit"
 
627
 
 
628
        if source!=None:
 
629
            if writethrough=="explicit" and \
 
630
                cmdutil.prompt("Writethrough"):
 
631
                writeversion=arch.Version(str(source)+"/"+str(version.get_nonarch()))
 
632
            elif writethrough=="none":
 
633
                raise CommitToMirror(archive)
 
634
 
 
635
        elif archive.is_mirror:
 
636
            raise CommitToMirror(archive)
 
637
 
 
638
        try:
 
639
            last_revision=tree.iter_logs(version, True).next().revision
 
640
        except StopIteration, e:
 
641
            last_revision = None
 
642
            if ancestor is None:
 
643
                if cmdutil.prompt("Import from commit"):
 
644
                    return do_import(version)
 
645
                else:
 
646
                    raise NoVersionLogs(version)
 
647
        try:
 
648
            arch_last_revision = version.iter_revisions(True).next()
 
649
        except StopIteration, e:
 
650
            arch_last_revision = None
 
651
 
 
652
        if last_revision != arch_last_revision:
 
653
            print "Tree is not up to date with %s" % str(version)
 
654
            if not cmdutil.prompt("Out of date"):
 
655
                raise OutOfDate
 
656
            else:
 
657
                allow_old=True
 
658
 
 
659
        try:
 
660
            if not pylon.has_changed(ancestor):
 
661
                if not cmdutil.prompt("Empty commit"):
 
662
                    raise EmptyCommit
 
663
        except arch.util.ExecProblem, e:
 
664
            if e.proc.error and e.proc.error.startswith(
 
665
                "missing explicit id for file"):
 
666
                raise MissingID(e)
 
667
            else:
 
668
                raise
 
669
        log = tree.log_message(create=False)
 
670
        if log is None:
 
671
            try:
 
672
                if cmdutil.prompt("Create log"):
 
673
                    edit_log(tree)
 
674
 
 
675
            except cmdutil.NoEditorSpecified, e:
 
676
                raise CommandFailed(e)
 
677
            log = tree.log_message(create=False)
 
678
        if log is None: 
 
679
            raise NoLogMessage
 
680
        if log["Summary"] is None or len(log["Summary"].strip()) == 0:
 
681
            if not cmdutil.prompt("Omit log summary"):
 
682
                raise errors.NoLogSummary
 
683
        try:
 
684
            for line in tree.iter_commit(seal=options.seal_version,
 
685
                base=base, version=version, out_of_date_ok=allow_old, 
 
686
                file_list=files):
 
687
                cmdutil.colorize(line, options.suppress_chatter)
 
688
 
 
689
        except arch.util.ExecProblem, e:
 
690
            if e.proc.error and e.proc.error.startswith(
 
691
                "These files violate naming conventions:"):
 
692
                raise LintFailure(e.proc.error)
 
693
            else:
 
694
                raise
 
695
 
 
696
    def get_parser(self):
 
697
        """
 
698
        Returns the options parser to use for the "commit" command.
 
699
 
 
700
        :rtype: cmdutil.CmdOptionParser
 
701
        """
 
702
 
 
703
        parser=cmdutil.CmdOptionParser("fai commit [options] [file1]"
 
704
                                       " [file2...]")
 
705
        parser.add_option("--seal", action="store_true", 
 
706
                          dest="seal_version", default=False, 
 
707
                          help="seal this version")
 
708
        parser.add_option("-s", "--silent", action="store_true", 
 
709
                          dest="suppress_chatter", default=False, 
 
710
                          help="Suppress chatter messages")
 
711
        if cmdutil.supports_switch("commit", "--base"):
 
712
            parser.add_option("--base", dest="base", help="", 
 
713
                              metavar="REVISION")
 
714
        return parser
 
715
 
 
716
    def help(self, parser=None):
 
717
        """
 
718
        Prints a help message.
 
719
 
 
720
        :param parser: If supplied, the parser to use for generating help.  If \
 
721
        not supplied, it is retrieved.
 
722
        :type parser: cmdutil.CmdOptionParser
 
723
        """
 
724
        if parser is None:
 
725
            parser=self.get_parser()
 
726
        parser.print_help()
 
727
        print """
 
728
Updates a working tree to the current archive revision
 
729
 
 
730
If a version is specified, that is used instead 
 
731
        """
 
732
#        help_tree_spec() 
 
733
        return
 
734
 
 
735
 
 
736
 
 
737
class CatLog(BaseCommand):
 
738
    """
 
739
    Print the log of a given file (from current tree)
 
740
    """
 
741
    def __init__(self):
 
742
        self.description="Prints the patch log for a revision"
 
743
 
 
744
    def get_completer(self, arg, index):
 
745
        if index > 0:
 
746
            return None
 
747
        try:
 
748
            tree = arch.tree_root(".")
 
749
        except:
 
750
            tree = None
 
751
        return cmdutil.iter_revision_completions(arg, tree)
 
752
 
 
753
    def do_command(self, cmdargs):
 
754
        """
 
755
        Master function that perfoms the "cat-log" command.
 
756
        """
 
757
        parser=self.get_parser()
 
758
        (options, args) = parser.parse_args(cmdargs)
 
759
        try:
 
760
            tree = arch.tree_root(".")
 
761
        except arch.errors.TreeRootError, e:
 
762
            tree = None
 
763
        spec=None
 
764
        if len(args) > 0:
 
765
            spec=args[0]
 
766
        if len(args) > 1:
 
767
            raise cmdutil.GetHelp()
 
768
        try:
 
769
            if tree:
 
770
                revision = cmdutil.determine_revision_tree(tree, spec)
 
771
            else:
 
772
                revision = cmdutil.determine_revision_arch(tree, spec)
 
773
        except cmdutil.CantDetermineRevision, e:
 
774
            raise CommandFailedWrapper(e)
 
775
        log = None
 
776
        
 
777
        use_tree = (options.source == "tree" or \
 
778
            (options.source == "any" and tree))
 
779
        use_arch = (options.source == "archive" or options.source == "any")
 
780
        
 
781
        log = None
 
782
        if use_tree:
 
783
            for log in tree.iter_logs(revision.version):
 
784
                if log.revision == revision:
 
785
                    break
 
786
                else:
 
787
                    log = None
 
788
        if log is None and use_arch:
 
789
            cmdutil.ensure_revision_exists(revision)
 
790
            log = arch.Patchlog(revision)
 
791
        if log is not None:
 
792
            for item in log.items():
 
793
                print "%s: %s" % item
 
794
            print log.description
 
795
 
 
796
    def get_parser(self):
 
797
        """
 
798
        Returns the options parser to use for the "cat-log" command.
 
799
 
 
800
        :rtype: cmdutil.CmdOptionParser
 
801
        """
 
802
        parser=cmdutil.CmdOptionParser("fai cat-log [revision]")
 
803
        parser.add_option("--archive", action="store_const", dest="source",
 
804
                          const="archive", default="any",
 
805
                          help="Always get the log from the archive")
 
806
        parser.add_option("--tree", action="store_const", dest="source",
 
807
                          const="tree", help="Always get the log from the tree")
 
808
        return parser 
 
809
 
 
810
    def help(self, parser=None):
 
811
        """
 
812
        Prints a help message.
 
813
 
 
814
        :param parser: If supplied, the parser to use for generating help.  If \
 
815
        not supplied, it is retrieved.
 
816
        :type parser: cmdutil.CmdOptionParser
 
817
        """
 
818
        if parser==None:
 
819
            parser=self.get_parser()
 
820
        parser.print_help()
 
821
        print """
 
822
Prints the log for the specified revision
 
823
        """
 
824
        help_tree_spec()
 
825
        return
 
826
 
 
827
class Revert(BaseCommand):
 
828
    """ Reverts a tree (or aspects of it) to a revision
 
829
    """
 
830
    def __init__(self):
 
831
        self.description="Reverts a tree (or aspects of it) to a revision "
 
832
 
 
833
    def get_completer(self, arg, index):
 
834
        if index > 0:
 
835
            return None
 
836
        try:
 
837
            tree = arch.tree_root(".")
 
838
        except:
 
839
            tree = None
 
840
        return iter_modified_file_completions(tree, arg)
 
841
 
 
842
    def do_command(self, cmdargs):
 
843
        """
 
844
        Master function that perfoms the "revert" command.
 
845
        """
 
846
        parser=self.get_parser()
 
847
        (options, args) = parser.parse_args(cmdargs)
 
848
        try:
 
849
            tree = arch.tree_root(".")
 
850
        except arch.errors.TreeRootError, e:
 
851
            raise CommandFailed(e)
 
852
        spec=None
 
853
        if options.revision is not None:
 
854
            spec=options.revision
 
855
        try:
 
856
            if spec is not None:
 
857
                revision = cmdutil.determine_revision_tree(tree, spec)
 
858
            else:
 
859
                revision = pylon.comp_revision(tree)
 
860
        except cmdutil.CantDetermineRevision, e:
 
861
            raise CommandFailedWrapper(e)
 
862
        munger = None
 
863
 
 
864
        if options.file_contents or options.file_perms or options.deletions\
 
865
            or options.additions or options.renames or options.hunk_prompt:
 
866
            munger = pylon.MungeOpts()
 
867
            munger.set_hunk_prompt(cmdutil.colorize, cmdutil.user_hunk_confirm,
 
868
                                   options.hunk_prompt)
 
869
 
 
870
        if len(args) > 0 or options.logs or options.pattern_files or \
 
871
            options.control:
 
872
            if munger is None:
 
873
                munger = pylon.MungeOpts(True)
 
874
                munger.all_types(True)
 
875
        if len(args) > 0:
 
876
            t_cwd = pylon.tree_cwd(tree)
 
877
            for name in args:
 
878
                if len(t_cwd) > 0:
 
879
                    t_cwd += "/"
 
880
                name = os.path.normpath(os.path.join(t_cwd, name))
 
881
                name = os.path.join("./", name)
 
882
                munger.add_keep_file(name);
 
883
 
 
884
        if options.file_perms:
 
885
            munger.file_perms = True
 
886
        if options.file_contents:
 
887
            munger.file_contents = True
 
888
        if options.deletions:
 
889
            munger.deletions = True
 
890
        if options.additions:
 
891
            munger.additions = True
 
892
        if options.renames:
 
893
            munger.renames = True
 
894
        if options.logs:
 
895
            munger.add_keep_pattern('^\./\{arch\}/[^=].*')
 
896
        if options.control:
 
897
            munger.add_keep_pattern("/\.arch-ids|^\./\{arch\}|"\
 
898
                                    "/\.arch-inventory$")
 
899
        if options.pattern_files:
 
900
            munger.add_keep_pattern(options.pattern_files)
 
901
                
 
902
        for line in pylon.revert(tree, revision, munger, 
 
903
                                   not options.no_output):
 
904
            cmdutil.colorize(line)
 
905
 
 
906
 
 
907
    def get_parser(self):
 
908
        """
 
909
        Returns the options parser to use for the "cat-log" command.
 
910
 
 
911
        :rtype: cmdutil.CmdOptionParser
 
912
        """
 
913
        parser=cmdutil.CmdOptionParser("fai revert [options] [FILE...]")
 
914
        parser.add_option("", "--contents", action="store_true", 
 
915
                          dest="file_contents", 
 
916
                          help="Revert file content changes")
 
917
        parser.add_option("", "--permissions", action="store_true", 
 
918
                          dest="file_perms", 
 
919
                          help="Revert file permissions changes")
 
920
        parser.add_option("", "--deletions", action="store_true", 
 
921
                          dest="deletions", 
 
922
                          help="Restore deleted files")
 
923
        parser.add_option("", "--additions", action="store_true", 
 
924
                          dest="additions", 
 
925
                          help="Remove added files")
 
926
        parser.add_option("", "--renames", action="store_true", 
 
927
                          dest="renames", 
 
928
                          help="Revert file names")
 
929
        parser.add_option("--hunks", action="store_true", 
 
930
                          dest="hunk_prompt", default=False,
 
931
                          help="Prompt which hunks to revert")
 
932
        parser.add_option("--pattern-files", dest="pattern_files", 
 
933
                          help="Revert files that match this pattern", 
 
934
                          metavar="REGEX")
 
935
        parser.add_option("--logs", action="store_true", 
 
936
                          dest="logs", default=False,
 
937
                          help="Revert only logs")
 
938
        parser.add_option("--control-files", action="store_true", 
 
939
                          dest="control", default=False,
 
940
                          help="Revert logs and other control files")
 
941
        parser.add_option("-n", "--no-output", action="store_true", 
 
942
                          dest="no_output", 
 
943
                          help="Don't keep an undo changeset")
 
944
        parser.add_option("--revision", dest="revision", 
 
945
                          help="Revert to the specified revision", 
 
946
                          metavar="REVISION")
 
947
        return parser 
 
948
 
 
949
    def help(self, parser=None):
 
950
        """
 
951
        Prints a help message.
 
952
 
 
953
        :param parser: If supplied, the parser to use for generating help.  If \
 
954
        not supplied, it is retrieved.
 
955
        :type parser: cmdutil.CmdOptionParser
 
956
        """
 
957
        if parser==None:
 
958
            parser=self.get_parser()
 
959
        parser.print_help()
 
960
        print """
 
961
Reverts changes in the current working tree.  If no flags are specified, all
 
962
types of changes are reverted.  Otherwise, only selected types of changes are
 
963
reverted.  
 
964
 
 
965
If a revision is specified on the commandline, differences between the current
 
966
tree and that revision are reverted.  If a version is specified, the current
 
967
tree is used to determine the revision.
 
968
 
 
969
If files are specified, only those files listed will have any changes applied.
 
970
To specify a renamed file, you can use either the old or new name. (or both!)
 
971
 
 
972
Unless "-n" is specified, reversions can be undone with "redo".
 
973
        """
 
974
        return
 
975
 
 
976
class Revision(BaseCommand):
 
977
    """
 
978
    Print a revision name based on a revision specifier
 
979
    """
 
980
    def __init__(self):
 
981
        self.description="Prints the name of a revision"
 
982
 
 
983
    def get_completer(self, arg, index):
 
984
        if index > 0:
 
985
            return None
 
986
        try:
 
987
            tree = arch.tree_root(".")
 
988
        except:
 
989
            tree = None
 
990
        return cmdutil.iter_revision_completions(arg, tree)
 
991
 
 
992
    def do_command(self, cmdargs):
 
993
        """
 
994
        Master function that perfoms the "revision" command.
 
995
        """
 
996
        parser=self.get_parser()
 
997
        (options, args) = parser.parse_args(cmdargs)
 
998
 
 
999
        try:
 
1000
            tree = arch.tree_root(".")
 
1001
        except arch.errors.TreeRootError:
 
1002
            tree = None
 
1003
 
 
1004
        spec=None
 
1005
        if len(args) > 0:
 
1006
            spec=args[0]
 
1007
        if len(args) > 1:
 
1008
            raise cmdutil.GetHelp
 
1009
        try:
 
1010
            if tree:
 
1011
                revision = cmdutil.determine_revision_tree(tree, spec)
 
1012
            else:
 
1013
                revision = cmdutil.determine_revision_arch(tree, spec)
 
1014
        except cmdutil.CantDetermineRevision, e:
 
1015
            print str(e)
 
1016
            return
 
1017
        print options.display(revision)
 
1018
 
 
1019
    def get_parser(self):
 
1020
        """
 
1021
        Returns the options parser to use for the "revision" command.
 
1022
 
 
1023
        :rtype: cmdutil.CmdOptionParser
 
1024
        """
 
1025
        parser=cmdutil.CmdOptionParser("fai revision [revision]")
 
1026
        parser.add_option("", "--location", action="store_const", 
 
1027
                         const=pylon.paths.determine_path, dest="display", 
 
1028
                         help="Show location instead of name", default=str)
 
1029
        parser.add_option("--import", action="store_const", 
 
1030
                         const=pylon.paths.determine_import_path, 
 
1031
                         dest="display",  help="Show location of import file")
 
1032
        parser.add_option("--log", action="store_const", 
 
1033
                         const=pylon.paths.determine_log_path, dest="display", 
 
1034
                         help="Show location of log file")
 
1035
        parser.add_option("--patch", action="store_const", 
 
1036
                         dest="display", const=pylon.paths.determine_patch_path,
 
1037
                         help="Show location of patchfile")
 
1038
        parser.add_option("--continuation", action="store_const", 
 
1039
                         const=pylon.paths.determine_continuation_path, 
 
1040
                         dest="display",
 
1041
                         help="Show location of continuation file")
 
1042
        parser.add_option("--cacherev", action="store_const", 
 
1043
                         const=pylon.paths.determine_cacherev_path, 
 
1044
                         dest="display", help="Show location of cacherev file")
 
1045
        return parser 
 
1046
 
 
1047
    def help(self, parser=None):
 
1048
        """
 
1049
        Prints a help message.
 
1050
 
 
1051
        :param parser: If supplied, the parser to use for generating help.  If \
 
1052
        not supplied, it is retrieved.
 
1053
        :type parser: cmdutil.CmdOptionParser
 
1054
        """
 
1055
        if parser==None:
 
1056
            parser=self.get_parser()
 
1057
        parser.print_help()
 
1058
        print """
 
1059
Expands aliases and prints the name of the specified revision.  Instead of
 
1060
the name, several options can be used to print locations.  If more than one is
 
1061
specified, the last one is used.
 
1062
        """
 
1063
        help_tree_spec()
 
1064
        return
 
1065
 
 
1066
class Revisions(BaseCommand):
 
1067
    """
 
1068
    Print a revision name based on a revision specifier
 
1069
    """
 
1070
    def __init__(self):
 
1071
        self.description="Lists revisions"
 
1072
        self.cl_revisions = []
 
1073
    
 
1074
    def do_command(self, cmdargs):
 
1075
        """
 
1076
        Master function that perfoms the "revision" command.
 
1077
        """
 
1078
        (options, args) = self.get_parser().parse_args(cmdargs)
 
1079
        if len(args) > 1:
 
1080
            raise cmdutil.GetHelp
 
1081
        try:
 
1082
            self.tree = arch.tree_root(".")
 
1083
        except arch.errors.TreeRootError:
 
1084
            self.tree = None
 
1085
        if options.type == "default":
 
1086
            options.type = "archive"
 
1087
        try:
 
1088
            iter = cmdutil.revision_iterator(self.tree, options.type, args, 
 
1089
                                             options.reverse, options.modified,
 
1090
                                             options.shallow)
 
1091
        except cmdutil.CantDetermineRevision, e:
 
1092
            raise CommandFailedWrapper(e)
 
1093
        except cmdutil.CantDetermineVersion, e:
 
1094
            raise CommandFailedWrapper(e)
 
1095
        if options.skip is not None:
 
1096
            iter = cmdutil.iter_skip(iter, int(options.skip))
 
1097
 
 
1098
        try:
 
1099
            for revision in iter:
 
1100
                log = None
 
1101
                if isinstance(revision, arch.Patchlog):
 
1102
                    log = revision
 
1103
                    revision=revision.revision
 
1104
                out = options.display(revision)
 
1105
                if out is not None:
 
1106
                    print out
 
1107
                if log is None and (options.summary or options.creator or 
 
1108
                                    options.date or options.merges):
 
1109
                    log = revision.patchlog
 
1110
                if options.creator:
 
1111
                    print "    %s" % log.creator
 
1112
                if options.date:
 
1113
                    print "    %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date)
 
1114
                if options.summary:
 
1115
                    print "    %s" % log.summary
 
1116
                if options.merges:
 
1117
                    showed_title = False
 
1118
                    for revision in log.merged_patches:
 
1119
                        if not showed_title:
 
1120
                            print "    Merged:"
 
1121
                            showed_title = True
 
1122
                        print "    %s" % revision
 
1123
            if len(self.cl_revisions) > 0:
 
1124
                print pylon.changelog_for_merge(self.cl_revisions, None)
 
1125
        except pylon.errors.TreeRootNone:
 
1126
            raise CommandFailedWrapper(
 
1127
                Exception("This option can only be used in a project tree."))
 
1128
 
 
1129
    def changelog_append(self, revision):
 
1130
        if isinstance(revision, arch.Revision):
 
1131
            revision=arch.Patchlog(revision)
 
1132
        self.cl_revisions.append(revision)
 
1133
   
 
1134
    def get_parser(self):
 
1135
        """
 
1136
        Returns the options parser to use for the "revision" command.
 
1137
 
 
1138
        :rtype: cmdutil.CmdOptionParser
 
1139
        """
 
1140
        parser=cmdutil.CmdOptionParser("fai revisions [version/revision]")
 
1141
        select = cmdutil.OptionGroup(parser, "Selection options",
 
1142
                          "Control which revisions are listed.  These options"
 
1143
                          " are mutually exclusive.  If more than one is"
 
1144
                          " specified, the last is used.")
 
1145
 
 
1146
        cmdutil.add_revision_iter_options(select)
 
1147
        parser.add_option("", "--skip", dest="skip", 
 
1148
                          help="Skip revisions.  Positive numbers skip from "
 
1149
                          "beginning, negative skip from end.",
 
1150
                          metavar="NUMBER")
 
1151
 
 
1152
        parser.add_option_group(select)
 
1153
 
 
1154
        format = cmdutil.OptionGroup(parser, "Revision format options",
 
1155
                          "These control the appearance of listed revisions")
 
1156
        format.add_option("", "--location", action="store_const", 
 
1157
                         const=pylon.paths.determine_path, dest="display", 
 
1158
                         help="Show location instead of name", default=str)
 
1159
        format.add_option("--import", action="store_const", 
 
1160
                         const=pylon.paths.determine_import_path, 
 
1161
                         dest="display",  help="Show location of import file")
 
1162
        format.add_option("--log", action="store_const", 
 
1163
                         const=pylon.paths.determine_log_path, dest="display", 
 
1164
                         help="Show location of log file")
 
1165
        format.add_option("--patch", action="store_const", 
 
1166
                         dest="display", const=pylon.paths.determine_patch_path,
 
1167
                         help="Show location of patchfile")
 
1168
        format.add_option("--continuation", action="store_const", 
 
1169
                         const=pylon.paths.determine_continuation_path, 
 
1170
                         dest="display",
 
1171
                         help="Show location of continuation file")
 
1172
        format.add_option("--cacherev", action="store_const", 
 
1173
                         const=pylon.paths.determine_cacherev_path, 
 
1174
                         dest="display", help="Show location of cacherev file")
 
1175
        format.add_option("--changelog", action="store_const", 
 
1176
                         const=self.changelog_append, dest="display",
 
1177
                         help="Show location of cacherev file")
 
1178
        parser.add_option_group(format)
 
1179
        display = cmdutil.OptionGroup(parser, "Display format options",
 
1180
                          "These control the display of data")
 
1181
        display.add_option("-r", "--reverse", action="store_true", 
 
1182
                          dest="reverse", help="Sort from newest to oldest")
 
1183
        display.add_option("-s", "--summary", action="store_true", 
 
1184
                          dest="summary", help="Show patchlog summary")
 
1185
        display.add_option("-D", "--date", action="store_true", 
 
1186
                          dest="date", help="Show patchlog date")
 
1187
        display.add_option("-c", "--creator", action="store_true", 
 
1188
                          dest="creator", help="Show the id that committed the"
 
1189
                          " revision")
 
1190
        display.add_option("-m", "--merges", action="store_true", 
 
1191
                          dest="merges", help="Show the revisions that were"
 
1192
                          " merged")
 
1193
        parser.add_option_group(display)
 
1194
        return parser 
 
1195
    def help(self, parser=None):
 
1196
        """Attempt to explain the revisions command
 
1197
        
 
1198
        :param parser: If supplied, used to determine options
 
1199
        """
 
1200
        if parser==None:
 
1201
            parser=self.get_parser()
 
1202
        parser.print_help()
 
1203
        print """List revisions.
 
1204
        """
 
1205
        help_tree_spec()
 
1206
 
 
1207
 
 
1208
class Get(BaseCommand):
 
1209
    """
 
1210
    Retrieve a revision from the archive
 
1211
    """
 
1212
    def __init__(self):
 
1213
        self.description="Create a working tree for a revision"
 
1214
        self.parser=self.get_parser()
 
1215
 
 
1216
 
 
1217
    def get_completer(self, arg, index):
 
1218
        if index > 0:
 
1219
            return None
 
1220
        try:
 
1221
            tree = arch.tree_root(".")
 
1222
        except:
 
1223
            tree = None
 
1224
        return cmdutil.iter_revision_completions(arg, tree)
 
1225
 
 
1226
 
 
1227
    def do_command(self, cmdargs):
 
1228
        """
 
1229
        Master function that perfoms the "get" command.
 
1230
        """
 
1231
        (options, args) = self.parser.parse_args(cmdargs)
 
1232
        if len(args) < 1:
 
1233
            return self.help()            
 
1234
        try:
 
1235
            tree = arch.tree_root(".")
 
1236
        except arch.errors.TreeRootError:
 
1237
            tree = None
 
1238
        
 
1239
        arch_loc = None
 
1240
        try:
 
1241
            revision, arch_loc = pylon.paths.full_path_decode(args[0])
 
1242
        except Exception, e:
 
1243
            revision = cmdutil.determine_revision_arch(tree, args[0], 
 
1244
                check_existence=False, allow_package=True)
 
1245
        if len(args) > 1:
 
1246
            directory = args[1]
 
1247
        else:
 
1248
            directory = str(revision.nonarch)
 
1249
        if os.path.exists(directory):
 
1250
            raise DirectoryExists(directory)
 
1251
        cmdutil.ensure_archive_registered(revision.archive, arch_loc)
 
1252
        try:
 
1253
            cmdutil.ensure_revision_exists(revision)
 
1254
        except cmdutil.NoSuchRevision, e:
 
1255
            raise CommandFailedWrapper(e)
 
1256
 
 
1257
        link = cmdutil.prompt ("get link")
 
1258
        for line in pylon.iter_get(revision, directory, link,
 
1259
                                     options.no_pristine,
 
1260
                                     options.no_greedy_add):
 
1261
            cmdutil.colorize(line)
 
1262
 
 
1263
    def get_parser(self):
 
1264
        """
 
1265
        Returns the options parser to use for the "get" command.
 
1266
 
 
1267
        :rtype: cmdutil.CmdOptionParser
 
1268
        """
 
1269
        parser=cmdutil.CmdOptionParser("fai get revision [dir]")
 
1270
        parser.add_option("--no-pristine", action="store_true", 
 
1271
                         dest="no_pristine", 
 
1272
                         help="Do not make pristine copy for reference")
 
1273
        parser.add_option("--no-greedy-add", action="store_true", 
 
1274
                         dest="no_greedy_add", 
 
1275
                         help="Never add to greedy libraries")
 
1276
 
 
1277
        return parser 
 
1278
 
 
1279
    def help(self, parser=None):
 
1280
        """
 
1281
        Prints a help message.
 
1282
 
 
1283
        :param parser: If supplied, the parser to use for generating help.  If \
 
1284
        not supplied, it is retrieved.
 
1285
        :type parser: cmdutil.CmdOptionParser
 
1286
        """
 
1287
        if parser==None:
 
1288
            parser=self.get_parser()
 
1289
        parser.print_help()
 
1290
        print """
 
1291
Expands aliases and constructs a project tree for a revision.  If the optional
 
1292
"dir" argument is provided, the project tree will be stored in this directory.
 
1293
        """
 
1294
        help_tree_spec()
 
1295
        return
 
1296
 
 
1297
class PromptCmd(cmd.Cmd):
 
1298
    def __init__(self):
 
1299
        cmd.Cmd.__init__(self)
 
1300
        self.prompt = "Fai> "
 
1301
        try:
 
1302
            self.tree = arch.tree_root(".")
 
1303
        except:
 
1304
            self.tree = None
 
1305
        self.set_title()
 
1306
        self.set_prompt()
 
1307
        self.fake_aba = abacmds.AbaCmds()
 
1308
        self.identchars += '-'
 
1309
        self.history_file = os.path.expanduser("~/.fai-history")
 
1310
        readline.set_completer_delims(string.whitespace)
 
1311
        if os.access(self.history_file, os.R_OK) and \
 
1312
            os.path.isfile(self.history_file):
 
1313
            readline.read_history_file(self.history_file)
 
1314
        self.cwd = os.getcwd()
 
1315
 
 
1316
    def write_history(self):
 
1317
        readline.write_history_file(self.history_file)
 
1318
 
 
1319
    def do_quit(self, args):
 
1320
        self.write_history()
 
1321
        sys.exit(0)
 
1322
 
 
1323
    def do_exit(self, args):
 
1324
        self.do_quit(args)
 
1325
 
 
1326
    def do_EOF(self, args):
 
1327
        print
 
1328
        self.do_quit(args)
 
1329
 
 
1330
    def postcmd(self, line, bar):
 
1331
        self.set_title()
 
1332
        self.set_prompt()
 
1333
 
 
1334
    def set_prompt(self):
 
1335
        if self.tree is not None:
 
1336
            try:
 
1337
                prompt = pylon.alias_or_version(self.tree.tree_version, 
 
1338
                                                self.tree, 
 
1339
                                                full=False)
 
1340
                if prompt is not None:
 
1341
                    prompt = " " + prompt +":"+ pylon.tree_cwd(self.tree)
 
1342
            except:
 
1343
                prompt = ""
 
1344
        else:
 
1345
            prompt = ""
 
1346
        self.prompt = "Fai%s> " % prompt
 
1347
 
 
1348
    def set_title(self, command=None):
 
1349
        try:
 
1350
            version = pylon.alias_or_version(self.tree.tree_version, self.tree, 
 
1351
                                             full=False)
 
1352
        except:
 
1353
            version = "[no version]"
 
1354
        if command is None:
 
1355
            command = ""
 
1356
        sys.stdout.write(terminal.term_title("Fai %s %s" % (command, version)))
 
1357
 
 
1358
    def do_cd(self, line):
 
1359
        if line == "":
 
1360
            line = "~"
 
1361
        line = os.path.expanduser(line)
 
1362
        if os.path.isabs(line):
 
1363
            newcwd = line
 
1364
        else:
 
1365
            newcwd = self.cwd+'/'+line
 
1366
        newcwd = os.path.normpath(newcwd)
 
1367
        try:
 
1368
            os.chdir(newcwd)
 
1369
            self.cwd = newcwd
 
1370
        except Exception, e:
 
1371
            print e
 
1372
        try:
 
1373
            self.tree = arch.tree_root(".")
 
1374
        except:
 
1375
            self.tree = None
 
1376
 
 
1377
    def do_help(self, line):
 
1378
        Help()(line)
 
1379
 
 
1380
    def default(self, line):
 
1381
        args = line.split()
 
1382
        if find_command(args[0]):
 
1383
            try:
 
1384
                find_command(args[0]).do_command(args[1:])
 
1385
            except cmdutil.BadCommandOption, e:
 
1386
                print e
 
1387
            except cmdutil.GetHelp, e:
 
1388
                find_command(args[0]).help()
 
1389
            except CommandFailed, e:
 
1390
                print e
 
1391
            except arch.errors.ArchiveNotRegistered, e:
 
1392
                print e
 
1393
            except KeyboardInterrupt, e:
 
1394
                print "Interrupted"
 
1395
            except arch.util.ExecProblem, e:
 
1396
                print e.proc.error.rstrip('\n')
 
1397
            except cmdutil.CantDetermineVersion, e:
 
1398
                print e
 
1399
            except cmdutil.CantDetermineRevision, e:
 
1400
                print e
 
1401
            except Exception, e:
 
1402
                print "Unhandled error:\n%s" % errors.exception_str(e)
 
1403
 
 
1404
        elif suggestions.has_key(args[0]):
 
1405
            print suggestions[args[0]]
 
1406
 
 
1407
        elif self.fake_aba.is_command(args[0]):
 
1408
            tree = None
 
1409
            try:
 
1410
                tree = arch.tree_root(".")
 
1411
            except arch.errors.TreeRootError:
 
1412
                pass
 
1413
            cmd = self.fake_aba.is_command(args[0])
 
1414
            try:
 
1415
                cmd.run(cmdutil.expand_prefix_alias(args[1:], tree))
 
1416
            except KeyboardInterrupt, e:
 
1417
                print "Interrupted"
 
1418
 
 
1419
        elif options.tla_fallthrough and args[0] != "rm" and \
 
1420
            cmdutil.is_tla_command(args[0]):
 
1421
            try:
 
1422
                tree = None
 
1423
                try:
 
1424
                    tree = arch.tree_root(".")
 
1425
                except arch.errors.TreeRootError:
 
1426
                    pass
 
1427
                args = cmdutil.expand_prefix_alias(args, tree)
 
1428
                arch.util.exec_safe('tla', args, stderr=sys.stderr,
 
1429
                                    stdout=sys.stdout, expected=(0, 1))
 
1430
            except arch.util.ExecProblem, e:
 
1431
                pass
 
1432
            except KeyboardInterrupt, e:
 
1433
                print "Interrupted"
 
1434
        else:
 
1435
            try:
 
1436
                try:
 
1437
                    tree = arch.tree_root(".")
 
1438
                except arch.errors.TreeRootError:
 
1439
                    tree = None
 
1440
                args=line.split()
 
1441
                os.system(" ".join(cmdutil.expand_prefix_alias(args, tree)))
 
1442
            except KeyboardInterrupt, e:
 
1443
                print "Interrupted"
 
1444
 
 
1445
    def completenames(self, text, line, begidx, endidx):
 
1446
        completions = []
 
1447
        iter = iter_command_names(self.fake_aba)
 
1448
        try:
 
1449
            if len(line) > 0:
 
1450
                arg = line.split()[-1]
 
1451
            else:
 
1452
                arg = ""
 
1453
            iter = cmdutil.iter_munged_completions(iter, arg, text)
 
1454
        except Exception, e:
 
1455
            print e
 
1456
        return list(iter)
 
1457
 
 
1458
    def completedefault(self, text, line, begidx, endidx):
 
1459
        """Perform completion for native commands.
 
1460
        
 
1461
        :param text: The text to complete
 
1462
        :type text: str
 
1463
        :param line: The entire line to complete
 
1464
        :type line: str
 
1465
        :param begidx: The start of the text in the line
 
1466
        :type begidx: int
 
1467
        :param endidx: The end of the text in the line
 
1468
        :type endidx: int
 
1469
        """
 
1470
        try:
 
1471
            (cmd, args, foo) = self.parseline(line)
 
1472
            command_obj=find_command(cmd)
 
1473
            if command_obj is not None:
 
1474
                return command_obj.complete(args.split(), text)
 
1475
            elif not self.fake_aba.is_command(cmd) and \
 
1476
                cmdutil.is_tla_command(cmd):
 
1477
                iter = cmdutil.iter_supported_switches(cmd)
 
1478
                if len(args) > 0:
 
1479
                    arg = args.split()[-1]
 
1480
                else:
 
1481
                    arg = ""
 
1482
                if arg.startswith("-"):
 
1483
                    return list(cmdutil.iter_munged_completions(iter, arg, 
 
1484
                                                                text))
 
1485
                else:
 
1486
                    return list(cmdutil.iter_munged_completions(
 
1487
                        cmdutil.iter_file_completions(arg), arg, text))
 
1488
 
 
1489
 
 
1490
            elif cmd == "cd":
 
1491
                if len(args) > 0:
 
1492
                    arg = args.split()[-1]
 
1493
                else:
 
1494
                    arg = ""
 
1495
                iter = cmdutil.iter_dir_completions(arg)
 
1496
                iter = cmdutil.iter_munged_completions(iter, arg, text)
 
1497
                return list(iter)
 
1498
            elif len(args)>0:
 
1499
                arg = args.split()[-1]
 
1500
                iter = cmdutil.iter_file_completions(arg)
 
1501
                return list(cmdutil.iter_munged_completions(iter, arg, text))
 
1502
            else:
 
1503
                return self.completenames(text, line, begidx, endidx)
 
1504
        except Exception, e:
 
1505
            print e
 
1506
 
 
1507
 
 
1508
def iter_command_names(fake_aba):
 
1509
    for entry in cmdutil.iter_combine([commands.iterkeys(), 
 
1510
                                     fake_aba.get_commands(), 
 
1511
                                     pylon.iter_tla_commands(False)]):
 
1512
        if not suggestions.has_key(str(entry)):
 
1513
            yield entry
 
1514
 
 
1515
 
 
1516
def iter_source_file_completions(tree, arg):
 
1517
    treepath = pylon.tree_cwd(tree)
 
1518
    if len(treepath) > 0:
 
1519
        dirs = [treepath]
 
1520
    else:
 
1521
        dirs = None
 
1522
    for file in tree.iter_inventory(dirs, source=True, both=True):
 
1523
        file = file_completion_match(file, treepath, arg)
 
1524
        if file is not None:
 
1525
            yield file
 
1526
 
 
1527
 
 
1528
def iter_untagged(tree, dirs):
 
1529
    for file in pylon.iter_inventory_filter(tree, dirs, tagged=False, 
 
1530
                                                categories=pylon.non_root,
 
1531
                                                control_files=True):
 
1532
        yield file.name 
 
1533
 
 
1534
 
 
1535
def iter_untagged_completions(tree, arg):
 
1536
    """Generate an iterator for all visible untagged files that match arg.
 
1537
 
 
1538
    :param tree: The tree to look for untagged files in
 
1539
    :type tree: `arch.WorkingTree`
 
1540
    :param arg: The argument to match
 
1541
    :type arg: str
 
1542
    :return: An iterator of all matching untagged files
 
1543
    :rtype: iterator of str
 
1544
    """
 
1545
    treepath = pylon.tree_cwd(tree)
 
1546
 
 
1547
    for file in iter_untagged(tree, None):
 
1548
        file = file_completion_match(file, "", arg)
 
1549
        if file is not None:
 
1550
            yield file
 
1551
 
 
1552
 
 
1553
def file_completion_match(file, treepath, arg):
 
1554
    """Determines whether a file within an arch tree matches the argument.
 
1555
 
 
1556
    :param file: The rooted filename
 
1557
    :type file: str
 
1558
    :param treepath: The path to the cwd within the tree
 
1559
    :type treepath: str
 
1560
    :param arg: The prefix to match
 
1561
    :return: The completion name, or None if not a match
 
1562
    :rtype: str
 
1563
    """
 
1564
    if not file.startswith(treepath):
 
1565
        return None
 
1566
    if treepath != "":
 
1567
        file = file[len(treepath)+1:]
 
1568
 
 
1569
    if not file.startswith(arg):
 
1570
        return None 
 
1571
    if os.path.isdir(file):
 
1572
        file += '/'
 
1573
    return file
 
1574
 
 
1575
def iter_modified_file_completions(tree, arg):
 
1576
    """Returns a list of modified files that match the specified prefix.
 
1577
 
 
1578
    :param tree: The current tree
 
1579
    :type tree: `arch.WorkingTree`
 
1580
    :param arg: The prefix to match
 
1581
    :type arg: str
 
1582
    """
 
1583
    treepath = pylon.tree_cwd(tree)
 
1584
    tmpdir = util.tmpdir()
 
1585
    changeset = tmpdir+"/changeset"
 
1586
    completions = []
 
1587
    revision = cmdutil.determine_revision_tree(tree)
 
1588
    for line in arch.iter_delta(revision, tree, changeset):
 
1589
        if isinstance(line, arch.FileModification):
 
1590
            file = file_completion_match(line.name[1:], treepath, arg)
 
1591
            if file is not None:
 
1592
                completions.append(file)
 
1593
    shutil.rmtree(tmpdir)
 
1594
    return completions
 
1595
 
 
1596
class Shell(BaseCommand):
 
1597
    def __init__(self):
 
1598
        self.description = "Runs Fai as a shell"
 
1599
 
 
1600
    def do_command(self, cmdargs):
 
1601
        if len(cmdargs)!=0:
 
1602
            raise cmdutil.GetHelp
 
1603
        prompt = PromptCmd()
 
1604
        try:
 
1605
            prompt.cmdloop()
 
1606
        finally:
 
1607
            prompt.write_history()
 
1608
 
 
1609
class AddID(BaseCommand):
 
1610
    """
 
1611
    Adds an inventory id for the given file
 
1612
    """
 
1613
    def __init__(self):
 
1614
        self.description="Add an inventory id for a given file"
 
1615
 
 
1616
    def get_completer(self, arg, index):
 
1617
        tree = arch.tree_root(".")
 
1618
        return iter_untagged_completions(tree, arg)
 
1619
 
 
1620
    def do_command(self, cmdargs):
 
1621
        """
 
1622
        Master function that perfoms the "revision" command.
 
1623
        """
 
1624
        parser=self.get_parser()
 
1625
        (options, args) = parser.parse_args(cmdargs)
 
1626
 
 
1627
        try:
 
1628
            tree = arch.tree_root(".")
 
1629
        except arch.errors.TreeRootError, e:
 
1630
            raise pylon.errors.CommandFailedWrapper(e)
 
1631
            
 
1632
 
 
1633
        if (len(args) == 0) == (options.untagged == False):
 
1634
            raise cmdutil.GetHelp
 
1635
 
 
1636
       #if options.id and len(args) != 1:
 
1637
       #    print "If --id is specified, only one file can be named."
 
1638
       #    return
 
1639
        
 
1640
        method = tree.tagging_method
 
1641
        
 
1642
        if options.id_type == "tagline":
 
1643
            if method != "tagline":
 
1644
                if not cmdutil.prompt("Tagline in other tree"):
 
1645
                    if method == "explicit" or method == "implicit":
 
1646
                        options.id_type == method
 
1647
                    else:
 
1648
                        print "add-id not supported for \"%s\" tagging method"\
 
1649
                            % method 
 
1650
                        return
 
1651
        
 
1652
        elif options.id_type == "implicit":
 
1653
            if method != "implicit":
 
1654
                if not cmdutil.prompt("Implicit in other tree"):
 
1655
                    if method == "explicit" or method == "tagline":
 
1656
                        options.id_type == method
 
1657
                    else:
 
1658
                        print "add-id not supported for \"%s\" tagging method"\
 
1659
                            % method 
 
1660
                        return
 
1661
        elif options.id_type == "explicit":
 
1662
            if method != "tagline" and method != "explicit" and \
 
1663
                method != "implicit":
 
1664
                if not cmdutil.prompt("Explicit in other tree"):
 
1665
                    print "add-id not supported for \"%s\" tagging method" % \
 
1666
                        method
 
1667
                    return
 
1668
        
 
1669
        if options.id_type == "auto":
 
1670
            if method != "tagline" and method != "explicit" \
 
1671
                and method !="implicit":
 
1672
                print "add-id not supported for \"%s\" tagging method" % method
 
1673
                return
 
1674
            else:
 
1675
                options.id_type = method
 
1676
        if options.untagged:
 
1677
            args = None
 
1678
        self.add_ids(tree, options.id_type, args)
 
1679
 
 
1680
    def add_ids(self, tree, id_type, files=()):
 
1681
        """Add inventory ids to files.
 
1682
        
 
1683
        :param tree: the tree the files are in
 
1684
        :type tree: `arch.WorkingTree`
 
1685
        :param id_type: the type of id to add: "explicit" or "tagline"
 
1686
        :type id_type: str
 
1687
        :param files: The list of files to add.  If None do all untagged.
 
1688
        :type files: tuple of str
 
1689
        """
 
1690
 
 
1691
        untagged = (files is None)
 
1692
        if untagged:
 
1693
            files = [os.path.join(tree, f) for f in iter_untagged(tree, None)]
 
1694
        previous_files = []
 
1695
        while len(files) > 0:
 
1696
            previous_files.extend(files)
 
1697
            if id_type == "explicit":
 
1698
                pylon.add_id(files)
 
1699
            elif id_type == "tagline" or id_type == "implicit":
 
1700
                for file in files:
 
1701
                    try:
 
1702
                        implicit = (id_type == "implicit")
 
1703
                        cmdutil.add_tagline_or_explicit_id(file, False,
 
1704
                                                           implicit)
 
1705
                    except cmdutil.AlreadyTagged:
 
1706
                        print "\"%s\" already has a tagline." % file
 
1707
                    except cmdutil.NoCommentSyntax:
 
1708
                        pass
 
1709
            #do inventory after tagging until no untagged files are encountered
 
1710
            if untagged:
 
1711
                files = []
 
1712
                for file in iter_untagged(tree, None):
 
1713
                    if not file in previous_files:
 
1714
                        files.append(file)
 
1715
 
 
1716
            else:
 
1717
                break
 
1718
 
 
1719
    def get_parser(self):
 
1720
        """
 
1721
        Returns the options parser to use for the "revision" command.
 
1722
 
 
1723
        :rtype: cmdutil.CmdOptionParser
 
1724
        """
 
1725
        parser=cmdutil.CmdOptionParser("fai add-id file1 [file2] [file3]...")
 
1726
# ddaa suggests removing this to promote GUIDs.  Let's see who squalks.
 
1727
#        parser.add_option("-i", "--id", dest="id", 
 
1728
#                         help="Specify id for a single file", default=None)
 
1729
        parser.add_option("--tltl", action="store_true", 
 
1730
                         dest="lord_style",  help="Use Tom Lord's style of id.")
 
1731
        parser.add_option("--explicit", action="store_const", 
 
1732
                         const="explicit", dest="id_type", 
 
1733
                         help="Use an explicit id", default="auto")
 
1734
        parser.add_option("--tagline", action="store_const", 
 
1735
                         const="tagline", dest="id_type", 
 
1736
                         help="Use a tagline id")
 
1737
        parser.add_option("--implicit", action="store_const", 
 
1738
                         const="implicit", dest="id_type", 
 
1739
                         help="Use an implicit id (deprecated)")
 
1740
        parser.add_option("--untagged", action="store_true", 
 
1741
                         dest="untagged", default=False, 
 
1742
                         help="tag all untagged files")
 
1743
        return parser 
 
1744
 
 
1745
    def help(self, parser=None):
 
1746
        """
 
1747
        Prints a help message.
 
1748
 
 
1749
        :param parser: If supplied, the parser to use for generating help.  If \
 
1750
        not supplied, it is retrieved.
 
1751
        :type parser: cmdutil.CmdOptionParser
 
1752
        """
 
1753
        if parser==None:
 
1754
            parser=self.get_parser()
 
1755
        parser.print_help()
 
1756
        print """
 
1757
Adds an inventory to the specified file(s) and directories.  If --untagged is
 
1758
specified, adds inventory to all untagged files and directories.
 
1759
        """
 
1760
        return
 
1761
 
 
1762
 
 
1763
class Merge(BaseCommand):
 
1764
    """
 
1765
    Merges changes from other versions into the current tree
 
1766
    """
 
1767
    def __init__(self):
 
1768
        self.description="Merges changes from other versions"
 
1769
        try:
 
1770
            self.tree = arch.tree_root(".")
 
1771
        except:
 
1772
            self.tree = None
 
1773
 
 
1774
 
 
1775
    def get_completer(self, arg, index):
 
1776
        if self.tree is None:
 
1777
            raise arch.errors.TreeRootError
 
1778
        return cmdutil.merge_completions(self.tree, arg, index)
 
1779
 
 
1780
    def do_command(self, cmdargs):
 
1781
        """
 
1782
        Master function that perfoms the "merge" command.
 
1783
        """
 
1784
        parser=self.get_parser()
 
1785
        (options, args) = parser.parse_args(cmdargs)
 
1786
        action = options.action
 
1787
        
 
1788
        if self.tree is None:
 
1789
            raise arch.errors.TreeRootError(os.getcwd())
 
1790
        if pylon.has_changed(pylon.comp_revision(self.tree)):
 
1791
            raise UncommittedChanges(self.tree)
 
1792
 
 
1793
        if len(args) > 0:
 
1794
            revisions = []
 
1795
            for arg in args:
 
1796
                revisions.append(cmdutil.determine_revision_arch(self.tree, 
 
1797
                                                                 arg))
 
1798
            source = "from commandline"
 
1799
        else:
 
1800
            revisions = pylon.iter_partner_revisions(self.tree, 
 
1801
                                                         self.tree.tree_version)
 
1802
            source = "from partner version"
 
1803
        revisions = pylon.misc.rewind_iterator(revisions)
 
1804
        try:
 
1805
            revisions.next()
 
1806
            revisions.rewind()
 
1807
        except StopIteration, e:
 
1808
            revision = cmdutil.tag_cur(self.tree)
 
1809
            if revision is None:
 
1810
                raise CantDetermineRevision("", "No version specified, no "
 
1811
                                            "partner-versions, and no tag"
 
1812
                                            " source")
 
1813
            revisions = [revision]
 
1814
            source = "from tag source"
 
1815
        for revision in revisions:
 
1816
            cmdutil.ensure_archive_registered(revision.archive)
 
1817
            cmdutil.colorize(arch.Chatter("* Merging %s [%s]" % 
 
1818
                             (revision, source)))
 
1819
            if action=="native-merge" or action=="update":
 
1820
                if self.native_merge(revision, action, 
 
1821
                                     options.apply_type) == 0:
 
1822
                    continue
 
1823
            elif action=="star-merge":
 
1824
                try: 
 
1825
                    self.star_merge(revision, options.diff3)
 
1826
                except errors.MergeProblem, e:
 
1827
                    break
 
1828
            if pylon.has_changed(self.tree.tree_version):
 
1829
                break
 
1830
 
 
1831
    def star_merge(self, revision, diff3):
 
1832
        """Perform a star-merge on the current tree.
 
1833
        
 
1834
        :param revision: The revision to use for the merge
 
1835
        :type revision: `arch.Revision`
 
1836
        :param diff3: If true, do a diff3 merge
 
1837
        :type diff3: bool
 
1838
        """
 
1839
        try:
 
1840
            for line in self.tree.iter_star_merge(revision, diff3=diff3):
 
1841
                cmdutil.colorize(line)
 
1842
        except arch.util.ExecProblem, e:
 
1843
            if e.proc.status is not None and e.proc.status == 1:
 
1844
                if e.proc.error:
 
1845
                    print e.proc.error
 
1846
                raise MergeProblem
 
1847
            else:
 
1848
                raise
 
1849
 
 
1850
    def native_merge(self, other_revision, action, apply_type):
 
1851
        """Perform a native-merge on the current tree.
 
1852
        
 
1853
        :param other_revision: The revision to use for the merge
 
1854
        :type other_revision: `arch.Revision`
 
1855
        :return: 0 if the merge was skipped, 1 if it was applied
 
1856
        """
 
1857
        other_tree = pylon.find_or_make_local_revision(other_revision)
 
1858
        try:
 
1859
            if action == "native-merge":
 
1860
                ancestor = pylon.merge_ancestor2(self.tree, other_tree, 
 
1861
                                                         other_revision)
 
1862
            elif action == "update":
 
1863
                ancestor = pylon.tree_latest(self.tree, 
 
1864
                                                     other_revision.version)
 
1865
        except CantDetermineRevision, e:
 
1866
            raise CommandFailedWrapper(e)
 
1867
        cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor))
 
1868
        if (ancestor == other_revision):
 
1869
            cmdutil.colorize(arch.Chatter("* Skipping redundant merge" 
 
1870
                                          % ancestor))
 
1871
            return 0
 
1872
        if apply_type == "diff3":
 
1873
            delta=pylon.apply_delta_custom(ancestor, other_tree, 
 
1874
                                                   self.tree, 
 
1875
                apply_type=pylon.Diff3Handler)
 
1876
            merge_iter = cmdutil.iter_apply_delta_filter(delta)
 
1877
        elif apply_type == "normal":
 
1878
            delta = cmdutil.apply_delta(ancestor, other_tree, self.tree)    
 
1879
            merge_iter = cmdutil.iter_apply_delta_filter(delta)
 
1880
        elif apply_type == "python":
 
1881
            base_tree = pylon.find_or_make_local_revision(ancestor)
 
1882
            merge_iter = pylon.iter_arch_merge(self.tree, base_tree, 
 
1883
                                               other_tree, ancestor,
 
1884
                                               other_revision)
 
1885
        else:
 
1886
            assert(False)
 
1887
        for line in merge_iter:
 
1888
            cmdutil.colorize(line)
 
1889
        return 1
 
1890
 
 
1891
 
 
1892
 
 
1893
    def get_parser(self):
 
1894
        """
 
1895
        Returns the options parser to use for the "merge" command.
 
1896
 
 
1897
        :rtype: cmdutil.CmdOptionParser
 
1898
        """
 
1899
        parser=cmdutil.CmdOptionParser("fai merge [VERSION]")
 
1900
        parser.add_option("-s", "--star-merge", action="store_const",
 
1901
                          dest="action", help="Use star-merge",
 
1902
                          const="star-merge", default="native-merge")
 
1903
        parser.add_option("--update", action="store_const",
 
1904
                          dest="action", help="Use update picker",
 
1905
                          const="update")
 
1906
        parser.add_option("--diff3", action="store_const", 
 
1907
                         dest="apply_type", const="diff3", default="normal",
 
1908
                         help="Use diff3 for merge (implies star-merge)")
 
1909
        if "iter_arch_merge" in dir(pylon):
 
1910
            parser.add_option("--python", action="store_const",
 
1911
                              dest="apply_type", const="python",
 
1912
                              default="normal", help="Do native merging")
 
1913
        return parser 
 
1914
 
 
1915
    def help(self, parser=None):
 
1916
        """
 
1917
        Prints a help message.
 
1918
 
 
1919
        :param parser: If supplied, the parser to use for generating help.  If \
 
1920
        not supplied, it is retrieved.
 
1921
        :type parser: cmdutil.CmdOptionParser
 
1922
        """
 
1923
        if parser==None:
 
1924
            parser=self.get_parser()
 
1925
        parser.print_help()
 
1926
        print """
 
1927
Performs a merge operation using the specified version.
 
1928
        """
 
1929
        return
 
1930
 
 
1931
class ELog(BaseCommand):
 
1932
    """
 
1933
    Produces a raw patchlog and invokes the user's editor
 
1934
    """
 
1935
    def __init__(self):
 
1936
        self.description="Edit a patchlog to commit"
 
1937
        try:
 
1938
            self.tree = arch.tree_root(".")
 
1939
        except:
 
1940
            self.tree = None
 
1941
 
 
1942
 
 
1943
    def do_command(self, cmdargs):
 
1944
        """
 
1945
        Master function that perfoms the "elog" command.
 
1946
        """
 
1947
        parser=self.get_parser()
 
1948
        (options, args) = parser.parse_args(cmdargs)
 
1949
        if self.tree is None:
 
1950
            raise arch.errors.TreeRootError
 
1951
 
 
1952
        try:
 
1953
            edit_log(self.tree)
 
1954
        except pylon.errors.NoEditorSpecified, e:
 
1955
            raise pylon.errors.CommandFailedWrapper(e)
 
1956
 
 
1957
    def get_parser(self):
 
1958
        """
 
1959
        Returns the options parser to use for the "merge" command.
 
1960
 
 
1961
        :rtype: cmdutil.CmdOptionParser
 
1962
        """
 
1963
        parser=cmdutil.CmdOptionParser("fai elog")
 
1964
        return parser 
 
1965
 
 
1966
 
 
1967
    def help(self, parser=None):
 
1968
        """
 
1969
        Invokes $EDITOR to produce a log for committing.
 
1970
 
 
1971
        :param parser: If supplied, the parser to use for generating help.  If \
 
1972
        not supplied, it is retrieved.
 
1973
        :type parser: cmdutil.CmdOptionParser
 
1974
        """
 
1975
        if parser==None:
 
1976
            parser=self.get_parser()
 
1977
        parser.print_help()
 
1978
        print """
 
1979
Invokes $EDITOR to produce a log for committing.
 
1980
        """
 
1981
        return
 
1982
 
 
1983
def edit_log(tree):
 
1984
    """Makes and edits the log for a tree.  Does all kinds of fancy things
 
1985
    like log templates and merge summaries and log-for-merge
 
1986
    
 
1987
    :param tree: The tree to edit the log for
 
1988
    :type tree: `arch.WorkingTree`
 
1989
    """
 
1990
    #ensure we have an editor before preparing the log
 
1991
    cmdutil.find_editor()
 
1992
    log = tree.log_message(create=False)
 
1993
    log_is_new = False
 
1994
    if log is None or cmdutil.prompt("Overwrite log"):
 
1995
        if log is not None:
 
1996
           os.remove(log.name)
 
1997
        log = tree.log_message(create=True)
 
1998
        log_is_new = True
 
1999
        tmplog = log.name
 
2000
        template = pylon.log_template_path(tree)
 
2001
        if template:
 
2002
            shutil.copyfile(template, tmplog)
 
2003
        comp_version = pylon.comp_revision(tree).version
 
2004
        new_merges = list(pylon.iter_new_merges(tree, comp_version))
 
2005
        direct_merges = cmdutil.direct_merges(new_merges)
 
2006
        log["Summary"] = pylon.merge_summary(direct_merges, tree.tree_version)
 
2007
        if len(direct_merges) > 0:   
 
2008
            if cmdutil.prompt("Log for merge"):
 
2009
                if cmdutil.prompt("changelog for merge"):
 
2010
                    mergestuff = "Patches applied:\n"
 
2011
                    mergestuff += pylon.changelog_for_merge(direct_merges, 
 
2012
                                                            new_merges)
 
2013
                else:
 
2014
                    mergestuff = cmdutil.log_for_merge(tree, comp_version)
 
2015
                log.description += mergestuff
 
2016
        log.save()
 
2017
    try:
 
2018
        cmdutil.invoke_editor(log.name)
 
2019
    except:
 
2020
        if log_is_new:
 
2021
            os.remove(log.name)
 
2022
        raise
 
2023
 
 
2024
 
 
2025
class MirrorArchive(BaseCommand):
 
2026
    """
 
2027
    Updates a mirror from an archive
 
2028
    """
 
2029
    def __init__(self):
 
2030
        self.description="Update a mirror from an archive"
 
2031
 
 
2032
    def do_command(self, cmdargs):
 
2033
        """
 
2034
        Master function that perfoms the "revision" command.
 
2035
        """
 
2036
 
 
2037
        parser=self.get_parser()
 
2038
        (options, args) = parser.parse_args(cmdargs)
 
2039
        if len(args) > 1:
 
2040
            raise GetHelp
 
2041
        try:
 
2042
            tree = arch.tree_root(".")
 
2043
        except:
 
2044
            tree = None
 
2045
 
 
2046
        if len(args) == 0:
 
2047
            if tree is not None:
 
2048
                name = tree.tree_version
 
2049
            else:
 
2050
                raise errors.CommandFailedWrapper(\
 
2051
                    errors.CantDetermineArchive(None, "Not in a project tree"))
 
2052
        else:
 
2053
            name = cmdutil.expand_alias(args[0], tree)
 
2054
            name = arch.NameParser(name)
 
2055
 
 
2056
        to_arch = name.get_archive()
 
2057
        from_arch = cmdutil.get_mirror_source(arch.Archive(to_arch))
 
2058
        limit = name.get_nonarch()
 
2059
 
 
2060
        for line in pylon.iter_mirror_archive(from_arch,to_arch, limit):
 
2061
            cmdutil.colorize(line)
 
2062
 
 
2063
    def get_parser(self):
 
2064
        """
 
2065
        Returns the options parser to use for the "revision" command.
 
2066
 
 
2067
        :rtype: cmdutil.CmdOptionParser
 
2068
        """
 
2069
        parser=cmdutil.CmdOptionParser("fai mirror-archive ARCHIVE")
 
2070
        return parser 
 
2071
 
 
2072
    def help(self, parser=None):
 
2073
        """
 
2074
        Prints a help message.
 
2075
 
 
2076
        :param parser: If supplied, the parser to use for generating help.  If \
 
2077
        not supplied, it is retrieved.
 
2078
        :type parser: cmdutil.CmdOptionParser
 
2079
        """
 
2080
        if parser==None:
 
2081
            parser=self.get_parser()
 
2082
        parser.print_help()
 
2083
        print """
 
2084
Updates a mirror from an archive.  If a branch, package, or version is
 
2085
supplied, only changes under it are mirrored.
 
2086
        """
 
2087
        return
 
2088
 
 
2089
def help_tree_spec():
 
2090
    print """Specifying revisions (default: tree)
 
2091
Revisions may be specified by alias, revision, version or patchlevel.
 
2092
Revisions or versions may be fully qualified.  Unqualified revisions, versions, 
 
2093
or patchlevels use the archive of the current project tree.  Versions will
 
2094
use the latest patchlevel in the tree.  Patchlevels will use the current tree-
 
2095
version.
 
2096
 
 
2097
Use "alias" to list available (user and automatic) aliases."""
 
2098
 
 
2099
auto_alias = [
 
2100
"acur", 
 
2101
"The latest revision in the archive of the tree-version.  You can specify \
 
2102
a different version like so: acur:foo--bar--0 (aliases can be used)",
 
2103
"tcur",
 
2104
"""(tree current) The latest revision in the tree of the tree-version. \
 
2105
You can specify a different version like so: tcur:foo--bar--0 (aliases can be \
 
2106
used).""",
 
2107
"tprev" , 
 
2108
"""(tree previous) The previous revision in the tree of the tree-version.  To \
 
2109
specify an older revision, use a number, e.g. "tprev:4" """,
 
2110
"tanc" , 
 
2111
"""(tree ancestor) The ancestor revision of the tree To specify an older \
 
2112
revision, use a number, e.g. "tanc:4".""",
 
2113
"tdate" , 
 
2114
"""(tree date) The latest revision from a given date, e.g. "tdate:July 6".""",
 
2115
"tmod" , 
 
2116
""" (tree modified) The latest revision to modify a given file, e.g. \
 
2117
"tmod:engine.cpp" or "tmod:engine.cpp:16".""",
 
2118
"ttag" , 
 
2119
"""(tree tag) The revision that was tagged into the current tree revision, \
 
2120
according to the tree""",
 
2121
"tagcur", 
 
2122
"""(tag current) The latest revision of the version that the current tree \
 
2123
was tagged from.""",
 
2124
"mergeanc" , 
 
2125
"""The common ancestor of the current tree and the specified revision. \
 
2126
Defaults to the first partner-version's latest revision or to tagcur.""",
 
2127
]
 
2128
 
 
2129
 
 
2130
def is_auto_alias(name):
 
2131
    """Determine whether a name is an auto alias name
 
2132
 
 
2133
    :param name: the name to check
 
2134
    :type name: str
 
2135
    :return: True if the name is an auto alias, false if not
 
2136
    :rtype: bool
 
2137
    """
 
2138
    return name in [f for (f, v) in pylon.util.iter_pairs(auto_alias)]
 
2139
 
 
2140
 
 
2141
def display_def(iter, wrap = 80):
 
2142
    """Display a list of definitions
 
2143
 
 
2144
    :param iter: iter of name, definition pairs
 
2145
    :type iter: iter of (str, str)
 
2146
    :param wrap: The width for text wrapping
 
2147
    :type wrap: int
 
2148
    """
 
2149
    vals = list(iter)
 
2150
    maxlen = 0
 
2151
    for (key, value) in vals:
 
2152
        if len(key) > maxlen:
 
2153
            maxlen = len(key)
 
2154
    for (key, value) in vals:
 
2155
        tw=textwrap.TextWrapper(width=wrap, 
 
2156
                                initial_indent=key.rjust(maxlen)+" : ",
 
2157
                                subsequent_indent="".rjust(maxlen+3))
 
2158
        print tw.fill(value)
 
2159
 
 
2160
 
 
2161
def help_aliases(tree):
 
2162
    print """Auto-generated aliases"""
 
2163
    display_def(pylon.util.iter_pairs(auto_alias))
 
2164
    print "User aliases"
 
2165
    display_def(pylon.iter_all_alias(tree))
 
2166
 
 
2167
class Inventory(BaseCommand):
 
2168
    """List the status of files in the tree"""
 
2169
    def __init__(self):
 
2170
        self.description=self.__doc__
 
2171
 
 
2172
    def do_command(self, cmdargs):
 
2173
        """
 
2174
        Master function that perfoms the "revision" command.
 
2175
        """
 
2176
 
 
2177
        parser=self.get_parser()
 
2178
        (options, args) = parser.parse_args(cmdargs)
 
2179
        tree = arch.tree_root(".")
 
2180
        categories = []
 
2181
 
 
2182
        if (options.source):
 
2183
            categories.append(pylon.SourceFile)
 
2184
        if (options.precious):
 
2185
            categories.append(pylon.PreciousFile)
 
2186
        if (options.backup):
 
2187
            categories.append(pylon.BackupFile)
 
2188
        if (options.junk):
 
2189
            categories.append(pylon.JunkFile)
 
2190
 
 
2191
        if len(categories) == 1:
 
2192
            show_leading = False
 
2193
        else:
 
2194
            show_leading = True
 
2195
 
 
2196
        if len(categories) == 0:
 
2197
            categories = None
 
2198
 
 
2199
        if options.untagged:
 
2200
            categories = pylon.non_root
 
2201
            show_leading = False
 
2202
            tagged = False
 
2203
        else:
 
2204
            tagged = None
 
2205
        
 
2206
        for file in pylon.iter_inventory_filter(tree, None, 
 
2207
            control_files=options.control_files, 
 
2208
            categories = categories, tagged=tagged):
 
2209
            print pylon.file_line(file, 
 
2210
                                      category = show_leading, 
 
2211
                                      untagged = show_leading,
 
2212
                                      id = options.ids)
 
2213
 
 
2214
    def get_parser(self):
 
2215
        """
 
2216
        Returns the options parser to use for the "revision" command.
 
2217
 
 
2218
        :rtype: cmdutil.CmdOptionParser
 
2219
        """
 
2220
        parser=cmdutil.CmdOptionParser("fai inventory [options]")
 
2221
        parser.add_option("--ids", action="store_true", dest="ids", 
 
2222
                          help="Show file ids")
 
2223
        parser.add_option("--control", action="store_true", 
 
2224
                          dest="control_files", help="include control files")
 
2225
        parser.add_option("--source", action="store_true", dest="source",
 
2226
                          help="List source files")
 
2227
        parser.add_option("--backup", action="store_true", dest="backup",
 
2228
                          help="List backup files")
 
2229
        parser.add_option("--precious", action="store_true", dest="precious",
 
2230
                          help="List precious files")
 
2231
        parser.add_option("--junk", action="store_true", dest="junk",
 
2232
                          help="List junk files")
 
2233
        parser.add_option("--unrecognized", action="store_true", 
 
2234
                          dest="unrecognized", help="List unrecognized files")
 
2235
        parser.add_option("--untagged", action="store_true", 
 
2236
                          dest="untagged", help="List only untagged files")
 
2237
        return parser 
 
2238
 
 
2239
    def help(self, parser=None):
 
2240
        """
 
2241
        Prints a help message.
 
2242
 
 
2243
        :param parser: If supplied, the parser to use for generating help.  If \
 
2244
        not supplied, it is retrieved.
 
2245
        :type parser: cmdutil.CmdOptionParser
 
2246
        """
 
2247
        if parser==None:
 
2248
            parser=self.get_parser()
 
2249
        parser.print_help()
 
2250
        print """
 
2251
Lists the status of files in the archive:
 
2252
S source
 
2253
P precious
 
2254
B backup
 
2255
J junk
 
2256
U unrecognized
 
2257
T tree root
 
2258
? untagged-source
 
2259
Leading letter are not displayed if only one kind of file is shown
 
2260
        """
 
2261
        return
 
2262
 
 
2263
 
 
2264
class Alias(BaseCommand):
 
2265
    """List or adjust aliases"""
 
2266
    def __init__(self):
 
2267
        self.description=self.__doc__
 
2268
 
 
2269
    def get_completer(self, arg, index):
 
2270
        if index > 2:
 
2271
            return ()
 
2272
        try:
 
2273
            self.tree = arch.tree_root(".")
 
2274
        except:
 
2275
            self.tree = None
 
2276
 
 
2277
        if index == 0:
 
2278
            return [part[0]+" " for part in pylon.iter_all_alias(self.tree)]
 
2279
        elif index == 1:
 
2280
            return cmdutil.iter_revision_completions(arg, self.tree)
 
2281
 
 
2282
 
 
2283
    def do_command(self, cmdargs):
 
2284
        """
 
2285
        Master function that perfoms the "revision" command.
 
2286
        """
 
2287
 
 
2288
        parser=self.get_parser()
 
2289
        (options, args) = parser.parse_args(cmdargs)
 
2290
        try:
 
2291
            self.tree =  arch.tree_root(".")
 
2292
        except:
 
2293
            self.tree = None
 
2294
 
 
2295
 
 
2296
        try:
 
2297
            options.action(args, options)
 
2298
        except cmdutil.ForbiddenAliasSyntax, e:
 
2299
            raise CommandFailedWrapper(e)
 
2300
 
 
2301
    def no_prefix(self, alias):
 
2302
        if alias.startswith("^"):
 
2303
            alias = alias[1:]
 
2304
        return alias
 
2305
        
 
2306
    def arg_dispatch(self, args, options):
 
2307
        """Add, modify, or list aliases, depending on number of arguments
 
2308
 
 
2309
        :param args: The list of commandline arguments
 
2310
        :type args: list of str
 
2311
        :param options: The commandline options
 
2312
        """
 
2313
        if len(args) == 0:
 
2314
            help_aliases(self.tree)
 
2315
            return
 
2316
        else:
 
2317
            alias = self.no_prefix(args[0])
 
2318
            if len(args) == 1:
 
2319
                self.print_alias(alias)
 
2320
            elif (len(args)) == 2:
 
2321
                self.add(alias, args[1], options)
 
2322
            else:
 
2323
                raise cmdutil.GetHelp
 
2324
 
 
2325
    def print_alias(self, alias):
 
2326
        answer = None
 
2327
        if is_auto_alias(alias):
 
2328
            raise pylon.errors.IsAutoAlias(alias, "\"%s\" is an auto alias."
 
2329
                "  Use \"revision\" to expand auto aliases." % alias)
 
2330
        for pair in pylon.iter_all_alias(self.tree):
 
2331
            if pair[0] == alias:
 
2332
                answer = pair[1]
 
2333
        if answer is not None:
 
2334
            print answer
 
2335
        else:
 
2336
            print "The alias %s is not assigned." % alias
 
2337
 
 
2338
    def add(self, alias, expansion, options):
 
2339
        """Add or modify aliases
 
2340
 
 
2341
        :param alias: The alias name to create/modify
 
2342
        :type alias: str
 
2343
        :param expansion: The expansion to assign to the alias name
 
2344
        :type expansion: str
 
2345
        :param options: The commandline options
 
2346
        """
 
2347
        if is_auto_alias(alias):
 
2348
            raise IsAutoAlias(alias)
 
2349
        newlist = ""
 
2350
        written = False
 
2351
        new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, 
 
2352
            self.tree))
 
2353
        pylon.check_alias(new_line.rstrip("\n"), [alias, expansion])
 
2354
 
 
2355
        for pair in self.get_iterator(options):
 
2356
            if pair[0] != alias:
 
2357
                newlist+="%s=%s\n" % (pair[0], pair[1])
 
2358
            elif not written:
 
2359
                newlist+=new_line
 
2360
                written = True
 
2361
        if not written:
 
2362
            newlist+=new_line
 
2363
        self.write_aliases(newlist, options)
 
2364
            
 
2365
    def delete(self, args, options):
 
2366
        """Delete the specified alias
 
2367
 
 
2368
        :param args: The list of arguments
 
2369
        :type args: list of str
 
2370
        :param options: The commandline options
 
2371
        """
 
2372
        deleted = False
 
2373
        if len(args) != 1:
 
2374
            raise cmdutil.GetHelp
 
2375
        alias = self.no_prefix(args[0])
 
2376
        if is_auto_alias(alias):
 
2377
            raise IsAutoAlias(alias)
 
2378
        newlist = ""
 
2379
        for pair in self.get_iterator(options):
 
2380
            if pair[0] != alias:
 
2381
                newlist+="%s=%s\n" % (pair[0], pair[1])
 
2382
            else:
 
2383
                deleted = True
 
2384
        if not deleted:
 
2385
            raise errors.NoSuchAlias(alias)
 
2386
        self.write_aliases(newlist, options)
 
2387
 
 
2388
    def get_alias_file(self, options):
 
2389
        """Return the name of the alias file to use
 
2390
 
 
2391
        :param options: The commandline options
 
2392
        """
 
2393
        if options.tree:
 
2394
            if self.tree is None:
 
2395
                self.tree == arch.tree_root(".")
 
2396
            return str(self.tree)+"/{arch}/+aliases"
 
2397
        else:
 
2398
            return "~/.aba/aliases"
 
2399
 
 
2400
    def get_iterator(self, options):
 
2401
        """Return the alias iterator to use
 
2402
 
 
2403
        :param options: The commandline options
 
2404
        """
 
2405
        return pylon.iter_alias(self.get_alias_file(options))
 
2406
 
 
2407
    def write_aliases(self, newlist, options):
 
2408
        """Safely rewrite the alias file
 
2409
        :param newlist: The new list of aliases
 
2410
        :type newlist: str
 
2411
        :param options: The commandline options
 
2412
        """
 
2413
        filename = os.path.expanduser(self.get_alias_file(options))
 
2414
        file = pylon.util.NewFileVersion(filename)
 
2415
        file.write(newlist)
 
2416
        file.commit()
 
2417
 
 
2418
 
 
2419
    def get_parser(self):
 
2420
        """
 
2421
        Returns the options parser to use for the "alias" command.
 
2422
 
 
2423
        :rtype: cmdutil.CmdOptionParser
 
2424
        """
 
2425
        parser=cmdutil.CmdOptionParser("fai alias [ALIAS] [NAME]")
 
2426
        parser.add_option("-d", "--delete", action="store_const", dest="action",
 
2427
                          const=self.delete, default=self.arg_dispatch, 
 
2428
                          help="Delete an alias")
 
2429
        parser.add_option("--tree", action="store_true", dest="tree", 
 
2430
                          help="Create a per-tree alias", default=False)
 
2431
        return parser 
 
2432
 
 
2433
    def help(self, parser=None):
 
2434
        """
 
2435
        Prints a help message.
 
2436
 
 
2437
        :param parser: If supplied, the parser to use for generating help.  If \
 
2438
        not supplied, it is retrieved.
 
2439
        :type parser: cmdutil.CmdOptionParser
 
2440
        """
 
2441
        if parser==None:
 
2442
            parser=self.get_parser()
 
2443
        parser.print_help()
 
2444
        print """
 
2445
Lists current aliases or modifies the list of aliases.
 
2446
 
 
2447
If no arguments are supplied, aliases will be listed.  If two arguments are
 
2448
supplied, the specified alias will be created or modified.  If -d or --delete
 
2449
is supplied, the specified alias will be deleted.
 
2450
 
 
2451
You can create aliases that refer to any fully-qualified part of the
 
2452
Arch namespace, e.g. 
 
2453
archive, 
 
2454
archive/category, 
 
2455
archive/category--branch, 
 
2456
archive/category--branch--version (my favourite)
 
2457
archive/category--branch--version--patchlevel
 
2458
 
 
2459
Aliases can be used automatically by native commands.  To use them
 
2460
with external or tla commands, prefix them with ^ (you can do this
 
2461
with native commands, too).
 
2462
"""
 
2463
 
 
2464
 
 
2465
class RequestMerge(BaseCommand):
 
2466
    """Submit a merge request to Bug Goo"""
 
2467
    def __init__(self):
 
2468
        self.description=self.__doc__
 
2469
 
 
2470
    def do_command(self, cmdargs):
 
2471
        """Submit a merge request
 
2472
 
 
2473
        :param cmdargs: The commandline arguments
 
2474
        :type cmdargs: list of str
 
2475
        """
 
2476
        parser = self.get_parser()
 
2477
        (options, args) = parser.parse_args(cmdargs)
 
2478
        try:
 
2479
            cmdutil.find_editor()
 
2480
        except pylon.errors.NoEditorSpecified, e:
 
2481
            raise pylon.errors.CommandFailedWrapper(e)
 
2482
        try:
 
2483
            self.tree=arch.tree_root(".")
 
2484
        except:
 
2485
            self.tree=None
 
2486
        base, revisions = self.revision_specs(args)
 
2487
        message = self.make_headers(base, revisions)
 
2488
        message += self.make_summary(revisions)
 
2489
        path = self.edit_message(message)
 
2490
        message = self.tidy_message(path)
 
2491
        if cmdutil.prompt("Send merge"):
 
2492
            self.send_message(message)
 
2493
            print "Merge request sent"
 
2494
 
 
2495
    def make_headers(self, base, revisions):
 
2496
        """Produce email and Bug Goo header strings
 
2497
 
 
2498
        :param base: The base revision to apply merges to
 
2499
        :type base: `arch.Revision`
 
2500
        :param revisions: The revisions to replay into the base
 
2501
        :type revisions: list of `arch.Patchlog`
 
2502
        :return: The headers
 
2503
        :rtype: str
 
2504
        """
 
2505
        headers = "To: gnu-arch-users@gnu.org\n"
 
2506
        headers += "From: %s\n" % options.fromaddr
 
2507
        if len(revisions) == 1:
 
2508
            headers += "Subject: [MERGE REQUEST] %s\n" % revisions[0].summary
 
2509
        else:
 
2510
            headers += "Subject: [MERGE REQUEST]\n"
 
2511
        headers += "\n"
 
2512
        headers += "Base-Revision: %s\n" % base
 
2513
        for revision in revisions:
 
2514
            headers += "Revision: %s\n" % revision.revision
 
2515
        headers += "Bug: \n\n"
 
2516
        return headers
 
2517
 
 
2518
    def make_summary(self, logs):
 
2519
        """Generate a summary of merges
 
2520
 
 
2521
        :param logs: the patchlogs that were directly added by the merges
 
2522
        :type logs: list of `arch.Patchlog`
 
2523
        :return: the summary
 
2524
        :rtype: str
 
2525
        """ 
 
2526
        summary = ""
 
2527
        for log in logs:
 
2528
            summary+=str(log.revision)+"\n"
 
2529
            summary+=log.summary+"\n"
 
2530
            if log.description.strip():
 
2531
                summary+=log.description.strip('\n')+"\n\n"
 
2532
        return summary
 
2533
 
 
2534
    def revision_specs(self, args):
 
2535
        """Determine the base and merge revisions from tree and arguments.
 
2536
 
 
2537
        :param args: The parsed arguments
 
2538
        :type args: list of str
 
2539
        :return: The base revision and merge revisions 
 
2540
        :rtype: `arch.Revision`, list of `arch.Patchlog`
 
2541
        """
 
2542
        if len(args) > 0:
 
2543
            target_revision = cmdutil.determine_revision_arch(self.tree, 
 
2544
                                                              args[0])
 
2545
        else:
 
2546
            target_revision = pylon.tree_latest(self.tree)
 
2547
        if len(args) > 1:
 
2548
            merges = [ arch.Patchlog(cmdutil.determine_revision_arch(
 
2549
                       self.tree, f)) for f in args[1:] ]
 
2550
        else:
 
2551
            if self.tree is None:
 
2552
                raise CantDetermineRevision("", "Not in a project tree")
 
2553
            merge_iter = pylon.iter_new_merges(self.tree, 
 
2554
                                                 target_revision.version, 
 
2555
                                                 False)
 
2556
            merges = [f for f in cmdutil.direct_merges(merge_iter)]
 
2557
        return (target_revision, merges)
 
2558
 
 
2559
    def edit_message(self, message):
 
2560
        """Edit an email message in the user's standard editor
 
2561
 
 
2562
        :param message: The message to edit
 
2563
        :type message: str
 
2564
        :return: the path of the edited message
 
2565
        :rtype: str
 
2566
        """
 
2567
        if self.tree is None:
 
2568
            path = os.get_cwd()
 
2569
        else:
 
2570
            path = self.tree
 
2571
        path += "/,merge-request"
 
2572
        file = open(path, 'w')
 
2573
        file.write(message)
 
2574
        file.flush()
 
2575
        cmdutil.invoke_editor(path)
 
2576
        return path
 
2577
 
 
2578
    def tidy_message(self, path):
 
2579
        """Validate and clean up message.
 
2580
 
 
2581
        :param path: The path to the message to clean up
 
2582
        :type path: str
 
2583
        :return: The parsed message
 
2584
        :rtype: `email.Message`
 
2585
        """
 
2586
        mail = email.message_from_file(open(path))
 
2587
        if mail["Subject"].strip() == "[MERGE REQUEST]":
 
2588
            raise BlandSubject
 
2589
        
 
2590
        request = email.message_from_string(mail.get_payload())
 
2591
        if request.has_key("Bug"):
 
2592
            if request["Bug"].strip()=="":
 
2593
                del request["Bug"]
 
2594
        mail.set_payload(request.as_string())
 
2595
        return mail
 
2596
 
 
2597
    def send_message(self, message):
 
2598
        """Send a message, using its headers to address it.
 
2599
 
 
2600
        :param message: The message to send
 
2601
        :type message: `email.Message`"""
 
2602
        server = smtplib.SMTP("localhost")
 
2603
        server.sendmail(message['From'], message['To'], message.as_string())
 
2604
        server.quit()
 
2605
 
 
2606
    def help(self, parser=None):
 
2607
        """Print a usage message
 
2608
 
 
2609
        :param parser: The options parser to use
 
2610
        :type parser: `cmdutil.CmdOptionParser`
 
2611
        """
 
2612
        if parser is None:
 
2613
            parser = self.get_parser()
 
2614
        parser.print_help()
 
2615
        print """
 
2616
Sends a merge request formatted for Bug Goo.  Intended use: get the tree
 
2617
you'd like to merge into.  Apply the merges you want.  Invoke request-merge.
 
2618
The merge request will open in your $EDITOR.
 
2619
 
 
2620
When no TARGET is specified, it uses the current tree revision.  When
 
2621
no MERGE is specified, it uses the direct merges (as in "revisions
 
2622
--direct-merges").  But you can specify just the TARGET, or all the MERGE
 
2623
revisions.
 
2624
"""
 
2625
 
 
2626
    def get_parser(self):
 
2627
        """Produce a commandline parser for this command.
 
2628
 
 
2629
        :rtype: `cmdutil.CmdOptionParser`
 
2630
        """
 
2631
        parser=cmdutil.CmdOptionParser("request-merge [TARGET] [MERGE1...]")
 
2632
        return parser
 
2633
 
 
2634
commands = { 
 
2635
'changes' : Changes,
 
2636
'help' : Help,
 
2637
'update': Update,
 
2638
'apply-changes':ApplyChanges,
 
2639
'cat-log': CatLog,
 
2640
'commit': Commit,
 
2641
'revision': Revision,
 
2642
'revisions': Revisions,
 
2643
'get': Get,
 
2644
'revert': Revert,
 
2645
'shell': Shell,
 
2646
'add-id': AddID,
 
2647
'merge': Merge,
 
2648
'elog': ELog,
 
2649
'mirror-archive': MirrorArchive,
 
2650
'ninventory': Inventory,
 
2651
'alias' : Alias,
 
2652
'request-merge': RequestMerge,
 
2653
}
 
2654
 
 
2655
def my_import(mod_name):
 
2656
    module = __import__(mod_name)
 
2657
    components = mod_name.split('.')
 
2658
    for comp in components[1:]:
 
2659
        module = getattr(module, comp)
 
2660
    return module
 
2661
 
 
2662
def plugin(mod_name):
 
2663
    module = my_import(mod_name)
 
2664
    module.add_command(commands)
 
2665
 
 
2666
for file in os.listdir(sys.path[0]+"/command"):
 
2667
    if len(file) > 3 and file[-3:] == ".py" and file != "__init__.py":
 
2668
        plugin("command."+file[:-3])
 
2669
 
 
2670
suggestions = {
 
2671
'apply-delta' : "Try \"apply-changes\".",
 
2672
'delta' : "To compare two revisions, use \"changes\".",
 
2673
'diff-rev' : "To compare two revisions, use \"changes\".",
 
2674
'undo' : "To undo local changes, use \"revert\".",
 
2675
'undelete' : "To undo only deletions, use \"revert --deletions\"",
 
2676
'missing-from' : "Try \"revisions --missing-from\".",
 
2677
'missing' : "Try \"revisions --missing\".",
 
2678
'missing-merge' : "Try \"revisions --partner-missing\".",
 
2679
'new-merges' : "Try \"revisions --new-merges\".",
 
2680
'cachedrevs' : "Try \"revisions --cacherevs\". (no 'd')",
 
2681
'logs' : "Try \"revisions --logs\"",
 
2682
'tree-source' : "Use the \"^ttag\" alias (\"revision ^ttag\")",
 
2683
'latest-revision' : "Use the \"^acur\" alias (\"revision ^acur\")",
 
2684
'change-version' : "Try \"update REVISION\"",
 
2685
'tree-revision' : "Use the \"^tcur\" alias (\"revision ^tcur\")",
 
2686
'rev-depends' : "Use revisions --dependencies",
 
2687
'auto-get' : "Plain get will do archive lookups",
 
2688
'tagline' : "Use add-id.  It uses taglines in tagline trees",
 
2689
'emlog' : "Use elog.  It automatically adds log-for-merge text, if any",
 
2690
'library-revisions' : "Use revisions --library",
 
2691
'file-revert' : "Use revert FILE",
 
2692
'join-branch' : "Use replay --logs-only",
 
2693
'show-rev' : "Use show-changeset REVISION",
 
2694
'replay-micro' : "Use replay --micro"
 
2695
}
 
2696
# arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7