source: tailor/vcpx/session.py @ 340

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

Create the directory if it's missing

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