source: tailor/vcpx/source.py @ 1612

Revision 1612, 8.4 KB checked in by lele@…, 5 years ago (diff)

Show the actual changeset that triggers the error

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 vcpx import TailorBug, TailorException
16from vcpx.workdir import WorkingDir
17
18
19CONFLICTS_PROMPT = """
20The changeset
21
22%s
23
24caused conflicts on the following files:
25
26 * %s
27
28This is quite unusual, and most probably it means someone else has
29changed the working dir, beyond tailor control, or maybe a tailor bug
30is showing up.
31
32Either abort the session with Ctrl-C, or manually correct the situation
33with a Ctrl-Z, explore and correct, and coming back from the shell with
34'fg'.
35
36What would you like to do?
37"""
38
39
40class GetUpstreamChangesetsFailure(TailorException):
41    "Failure getting upstream changes"
42
43
44class ChangesetApplicationFailure(TailorException):
45    "Failure applying upstream changes"
46
47
48class InvocationError(TailorException):
49    "Bad invocation, use --help for details"
50
51
52class UpdatableSourceWorkingDir(WorkingDir):
53    """
54    This is an abstract working dir able to follow an upstream
55    source of ``changesets``.
56
57    It has three main functionalities:
58
59    getPendingChangesets
60        to query the upstream server about new changesets
61
62    applyPendingChangesets
63        to apply them to the working directory
64
65    checkoutUpstreamRevision
66        to extract a new copy of the sources, actually initializing
67        the mechanism.
68
69    Subclasses MUST override at least the _underscoredMethods.
70    """
71
72    def applyPendingChangesets(self, applyable=None, replayable=None,
73                               replay=None, applied=None):
74        """
75        Apply the collected upstream changes.
76
77        Loop over the collected changesets, doing whatever is needed
78        to apply each one to the working dir and if the changes do
79        not raise conflicts call the `replay` function to mirror the
80        changes on the target.
81
82        Return a tuple of two elements:
83
84        - the last applied changeset, if any
85        - the sequence (potentially empty!) of conflicts.
86        """
87
88        from time import sleep
89
90        c = None
91        last = None
92        conflicts = []
93
94        try:
95            i = 0
96            for c in self.state_file:
97                i += 1
98                self.log.info('Changeset #%d', i)
99                # Give the opportunity to subclasses to stop the application
100                # of the queue, before the application of the patch by the
101                # source backend.
102                if not self._willApplyChangeset(c, applyable):
103                    self.log.info('Stopping application, %r remains pending',
104                                  c.revision)
105                    break
106
107                # Sometime is better to wait a little while before each
108                # changeset, to avoid upstream server stress.
109                if self.repository.delay_before_apply:
110                    sleep(self.repository.delay_before_apply)
111
112                try:
113                    res = self._applyChangeset(c)
114                except TailorException, e:
115                    self.log.critical("Couldn't apply changeset:\n%s", c)
116                    raise
117                except KeyboardInterrupt:
118                    self.log.warning("INTERRUPTED BY THE USER!")
119                    raise
120
121                if res:
122                    # We have a conflict.  Give the user a chance of fixing
123                    # the situation, or abort with Ctrl-C, or whatever the
124                    # subclasses decide.
125                    try:
126                        self._handleConflict(c, conflicts, res)
127                    except KeyboardInterrupt:
128                        self.log.warning("INTERRUPTED BY THE USER!")
129                        break
130
131                # Give the opportunity to subclasses to skip the commit on
132                # the target backend.
133                if self._didApplyChangeset(c, replayable):
134                    if replay:
135                        try:
136                            replay(c)
137                        except Exception, e:
138                            self.log.critical("Couldn't replay changeset:\n%s", c)
139                            raise
140
141                # Remember it for the finally clause and notify the state
142                # file so that it gets removed from the queue
143                last = c
144                self.state_file.applied()
145
146                # Another hook (last==c here)
147                if applied:
148                    applied(last)
149        finally:
150            # For whatever reason we exit the loop, save the last state
151            self.state_file.finalize()
152
153        return last, conflicts
154
155    def _willApplyChangeset(self, changeset, applyable=None):
156        """
157        This gets called just before applying each changeset.  The whole
158        process will be stopped if this returns False.
159
160        Subclasses may use this to stop the process on some conditions,
161        or to do whatever before application.
162        """
163
164        if applyable:
165            return applyable(changeset)
166        else:
167            return True
168
169    def _didApplyChangeset(self, changeset, replayable=None):
170        """
171        This gets called right after changeset application.  The final
172        commit on the target system won't be carried out if this
173        returns False.
174
175        Subclasses may use this to alter the changeset in any way, before
176        committing its changes to the target system.
177        """
178
179        if replayable:
180            return replayable(changeset)
181        else:
182            return True
183
184    def _handleConflict(self, changeset, conflicts, conflict):
185        """
186        Handle the conflict raised by the application of the upstream changeset.
187
188        This implementation just append a (changeset, conflict) to the
189        list of all conflicts, and present a prompt to the user that
190        may abort with Ctrl-C (that in turn generates a KeyboardInterrupt).
191        """
192
193        conflicts.append((changeset, conflict))
194        raw_input(CONFLICTS_PROMPT % (str(changeset), '\n * '.join(conflict)))
195
196    def getPendingChangesets(self, sincerev=None):
197        """
198        Load the pending changesets from the state file, or query the
199        upstream repository if there's none. Return an iterator over
200        pending changesets.
201        """
202
203        if not self.state_file.pending():
204            last = self.state_file.lastAppliedChangeset()
205            if last:
206                revision = last.revision
207            else:
208                revision = sincerev
209            changesets = self._getUpstreamChangesets(revision)
210            self.state_file.setPendingChangesets(changesets)
211        return self.state_file
212
213    def _getUpstreamChangesets(self, sincerev):
214        """
215        Query the upstream repository about what happened on the
216        sources since last sync, returning a sequence of Changesets
217        instances.
218
219        This method must be overridden by subclasses.
220        """
221
222        raise TailorBug("%s should override this method!" % self.__class__)
223
224    def _applyChangeset(self, changeset):
225        """
226        Do the actual work of applying the changeset to the working copy.
227
228        Subclasses should reimplement this method performing the
229        necessary steps to *merge* given `changeset`, returning a list
230        with the conflicts, if any.
231        """
232
233        raise TailorBug("%s should override this method!" % self.__class__)
234
235    def checkoutUpstreamRevision(self, revision):
236        """
237        Extract a working copy of the given revision from a repository.
238
239        Return the last applied changeset.
240        """
241
242        last = self._checkoutUpstreamRevision(revision)
243        # Notify the state file about latest applied changeset
244        self.state_file.applied(last)
245        self.state_file.finalize()
246        return last
247
248    def _checkoutUpstreamRevision(self, revision):
249        """
250        Concretely do the checkout of the upstream revision.
251        """
252
253        raise TailorBug("%s should override this method!" % self.__class__)
254
255    def prepareSourceRepository(self):
256        """
257        Do whatever is needed to setup or connect to the source
258        repository.
259        """
260
261        self._prepareSourceRepository()
262
263    def _prepareSourceRepository(self):
264        """
265        Possibly connect to the source repository, when overriden
266        by subclasses.
267        """
Note: See TracBrowser for help on using the repository browser.