summaryrefslogtreecommitdiff
path: root/morphlib/branchmanager.py
blob: 87a75ddce54ec1680218ec71b23fe03ff7213dd6 (plain)
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
# Copyright (C) 2013  Codethink Limited
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 2 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.


import cliapp
import collections

import morphlib


class RefCleanupError(cliapp.AppException):
    def __init__(self, primary_exception, exceptions):
        self.exceptions = exceptions
        self.ex_nr = ex_nr = len(exceptions)
        self.primary_exception = primary_exception
        cliapp.AppException.__init__(
           self, '%(ex_nr)d exceptions caught when cleaning up '\
                 'after exception: %(primary_exception)r: '\
                 '%(exceptions)r' % locals())


class LocalRefManager(object):
    '''Provide atomic update over a set of refs in a set of repositories.

    Any ref changes made with the update, add and delete methods will
    be reversed after the end of the with statement the LocalRefManager
    is used in, if an exception is raised in the aforesaid with statement.

    '''

    def __init__(self):
        self._cleanup = None

    def __enter__(self):
        self._cleanup = collections.deque()
        return self

    def __exit__(self, etype, evalue, estack):
        # No exception was raised, so no cleanup is required
        if (etype, evalue, estack) == (None, None, None):
            return
        exceptions = []
        d = self._cleanup
        while d:
            op, args = d.pop()
            try:
                op(*args)
            except Exception, e:
                exceptions.append((op, args, e))
        if exceptions:
            raise RefCleanupError(evalue, exceptions)

    def update(self, gd, ref, commit, old_commit, message=None):
        '''Update a git repository's ref, reverting it on failure.

        Use gd and the other parameters to update a ref to a new value,
        and if an execption is raised in the body of the with statement
        the LocalRefManager is used in, revert the update back to its
        old value.

        See morphlib.gitdir.update_ref for more information.

        '''

        gd.update_ref(ref, commit, old_commit, message)
        # Register a cleanup callback of setting the ref back to its old value
        self._cleanup.append((type(gd).update_ref,
                              (gd, ref, old_commit, commit,
                              message and 'Revert ' + message)))

    def add(self, gd, ref, commit, message=None):
        '''Add ref to a git repository, removing it on failure.

        Use gd and the other parameters to add a new ref to the repository,
        and if an execption is raised in the body of the with statement
        the LocalRefManager is used in, delete the ref.

        See morphlib.gitdir.add_ref for more information.

        '''

        gd.add_ref(ref, commit, message)
        # Register a cleanup callback of deleting the newly added ref.
        self._cleanup.append((type(gd).delete_ref, (gd, ref, commit,
                              message and 'Revert ' + message)))

    def delete(self, gd, ref, old_commit, message=None):
        '''Delete ref from a git repository, reinstating it on failure.

        Use gd and the other parameters to delete an existing ref from
        the repository, and if an execption is raised in the body of the
        with statement the LocalRefManager is used in, re-create the ref.

        See morphlib.gitdir.add_ref for more information.

        '''

        gd.delete_ref(ref, old_commit, message)
        # Register a cleanup callback of replacing the deleted ref.
        self._cleanup.append((type(gd).add_ref, (gd, ref, old_commit,
                              message and 'Revert ' + message)))


class RemoteRefManager(object):
    '''Provide temporary pushes to remote repositories.

    Any ref changes made with the push method will be reversed after
    the end of the with statement the RemoteRefManager is used in.

    '''

    def __init__(self):
        self._cleanup = None

    def __enter__(self):
        self._cleanup = collections.deque()
        return self

    def __exit__(self, etype, evalue, estack):
        exceptions = []
        d = self._cleanup
        while d:
            remote, refspecs = d.pop()
            try:
                remote.push(*refspecs)
            except Exception, e:
                exceptions.append((remote, refspecs, e))
        if exceptions:
            raise RefCleanupError(evalue, exceptions)

    def push(self, remote, *refspecs):
        '''Push refspecs to remote and revert on failure.

        Push the specified refspecs to the remote and reverse the change
        after the end of the block the with statement the RemoteRefManager
        is used in.

        '''

        # Calculate the refspecs required to undo the pushed changes.
        delete_specs = tuple(rs.revert() for rs in refspecs)
        result = remote.push(*refspecs)
        # Register cleanup after pushing, so that if this push fails,
        # we don't try to undo it.
        self._cleanup.append((remote, delete_specs))
        return result