source: tailor/vcpx/session.py @ 439

Revision 439, 21.6 KB checked in by lele@…, 8 years ago (diff)

M-x whitespace-cleanup

Line 
1# -*- mode: python; coding: utf-8 -*-
2# :Progetto: vcpx -- Interactive session
3# :Creato:   ven 13 mag 2005 02:00:57 CEST
4# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
5# :Licenza:  GNU General Public License
6#
7
8"""
9Tailor interactive session.
10
11This module implements an alternative approach at driving the various
12steps tipically performed by tailor, using an interaction with the user
13instead of pushing options madness on him.
14"""
15
16__docformat__ = 'reStructuredText'
17
18from cmd import Cmd
19
20
21INTRO = """\
22Welcome to the Tailor interactive session: you can issue several commands
23with the usual `readline` facilities. With "help" you'll get a list of
24available commands.
25"""
26
27def yesno(arg):
28    "Return True for '1', 'true' or 'yes', False otherwise."
29
30    try:
31        return bool(int(arg))
32    except ValueError:
33        return arg.lower() in ('true', 'yes')
34
35class Session(Cmd):
36    """Tailor interactive session."""
37
38    prompt = "tailor $ "
39
40    def __init__(self, options, args):
41        """
42        Initialize a new interactive session.
43
44        Set the default values, and override them with option settings,
45        then slurp in each command line argument that should contain
46        a list of commands to be executed.
47        """
48
49        from os import getcwd
50
51        Cmd.__init__(self)
52        self.options = options
53        self.args = args
54
55        self.source_repository = options.repository
56        self.source_kind = options.source_kind
57        self.source_module = options.module
58        self.target_repository = None
59        self.target_kind = options.target_kind
60        self.target_module = None
61        self.current_directory = getcwd()
62        self.sub_directory = None
63
64        self.state_file = None
65        self.logfile = None
66        self.logger = None
67
68        self.__processArgs()
69
70        # Persistent
71
72        self.changesets = None
73        self.source_revision = None
74
75    def __processArgs(self):
76        """
77        Process optional command line arguments.
78
79        Each argument is assumed to contain a list of tailor commands
80        to execute in order.
81        """
82
83        for arg in self.args:
84            self.cmdqueue.extend(file(arg).readlines())
85
86    def __log(self, what):
87        if self.logger:
88            self.logger.info(what)
89
90        if self.options.verbose:
91            self.stdout.write(what)
92            self.stdout.write('\n')
93
94    def __err(self, what, exc=False):
95        if self.logger:
96            if exc:
97                self.logger.exception(what)
98            else:
99                self.logger.error(what)
100
101
102        self.stdout.write('Error: ')
103        self.stdout.write(what)
104        if exc:
105            from sys import exc_info
106
107            ei = exc_info()
108            self.stdout.write(' -- Exception %s: %s' % ei[0:2])
109        self.stdout.write('\n')
110
111    def emptyline(self):
112        """Override the default impl of reexecuting last command."""
113        pass
114
115    def precmd(self, line):
116        """Strip anything after the first '#', to allow comments."""
117
118        try:
119            line = line[:line.index('#')]
120        except ValueError:
121            pass
122
123        return line
124
125    ## Interactive commands
126
127    def do_exit(self, arg):
128        """
129        Usage: exit
130
131        Terminate the interactive session. This is the same thing
132        happening upon EOF (Ctrl-D).
133        """
134
135        self.__log('Exiting...')
136        return True
137
138    do_EOF = do_exit
139
140    def do_save(self, arg):
141        """
142        Usage: save filename
143
144        Save the commands history on the specified file.
145        """
146
147        import readline
148
149        if not arg:
150            return
151
152        readline.write_history_file(arg)
153        self.__log('History saved in: %s' % arg)
154
155    def do_cd(self, arg):
156        """
157        Usage: cd [dirname]
158
159        Print or set current active directory. If the directory does not
160        exist it is created.
161        """
162
163        from os import chdir, makedirs, getcwd
164        from os.path import isabs, abspath, expanduser
165
166        if arg:
167            arg = expanduser(arg)
168            if not isabs(arg):
169                arg = abspath(arg)
170
171        if arg and self.current_directory <> arg:
172            try:
173                chdir(arg)
174            except OSError:
175                self.__log('Creating directory %s' % arg)
176                try:
177                    makedirs(arg)
178                    chdir(arg)
179                except:
180                    self.__err("Cannot create directory '%s'" % arg, True)
181                    return
182
183            self.current_directory = getcwd()
184
185        self.__log('Current directory: %s' % self.current_directory)
186
187    do_current_directory = do_cd
188
189    def do_sub_directory(self, arg):
190        """
191        Usage: sub_directory dirname
192
193        Print or set the subdirectory that actually contains the
194        working copy. When not explicitly set, this is desumed from
195        the last component of the upstream module name or repository.
196        """
197
198        if arg and self.sub_directory <> arg:
199            self.sub_directory = arg
200
201        self.__log('Sub directory: %s' % self.sub_directory)
202
203    def do_logfile(self, arg):
204        """
205        Usage: logfile [filename]
206
207        Print or set the logfile of operations. By default there's no log.
208        """
209
210        import logging
211
212        if arg:
213            self.logfile = arg
214            self.logger = logging.getLogger('tailor')
215            hdlr = logging.FileHandler(self.logfile)
216            formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')
217            hdlr.setFormatter(formatter)
218            self.logger.addHandler(hdlr)
219            self.logger.setLevel(logging.INFO)
220
221        self.__log('Logging to: %s' % self.logfile)
222
223    def do_print_executed_commands(self, arg):
224        """
225        Usage: print_executed_commands [0|1]
226
227        Print or set the verbosity on external commands execution.
228        """
229
230        from shwrap import ExternalCommand
231
232        if arg:
233            ExternalCommand.VERBOSE = yesno(arg)
234
235        self.__log('Print executed commands: %s' % ExternalCommand.VERBOSE)
236
237    def do_force_output_encoding(self, arg):
238        """
239        Usage: force_output_encoding [charset]
240
241        Print or set the current output encoding. When given, charset must
242        be either the string "None" or a recognized Python charset name.
243        In the former case (the default), tailor will use the current
244        settings from the user environment.
245        """
246
247        from shwrap import ExternalCommand
248
249        if arg:
250            if arg == 'None':
251                ExternalCommand.FORCE_ENCODING = None
252            else:
253                ExternalCommand.FORCE_ENCODING = arg
254
255        self.__log('Forced output encoding: %s' % ExternalCommand.FORCE_ENCODING)
256
257    def do_patch_name_format(self, arg):
258        """
259        Usage: patch_name_format [format]
260
261        Print or set the patch name format, ie the prototype that will
262        be used to compute the patch name.
263
264        The prototype may contain %(keyword)s such as 'module',
265        'author', 'date', 'revision', 'firstlogline', 'remaininglog'
266        for normal updates, otherwise 'module', 'authors',
267        'nchangesets', 'mindate' and 'maxdate'.
268        """
269
270        from target import SyncronizableTargetWorkingDir
271
272        if arg:
273            SyncronizableTargetWorkingDir.PATCH_NAME_FORMAT = arg
274
275        self.__log('Patch name format: %s' %
276                   SyncronizableTargetWorkingDir.PATCH_NAME_FORMAT)
277
278    def do_remove_first_log_line(self, arg):
279        """
280        Usage: remove_first_log_line [0|1]
281
282        Print or set if tailor should drop the first line of the
283        upstream changelog.
284
285        This is intended to go in pair with patch_name_format, when
286        using it's 'firstlogline' variable to build the name of the
287        patch.
288        """
289
290        from target import SyncronizableTargetWorkingDir
291
292        if arg:
293            SyncronizableTargetWorkingDir.REMOVE_FIRST_LOG_LINE = yesno(arg)
294
295        self.__log('Remove first log line: %s' %
296                   SyncronizableTargetWorkingDir.REMOVE_FIRST_LOG_LINE)
297
298    def do_refill_changelogs(self, arg):
299        """
300        Usage: refill_changelogs [0|1]
301
302        Print or set if tailor should refill the upstream changelogs,
303        as it does by default.
304        """
305
306        from changes import Changeset
307
308        if arg:
309            Changeset.REFILL_MESSAGE = yesno(arg)
310
311        self.__log('Refill changelogs: %s' % Changeset.REFILL_MESSAGE)
312
313    def do_source_kind(self, arg):
314        """
315        Usage: source_kind [svn|darcs|cvs]
316
317        Print or set the source repository kind.
318        """
319
320        if arg and self.source_kind <> arg:
321            self.source_kind = arg
322
323        self.__log('Current source kind: %s' % self.source_kind)
324
325    def do_target_kind(self, arg):
326        """
327        Usage: target_kind [svn|darcs|cvs|monotone|cdv|bzr]
328
329        Print or set the target repository kind.
330        """
331
332        if arg and self.target_kind <> arg:
333            self.target_kind = arg
334
335        self.__log('Current target kind: %s' % self.target_kind)
336
337    def do_source_repository(self, arg):
338        """
339        Usage: source_repository [repos]
340
341        Print or set the source repository.
342        """
343
344        from os.path import sep
345
346        if arg and self.source_repository <> arg:
347            if arg.endswith(sep):
348                arg = arg[:-1]
349            self.source_repository = arg
350
351        self.__log('Current source repository: %s' % self.source_repository)
352
353    def do_target_repository(self, arg):
354        """
355        Usage: target_repository [repos]
356
357        Print or set the target repository. This is currently unused.
358        """
359
360        from os.path import sep
361
362        if arg and self.target_repository <> arg:
363            if arg.endswith(sep):
364                arg = arg[:-1]
365            self.target_repository = arg
366
367        self.__log('Current target repository: %s' % self.target_repository)
368
369    def do_source_module(self, arg):
370        """
371        Usage: source_module [module]
372
373        Print or set the source module.
374        """
375
376        from os.path import sep
377
378        if arg and self.source_module <> arg:
379            if arg.endswith(sep):
380                arg = arg[:-1]
381            self.source_module = arg
382
383        self.__log('Current source module: %s' % self.source_module)
384
385    def do_target_module(self, arg):
386        """
387        Usage: target_module [module]
388
389        Print or set the target module. This is currently not used.
390        """
391
392        from os.path import sep
393
394        if arg and self.target_module <> arg:
395            if arg.endswith(sep):
396                arg = arg[:-1]
397            self.target_module = arg
398
399        self.__log('Current target module: %s' % self.target_module)
400
401    def loadStateFile(self):
402        """
403        Read the source revision and pending changesets from the state file.
404        """
405
406        from cPickle import load
407
408        try:
409            sf = open(self.state_file)
410            self.source_revision, self.changesets = load(sf)
411            sf.close()
412
413            self.__log('Source revision: %s' % self.source_revision)
414            if self.changesets:
415                self.__log('Pending changesets: %d' % len(self.changesets))
416        except IOError:
417            self.source_revision = None
418            self.changesets = None
419
420    def writeStateFile(self):
421        """
422        Write current source revision and pending changesets in the state file.
423        """
424
425        from cPickle import dump
426
427        sf = open(self.state_file, 'w')
428        dump((self.source_revision, self.changesets), sf)
429        sf.close()
430
431    def do_state_file(self, arg):
432        """
433        Usage: state_file [filename]
434
435        Print or set the current state file, where tailor stores the
436        source revision that has been applied last.
437
438        The argument must be a file name, possibly with the usual
439        "~user/file" convention.
440        """
441
442        from os.path import isabs, abspath, expanduser
443        from cPickle import load
444
445        if arg:
446            arg = expanduser(arg)
447            if not isabs(arg):
448                arg = abspath(arg)
449
450        if arg and self.state_file <> arg:
451            self.state_file = arg
452
453        self.__log('Current state file: %s' % self.state_file)
454
455        if not self.state_file:
456            self.__err('Need a state_file to proceed to load state file!')
457            return
458
459        try:
460            self.loadStateFile()
461        except:
462            self.__err("'%s' is not a valid state file" % self.state_file,
463                       True)
464            self.state_file = None
465            return
466
467    def do_bootstrap(self, arg):
468        """
469        Usage: bootstrap [revision]
470
471        Checkout the initial upstream revision, by default HEAD (or
472        specified by argument), then import the subtree into the
473        target repository.
474        """
475
476        from os.path import join, split, sep
477        from dualwd import DualWorkingDir
478
479        if not self.state_file:
480            self.__err('Need a state_file to proceed!')
481            return
482
483        if self.source_revision is not None:
484            self.__err('Already bootstrapped!')
485
486        if self.sub_directory:
487            subdir = self.sub_directory
488        else:
489            subdir = split(self.source_module or
490                           self.source_repository)[1] or ''
491            self.do_sub_directory(subdir)
492
493        revision = arg or self.options.revision or 'HEAD'
494
495        dwd = DualWorkingDir(self.source_kind, self.target_kind)
496        self.__log("Getting %s revision '%s' of '%s' from '%s'" % (
497            self.source_kind, revision,
498            self.source_module, self.source_repository))
499
500        try:
501            actual = dwd.checkoutUpstreamRevision(
502                self.current_directory, self.source_repository,
503                self.source_module, revision,
504                subdir=subdir, logger=self.logger)
505        except:
506            self.__err('Checkout failed', True)
507            return
508       
509        self.writeStateFile()
510
511        try:
512            dwd.initializeNewWorkingDir(self.current_directory,
513                                        self.source_repository,
514                                        self.source_module,
515                                        self.sub_directory,
516                                        actual, revision=='INITIAL')
517        except:
518            self.__err('Working copy initialization failed', True)
519            return
520
521    def willApply(self, root, changeset):
522        """
523        Print the changeset being applied.
524        """
525
526        try:
527            self.__log("Changeset %s:\n%s" % (changeset.revision,
528                                              changeset.log))
529        except UnicodeEncodeError:
530            self.__log("Changeset %s:\n%s" % (changeset.revision,
531                                              ">>Non-printable changelog<<"))
532        return True
533
534    def shouldApply(self, root, changeset):
535        """
536        Ask weather a changeset should be applied.
537        """
538
539        self.stdout.write("\nChangeset %s:\n%s" % (changeset.revision,
540                                                     changeset.log))
541
542        while 1:
543            self.stdout.write('\n')
544            ans = raw_input("Apply [Y/n/v/h/q]? ")
545            ans = ans=='' and 'y' or ans[0].lower()
546
547            if ans == 'y':
548                return True
549            elif ans == 'n':
550                return False
551            elif ans == 'h':
552                self.stdout.write('y: yes, apply it and keep going\n'
553                                  'n: no, skip the current changeset\n'
554                                  'v: view more detailed information\n'
555                                  'q: do not apply the current changeset '
556                                  'and stop iterating\n')
557            elif ans == 'q':
558                raise StopIteration()
559            else:
560                self.stdout.write(str(changeset) + '\n')
561
562    def applied(self, root, changeset):
563        """
564        Save current status.
565        """
566
567        self.source_revision = changeset.revision
568        self.changesets.remove(changeset)
569
570    def do_update(self, arg):
571        """
572        Usage: update [arg]
573
574        Fetch information on upstream changes and replay them with the
575        target system.
576
577        Argument may be either an integer value or the string 'ask'. The
578        number specify the maximum number of changesets that will be
579        applied. With 'ask' tailor will propose a "y/n" question for each
580        changeset before applying it.
581        """
582
583        from dualwd import DualWorkingDir
584        from os.path import join, split
585        from source import GetUpstreamChangesetsFailure
586
587        if not self.state_file:
588            self.__err('Need a state_file to proceed!')
589            return
590
591        if self.source_revision is None:
592            self.__log('Boostrapping, because source_revision is None!')
593            return self.do_bootstrap(None)
594
595        if self.sub_directory:
596            subdir = self.sub_directory
597        else:
598            if not self.source_module:
599                self.__err('Need a source_module to proceed!')
600                return
601            subdir = split(self.source_module or
602                           self.source_repository)[1] or ''
603            self.do_sub_directory(subdir)
604
605        repodir = join(self.current_directory, subdir)
606        dwd = DualWorkingDir(self.source_kind, self.target_kind)
607
608        # If we have no pending changesets, ask the upstream server
609        # about new changes
610
611        if not self.changesets:
612            try:
613                self.changesets = dwd.getUpstreamChangesets(
614                                           repodir,
615                                           self.source_repository,
616                                           self.source_module,
617                                           self.source_revision)
618            except GetUpstreamChangesetsFailure, exc:
619                self.__err('Unable to collect upstream changes from %s: %s' %
620                           (self.source_repository, exc))
621                return
622            except:
623                self.__err('Unable to collect upstream changes', True)
624                return
625
626        nchanges = len(self.changesets)
627        if nchanges:
628            applyable = self.willApply
629            if arg:
630                try:
631                    howmany = min(int(arg), nchanges)
632                    changesets = self.changesets[:howmany]
633                except ValueError:
634                    changesets = self.changesets[:]
635                    if arg.lower() == 'ask':
636                        applyable = self.shouldApply
637            else:
638                changesets = self.changesets[:]
639
640            self.__log('Applying %d changesets (out of %d)' %
641                       (len(changesets), nchanges))
642
643            last = None
644            try:
645                try:
646                    last, conflicts = dwd.applyUpstreamChangesets(
647                        repodir, self.source_module, changesets,
648                        applyable=applyable, applied=self.applied,
649                        logger=self.logger) # , delayed_commit=single_commit)
650                except StopIteration, KeyboardInterrupt:
651                    if self.logger:
652                        self.logger.warning("Stopped by user")
653                except:
654                    self.__err('Stopping after upstream change application '
655                               'failure', True)
656            finally:
657                self.writeStateFile()
658
659                if self.changesets:
660                    self.__log("There are still %d pending changesets, "
661                               "now at revision '%s'" %
662                               (len(self.changesets), self.source_revision))
663                else:
664                    self.__log("Update completed, now at revision '%s'" %
665                               self.source_revision)
666        else:
667            self.__log("Update completed with no upstream changes")
668
669    def do_dopplebanger(self, arg):
670        """
671        Usage: dopplebanger patchname
672
673        Given two repositories (in the actual implementation, the source
674        must be a local darcs repository), do something similar to update
675        but using diff and patch instead.
676        """
677
678        from os.path import isdir
679        from dualwd import DualWorkingDir
680        from darcs import DARCS_CMD, changesets_from_darcschanges
681        from shwrap import ExternalCommand, PIPE
682
683        if not (self.source_repository and self.target_repository and
684                isdir(self.source_repository) and
685                isdir(self.target_repository) and
686                self.source_kind == 'darcs' and
687                self.target_kind):
688            self.__err('Both source and target repository must be under '
689                       'darcs and on the local filesystem!')
690            return
691
692        if not arg:
693            self.__err('Needs a patchname to proceed')
694            return
695
696        c = ExternalCommand(cwd=self.source_repository,
697                            command=[DARCS_CMD, "changes", "--patches",
698                                     arg, "--xml-output", "--summ"])
699        last = changesets_from_darcschanges(c.execute(output=PIPE),
700                                            unidiff=True,
701                                            repodir=self.source_repository)
702
703        if not last:
704            self.__err('Specified patchname does not exist!')
705            return
706
707        cset = last[0]
708        cset.applyPatch(working_dir=self.target_repository,
709                        patch_options=["-p1", "--force"])
710
711        dwd = DualWorkingDir(self.source_kind, self.target_kind)
712        dwd.replayChangeset(self.target_repository, self.target_module, cset,
713                            logger=self.logger)
714
715def interactive(options, args):
716    session = Session(options, args)
717    session.cmdloop(options.verbose and INTRO or "")
Note: See TracBrowser for help on using the repository browser.