| 1 | # -*- mode: python; coding: utf-8 -*- |
|---|
| 2 | # :Progetto: vcpx -- Persistent information details |
|---|
| 3 | # :Creato: ven 19 ago 2005 22:53:18 CEST |
|---|
| 4 | # :Autore: Lele Gaifax <lele@nautilus.homeip.net> |
|---|
| 5 | # :Licenza: GNU General Public License |
|---|
| 6 | # |
|---|
| 7 | |
|---|
| 8 | """ |
|---|
| 9 | Tailor needs a generic way to remember which was the last migrated |
|---|
| 10 | revision between the two repositories. For some backends it could |
|---|
| 11 | derive such information directly from the working dir/repository, |
|---|
| 12 | others have no such capability. |
|---|
| 13 | |
|---|
| 14 | Moreover, since fetching and digesting the history to produce a |
|---|
| 15 | sequence of ChangeSets it's time and bandwidth consuming, the state |
|---|
| 16 | file helps keeping a cache of pending changes. |
|---|
| 17 | """ |
|---|
| 18 | |
|---|
| 19 | __docformat__ = 'reStructuredText' |
|---|
| 20 | |
|---|
| 21 | from cPickle import load, dump |
|---|
| 22 | from signal import signal, SIGINT, SIG_IGN |
|---|
| 23 | |
|---|
| 24 | class StateFile(object): |
|---|
| 25 | """ |
|---|
| 26 | State file that stores current revision and pending changesets. |
|---|
| 27 | |
|---|
| 28 | It behaves as an iterator, and source backends loop over not yet |
|---|
| 29 | applied changesets, calling .applied() after each one: that writes |
|---|
| 30 | the applied changeset in a *journal* file, much more atomic than |
|---|
| 31 | rewriting the whole archive each time. |
|---|
| 32 | |
|---|
| 33 | When the source backend finishes it's job, either because there |
|---|
| 34 | are no more pending changeset or stopped by an error, it calls |
|---|
| 35 | .finalize(), that in presence of a journal file adjust the |
|---|
| 36 | archive filtering out already applied changesets. |
|---|
| 37 | |
|---|
| 38 | Should an hard error prevent .finalize() call, it will happen |
|---|
| 39 | automatically next time the state file is loaded. |
|---|
| 40 | """ |
|---|
| 41 | |
|---|
| 42 | def __init__(self, fname, config): |
|---|
| 43 | """ |
|---|
| 44 | Initialize a new instance, logging to `tailor.statefile`. |
|---|
| 45 | """ |
|---|
| 46 | |
|---|
| 47 | from logging import getLogger |
|---|
| 48 | |
|---|
| 49 | self.filename = fname |
|---|
| 50 | self.archive = None |
|---|
| 51 | self.last_applied = None |
|---|
| 52 | self.current = None |
|---|
| 53 | self.log = getLogger('tailor.statefile') |
|---|
| 54 | |
|---|
| 55 | def _load(self): |
|---|
| 56 | """ |
|---|
| 57 | Open the pickle file and load the last applied changeset. |
|---|
| 58 | The second pickled object is ignored for backward compatibility. |
|---|
| 59 | """ |
|---|
| 60 | |
|---|
| 61 | # Take care of the journal file, if present. |
|---|
| 62 | self.finalize() |
|---|
| 63 | |
|---|
| 64 | self.current = None |
|---|
| 65 | try: |
|---|
| 66 | self.archive = open(self.filename) |
|---|
| 67 | self.last_applied = load(self.archive) |
|---|
| 68 | # compatibility dummity: there was the queuelen here |
|---|
| 69 | load(self.archive) |
|---|
| 70 | except IOError: |
|---|
| 71 | self.archive = None |
|---|
| 72 | self.last_applied = None |
|---|
| 73 | |
|---|
| 74 | def _write(self, changesets): |
|---|
| 75 | """ |
|---|
| 76 | Write the state file, that is dump last applied changeset, |
|---|
| 77 | a dummy None, then one changeset at a time. |
|---|
| 78 | """ |
|---|
| 79 | |
|---|
| 80 | count = 0 |
|---|
| 81 | previous = signal(SIGINT, SIG_IGN) |
|---|
| 82 | try: |
|---|
| 83 | sf = open(self.filename, 'w') |
|---|
| 84 | dump(self.last_applied, sf) |
|---|
| 85 | dump(None, sf) |
|---|
| 86 | for cs in changesets: |
|---|
| 87 | dump(cs, sf) |
|---|
| 88 | count += 1 |
|---|
| 89 | sf.close() |
|---|
| 90 | finally: |
|---|
| 91 | signal(SIGINT, previous) |
|---|
| 92 | self.log.info('Cached information about %d pending changesets', count) |
|---|
| 93 | |
|---|
| 94 | def __str__(self): |
|---|
| 95 | return self.filename |
|---|
| 96 | |
|---|
| 97 | def __iter__(self): |
|---|
| 98 | return self |
|---|
| 99 | |
|---|
| 100 | def next(self): |
|---|
| 101 | if not self.archive: |
|---|
| 102 | raise StopIteration |
|---|
| 103 | try: |
|---|
| 104 | self.current = load(self.archive) |
|---|
| 105 | except EOFError: |
|---|
| 106 | self.archive.close() |
|---|
| 107 | self.archive = None |
|---|
| 108 | raise StopIteration |
|---|
| 109 | return self.current |
|---|
| 110 | |
|---|
| 111 | def reversed(self): |
|---|
| 112 | """ |
|---|
| 113 | Iterate over the changesets, going backward. |
|---|
| 114 | """ |
|---|
| 115 | |
|---|
| 116 | if self.archive is None: |
|---|
| 117 | self._load() |
|---|
| 118 | |
|---|
| 119 | index = [] |
|---|
| 120 | while True: |
|---|
| 121 | pos = self.archive.tell() |
|---|
| 122 | try: |
|---|
| 123 | load(self.archive) |
|---|
| 124 | index.append(pos) |
|---|
| 125 | except EOFError: |
|---|
| 126 | break |
|---|
| 127 | |
|---|
| 128 | index.reverse() |
|---|
| 129 | |
|---|
| 130 | for pos in index: |
|---|
| 131 | self.archive.seek(pos) |
|---|
| 132 | yield load(self.archive) |
|---|
| 133 | |
|---|
| 134 | def pending(self): |
|---|
| 135 | """ |
|---|
| 136 | Verify if there's at least one changeset still pending. |
|---|
| 137 | """ |
|---|
| 138 | |
|---|
| 139 | if self.archive is None: |
|---|
| 140 | self._load() |
|---|
| 141 | if self.archive is None: |
|---|
| 142 | return False |
|---|
| 143 | |
|---|
| 144 | pos = self.archive.tell() |
|---|
| 145 | try: |
|---|
| 146 | next = load(self.archive) |
|---|
| 147 | except EOFError: |
|---|
| 148 | next = None |
|---|
| 149 | self.archive.seek(pos) |
|---|
| 150 | return next is not None |
|---|
| 151 | |
|---|
| 152 | def applied(self, current=None): |
|---|
| 153 | """ |
|---|
| 154 | Write the applied changeset to the journal file. |
|---|
| 155 | """ |
|---|
| 156 | |
|---|
| 157 | previous = signal(SIGINT, SIG_IGN) |
|---|
| 158 | try: |
|---|
| 159 | self.last_applied = current or self.current |
|---|
| 160 | journal = open(self.filename + '.journal', 'w') |
|---|
| 161 | dump(self.last_applied, journal) |
|---|
| 162 | journal.close() |
|---|
| 163 | finally: |
|---|
| 164 | signal(SIGINT, previous) |
|---|
| 165 | |
|---|
| 166 | def finalize(self): |
|---|
| 167 | """ |
|---|
| 168 | If there is a journal file, adjust the archive accordingly, |
|---|
| 169 | dropping already applied changesets. |
|---|
| 170 | """ |
|---|
| 171 | |
|---|
| 172 | from os.path import exists |
|---|
| 173 | from os import unlink, rename |
|---|
| 174 | |
|---|
| 175 | previous = signal(SIGINT, SIG_IGN) |
|---|
| 176 | try: |
|---|
| 177 | if self.archive is not None: |
|---|
| 178 | self.archive.close() |
|---|
| 179 | self.archive = None |
|---|
| 180 | |
|---|
| 181 | if exists(self.filename + '.journal'): |
|---|
| 182 | self.log.debug('Adjusting the state accordingly to journal') |
|---|
| 183 | # Load last applied changeset from the journal |
|---|
| 184 | journal = open(self.filename + '.journal') |
|---|
| 185 | last_applied = load(journal) |
|---|
| 186 | journal.close() |
|---|
| 187 | |
|---|
| 188 | # If there is an actual archive (ie, this is not |
|---|
| 189 | # bootstrap time) load the changesets from there, |
|---|
| 190 | # skipping the changesets until the last_applied one, |
|---|
| 191 | # then transfer the remaining to the new archive. |
|---|
| 192 | if exists(self.filename): |
|---|
| 193 | old = open(self.filename) |
|---|
| 194 | load(old) # last applied |
|---|
| 195 | load(old) # dummy queuelen |
|---|
| 196 | try: |
|---|
| 197 | cs = load(old) |
|---|
| 198 | # Skip already applied changesets |
|---|
| 199 | while cs <> last_applied: |
|---|
| 200 | cs = load(old) |
|---|
| 201 | except EOFError: |
|---|
| 202 | cs = None |
|---|
| 203 | sf = open(self.filename + '.new', 'w') |
|---|
| 204 | dump(last_applied, sf) |
|---|
| 205 | dump(None, sf) |
|---|
| 206 | if cs is not None: |
|---|
| 207 | count = 0 |
|---|
| 208 | while True: |
|---|
| 209 | try: |
|---|
| 210 | cs = load(old) |
|---|
| 211 | except EOFError: |
|---|
| 212 | break |
|---|
| 213 | dump(cs, sf) |
|---|
| 214 | count += 1 |
|---|
| 215 | self.log.info('%d pending changesets in state file', |
|---|
| 216 | count) |
|---|
| 217 | sf.close() |
|---|
| 218 | old.close() |
|---|
| 219 | |
|---|
| 220 | oldname = self.filename + '.old' |
|---|
| 221 | if exists(oldname): |
|---|
| 222 | unlink(oldname) |
|---|
| 223 | rename(self.filename, oldname) |
|---|
| 224 | rename(sf.name, self.filename) |
|---|
| 225 | else: |
|---|
| 226 | sf = open(self.filename, 'w') |
|---|
| 227 | dump(last_applied, sf) |
|---|
| 228 | dump(None, sf) |
|---|
| 229 | sf.close() |
|---|
| 230 | |
|---|
| 231 | unlink(journal.name) |
|---|
| 232 | finally: |
|---|
| 233 | signal(SIGINT, previous) |
|---|
| 234 | |
|---|
| 235 | def lastAppliedChangeset(self): |
|---|
| 236 | """ |
|---|
| 237 | Return the last applied changeset, if any, None otherwise. |
|---|
| 238 | """ |
|---|
| 239 | |
|---|
| 240 | if self.archive is None: |
|---|
| 241 | self._load() |
|---|
| 242 | return self.last_applied |
|---|
| 243 | |
|---|
| 244 | def setPendingChangesets(self, changesets): |
|---|
| 245 | """ |
|---|
| 246 | Write pending changesets to the state file. |
|---|
| 247 | """ |
|---|
| 248 | |
|---|
| 249 | if self.archive is not None: |
|---|
| 250 | self.archive.close() |
|---|
| 251 | self.archive = None |
|---|
| 252 | |
|---|
| 253 | self._write(changesets) |
|---|
| 254 | self._load() |
|---|