source: tailor/vcpx/session.py @ 323

Revision 323, 15.7 KB checked in by lele@…, 8 years ago (diff)

Implement 'update' command

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
20from os import chdir, getcwd       
21
22
23INTRO = """\
24Welcome to the Tailor interactive session: you can issue several commands
25with the usual `readline` facilities. With "help" you'll get a list of
26available commands.
27"""
28
29def yesno(arg):
30    "Return True for '1', 'true' or 'yes', False otherwise."
31
32    try:
33        return bool(int(arg))
34    except ValueError:
35        return arg.lower() in ('true', 'yes')
36   
37class Session(Cmd):
38    """Tailor interactive session."""
39   
40    prompt = "tailor $ "
41   
42    def __init__(self, options, args):
43        """
44        Initialize a new interactive session.
45
46        Set the default values, and override them with option settings,
47        then slurp in each command line argument that should contain
48        a list of commands to be executed.
49        """
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 = 'tailor.state'
65       
66        self.changesets = None
67        self.logfile = None
68        self.logger = None
69       
70        self.__processArgs()
71
72    def __processArgs(self):
73        """
74        Process optional command line arguments.
75
76        Each argument is assumed to contain a list of tailor commands
77        to execute in order.
78        """
79
80        for arg in self.args:
81            self.cmdqueue.extend(file(arg).readlines())
82
83    def __log(self, what):
84        if self.logger:
85            self.logger.info(what)
86           
87        if self.options.verbose:
88            self.stdout.write(what)
89
90    def __err(self, what):
91        if self.logger:
92            self.logger.error(what)
93           
94        self.stdout.write('Error: ')
95        self.stdout.write(what)
96       
97
98    ## Interactive commands
99
100    def emptyline(self):
101        """Override the default impl of reexecuting last command."""
102        pass
103       
104    def do_exit(self, arg):
105        """
106        Usage: exit
107
108        Terminate the interactive session. This is the same thing
109        happening upon EOF (Ctrl-D).
110        """
111
112        self.__log('Exiting...\n')
113        return True
114
115    do_EOF = do_exit
116
117    def do_save(self, arg):
118        """
119        Usage: save filename
120
121        Save the commands history on the specified file.
122        """
123
124        import readline
125
126        if not arg:
127            return
128           
129        readline.write_history_file(arg)
130        self.__log('History saved in: %s\n' % arg)
131       
132    def do_cd(self, arg):
133        """
134        Usage: cd [dirname]
135
136        Print or set current active directory.
137        """
138
139        if arg and self.current_directory <> arg:
140            try:
141                chdir(arg)
142                self.current_directory = getcwd()
143            except:
144                self.__log('Cannot change current directory to %s\n' %
145                           arg)
146        self.__log('Current directory: %s\n' % self.current_directory)
147
148    do_current_directory = do_cd
149
150    def do_sub_directory(self, arg):
151        """
152        Usage: sub_directory dirname
153       
154        Print or set the subdirectory that actually contains the
155        working copy. When not explicitly set, this is desumed from
156        the last component of the upstream module name or repository.
157        """
158       
159        if arg and self.sub_directory <> arg:
160            self.sub_directory = arg
161
162        self.__log('Sub directory: %s\n' % self.sub_directory)
163
164    def do_logfile(self, arg):
165        """
166        Usage: logfile [filename]
167       
168        Print or set the logfile of operations. By default there's no log.
169        """
170
171        import logging
172       
173        if arg:
174            self.logfile = arg
175            self.logger = logging.getLogger('tailor')
176            hdlr = logging.FileHandler(self.logfile)
177            formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')
178            hdlr.setFormatter(formatter)
179            self.logger.addHandler(hdlr) 
180            self.logger.setLevel(logging.INFO)
181           
182        self.__log('Logging to: %s\n' % self.logfile)
183
184    def do_print_executed_commands(self, arg):
185        """
186        Usage: print_executed_commands [0|1]
187
188        Print or set the verbosity on external commands execution.
189        """
190
191        from shwrap import SystemCommand
192       
193        if arg:
194            SystemCommand.VERBOSE = yesno(arg)
195
196        self.__log('Print executed commands: %s\n' % SystemCommand.VERBOSE)
197
198    def do_patch_name_format(self, arg):
199        """
200        Usage: patch_name_format [format]
201
202        Print or set the patch name format, ie the prototype that will
203        be used to compute the patch name.
204
205        The prototype may contain %(keyword)s such as 'module',
206        'author', 'date', 'revision', 'firstlogline', 'remaininglog'
207        for normal updates, otherwise 'module', 'authors',
208        'nchangesets', 'mindate' and 'maxdate'.
209        """
210
211        from target import SyncronizableTargetWorkingDir
212
213        if arg:
214            SyncronizableTargetWorkingDir.PATCH_NAME_FORMAT = arg
215
216        self.__log('Patch name format: %s\n' %
217                   SyncronizableTargetWorkingDir.PATCH_NAME_FORMAT)
218       
219    def do_remove_first_log_line(self, arg):
220        """
221        Usage: remove_first_log_line [0|1]
222       
223        Print or set if tailor should drop the first line of the
224        upstream changelog.
225
226        This is intended to go in pair with patch_name_format, when
227        using it's 'firstlogline' variable to build the name of the
228        patch.
229        """
230
231        from target import SyncronizableTargetWorkingDir
232
233        if arg:
234            SyncronizableTargetWorkingDir.REMOVE_FIRST_LOG_LINE = yesno(arg)
235
236        self.__log('Remove first log line: %s\n' %
237                   SyncronizableTargetWorkingDir.REMOVE_FIRST_LOG_LINE)
238
239    def do_refill_changelogs(self, arg):
240        """
241        Usage: refill_changelogs [0|1]
242
243        Print or set if tailor should refill the upstream changelogs,
244        as it does by default.
245        """
246
247        from changes import Changeset
248       
249        if arg:
250            Changeset.REFILL_MESSAGE = yesno(arg)
251
252        self.__log('Refill changelogs: %s\n' % Changeset.REFILL_MESSAGE)
253       
254    def do_source_kind(self, arg):
255        """
256        Usage: source_kind [svn|darcs|cvs]
257
258        Print or set the source repository kind.
259        """
260
261        if arg and self.source_kind <> arg:
262            self.source_kind = arg
263
264        self.__log('Current source kind: %s\n' % self.source_kind)
265       
266    def do_target_kind(self, arg):
267        """
268        Usage: target_kind [svn|darcs|cvs|monotone|cdv|bzr]
269
270        Print or set the target repository kind.
271        """
272
273        if arg and self.target_kind <> arg:
274            self.target_kind = arg
275
276        self.__log('Current target kind: %s\n' % self.target_kind)
277
278    def do_source_repository(self, arg):
279        """
280        Usage: source_repository [repos]
281
282        Print or set the source repository.
283        """
284
285        from os.path import sep
286       
287        if arg and self.source_repository <> arg:
288            if arg.endswith(sep):
289                arg = arg[:-1]
290            self.source_repository = arg
291
292        self.__log('Current source repository: %s\n' % self.source_repository)
293
294    def do_target_repository(self, arg):
295        """
296        Usage: target_repository [repos]
297
298        Print or set the target repository. This is currently unused.
299        """
300
301        from os.path import sep
302       
303        if arg and self.target_repository <> arg:
304            if arg.endswith(sep):
305                arg = arg[:-1]
306            self.target_repository = arg
307
308        self.__log('Current target repository: %s\n' % self.target_repository)
309
310    def do_source_module(self, arg):
311        """
312        Usage: source_module [module]
313       
314        Print or set the source module.
315        """
316
317        from os.path import sep
318       
319        if arg and self.source_module <> arg:
320            if arg.endswith(sep):
321                arg = arg[:-1]
322            self.source_module = arg
323
324        self.__log('Current target kind: %s\n' % self.source_module)
325
326    def do_target_module(self, arg):
327        """
328        Usage: target_module [module]
329
330        Print or set the target module. This is currently not used.
331        """
332
333        from os.path import sep
334       
335        if arg and self.target_module <> arg:
336            if arg.endswith(sep):
337                arg = arg[:-1]
338            self.target_module = arg
339
340        self.__log('Current target kind: %s\n' % self.target_module)
341
342    def readSourceRevision(self):
343        """Read the source revision from the state file."""
344
345        try:
346            sf = open(self.state_file)
347            revision = sf.read()
348            sf.close()
349        except IOError:
350            revision = None
351
352        return revision
353           
354    def saveSourceRevision(self, revision):
355        """Write current source revision in the state file."""
356
357        sf = open(self.state_file, 'w')
358        sf.write(revision)
359        sf.close()
360       
361    def do_state_file(self, arg):
362        """
363        Usage: state_file [filename]
364       
365        Print or set the current state file, where tailor stores the
366        source revision that has been applied last.
367       
368        The argument must be a file name, possibly with the usual
369        "~user/file" convention.       
370        """
371
372        from os.path import isabs, abspath, expanduser
373
374        if arg:
375            arg = expanduser(arg)
376            if not isabs(arg):
377                arg = abspath(arg)
378       
379        if arg and self.state_file <> arg:
380            self.state_file = arg
381               
382        self.__log('Current state file: %s\n' % self.state_file)
383
384    def do_bootstrap(self, arg):
385        """
386        Usage: bootstrap [revision]
387       
388        Checkout the initial upstream revision, by default HEAD (or
389        specified by argument), then import the subtree into the
390        target repository.
391        """
392       
393        from os.path import join, split, sep
394        from dualwd import DualWorkingDir
395
396        if self.sub_directory:
397            subdir = self.sub_directory
398        else:
399            subdir = split(self.source_module or self.source_repository)[1] or ''
400            self.do_sub_directory(subdir)
401
402        revision = arg or self.options.revision or 'HEAD'
403       
404        dwd = DualWorkingDir(self.source_kind, self.target_kind)
405        self.__log("Getting %s revision '%s' of '%s' from '%s'\n" % (
406            self.source_kind, revision,
407            self.source_module, self.source_repository))
408
409        try:
410            actual = dwd.checkoutUpstreamRevision(self.current_directory,
411                                                  self.source_repository,
412                                                  self.source_module,
413                                                  revision,
414                                                  subdir=subdir,
415                                                  logger=self.logger)
416            self.saveSourceRevision(actual)
417        except Exception, exc:
418            self.__err('Checkout failed: %s, %s' % (exc.__doc__, exc))
419            if self.logger:
420                self.logger.exception('Checkout failed')
421
422        try:
423            dwd.initializeNewWorkingDir(self.current_directory,
424                                        self.target_repository,
425                                        self.target_module,
426                                        self.sub_directory,
427                                        actual)
428        except:
429            self.__err('Working copy initialization failed: %s, %s' % (exc.__doc__, exc))
430            if self.logger:
431                self.logger.exception('Working copy initialization failed')
432
433    def willApply(self, root, changeset):
434        """
435        Print the changeset being applied.
436        """
437
438        self.__log("Changeset %s:\n%s\n" % (changeset.revision,
439                                            changeset.log))
440        return True
441
442    def shouldApply(self, root, changeset):
443        """
444        Ask weather a changeset should be applied.
445        """
446
447        self.stdout.write("Changeset %s:\n%s\n" % (changeset.revision,
448                                                   changeset.log))
449        ans = raw_input("Apply [Y/n]? ")
450       
451        return ans == '' or ans[0].lower() == 'y'
452
453    def applied(self, root, changeset):
454        """
455        Save current status.
456        """
457
458        self.saveSourceRevision(changeset.revision)
459
460    def do_update(self, arg):
461        """
462        Usage: update [arg]
463
464        Fetch information on upstream changes and replay them with the
465        target system.
466
467        Argument may be either an integer value or the string 'ask'. The
468        number specify the maximum number of changesets the will be
469        applied. With 'ask' tailor will propose a "y/n" question for each
470        changeset before applying it.
471        """
472
473        source_revision = self.readSourceRevision()
474        if self.source_kind and \
475           self.source_repository and \
476           self.source_module and \
477           source_revision:
478
479            dwd = DualWorkingDir(self.source_kind, self.target_kind)
480            self.changesets = dwd.getUpstreamChangesets(self.current_directory,
481                                                        self.source_repository,
482                                                        self.source_module,
483                                                        source_revision)
484            nchanges = len(changesets)
485            if nchanges:
486                self.__log('Collected %d upstream changesets\n' % nchanges)
487
488                if arg:
489                    appliable = self.willApply
490                    try:
491                        howmany = min(int(arg), nchanges)
492                        changesets = changesets[:howmany]
493                        self.__log('Applying first %d of them\n' % howmany)
494                    except ValueError:
495                        if arg.lower() == 'ask':
496                            appliable = self.shouldApply
497
498                try:
499                    last, conflicts = dwd.applyUpstreamChangesets(
500                        proj, self.module, changesets, applyable=applyable,
501                        applied=self.applied, logger=self.logger,
502                        delayed_commit=single_commit)
503                except:
504                    if self.logger:
505                        self.logger.exception('Upstream change application '
506                                              'failed')
507                    self.__err('Stopping after upstream change application '
508                               'failure.')
509                    return
510
511                if last:
512                    self.__log("Update completed, now at revision '%s'" %
513                               self.readSourceRevision())
514            else:
515                self.__log("Update completed with no upstream changes")
516        else:
517            self.__err("needs 'source_kind', 'source_repository' and "
518                       "'source_module' to proceed.\n")
519
520       
521def interactive(options, args):
522    session = Session(options, args)
523    session.cmdloop(options.verbose and INTRO or "")
Note: See TracBrowser for help on using the repository browser.