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