File: transaction.py

package info (click to toggle)
stgit 0.19-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,748 kB
  • sloc: python: 10,558; sh: 5,739; lisp: 2,678; makefile: 142; perl: 42
file content (482 lines) | stat: -rw-r--r-- 17,554 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
# -*- coding: utf-8 -*-
"""The L{StackTransaction} class makes it possible to make complex
updates to an StGit stack in a safe and convenient way."""

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
from itertools import takewhile
import atexit

from stgit import exception, utils
from stgit.config import config
from stgit.lib import git, log
from stgit.out import out


class TransactionException(exception.StgException):
    """Exception raised when something goes wrong with a
    L{StackTransaction}."""

class TransactionHalted(TransactionException):
    """Exception raised when a L{StackTransaction} stops part-way through.
    Used to make a non-local jump from the transaction setup to the
    part of the transaction code where the transaction is run."""

def _print_current_patch(old_applied, new_applied):
    def now_at(pn):
        out.info('Now at patch "%s"' % pn)
    if not old_applied and not new_applied:
        pass
    elif not old_applied:
        now_at(new_applied[-1])
    elif not new_applied:
        out.info('No patch applied')
    elif old_applied[-1] == new_applied[-1]:
        pass
    else:
        now_at(new_applied[-1])

class _TransPatchMap(dict):
    """Maps patch names to Commit objects."""
    def __init__(self, stack):
        dict.__init__(self)
        self.__stack = stack
    def __getitem__(self, pn):
        try:
            return dict.__getitem__(self, pn)
        except KeyError:
            return self.__stack.patches.get(pn).commit

class StackTransaction(object):
    """A stack transaction, used for making complex updates to an StGit
    stack in one single operation that will either succeed or fail
    cleanly.

    The basic theory of operation is the following:

      1. Create a transaction object.

      2. Inside a::

         try
           ...
         except TransactionHalted:
           pass

      block, update the transaction with e.g. methods like
      L{pop_patches} and L{push_patch}. This may create new git
      objects such as commits, but will not write any refs; this means
      that in case of a fatal error we can just walk away, no clean-up
      required.

      (Some operations may need to touch your index and working tree,
      though. But they are cleaned up when needed.)

      3. After the C{try} block -- wheher or not the setup ran to
      completion or halted part-way through by raising a
      L{TransactionHalted} exception -- call the transaction's L{run}
      method. This will either succeed in writing the updated state to
      your refs and index+worktree, or fail without having done
      anything."""
    def __init__(self, stack, msg, discard_changes = False,
                 allow_conflicts = False, allow_bad_head = False,
                 check_clean_iw = None):
        """Create a new L{StackTransaction}.

        @param discard_changes: Discard any changes in index+worktree
        @type discard_changes: bool
        @param allow_conflicts: Whether to allow pre-existing conflicts
        @type allow_conflicts: bool or function of L{StackTransaction}"""
        self.__stack = stack
        self.__msg = msg
        self.__patches = _TransPatchMap(stack)
        self.__applied = list(self.__stack.patchorder.applied)
        self.__unapplied = list(self.__stack.patchorder.unapplied)
        self.__hidden = list(self.__stack.patchorder.hidden)
        self.__error = None
        self.__current_tree = self.__stack.head.data.tree
        self.__base = self.__stack.base
        self.__discard_changes = discard_changes
        self.__bad_head = None
        self.__conflicts = None
        if isinstance(allow_conflicts, bool):
            self.__allow_conflicts = lambda trans: allow_conflicts
        else:
            self.__allow_conflicts = allow_conflicts
        self.__temp_index = self.temp_index_tree = None
        if not allow_bad_head:
            self.__assert_head_top_equal()
        if check_clean_iw:
            self.__assert_index_worktree_clean(check_clean_iw)

    @property
    def stack(self):
        return self.__stack

    @property
    def patches(self):
        return self.__patches

    @property
    def applied(self):
        return self.__applied

    @applied.setter
    def applied(self, value):
        self.__applied = list(value)

    @property
    def unapplied(self):
        return self.__unapplied

    @unapplied.setter
    def unapplied(self, value):
        self.__unapplied = list(value)

    @property
    def hidden(self):
        return self.__hidden

    @hidden.setter
    def hidden(self, value):
        self.__hidden = list(value)

    @property
    def all_patches(self):
        return self.__applied + self.__unapplied + self.__hidden

    @property
    def base(self):
        return self.__base

    @base.setter
    def base(self, value):
        assert (not self.__applied
                or self.patches[self.applied[0]].data.parent == value)
        self.__base = value

    @property
    def temp_index(self):
        if not self.__temp_index:
            self.__temp_index = self.__stack.repository.temp_index()
            atexit.register(self.__temp_index.delete)
        return self.__temp_index

    @property
    def top(self):
        if self.__applied:
            return self.__patches[self.__applied[-1]]
        else:
            return self.__base

    @property
    def head(self):
        if self.__bad_head:
            return self.__bad_head
        else:
            return self.top

    @head.setter
    def head(self, value):
        self.__bad_head = value

    def __assert_head_top_equal(self):
        if not self.__stack.head_top_equal():
            out.error(
                'HEAD and top are not the same.',
                'This can happen if you modify a branch with git.',
                '"stg repair --help" explains more about what to do next.')
            self.__abort()
    def __assert_index_worktree_clean(self, iw):
        if not iw.worktree_clean():
            self.__halt('Worktree not clean. Use "refresh" or "reset --hard"')
        if not iw.index.is_clean(self.stack.head):
            self.__halt('Index not clean. Use "refresh" or "reset --hard"')
    def __checkout(self, tree, iw, allow_bad_head):
        if not allow_bad_head:
            self.__assert_head_top_equal()
        if self.__current_tree == tree and not self.__discard_changes:
            # No tree change, but we still want to make sure that
            # there are no unresolved conflicts. Conflicts
            # conceptually "belong" to the topmost patch, and just
            # carrying them along to another patch is confusing.
            if (self.__allow_conflicts(self) or iw is None
                or not iw.index.conflicts()):
                return
            out.error('Need to resolve conflicts first')
            self.__abort()
        assert iw is not None
        if self.__discard_changes:
            iw.checkout_hard(tree)
        else:
            iw.checkout(self.__current_tree, tree)
        self.__current_tree = tree
    @staticmethod
    def __abort():
        raise TransactionException(
            'Command aborted (all changes rolled back)')
    def __check_consistency(self):
        remaining = set(self.all_patches)
        for pn, commit in self.__patches.items():
            if commit is None:
                assert self.__stack.patches.exists(pn)
            else:
                assert pn in remaining
    def abort(self, iw = None):
        # The only state we need to restore is index+worktree.
        if iw:
            self.__checkout(self.__stack.head.data.tree, iw,
                            allow_bad_head = True)
    def run(self, iw = None, set_head = True, allow_bad_head = False,
            print_current_patch = True):
        """Execute the transaction. Will either succeed, or fail (with an
        exception) and do nothing."""
        self.__check_consistency()
        log.log_external_mods(self.__stack)
        new_head = self.head

        # Set branch head.
        if set_head:
            if iw:
                try:
                    self.__checkout(new_head.data.tree, iw, allow_bad_head)
                except git.CheckoutException:
                    # We have to abort the transaction.
                    self.abort(iw)
                    self.__abort()
            self.__stack.set_head(new_head, self.__msg)

        if self.__error:
            if self.__conflicts:
                out.error(*([self.__error] + self.__conflicts))
            else:
                out.error(self.__error)

        # Write patches.
        def write(msg):
            for pn, commit in self.__patches.items():
                if self.__stack.patches.exists(pn):
                    p = self.__stack.patches.get(pn)
                    if commit is None:
                        p.delete()
                    else:
                        p.set_commit(commit, msg)
                else:
                    self.__stack.patches.new(pn, commit, msg)
            self.__stack.patchorder.applied = self.__applied
            self.__stack.patchorder.unapplied = self.__unapplied
            self.__stack.patchorder.hidden = self.__hidden
            log.log_entry(self.__stack, msg)
        old_applied = self.__stack.patchorder.applied
        if not self.__conflicts:
            write(self.__msg)
        else:
            write(self.__msg + ' (CONFLICT)')
        if print_current_patch:
            _print_current_patch(old_applied, self.__applied)

        if self.__error:
            return utils.STGIT_CONFLICT
        else:
            return utils.STGIT_SUCCESS

    def __halt(self, msg):
        self.__error = msg
        raise TransactionHalted(msg)

    @staticmethod
    def __print_popped(popped):
        if len(popped) == 0:
            pass
        elif len(popped) == 1:
            out.info('Popped %s' % popped[0])
        else:
            out.info('Popped %s -- %s' % (popped[-1], popped[0]))

    def pop_patches(self, p):
        """Pop all patches pn for which p(pn) is true. Return the list of
        other patches that had to be popped to accomplish this. Always
        succeeds."""
        popped = []
        for i in range(len(self.applied)):
            if p(self.applied[i]):
                popped = self.applied[i:]
                del self.applied[i:]
                break
        popped1 = [pn for pn in popped if not p(pn)]
        popped2 = [pn for pn in popped if p(pn)]
        self.unapplied = popped1 + popped2 + self.unapplied
        self.__print_popped(popped)
        return popped1

    def delete_patches(self, p, quiet = False):
        """Delete all patches pn for which p(pn) is true. Return the list of
        other patches that had to be popped to accomplish this. Always
        succeeds."""
        popped = []
        all_patches = self.applied + self.unapplied + self.hidden
        for i in range(len(self.applied)):
            if p(self.applied[i]):
                popped = self.applied[i:]
                del self.applied[i:]
                break
        popped = [pn for pn in popped if not p(pn)]
        self.unapplied = popped + [pn for pn in self.unapplied if not p(pn)]
        self.hidden = [pn for pn in self.hidden if not p(pn)]
        self.__print_popped(popped)
        for pn in all_patches:
            if p(pn):
                s = ['', ' (empty)'][self.patches[pn].data.is_nochange()]
                self.patches[pn] = None
                if not quiet:
                    out.info('Deleted %s%s' % (pn, s))
        return popped

    def push_patch(self, pn, iw = None, allow_interactive = False,
                   already_merged = False):
        """Attempt to push the named patch. If this results in conflicts,
        halts the transaction. If index+worktree are given, spill any
        conflicts to them."""
        out.start('Pushing patch "%s"' % pn)
        orig_cd = self.patches[pn].data
        cd = orig_cd.set_committer(None)
        oldparent = cd.parent
        cd = cd.set_parent(self.top)
        if already_merged:
            # the resulting patch is empty
            tree = cd.parent.data.tree
        else:
            base = oldparent.data.tree
            ours = cd.parent.data.tree
            theirs = cd.tree
            tree, self.temp_index_tree = self.temp_index.merge(
                base, ours, theirs, self.temp_index_tree)
        s = ''
        merge_conflict = False
        if not tree:
            if iw is None:
                self.__halt('%s does not apply cleanly' % pn)
            try:
                self.__checkout(ours, iw, allow_bad_head = False)
            except git.CheckoutException:
                self.__halt('Index/worktree dirty')
            try:
                interactive = (allow_interactive and
                               config.getbool('stgit.autoimerge'))
                iw.merge(base, ours, theirs, interactive = interactive)
                tree = iw.index.write_tree()
                self.__current_tree = tree
                s = 'modified'
            except git.MergeConflictException as e:
                tree = ours
                merge_conflict = True
                self.__conflicts = e.conflicts
                s = 'conflict'
            except git.MergeException as e:
                self.__halt(str(e))
        cd = cd.set_tree(tree)
        if any(getattr(cd, a) != getattr(orig_cd, a) for a in
               ['parent', 'tree', 'author', 'message']):
            comm = self.__stack.repository.commit(cd)
            if merge_conflict:
                # When we produce a conflict, we'll run the update()
                # function defined below _after_ having done the
                # checkout in run(). To make sure that we check out
                # the real stack top (as it will look after update()
                # has been run), set it hard here.
                self.head = comm
        else:
            comm = None
            s = 'unmodified'
        if already_merged:
            s = 'merged'
        elif not merge_conflict and cd.is_nochange():
            s = 'empty'
        out.done(s)

        if merge_conflict:
            # We've just caused conflicts, so we must allow them in
            # the final checkout.
            self.__allow_conflicts = lambda trans: True

        # Update the stack state
        if comm:
            self.patches[pn] = comm
        if pn in self.hidden:
            x = self.hidden
        else:
            x = self.unapplied
        del x[x.index(pn)]
        self.applied.append(pn)

        if merge_conflict:
            self.__halt("%d merge conflict(s)" % len(self.__conflicts))

    def push_tree(self, pn):
        """Push the named patch without updating its tree."""
        orig_cd = self.patches[pn].data
        cd = orig_cd.set_committer(None).set_parent(self.top)

        s = ''
        if any(getattr(cd, a) != getattr(orig_cd, a) for a in
               ['parent', 'tree', 'author', 'message']):
            self.patches[pn] = self.__stack.repository.commit(cd)
        else:
            s = ' (unmodified)'
        if cd.is_nochange():
            s = ' (empty)'
        out.info('Pushed %s%s' % (pn, s))

        if pn in self.hidden:
            x = self.hidden
        else:
            x = self.unapplied
        del x[x.index(pn)]
        self.applied.append(pn)

    def reorder_patches(self, applied, unapplied, hidden = None, iw = None,
                        allow_interactive = False):
        """Push and pop patches to attain the given ordering."""
        if hidden is None:
            hidden = self.hidden
        common = len(list(takewhile(lambda a: a[0] == a[1],
                                    zip(self.applied, applied))))
        to_pop = set(self.applied[common:])
        self.pop_patches(lambda pn: pn in to_pop)
        for pn in applied[common:]:
            self.push_patch(pn, iw, allow_interactive = allow_interactive)

        # We only get here if all the pushes succeeded.
        assert self.applied == applied
        assert set(self.unapplied + self.hidden) == set(unapplied + hidden)
        self.unapplied = unapplied
        self.hidden = hidden

    def check_merged(self, patches, tree = None, quiet = False):
        """Return a subset of patches already merged."""
        if not quiet:
            out.start('Checking for patches merged upstream')
        merged = []
        if tree:
            self.temp_index.read_tree(tree)
            self.temp_index_tree = tree
        elif self.temp_index_tree != self.stack.head.data.tree:
            self.temp_index.read_tree(self.stack.head.data.tree)
            self.temp_index_tree = self.stack.head.data.tree
        for pn in reversed(patches):
            # check whether patch changes can be reversed in the current index
            cd = self.patches[pn].data
            if cd.is_nochange():
                continue
            try:
                self.temp_index.apply_treediff(cd.tree, cd.parent.data.tree,
                                               quiet = True)
                merged.append(pn)
                # The self.temp_index was modified by apply_treediff() so
                # force read_tree() the next time merge() is used.
                self.temp_index_tree = None
            except git.MergeException:
                pass
        if not quiet:
            out.done('%d found' % len(merged))
        return merged