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