source: tailor/vcpx/session.py @ 345

Revision 345, 19.0 KB checked in by lele@…, 8 years ago (diff)

Do tilde expansion on directory name

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