source: tailor/vcpx/source.py @ 614

Revision 614, 7.5 KB checked in by lele@…, 8 years ago (diff)

Make the state file an interator, and use a journal to avoid rewrite

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        try:
86            for c in self.state_file:
87                # Give the opportunity to subclasses to stop the application
88                # of the queue, before the application of the patch by the
89                # source backend.
90                if not self._willApplyChangeset(c, applyable):
91                    break
92
93                self.log_info("Applying changeset %s" % c.revision)
94
95                try:
96                    res = self._applyChangeset(c)
97                except:
98                    self.log_error("Couldn't apply changeset %s" % c.revision,
99                                   exc=True)
100                    raise
101
102                if res:
103                    # Uh, we have a conflict: this should not ever
104                    # happen, but the user may have manually tweaked
105                    # the working directory. Give her a chance of
106                    # fixing the situation, or abort with Ctrl-C, or
107                    # whatever the subclasses decide.
108                    try:
109                        self._handleConflict(c, conflicts, res)
110                    except KeyboardInterrupt:
111                        self.log_info("INTERRUPTED BY THE USER!")
112                        break
113
114                # Give the opportunity to subclasses to skip the commit on
115                # the target backend.
116                if self._didApplyChangeset(c, replayable):
117                    if replay:
118                        replay(c)
119
120                # Remember it for the finally clause and notify the state
121                # file so that it gets removed from the queue
122                last = c
123                self.state_file.applied()
124
125                # Another hook (last==c here)
126                if applied:
127                    applied(last)
128        finally:
129            # For whatever reason we exit the loop, save the last state
130            self.state_file.finalize()
131
132        return last, conflicts
133
134    def _willApplyChangeset(self, changeset, applyable=None):
135        """
136        This gets called just before applying each changeset.  The whole
137        process will be stopped if this returns False.
138
139        Subclasses may use this to stop the process on some conditions,
140        or to do whatever before application.
141        """
142
143        if applyable:
144            return applyable(changeset)
145        else:
146            return True
147
148    def _didApplyChangeset(self, changeset, replayable=None):
149        """
150        This gets called right after changeset application.  The final
151        commit on the target system won't be carried out if this
152        returns False.
153
154        Subclasses may use this to alter the changeset in any way, before
155        committing its changes to the target system.
156        """
157
158        if replayable:
159            return replayable(changeset)
160        else:
161            return True
162
163    def _handleConflict(self, changeset, conflicts, conflict):
164        """
165        Handle the conflict raised by the application of the upstream changeset.
166
167        This implementation just append a (changeset, conflict) to the
168        list of all conflicts, and present a prompt to the user that
169        may abort with Ctrl-C (that in turn generates a KeyboardInterrupt).
170        """
171
172        conflicts.append((changeset, conflict))
173        raw_input(CONFLICTS_PROMPT % (str(changeset), '\n * '.join(conflict)))
174
175    def getPendingChangesets(self, sincerev=None):
176        """
177        Load the pending changesets from the state file, or query the
178        upstream repository if there's none. Return an iterator over
179        pending changesets.
180        """
181
182        pending = len(self.state_file)
183        if pending == 0:
184            last = self.state_file.lastAppliedChangeset()
185            if last:
186                revision = last.revision
187            else:
188                revision = sincerev
189            changesets = self._getUpstreamChangesets(revision)
190            self.state_file.setPendingChangesets(changesets)
191        return self.state_file
192
193    def _getUpstreamChangesets(self, sincerev):
194        """
195        Query the upstream repository about what happened on the
196        sources since last sync, returning a sequence of Changesets
197        instances.
198
199        This method must be overridden by subclasses.
200        """
201
202        raise "%s should override this method" % self.__class__
203
204    def _applyChangeset(self, changeset):
205        """
206        Do the actual work of applying the changeset to the working copy.
207
208        Subclasses should reimplement this method performing the
209        necessary steps to *merge* given `changeset`, returning a list
210        with the conflicts, if any.
211        """
212
213        raise "%s should override this method" % self.__class__
214
215    def checkoutUpstreamRevision(self, revision):
216        """
217        Extract a working copy of the given revision from a repository.
218
219        Return the last applied changeset.
220        """
221
222        last = self._checkoutUpstreamRevision(revision)
223        self.state_file.applied(last)
224        # Some backend may have already fetched the remaining history
225        pending = getattr(self, 'pending', None)
226        if pending:
227            self.state_file.setPendingChangesets(pending)
228        return last
229
230    def _checkoutUpstreamRevision(self, revision):
231        """
232        Concretely do the checkout of the upstream revision.
233        """
234
235        raise "%s should override this method" % self.__class__
Note: See TracBrowser for help on using the repository browser.