source: tailor/vcpx/session.py @ 369

Revision 369, 22.1 KB checked in by lele@…, 8 years ago (diff)

Print a more specific error message on upstream failure

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