source: tailor/vcpx/session.py @ 341

Revision 341, 18.9 KB checked in by lele@…, 8 years ago (diff)

Add newline only in verbose mode on stdout

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):
96        if self.logger:
97            self.logger.error(what)
98           
99        self.stdout.write('Error: ')
100        self.stdout.write(what)
101        self.stdout.write('\n')
102
103    def emptyline(self):
104        """Override the default impl of reexecuting last command."""
105        pass
106
107    def precmd(self, line):
108        """Strip anything after the first '#', to allow comments."""
109
110        try:
111            line = line[:line.index('#')]
112        except ValueError:
113            pass
114
115        return line
116       
117    ## Interactive commands
118
119    def do_exit(self, arg):
120        """
121        Usage: exit
122
123        Terminate the interactive session. This is the same thing
124        happening upon EOF (Ctrl-D).
125        """
126
127        self.__log('Exiting...')
128        return True
129
130    do_EOF = do_exit
131
132    def do_save(self, arg):
133        """
134        Usage: save filename
135
136        Save the commands history on the specified file.
137        """
138
139        import readline
140
141        if not arg:
142            return
143           
144        readline.write_history_file(arg)
145        self.__log('History saved in: %s' % arg)
146       
147    def do_cd(self, arg):
148        """
149        Usage: cd [dirname]
150
151        Print or set current active directory. If the directory does not
152        exist it is created.
153        """
154
155        from os import chdir, makedirs, getcwd
156       
157        if arg and self.current_directory <> arg:
158            try:
159                chdir(arg)
160            except OSError:
161                self.__log('Creating directory %s' % arg)
162                try:
163                    makedirs(arg)
164                    chdir(arg)
165                except:
166                    self.__err('Cannot create directory %s' % arg)
167                   
168            self.current_directory = getcwd()
169           
170        self.__log('Current directory: %s' % self.current_directory)
171
172    do_current_directory = do_cd
173
174    def do_sub_directory(self, arg):
175        """
176        Usage: sub_directory dirname
177       
178        Print or set the subdirectory that actually contains the
179        working copy. When not explicitly set, this is desumed from
180        the last component of the upstream module name or repository.
181        """
182       
183        if arg and self.sub_directory <> arg:
184            self.sub_directory = arg
185
186        self.__log('Sub directory: %s' % self.sub_directory)
187
188    def do_logfile(self, arg):
189        """
190        Usage: logfile [filename]
191       
192        Print or set the logfile of operations. By default there's no log.
193        """
194
195        import logging
196       
197        if arg:
198            self.logfile = arg
199            self.logger = logging.getLogger('tailor')
200            hdlr = logging.FileHandler(self.logfile)
201            formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')
202            hdlr.setFormatter(formatter)
203            self.logger.addHandler(hdlr) 
204            self.logger.setLevel(logging.INFO)
205           
206        self.__log('Logging to: %s' % self.logfile)
207
208    def do_print_executed_commands(self, arg):
209        """
210        Usage: print_executed_commands [0|1]
211
212        Print or set the verbosity on external commands execution.
213        """
214
215        from shwrap import SystemCommand
216       
217        if arg:
218            SystemCommand.VERBOSE = yesno(arg)
219
220        self.__log('Print executed commands: %s' % SystemCommand.VERBOSE)
221
222    def do_patch_name_format(self, arg):
223        """
224        Usage: patch_name_format [format]
225
226        Print or set the patch name format, ie the prototype that will
227        be used to compute the patch name.
228
229        The prototype may contain %(keyword)s such as 'module',
230        'author', 'date', 'revision', 'firstlogline', 'remaininglog'
231        for normal updates, otherwise 'module', 'authors',
232        'nchangesets', 'mindate' and 'maxdate'.
233        """
234
235        from target import SyncronizableTargetWorkingDir
236
237        if arg:
238            SyncronizableTargetWorkingDir.PATCH_NAME_FORMAT = arg
239
240        self.__log('Patch name format: %s' %
241                   SyncronizableTargetWorkingDir.PATCH_NAME_FORMAT)
242       
243    def do_remove_first_log_line(self, arg):
244        """
245        Usage: remove_first_log_line [0|1]
246       
247        Print or set if tailor should drop the first line of the
248        upstream changelog.
249
250        This is intended to go in pair with patch_name_format, when
251        using it's 'firstlogline' variable to build the name of the
252        patch.
253        """
254
255        from target import SyncronizableTargetWorkingDir
256
257        if arg:
258            SyncronizableTargetWorkingDir.REMOVE_FIRST_LOG_LINE = yesno(arg)
259
260        self.__log('Remove first log line: %s' %
261                   SyncronizableTargetWorkingDir.REMOVE_FIRST_LOG_LINE)
262
263    def do_refill_changelogs(self, arg):
264        """
265        Usage: refill_changelogs [0|1]
266
267        Print or set if tailor should refill the upstream changelogs,
268        as it does by default.
269        """
270
271        from changes import Changeset
272       
273        if arg:
274            Changeset.REFILL_MESSAGE = yesno(arg)
275
276        self.__log('Refill changelogs: %s' % Changeset.REFILL_MESSAGE)
277       
278    def do_source_kind(self, arg):
279        """
280        Usage: source_kind [svn|darcs|cvs]
281
282        Print or set the source repository kind.
283        """
284
285        if arg and self.source_kind <> arg:
286            self.source_kind = arg
287
288        self.__log('Current source kind: %s' % self.source_kind)
289       
290    def do_target_kind(self, arg):
291        """
292        Usage: target_kind [svn|darcs|cvs|monotone|cdv|bzr]
293
294        Print or set the target repository kind.
295        """
296
297        if arg and self.target_kind <> arg:
298            self.target_kind = arg
299
300        self.__log('Current target kind: %s' % self.target_kind)
301
302    def do_source_repository(self, arg):
303        """
304        Usage: source_repository [repos]
305
306        Print or set the source repository.
307        """
308
309        from os.path import sep
310       
311        if arg and self.source_repository <> arg:
312            if arg.endswith(sep):
313                arg = arg[:-1]
314            self.source_repository = arg
315
316        self.__log('Current source repository: %s' % self.source_repository)
317
318    def do_target_repository(self, arg):
319        """
320        Usage: target_repository [repos]
321
322        Print or set the target repository. This is currently unused.
323        """
324
325        from os.path import sep
326       
327        if arg and self.target_repository <> arg:
328            if arg.endswith(sep):
329                arg = arg[:-1]
330            self.target_repository = arg
331
332        self.__log('Current target repository: %s' % self.target_repository)
333
334    def do_source_module(self, arg):
335        """
336        Usage: source_module [module]
337       
338        Print or set the source module.
339        """
340
341        from os.path import sep
342       
343        if arg and self.source_module <> arg:
344            if arg.endswith(sep):
345                arg = arg[:-1]
346            self.source_module = arg
347
348        self.__log('Current source module: %s' % self.source_module)
349
350    def do_target_module(self, arg):
351        """
352        Usage: target_module [module]
353
354        Print or set the target module. This is currently not used.
355        """
356
357        from os.path import sep
358       
359        if arg and self.target_module <> arg:
360            if arg.endswith(sep):
361                arg = arg[:-1]
362            self.target_module = arg
363
364        self.__log('Current target module: %s' % self.target_module)
365
366    def loadStateFile(self):
367        """
368        Read the source revision and pending changesets from the state file.
369        """
370
371        from cPickle import load
372
373        try:
374            sf = open(self.state_file)
375            self.source_revision, self.changesets = load(sf)
376            sf.close()
377
378            self.__log('Source revision: %s' % self.source_revision)
379            if self.changesets:
380                self.__log('Pending changesets: %d' % len(self.changesets))
381        except IOError:
382            self.source_revision = None
383            self.changesets = None
384
385    def writeStateFile(self):
386        """
387        Write current source revision and pending changesets in the state file.
388        """
389
390        from cPickle import dump
391       
392        sf = open(self.state_file, 'w')
393        dump((self.source_revision, self.changesets), sf)
394        sf.close()
395
396    def do_state_file(self, arg):
397        """
398        Usage: state_file [filename]
399       
400        Print or set the current state file, where tailor stores the
401        source revision that has been applied last.
402       
403        The argument must be a file name, possibly with the usual
404        "~user/file" convention.       
405        """
406
407        from os.path import isabs, abspath, expanduser
408        from cPickle import load
409       
410        if arg:
411            arg = expanduser(arg)
412            if not isabs(arg):
413                arg = abspath(arg)
414       
415        if arg and self.state_file <> arg:
416            self.state_file = arg
417           
418        self.__log('Current state file: %s' % self.state_file)
419
420        self.loadStateFile()
421
422    def do_bootstrap(self, arg):
423        """
424        Usage: bootstrap [revision]
425       
426        Checkout the initial upstream revision, by default HEAD (or
427        specified by argument), then import the subtree into the
428        target repository.
429        """
430       
431        from os.path import join, split, sep
432        from dualwd import DualWorkingDir
433
434        if not self.state_file:
435            self.__err('Need a state_file to proceed!')
436            return
437
438        if self.source_revision is not None:
439            self.__err('Already bootstrapped!')
440           
441        if self.sub_directory:
442            subdir = self.sub_directory
443        else:
444            subdir = split(self.source_module or
445                           self.source_repository)[1] or ''
446            self.do_sub_directory(subdir)
447
448        revision = arg or self.options.revision or 'HEAD'
449
450        dwd = DualWorkingDir(self.source_kind, self.target_kind)
451        self.__log("Getting %s revision '%s' of '%s' from '%s'" % (
452            self.source_kind, revision,
453            self.source_module, self.source_repository))
454
455        try:
456            self.source_revision = dwd.checkoutUpstreamRevision(
457                self.current_directory, self.source_repository,
458                self.source_module, revision,
459                subdir=subdir, logger=self.logger)
460        except Exception, exc:
461            self.__err('Checkout failed: %s, %s' % (exc.__doc__, exc))
462            if self.logger:
463                self.logger.exception('Checkout failed')
464
465        self.writeStateFile()
466       
467        try:
468            dwd.initializeNewWorkingDir(self.current_directory,
469                                        self.target_repository,
470                                        self.target_module,
471                                        self.sub_directory,
472                                        self.source_revision)
473        except Exception, exc:
474            self.__err('Working copy initialization failed: %s, %s' %
475                       (exc.__doc__, exc))
476            if self.logger:
477                self.logger.exception('Working copy initialization failed')
478
479    def willApply(self, root, changeset):
480        """
481        Print the changeset being applied.
482        """
483
484        self.__log("Changeset %s:\n%s" % (changeset.revision,
485                                            changeset.log))
486        return True
487
488    def shouldApply(self, root, changeset):
489        """
490        Ask weather a changeset should be applied.
491        """
492
493        self.stdout.write("\nChangeset %s:\n%s" % (changeset.revision,
494                                                     changeset.log))
495
496        while 1:
497            self.stdout.write('\n')
498            ans = raw_input("Apply [Y/n/v/h/q]? ")
499            ans = ans=='' and 'y' or ans[0].lower()
500
501            if ans == 'y':
502                return True
503            elif ans == 'n':
504                return False
505            elif ans == 'h':
506                self.stdout.write('y: yes, apply it and keep going\n'
507                                  'n: no, skip the current changeset\n'
508                                  'v: view more detailed information\n'
509                                  'q: do not apply the current changeset '
510                                  'and stop iterating\n')
511            elif ans == 'q':
512                raise StopIteration()
513            else:
514                self.stdout.write(str(changeset) + '\n')
515
516    def applied(self, root, changeset):
517        """
518        Save current status.
519        """
520
521        self.source_revision = changeset.revision
522        self.changesets.remove(changeset)
523
524    def do_update(self, arg):
525        """
526        Usage: update [arg]
527
528        Fetch information on upstream changes and replay them with the
529        target system.
530
531        Argument may be either an integer value or the string 'ask'. The
532        number specify the maximum number of changesets that will be
533        applied. With 'ask' tailor will propose a "y/n" question for each
534        changeset before applying it.
535        """
536
537        from dualwd import DualWorkingDir
538        from os.path import join, split
539
540        if not self.state_file:
541            self.__err('Need a state_file to proceed!')
542            return
543               
544        if self.source_revision is None:
545            self.__err("Not yet bootstrapped!\n")
546            return
547       
548        if self.sub_directory:
549            subdir = self.sub_directory
550        else:
551            subdir = split(self.source_module or
552                           self.source_repository)[1] or ''
553            self.do_sub_directory(subdir)
554           
555        repodir = join(self.current_directory, subdir)
556        dwd = DualWorkingDir(self.source_kind, self.target_kind)
557
558        # If we have no pending changesets, ask the upstream server
559        # about new changes
560       
561        if not self.changesets:
562            try:
563                self.changesets = dwd.getUpstreamChangesets(
564                                           repodir,
565                                           self.source_repository,
566                                           self.source_module,
567                                           self.source_revision)
568            except KeyboardInterrupt:
569                if self.logger:
570                    self.logger.warning("Stopped by user")
571                return
572            except:
573                if self.logger:
574                    self.logger.exception('Unable to collect upstream changes')
575                self.__err('Unable to collect upstream changes')
576                return
577           
578        nchanges = len(self.changesets)
579        if nchanges:
580            if arg:
581                applyable = self.willApply
582                try:
583                    howmany = min(int(arg), nchanges)
584                    changesets = self.changesets[:howmany]
585                except ValueError:
586                    changesets = self.changesets[:]
587                    if arg.lower() == 'ask':
588                        applyable = self.shouldApply
589
590            self.__log('Applying %d changesets (out of %d)' %
591                       (len(changesets), nchanges))
592
593            last = None
594            try:
595                try:
596                    last, conflicts = dwd.applyUpstreamChangesets(
597                        repodir, self.source_module, changesets,
598                        applyable=applyable, applied=self.applied,
599                        logger=self.logger) # , delayed_commit=single_commit)
600                except StopIteration, KeyboardInterrupt:
601                    if self.logger:
602                        self.logger.warning("Stopped by user")
603                    return
604                except:
605                    if self.logger:
606                        self.logger.exception('Upstream change application '
607                                              'failed')
608                    self.__err('Stopping after upstream change application '
609                               'failure.')
610                    return
611            finally:
612                self.writeStateFile()
613               
614                if self.changesets:
615                    self.__log("There are still %d pending changesets, "
616                               "now at revision '%s'" %
617                               (len(self.changesets), self.source_revision))
618                else:
619                    self.__log("Update completed, now at revision '%s'" %
620                               self.source_revision)
621        else:
622            self.__log("Update completed with no upstream changes")
623
624       
625def interactive(options, args):
626    session = Session(options, args)
627    session.cmdloop(options.verbose and INTRO or "")
Note: See TracBrowser for help on using the repository browser.