source: tailor/vcpx/source.py @ 574

Revision 574, 7.2 KB checked in by lele@…, 8 years ago (diff)

Revised the applyPendingChangesets() to save the state only once
The method was rewriting the state file at each iteration over the
pending changesets, very safe but also very time expensive. Now it
does that just once, before leaving.

Also, introduced a new _handleConflict() that subclasses may override
to prompt the user about conflict raised by application of a changeset.

Line 
1# -*- mode: python; coding: utf-8 -*-
2# :Progetto: vcpx -- Updatable VC working directory
3# :Creato:   mer 09 giu 2004 13:55:35 CEST
4# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
5# :Licenza:  GNU General Public License
6#
7
8"""
9Updatable sources are the simplest abstract wrappers around a working
10directory under some kind of version control system.
11"""
12
13__docformat__ = 'reStructuredText'
14
15from workdir import WorkingDir
16
17CONFLICTS_PROMPT = """
18The changeset
19
20%s
21caused conflicts on the following files:
22
23 * %s
24
25This is quite unusual, and most probably it means someone else has
26changed the working dir, beyond tailor control, or maybe a tailor bug
27is showing up.
28
29Either abort the session with Ctrl-C, or manually correct the situation
30with a Ctrl-Z, explore and correct, and coming back from the shell with
31'fg'.
32
33What would you like to do?
34"""
35
36class GetUpstreamChangesetsFailure(Exception):
37    "Failure getting upstream changes"
38
39class ChangesetApplicationFailure(Exception):
40    "Failure applying upstream changes"
41
42class InvocationError(Exception):
43    "Bad invocation, use --help for details"
44
45class UpdatableSourceWorkingDir(WorkingDir):
46    """
47    This is an abstract working dir able to follow an upstream
48    source of ``changesets``.
49
50    It has three main functionalities:
51
52    getPendingChangesets
53        to query the upstream server about new changesets
54
55    applyPendingChangesets
56        to apply them to the working directory
57
58    checkoutUpstreamRevision
59        to extract a new copy of the sources, actually initializing
60        the mechanism.
61
62    Subclasses MUST override at least the _underscoredMethods.
63    """
64
65    def applyPendingChangesets(self, applyable=None, replayable=None,
66                               replay=None, applied=None):
67        """
68        Apply the collected upstream changes.
69
70        Loop over the collected changesets, doing whatever is needed
71        to apply each one to the working dir and if the changes do
72        not raise conflicts call the `replay` function to mirror the
73        changes on the target.
74
75        Return a tuple of two elements:
76
77        - the last applied changeset, if any
78        - the sequence (potentially empty!) of conflicts.
79        """
80
81        c = None
82        last = None
83        conflicts = []
84
85        if not self.pending:
86            return last, conflicts
87
88        try:
89            while self.pending:
90                c = self.pending[0]
91
92                # Give the opportunity to subclasses to stop the application
93                # of the queue, before the application of the patch by the
94                # source backend.
95                if not self._willApplyChangeset(c, applyable):
96                    break
97
98                self.log_info("Applying changeset %s" % c.revision)
99
100                try:
101                    res = self._applyChangeset(c)
102                except:
103                    self.log_error("Couldn't apply changeset %s" % c.revision,
104                                   exc=True)
105                    raise
106
107                if res:
108                    # Uh, we have a conflict: this should not ever
109                    # happen, but the user may have manually tweaked
110                    # the working directory. Give her a chance of
111                    # fixing the situation, or abort with Ctrl-C, or
112                    # whatever the subclasses decide.
113                    try:
114                        self._handleConflict(c, conflicts, res)
115                    except KeyboardInterrupt:
116                        self.log_info("INTERRUPTED BY THE USER!")
117                        break
118
119                # Give the opportunity to subclasses to skip the commit on
120                # the target backend.
121                if self._didApplyChangeset(c, replayable):
122                    if replay:
123                        replay(c)
124
125                # Remove it from the queue and remember it for the finally
126                # clause
127                last = self.pending.pop(0)
128
129                # Another hook (last==c here)
130                if applied:
131                    applied(last)
132        finally:
133            # For whatever reason we exit the loop, save the last state
134            self.state_file.write(last.revision, self.pending)
135
136        return last, conflicts
137
138    def _willApplyChangeset(self, changeset, applyable=None):
139        """
140        This gets called just before applying each changeset.  The whole
141        process will be stopped if this returns False.
142
143        Subclasses may use this to stop the process on some conditions,
144        or to do whatever before application.
145        """
146
147        if applyable:
148            return applyable(changeset)
149        else:
150            return True
151
152    def _didApplyChangeset(self, changeset, replayable=None):
153        """
154        This gets called right after changeset application.  The final
155        commit on the target system won't be carried out if this
156        returns False.
157
158        Subclasses may use this to alter the changeset in any way, before
159        committing its changes to the target system.
160        """
161
162        if replayable:
163            return replayable(changeset)
164        else:
165            return True
166
167    def _handleConflict(self, changeset, conflicts, conflict):
168        """
169        Handle the conflict raised by the application of the upstream changeset.
170
171        This implementation just append a (changeset, conflict) to the
172        list of all conflicts, and present a prompt to the user that
173        may abort with Ctrl-C (that in turn generates a KeyboardInterrupt).
174        """
175
176        conflicts.append((changeset, conflict))
177        raw_input(CONFLICTS_PROMPT % (str(changeset), '\n * '.join(conflict)))
178
179    def getPendingChangesets(self, sincerev=None):
180        """
181        Load the pending changesets from the state file, or query the
182        upstream repository if there's none.
183        """
184
185        revision, self.pending = self.state_file.load()
186        if not self.pending:
187            self.pending = self._getUpstreamChangesets(revision or sincerev)
188        return self.pending
189
190    def _getUpstreamChangesets(self, sincerev):
191        """
192        Query the upstream repository about what happened on the
193        sources since last sync, returning a sequence of Changesets
194        instances.
195
196        This method must be overridden by subclasses.
197        """
198
199        raise "%s should override this method" % self.__class__
200
201    def _applyChangeset(self, changeset):
202        """
203        Do the actual work of applying the changeset to the working copy.
204
205        Subclasses should reimplement this method performing the
206        necessary steps to *merge* given `changeset`, returning a list
207        with the conflicts, if any.
208        """
209
210        raise "%s should override this method" % self.__class__
211
212    def checkoutUpstreamRevision(self, revision):
213        """
214        Extract a working copy of the given revision from a repository.
215
216        Return the last applied changeset.
217        """
218
219        last = self._checkoutUpstreamRevision(revision)
220        self.state_file.write(last.revision, self.pending)
221        return last
222
223    def _checkoutUpstreamRevision(self, revision):
224        """
225        Concretely do the checkout of the upstream revision.
226        """
227
228        raise "%s should override this method" % self.__class__
Note: See TracBrowser for help on using the repository browser.