forked from chassing/gitflow
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbranches.py
More file actions
486 lines (407 loc) · 18 KB
/
Copy pathbranches.py
File metadata and controls
486 lines (407 loc) · 18 KB
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
483
484
485
486
# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, print_function,
unicode_literals)
#
# This file is part of `gitflow`.
# Copyright (c) 2010-2011 Vincent Driessen
# Copyright (c) 2012-2013 Hartmut Goebel
# Copyright (c) 2015 Christian Assing
# Distributed under a BSD-like license. For full terms see the file LICENSE.txt
#
from past.builtins import basestring
from io import BytesIO
from git import GitCommandError, Reference
from gitflow.exceptions import (NoSuchBranchError, BranchExistsError,
PrefixNotUniqueError, BaseNotOnBranch,
WorkdirIsDirtyError, BranchTypeExistsError,
TagExistsError, MergeError)
__copyright__ = "2010-2011 Vincent Driessen; 2012-2013 Hartmut Goebel; 2015 Christian Assing"
__license__ = "BSD"
class BranchManager(object):
"""
Initializes an instance of :class:`BranchManager`. A branch
manager is responsible for listing, creating, merging, deleting,
finishing (i.e. merging+deleting) branches of a given type.
:param gitflow:
The :class:`gitflow.core.GitFlow` instance that this branch
manager belongs to.
:param prefix:
The prefix to use for the type of branches that this branch
manager manages. If this is not `None`, it supersedes the
configuration of the `gitflow` object's repository.
"""
def _get_prefix(self):
if self._prefix is not None:
return self._prefix
try:
return self.gitflow.get_prefix(self.identifier)
except:
return self.DEFAULT_PREFIX
def _set_prefix(self, value):
self._prefix = value
prefix = property(_get_prefix, _set_prefix)
def __init__(self, gitflow, prefix=None):
from gitflow.core import GitFlow
assert isinstance(gitflow, GitFlow), "Argument 'gitflow' must be a GitFlow instance."
self.gitflow = gitflow
if prefix is not None:
assert isinstance(prefix, basestring), "Argument 'prefix' must be a string."
self.prefix = prefix
else:
self.prefix = None
def default_base(self):
"""
:returns:
The name of branch to use as the default base for branching off from
in case no explicit base is specified.
This method can be overriden in a subclass of :class:`BranchManager`.
If not overriden, the default is to use the 'develop' branch.
"""
return self.gitflow.develop_name()
def full_name(self, name):
return self.prefix + name
def shorten(self, full_name):
"""
Returns the friendly (short) name of this branch, without the prefix,
given the fully qualified branch name.
:param full_name:
The full name of the branch as it is known to Git, including the
prefix.
:returns:
The friendly name of the branch.
"""
if full_name.startswith(self.prefix):
return full_name[len(self.prefix):]
else:
return full_name
def by_name_prefix(self, nameprefix):
"""
If exactly one branch of the type that this manager manages starts with
the given name prefix, returns that branch. Raises
:exc:`NoSuchBranchError` in case no branches exist with the given
prefix, or :exc:`PrefixNotUniqueError` in case multiple matches are
found.
:param nameprefix:
The name prefix (or full name) of the short branch name to match.
:returns:
The :class:`git.refs.Head` instance of the branch that can be
uniquely identified by the given name prefix.
"""
nameprefix = self.full_name(nameprefix)
matches = [b
for b in self.iter()
if b.name.startswith(nameprefix)]
num_matches = len(matches)
if num_matches == 1:
return matches[0]
elif num_matches < 1:
raise NoSuchBranchError(
'There is no %s branch matching the '
'prefix "%s"' % (self.identifier, nameprefix))
else:
raise PrefixNotUniqueError(
'There are multiple %s branches '
'matching the prefix "%s": %s' % (self.identifier, nameprefix, matches))
def iter(self):
"""
:returns:
An iterator, iterating over all branches of the type that this
manager manages.
"""
for branch in self.gitflow.repo.branches:
if branch.name.startswith(self.prefix):
yield branch
def list(self):
"""
:returns:
A list of all branches of the type that this manager manages. See
also :meth:`iter`.
"""
return list(self.iter())
def create(self, name, base=None, fetch=False,
must_be_on_default_base=False):
"""
Creates a branch of the type that this manager manages and checks it
out.
:param name:
The (short) name of the branch to create.
:param base:
The base commit or ref to base the branch off from. If a base is
not provided explicitly, the default base for this type of branch is
used. See also :meth:`default_base`.
:param fetch:
If set, update the local repo with remote changes prior to
creating the new branch.
:param must_be_on_default_base:
If set, the `base` must be a valid commit on the branch
manager `default_base`.
:returns:
The newly created :class:`git.refs.Head` reference.
"""
gitflow = self.gitflow
repo = gitflow.repo
full_name = self.prefix + name
if full_name in repo.branches:
raise BranchExistsError(full_name)
gitflow.require_no_merge_conflict()
if gitflow.has_staged_commits():
raise WorkdirIsDirtyError('Contains local changes checked into '
'the index but not committed.')
# update the local repo with remote changes, if asked
if fetch:
# :fixme: Should this be really `fetch`, not `update`?
# :fixme: `fetch` does not change any refs, so it is quite
# :fixme: useless. But `update` would advance `develop` and
# :fixme: moan about required merges.
# :fixme: OTOH, `update` would also give new remote refs,
# :fixme: e.g. a remote branch with the same name.
gitflow.origin().fetch(self.default_base())
# If the origin branch counterpart exists, assert that the
# local branch isn't behind it (to avoid unnecessary rebasing).
if gitflow.origin_name(self.default_base()) in repo.refs:
# :todo: rethink: check this only if base == default_base()?
gitflow.require_branches_equal(
gitflow.origin_name(self.default_base()),
self.default_base())
if base is None:
base = self.default_base()
elif must_be_on_default_base:
if not gitflow.is_merged_into(base, self.default_base()):
raise BaseNotOnBranch(base, self.default_base())
# If there is a remote branch with the same name, use it
remote_branch = None
if gitflow.origin_name(full_name) in repo.refs:
remote_branch = repo.refs[gitflow.origin_name(full_name)]
if fetch:
gitflow.origin().fetch(remote_branch.remote_head)
# Base must be on the remote branch, too, to avoid conflicts
if not gitflow.is_merged_into(base, remote_branch):
raise BaseNotOnBranch(base, remote_branch)
# base the new local branch on the remote on
base = remote_branch
branch = repo.create_head(full_name, base)
branch.checkout()
if remote_branch:
branch.set_tracking_branch(remote_branch)
return branch
def _is_single_commit_branch(self, from_, to):
git = self.gitflow.repo.git
commits = git.rev_list('%s...%s' % (from_, to), n=2).split()
return len(commits) == 1
def merge(self, name, into, message=None):
"""
This merges the branch named :attr:`name` into the branch named
:attr:`into`, using commit message :attr:`message`.
:param name:
The (short) name of the branch that needs merging. Alternatively
this is a instance of git.Reference (or subclass).
:param into:
The name of the branch to merge into.
:param message:
The commit message to use for the merge commit. If it is not given,
a default merge message is used. You can use the following string
placeholders, which :meth:`merge` will expand::
%(name)s = The full name of the branch (e.g. 'feature/foo')
%(short_name)s = The friendly name of the branch (e.g. 'foo')
%(identifier)s = The type (e.g. 'feature', 'hotfix', etc.)
You typically don't need to override this method in a subclass.
"""
if isinstance(name, basestring):
full_name = self.prefix + name
else:
assert isinstance(name, Reference)
full_name = name
repo = self.gitflow.repo
repo.branches[into].checkout()
if self.gitflow.is_merged_into(full_name, into):
# already merged, nothing more to do
return
kwargs = dict()
if not self._is_single_commit_branch(into, full_name):
kwargs['no_ff'] = True
if message is not None:
message = (message
% dict(name=full_name, identifier=self.identifier,
short_name=name))
kwargs['message'] = message
# `git merge` does not send the error message to stderr, thus
# we need to capture stdout manually :-(
stdout = BytesIO()
try:
repo.git.merge(full_name, output_stream=stdout, **kwargs)
except GitCommandError as e:
txt = stdout.getvalue().rstrip()
if e.stderr:
txt = txt + b'\n' + e.stderr
raise MergeError(txt)
def delete(self, name, force=False):
"""
This deletes a branch of the type that this manager manages named
:attr:`name`.
:param name:
The (short) name of the branch to delete.
:param force:
Delete the branch, even if this would lead to data loss.
"""
repo = self.gitflow.repo
full_name = self.prefix + name
repo.delete_head(full_name, force=force)
class FeatureBranchManager(BranchManager):
identifier = 'feature'
DEFAULT_PREFIX = 'feature/'
def create(self, name, base=None, fetch=False):
"""
Creates a branch of type `feature` and checks it out.
:param name:
The (short) name of the branch to create. This will be
prefixed by `feature/` or whatever is configured in
`gitflow.prefix.feature`.
:param base:
The base commit or ref to base the branch off from. If no
base is provided, it defaults to the branch configured in
`gitflow.branch.develop`. See also :meth:`default_base`.
:param fetch:
If set, update the local repo with remote changes prior to
creating the new branch.
:returns:
The newly created :class:`git.refs.Head` reference.
"""
# :todo:rethink: feature branch is not required to start at `develop`
return super(FeatureBranchManager, self).create(
name, base, fetch=fetch, must_be_on_default_base=False)
def finish(self, name, fetch=False, rebase=False, keep=False,
force_delete=False, push=False, tagging_info=None):
"""
Finishes the branch of type `feature` named :attr:`name`.
Finishing means that:
* The `feature`-branch is merged into the `develop`-branch.
* The `feature`-branch is deleted if all merges are successful.
:param name:
The (short) name of the branch to finish.
:param fetch:
If set, update the local repo with remote changes prior to
merging.
:param rebase:
Rebase `featuer`-branch prior to merging.
:param keep:
Keep `feature`-branch after performing finish.
:param force_delete:
Force deleting the `feature`-branch even if merging failed.
:param push:
Push changes to the remote repository.
"""
assert not tagging_info, "FeatureBranchManager does not support tagging"
gitflow = self.gitflow
full_name = self.full_name(name)
gitflow.must_be_uptodate(full_name, fetch=fetch)
gitflow.must_be_uptodate(gitflow.develop_name(), fetch=fetch)
if rebase:
gitflow.rebase(self.identifier, name, interactive=False)
to_push = [self.gitflow.develop_name()]
self.merge(name, self.gitflow.develop_name(),
'Finished %(identifier)s %(short_name)s.')
if not keep:
self.delete(name, force=force_delete)
to_push.append(':' + full_name)
if push:
gitflow.origin().push(to_push)
class ReleaseBranchManager(BranchManager):
identifier = 'release'
DEFAULT_PREFIX = 'release/'
def create(self, version, base=None, fetch=False):
"""
Creates a branch of type `release` and checks it out.
:param version:
The version to be released and for which to create the
release-branch for. This will be prefixed by `release/`
or whatever is configured in `gitflow.prefix.release`.
:param base:
The base commit or ref to base the branch off from. If no
base is provided, it defaults to the branch configured in
`gitflow.branch.develop`. See also :meth:`default_base`.
:param fetch:
If set, update the local repo with remote changes prior to
creating the new branch.
:returns:
The newly created :class:`git.refs.Head` reference.
"""
# there must be no active `release` branch
if len(self.list()) > 0:
raise BranchTypeExistsError(self.identifier)
# there must be no tag for this version yet
tagname = self.gitflow.get('gitflow.prefix.versiontag') + version
if tagname in self.gitflow.repo.tags:
raise TagExistsError(tagname)
return super(ReleaseBranchManager, self).create(
version, base, fetch=fetch, must_be_on_default_base=True)
def finish(self, name, fetch=False, rebase=False, keep=False,
force_delete=False, push=False, tagging_info=None):
if rebase:
raise Exception("Rebasing a release branch does not make any sense.")
# require release branch to exist
# if flag-fetch: fetch master und develop
# diese muessen dann gleich $ORIGIN/master bzw. $ORIGIN/develop sein
gitflow = self.gitflow
full_name = self.full_name(name)
gitflow.must_be_uptodate(full_name, fetch=fetch)
gitflow.must_be_uptodate(gitflow.develop_name(), fetch=fetch)
gitflow.must_be_uptodate(gitflow.master_name(), fetch=fetch)
to_push = [self.gitflow.develop_name(), self.gitflow.master_name()]
self.merge(
name, self.gitflow.master_name(),
'Finished %s %s.' % (self.identifier, name))
tag = None
if tagging_info is not None:
# try to tag the release
tagname = self.gitflow.get('gitflow.prefix.versiontag') + name
# In case a previous attempt to finish this release branch
# has failed, but the tag was set successful, we skip it
# now.
# :todo: check: if tag exists, it must point to the commit
tag = gitflow.tag(
tagname, self.gitflow.master_name(),
**tagging_info)
to_push.append(tagname)
# merge the master branch back into develop; this makes the
# master branch - and the new tag (if provided) - a parent of
# the development branch, which in turn lets you use 'git
# describe' on either branch
self.merge(tag or self.gitflow.master(),
self.gitflow.develop_name(),
'Finished %s %s.' % (self.identifier, name))
if not keep:
self.delete(name, force=force_delete)
to_push.append(':' + full_name)
if push:
gitflow.origin().push(to_push)
class HotfixBranchManager(ReleaseBranchManager):
identifier = 'hotfix'
DEFAULT_PREFIX = 'hotfix/'
def default_base(self):
return self.gitflow.master_name()
class SupportBranchManager(BranchManager):
identifier = 'support'
DEFAULT_PREFIX = 'support/'
def default_base(self):
return self.gitflow.master_name()
def create(self, name, base=None, fetch=False):
"""
Creates a branch of type `support` and checks it out.
:param name:
The (short) name of the branch to create. This will be
prefixed by `support/` or whatever is configured in
`gitflow.prefix.support`.
:param base:
The base commit or ref to base the branch off from. If no
base is provided, it defaults to the branch configured in
`gitflow.branch.develop`. See also :meth:`default_base`.
:param fetch:
If set, update the local repo with remote changes prior to
creating the new branch.
:returns:
The newly created :class:`git.refs.Head` reference.
"""
return super(SupportBranchManager, self).create(
name, base, fetch=fetch, must_be_on_default_base=True)
def finish(self, *args):
raise NotImplementedError("Finishing support branches does not make any sense.")