source: tailor/vcpx/session.py @ 431

Revision 431, 22.2 KB checked in by lele@…, 8 years ago (diff)

Better handling of initial revision, expecially for darcs
Now tailor accepts --revision=INITIAL also for darcs: when specifying
INITIAL as revision, tailor uses the author, date and changelog of the
first changeset for the bootstrap.

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