root / devflow / flow.py @ 2c055772
History | View | Annotate | Download (16.9 kB)
1 |
import os |
---|---|
2 |
import re |
3 |
|
4 |
import logging |
5 |
logging.basicConfig() |
6 |
#from optparse import OptionParser
|
7 |
from argparse import ArgumentParser |
8 |
|
9 |
os.environ["GIT_PYTHON_TRACE"] = "full" |
10 |
from devflow import utils, versioning, RC_RE |
11 |
from devflow.version import __version__ |
12 |
from devflow.autopkg import call |
13 |
from devflow.ui import query_action, query_user, query_yes_no |
14 |
from functools import wraps, partial |
15 |
from contextlib import contextmanager |
16 |
from git.exc import GitCommandError |
17 |
from sh import mktemp |
18 |
|
19 |
|
20 |
def create_temp_file(suffix): |
21 |
create_dir_cmd = mktemp("/tmp/" + suffix + "-XXXXX") |
22 |
return create_dir_cmd.stdout.strip()
|
23 |
|
24 |
|
25 |
def cleanup(func): |
26 |
@wraps(func)
|
27 |
def wrapper(self, *args, **kwargs): |
28 |
try:
|
29 |
return func(self, *args, **kwargs) |
30 |
except:
|
31 |
self.log.debug("Unexpected ERROR. Cleaning up repository...") |
32 |
self.repo.git.reset("--hard", "HEAD") |
33 |
self.repo.git.checkout(self.start_branch) |
34 |
self.repo.git.reset("--hard", self.start_hex) |
35 |
for branch in self.new_branches: |
36 |
self.repo.git.branch("-D", branch) |
37 |
for tag in self.new_tags: |
38 |
self.repo.git.tag("-D", tag) |
39 |
raise
|
40 |
return wrapper
|
41 |
|
42 |
|
43 |
@contextmanager
|
44 |
def conflicts(): |
45 |
try:
|
46 |
yield
|
47 |
except GitCommandError as e: |
48 |
if e.status != 128: |
49 |
print "An error occured. Resolve it and type 'exit 0'" |
50 |
tmpbashrc=create_temp_file("bashrc")
|
51 |
f = open(tmpbashrc, 'w') |
52 |
f.write("source $HOME/.bashrc ; export PS1=(Conflict)\"$PS1\"")
|
53 |
f.close() |
54 |
call('bash --rcfile %s' % tmpbashrc)
|
55 |
os.unlink(tmpbashrc) |
56 |
else:
|
57 |
raise
|
58 |
|
59 |
def get_release_version(develop_version): |
60 |
version = develop_version.rstrip('next')
|
61 |
parts = version.split('.')
|
62 |
major_version = int(parts[0]) |
63 |
minor_version = int(parts[1]) |
64 |
#return str(major_version) + '.' + str(minor_version+1) + 'rc1'
|
65 |
return str(major_version) + '.' + str(minor_version+1) |
66 |
|
67 |
def get_develop_version_from_release(release_version): |
68 |
#version = re.sub('rc[0-9]+$', '', release_version)
|
69 |
version = release_version |
70 |
parts = version.split('.')
|
71 |
major_version = int(parts[0]) |
72 |
minor_version = int(parts[1]) |
73 |
return str(major_version) + '.' + str(minor_version+1) + 'next' |
74 |
|
75 |
def get_hotfix_version(version): |
76 |
parts = version.split('.')
|
77 |
major_version = int(parts[0]) |
78 |
minor_version = int(parts[1]) |
79 |
if (len(parts) > 2): |
80 |
hotfix_version = int(parts[2]) |
81 |
else:
|
82 |
hotfix_version = 0
|
83 |
|
84 |
return str(major_version) + '.' + str(minor_version) + '.'\ |
85 |
+ str(hotfix_version+1) |
86 |
|
87 |
class GitManager(object): |
88 |
def __init__(self): |
89 |
self.repo = utils.get_repository()
|
90 |
self.start_branch = self.repo.active_branch.name |
91 |
self.start_hex = self.repo.head.log()[-1].newhexsha |
92 |
self.log = logging.getLogger("") |
93 |
self.log.setLevel(logging.DEBUG)
|
94 |
self.log.info("Repository: %s. HEAD: %s", self.repo, self.start_hex) |
95 |
self.new_branches = []
|
96 |
self.new_tags = []
|
97 |
#self.repo.git.pull("origin")
|
98 |
|
99 |
def get_branch(self, mode, version): |
100 |
if mode not in ["release", "hotfix"]: |
101 |
raise ValueError("Unknown mode: %s" % mode) |
102 |
return "%s-%s" % (mode, version) |
103 |
|
104 |
def get_debian_branch(self, mode, version): |
105 |
if mode not in ["release", "hotfix"]: |
106 |
raise ValueError("Unknown mode: %s" % mode) |
107 |
return "debian-%s-%s" % (mode, version) |
108 |
|
109 |
def doit(self, action_yes=None, action_no=None, question="Do it", args=None, |
110 |
default=False):
|
111 |
if not args.defaults: |
112 |
ret = query_yes_no(question, default = "yes" if default else "no") |
113 |
else:
|
114 |
ret = default |
115 |
|
116 |
if ret and action_yes: |
117 |
action_yes() |
118 |
elif not ret and action_no: |
119 |
action_no() |
120 |
|
121 |
def __print_cleanup(self, branches): |
122 |
print "To remove obsolete branches run:" |
123 |
for b in branches: |
124 |
print "git branch -D %s" % b |
125 |
|
126 |
|
127 |
def __cleanup_branches(self, branches): |
128 |
repo = self.repo
|
129 |
for b in branches: |
130 |
repo.git.branch("-D", b)
|
131 |
|
132 |
def cleanup_branches(self, branches, args, default=False): |
133 |
if args.cleanup is not None: |
134 |
if args.cleanup:
|
135 |
self.__cleanup_branches(branches)
|
136 |
else:
|
137 |
self.__print_cleanup(branches)
|
138 |
return
|
139 |
|
140 |
question="Remove branches %s" % branches
|
141 |
action_yes = partial(self.__cleanup_branches, branches)
|
142 |
action_no = partial(self.__print_cleanup, branches)
|
143 |
self.doit(action_yes=action_yes, action_no=action_no,
|
144 |
question=question, args=args, default=default) |
145 |
|
146 |
|
147 |
def check_edit_changelog(self, edit_action, args, default=True): |
148 |
if args.edit_changelog is not None: |
149 |
if args.edit_changelog:
|
150 |
edit_action() |
151 |
return
|
152 |
question = "Edit changelog ?"
|
153 |
self.doit(action_yes=edit_action, question=question, args=args,
|
154 |
default=default) |
155 |
|
156 |
def _merge_branches(self, branch_to, branch_from): |
157 |
repo = self.repo
|
158 |
cur_branch = repo.active_branch.name |
159 |
repo.git.checkout(branch_to) |
160 |
with conflicts():
|
161 |
repo.git.merge("--no-ff", branch_from)
|
162 |
repo.git.checkout(cur_branch) |
163 |
|
164 |
def merge_branches(self, branch_to, branch_from, args, default=True): |
165 |
action = partial(self._merge_branches, branch_to, branch_from)
|
166 |
question = "Merge branch %s to %s ?" % (branch_from, branch_to)
|
167 |
self.doit(action_yes=action, question=question, args=args,
|
168 |
default=default) |
169 |
|
170 |
def edit_changelog(self, branch, base_branch=None): |
171 |
repo = self.repo
|
172 |
if not branch in repo.branches: |
173 |
raise ValueError("Branch %s does not exist." % branch) |
174 |
if base_branch and not base_branch in repo.branches: |
175 |
raise ValueError("Branch %s does not exist." % base_branch) |
176 |
|
177 |
repo.git.checkout(branch) |
178 |
topdir = repo.working_dir |
179 |
changelog = os.path.join(topdir, "Changelog")
|
180 |
|
181 |
lines = [] |
182 |
lines.append("#Changelog for %s\n" % branch)
|
183 |
if base_branch:
|
184 |
commits = repo.git.rev_list("%s..%s" % (base_branch, branch)).split("\n") |
185 |
for c in commits: |
186 |
commit = repo.commit(c) |
187 |
lines.append("* " + commit.message.split("\n")[0]) |
188 |
lines.append("\n")
|
189 |
|
190 |
f = open(changelog, 'rw+') |
191 |
lines.extend(f.readlines()) |
192 |
f.seek(0)
|
193 |
f.truncate(0)
|
194 |
f.writelines(lines) |
195 |
f.close() |
196 |
|
197 |
editor = os.getenv('EDITOR')
|
198 |
if not editor: |
199 |
editor = 'vim'
|
200 |
call("%s %s" % (editor, changelog))
|
201 |
repo.git.add(changelog) |
202 |
repo.git.commit(m="Update changelog")
|
203 |
print "Updated changelog on branch %s" % branch |
204 |
|
205 |
@cleanup
|
206 |
def start_release(self, args): |
207 |
repo = self.repo
|
208 |
upstream = "develop"
|
209 |
debian = "debian-develop"
|
210 |
repo.git.checkout(upstream) |
211 |
|
212 |
vcs = utils.get_vcs_info() |
213 |
develop_version = versioning.get_base_version(vcs) |
214 |
if not args.version: |
215 |
version = get_release_version(develop_version) |
216 |
if not args.defaults: |
217 |
version = query_user("Release version", default=version)
|
218 |
else:
|
219 |
#validate version?
|
220 |
pass
|
221 |
rc_version = "%src1" % version
|
222 |
new_develop_version = "%snext" % version
|
223 |
|
224 |
upstream_branch = self.get_branch("release", version) |
225 |
debian_branch = self.get_debian_branch("release", version) |
226 |
|
227 |
#create release branch
|
228 |
repo.git.branch(upstream_branch, upstream) |
229 |
self.new_branches.append(upstream_branch)
|
230 |
repo.git.checkout(upstream_branch) |
231 |
versioning.bump_version(rc_version) |
232 |
|
233 |
#create debian release branch
|
234 |
repo.git.checkout(debian) |
235 |
repo.git.branch(debian_branch, debian) |
236 |
self.new_branches.append(debian_branch)
|
237 |
|
238 |
repo.git.checkout(upstream_branch) |
239 |
repo.git.checkout(debian) |
240 |
|
241 |
#bump develop version
|
242 |
repo.git.checkout(upstream) |
243 |
versioning.bump_version(new_develop_version) |
244 |
|
245 |
repo.git.checkout(upstream_branch) |
246 |
|
247 |
|
248 |
@cleanup
|
249 |
def start_hotfix(self, args): |
250 |
repo = self.repo
|
251 |
upstream = "master"
|
252 |
debian = "debian"
|
253 |
repo.git.checkout(upstream) |
254 |
#maybe provide major.minor version, find the latest release/hotfix and
|
255 |
#branch from there ?
|
256 |
|
257 |
vcs = utils.get_vcs_info() |
258 |
version = versioning.get_base_version(vcs) |
259 |
if not args.version: |
260 |
version = get_hotfix_version(version) |
261 |
if not args.defaults: |
262 |
version = query_user("Hotfix version", default=version)
|
263 |
else:
|
264 |
#validate version?
|
265 |
pass
|
266 |
|
267 |
rc_version = "%src1" % version
|
268 |
new_develop_version = "%snext" % version
|
269 |
|
270 |
upstream_branch = self.get_branch("hotfix", version) |
271 |
debian_branch = self.get_debian_branch("hotfix", version) |
272 |
|
273 |
#create hotfix branch
|
274 |
repo.git.branch(upstream_branch, upstream) |
275 |
self.new_branches.append(upstream_branch)
|
276 |
repo.git.checkout(upstream_branch) |
277 |
versioning.bump_version(rc_version) |
278 |
|
279 |
#create debian hotfix branch
|
280 |
repo.git.checkout(debian) |
281 |
repo.git.branch(debian_branch, debian) |
282 |
self.new_branches.append(debian_branch)
|
283 |
|
284 |
repo.git.checkout(upstream_branch) |
285 |
repo.git.checkout(debian) |
286 |
|
287 |
#bump develop version. Ask first or verify we have the same
|
288 |
#major.minornext?
|
289 |
#repo.git.checkout(upstream)
|
290 |
#versioning.bump_version(new_develop_version)
|
291 |
|
292 |
repo.git.checkout(upstream_branch) |
293 |
|
294 |
@cleanup
|
295 |
def end_release(self, args): |
296 |
version = args.version |
297 |
repo = self.repo
|
298 |
master = "master"
|
299 |
debian_master = "debian"
|
300 |
upstream = "develop"
|
301 |
debian = "debian-develop"
|
302 |
upstream_branch = self.get_branch("release", version) |
303 |
debian_branch = self.get_debian_branch("release", version) |
304 |
tag = upstream_branch |
305 |
debian_tag = "debian/" + tag
|
306 |
|
307 |
edit_action = partial(self.edit_changelog, upstream_branch, "develop") |
308 |
self.check_edit_changelog(edit_action, args, default=True) |
309 |
|
310 |
vcs = utils.get_vcs_info() |
311 |
release_version = versioning.get_base_version(vcs) |
312 |
if re.match('.*'+RC_RE, release_version): |
313 |
new_version = re.sub(RC_RE, '', release_version)
|
314 |
versioning._bump_version(new_version, vcs) |
315 |
|
316 |
#merge to master
|
317 |
self._merge_branches(master, upstream_branch)
|
318 |
self._merge_branches(debian_master, debian_branch)
|
319 |
|
320 |
#create tags
|
321 |
repo.git.checkout(master) |
322 |
repo.git.tag("%s" % tag)
|
323 |
repo.git.checkout(debian) |
324 |
repo.git.tag("%s" % debian_tag)
|
325 |
|
326 |
#merge release changes to upstream
|
327 |
self.merge_branches(upstream, upstream_branch, args, default=True) |
328 |
self.merge_branches(debian, debian_branch, args, default=True) |
329 |
|
330 |
repo.git.checkout(upstream) |
331 |
|
332 |
branches = [upstream_branch, debian_branch] |
333 |
self.cleanup_branches(branches, args, default=True) |
334 |
|
335 |
@cleanup
|
336 |
def end_hotfix(self, args): |
337 |
version = args.version |
338 |
|
339 |
repo = self.repo
|
340 |
upstream = "master"
|
341 |
debian = "debian"
|
342 |
upstream_branch = self.get_branch("hotfix", version) |
343 |
debian_branch = self.get_debian_branch("hotfix", version) |
344 |
|
345 |
#create tags?
|
346 |
|
347 |
self._merge_branches(upstream, upstream_branch)
|
348 |
self._merge_branches(debian, debian_branch)
|
349 |
|
350 |
repo.git.checkout(upstream) |
351 |
|
352 |
branches = [upstream_branch, debian_branch] |
353 |
self.cleanup_branches(branches, args, default=True) |
354 |
|
355 |
@cleanup
|
356 |
def start_feature(self, args): |
357 |
feature_name = args.feature_name |
358 |
repo = self.repo
|
359 |
feature_upstream = "feature-%s" % feature_name
|
360 |
feature_debian = "debian-%s" % feature_upstream
|
361 |
repo.git.branch(feature_upstream, "develop")
|
362 |
self.new_branches.append(feature_upstream)
|
363 |
repo.git.branch(feature_debian, "debian-develop")
|
364 |
self.new_branches.append(feature_debian)
|
365 |
|
366 |
@cleanup
|
367 |
def end_feature(self, args): |
368 |
feature_name = args.feature_name |
369 |
repo = self.repo
|
370 |
feature_upstream = "feature-%s" % feature_name
|
371 |
if not feature_upstream in repo.branches: |
372 |
raise ValueError("Branch %s does not exist." % feature_upstream) |
373 |
feature_debian = "debian-%s" % feature_upstream
|
374 |
|
375 |
edit_action = partial(self.edit_changelog, feature_upstream, "develop") |
376 |
self.check_edit_changelog(edit_action, args, default=True) |
377 |
|
378 |
#merge to develop
|
379 |
self._merge_branches("develop", feature_upstream) |
380 |
if feature_debian in repo.branches: |
381 |
self._merge_branches("debian-develop", feature_debian) |
382 |
repo.git.checkout("develop")
|
383 |
|
384 |
branches = [feature_upstream] |
385 |
if feature_debian in repo.branches: |
386 |
branches.append(feature_debian) |
387 |
self.cleanup_branches(branches, args, default=True) |
388 |
|
389 |
|
390 |
def refhead(repo): |
391 |
return repo.head.log[-1].newhexsha |
392 |
|
393 |
|
394 |
def main(): |
395 |
parser = ArgumentParser(description="Devflow tool")
|
396 |
parser.add_argument('-V', '--version', action='version', |
397 |
version='devflow-flow %s' % __version__)
|
398 |
parser.add_argument('-d', '--defaults', action='store_true', default=False, |
399 |
help="Assume default on every choice, unless a value is provided")
|
400 |
|
401 |
subparsers = parser.add_subparsers() |
402 |
|
403 |
|
404 |
init_parser = subparsers.add_parser('init',
|
405 |
help="Initialize a new devflow repo")
|
406 |
init_parser.add_argument('-m', '--master', type=str, nargs='?', |
407 |
help="Master branch")
|
408 |
init_parser.add_argument('-d', '--develop', type=str, nargs='?', |
409 |
help="Develop branch")
|
410 |
init_parser.set_defaults(func='init_repo')
|
411 |
|
412 |
|
413 |
feature_parser = subparsers.add_parser('feature', help="Feature options") |
414 |
feature_subparsers = feature_parser.add_subparsers() |
415 |
|
416 |
feature_start_parser = feature_subparsers.add_parser('start',
|
417 |
help="Start a new feature")
|
418 |
feature_start_parser.set_defaults(func='start_feature')
|
419 |
feature_start_parser.add_argument('feature_name', type=str, |
420 |
help="Name of the feature")
|
421 |
|
422 |
feature_finish_parser = feature_subparsers.add_parser('finish',
|
423 |
help="Finish a feature")
|
424 |
feature_finish_parser.set_defaults(func='end_feature')
|
425 |
feature_finish_parser.add_argument('feature_name', type=str, |
426 |
help="Name of the feature")
|
427 |
feature_finish_parser.add_argument('--no-edit-changelog',
|
428 |
action='store_const', const=False, dest='edit_changelog', |
429 |
help="Do not edit the changelog")
|
430 |
feature_finish_parser.add_argument('--no-cleanup', action='store_const', |
431 |
const=True, dest='cleanup', help="Do not cleanup branches") |
432 |
|
433 |
release_parser = subparsers.add_parser('release', help="release options") |
434 |
release_subparsers = release_parser.add_subparsers() |
435 |
|
436 |
|
437 |
release_start_parser = release_subparsers.add_parser('start',
|
438 |
help="Start a new release")
|
439 |
release_start_parser.add_argument('--version', type=str, |
440 |
help="Version of the release")
|
441 |
release_start_parser.add_argument('--develop-version', type=str, |
442 |
help="New develop version")
|
443 |
release_start_parser.set_defaults(func='start_release')
|
444 |
|
445 |
|
446 |
release_finish_parser = release_subparsers.add_parser('finish',
|
447 |
help="Finish a release")
|
448 |
release_finish_parser.add_argument('version', type=str, |
449 |
help="Version of the release")
|
450 |
release_finish_parser.add_argument('--no-edit-changelog',
|
451 |
action='store_const', const=False, dest='edit_changelog', |
452 |
help="Do not edit the changelog")
|
453 |
release_finish_parser.add_argument('--no-cleanup', action='store_const', |
454 |
const=True, dest='cleanup', help="Do not cleanup branches") |
455 |
|
456 |
release_finish_parser.set_defaults(func='end_release')
|
457 |
|
458 |
hotfix_parser = subparsers.add_parser('hotfix', help="hotfix options") |
459 |
hotfix_subparsers = hotfix_parser.add_subparsers() |
460 |
|
461 |
|
462 |
hotfix_start_parser = hotfix_subparsers.add_parser('start',
|
463 |
help="Start a new hotfix")
|
464 |
hotfix_start_parser.add_argument('--version', type=str, |
465 |
help="Version of the hotfix")
|
466 |
hotfix_start_parser.add_argument('--develop-version', type=str, |
467 |
help="New develop version")
|
468 |
hotfix_start_parser.set_defaults(func='start_hotfix')
|
469 |
|
470 |
|
471 |
hotfix_finish_parser = hotfix_subparsers.add_parser('finish',
|
472 |
help="Finish a hotfix")
|
473 |
hotfix_finish_parser.add_argument('version', type=str, |
474 |
help="Version of the hotfix")
|
475 |
hotfix_finish_parser.add_argument('--no-edit-changelog',
|
476 |
action='store_const', const=False, dest='edit_changelog', |
477 |
help="Do not edit the changelog")
|
478 |
hotfix_finish_parser.add_argument('--no-cleanup', action='store_const', |
479 |
const=True, dest='cleanup', help="Do not cleanup branches") |
480 |
hotfix_finish_parser.set_defaults(func='end_hotfix')
|
481 |
|
482 |
|
483 |
|
484 |
args = parser.parse_args() |
485 |
|
486 |
gm = GitManager() |
487 |
getattr(gm, args.func)(args)
|
488 |
|
489 |
|
490 |
if __name__ == "__main__": |
491 |
main() |