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
|
# Copyright (C) 2013,2015 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, see <http://www.gnu.org/licenses/>.
import cliapp
import collections
import morphlib
from morphlib.gitdir import PushFailureError as _PushFailureError
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.
When used in a with statement, if an exception is raised in the
body, then any ref changes are reverted, so deletes get replaced,
new branches get deleted and ref changes are changed back to the
value before the LocalRefManager was created.
By default, changes are kept after the with statement ends. This can
be overridden to revert after the manager exits by passing True to
the construcor.
with LocalRefManager(True) as lrm:
# Update refs with lrm.update, lrm.add or lrm.delete
# Use changed refs
# refs are back to their previous value
There is also an explicit .close() method to clean up after the
context has exited like so:
with LocalRefManager() as lrm:
# update refs
# Do something with altered refs
lrm.close() # Explicitly clean up
The name .close() was chosen for the cleanup method, so the
LocalRefManager object may also be used again in a second with
statement using contextlib.closing().
with LocalRefManager() as lrm:
# update refs
with contextlib.closing(lrm) as lrm:
# Do something with pushed refs and clean up if there is an
# exception
This is also useful if the LocalRefManager is nested in another
object, since the .close() method can be called in that object's
cleanup method.
'''
def __init__(self, cleanup_on_success=False):
self._cleanup_on_success = cleanup_on_success
self._cleanup = collections.deque()
def __enter__(self):
return self
def __exit__(self, etype, evalue, estack):
# No exception was raised, so no cleanup is required
if not self._cleanup_on_success and evalue is None:
return
self.close(evalue)
def close(self, primary=None):
exceptions = []
d = self._cleanup
while d:
op, args = d.pop()
try:
op(*args)
except Exception as e:
exceptions.append((op, args, e))
if exceptions:
raise RefCleanupError(primary, 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.
When used in a with statement, if an exception is raised in the body,
then any pushed refs are reverted, so deletes get replaced and new
branches get deleted.
By default it will also undo pushed refs when an exception is not
raised, this can be overridden by passing False to the constructor.
There is also an explicit .close() method to clean up after the
context has exited like so:
with RemoteRefManager(False) as rrm:
# push refs with rrm.push(...)
# Do something with pushed refs
rrm.close() # Explicitly clean up
The name .close() was chosen for the cleanup method, so the
RemoteRefManager object may also be used again in a second with
statement using contextlib.closing().
with RemoteRefManager(False) as rrm:
rrm.push(...)
with contextlib.closing(rrm) as rrm:
# Do something with pushed refs and clean up if there is an
# exception
This is also useful if the RemoteRefManager is nested in another
object, since the .close() method can be called in that object's
cleanup method.
'''
def __init__(self, cleanup_on_success=True):
self._cleanup_on_success = cleanup_on_success
self._cleanup = collections.deque()
def __enter__(self):
return self
def __exit__(self, etype, evalue, estack):
if not self._cleanup_on_success and evalue is None:
return
self.close(evalue)
def close(self, primary=None):
exceptions = []
d = self._cleanup
while d:
remote, refspecs = d.pop()
try:
remote.push(*refspecs)
except Exception as e:
exceptions.append((remote, refspecs, e))
if exceptions:
raise RefCleanupError(primary, 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.
If any of the refspecs failed to push, then they are all rolled back.
The result includes ones that had succeeded, but if you want to re-try
some refspecs, you need to re-try the ones that succeeded as well as
the ones that failed.
'''
# Calculate the refspecs required to undo the pushed changes.
delete_specs = tuple(rs.revert() for rs in refspecs)
try:
result = remote.push(*refspecs)
except _PushFailureError as e: # pragma: no cover
results = set(e.results)
e.results = results
# Skip deletes if we didn't fail because a ref update failed
if not results:
raise
undorefspecs = set()
for flag, sha1, target, summary, reason in results:
for rs in refspecs:
if rs.target == target and rs.source == sha1:
break
if flag != '!':
undorefspecs.add(rs.revert())
# We may have nothing to undo because all our pushes failed
if undorefspecs:
remote.push(*undorefspecs)
raise
# 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
|