flow.py 16.9 KB
Newer Older
1
import os
Filippos Giannakos's avatar
Filippos Giannakos committed
2
import re
3
import subprocess
4
5
6

import logging
logging.basicConfig()
Nikos Skalkotos's avatar
Nikos Skalkotos committed
7

Filippos Giannakos's avatar
Filippos Giannakos committed
8
from argparse import ArgumentParser
9
10

os.environ["GIT_PYTHON_TRACE"] = "full"
11
from devflow import utils, versioning, RC_RE
12
from devflow.version import __version__
Filippos Giannakos's avatar
Filippos Giannakos committed
13
from devflow.ui import query_action, query_user, query_yes_no
14
from functools import wraps, partial
15
16
from contextlib import contextmanager
from git.exc import GitCommandError
Filippos Giannakos's avatar
Filippos Giannakos committed
17
18
19
20
21
22
from sh import mktemp


def create_temp_file(suffix):
    create_dir_cmd = mktemp("/tmp/" + suffix + "-XXXXX")
    return create_dir_cmd.stdout.strip()
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


def cleanup(func):
    @wraps(func)
    def wrapper(self, *args, **kwargs):
        try:
            return func(self, *args, **kwargs)
        except:
            self.log.debug("Unexpected ERROR. Cleaning up repository...")
            self.repo.git.reset("--hard", "HEAD")
            self.repo.git.checkout(self.start_branch)
            self.repo.git.reset("--hard", self.start_hex)
            for branch in self.new_branches:
                self.repo.git.branch("-D", branch)
            for tag in self.new_tags:
                self.repo.git.tag("-D", tag)
            raise
    return wrapper


@contextmanager
def conflicts():
    try:
        yield
    except GitCommandError as e:
        if e.status != 128:
Filippos Giannakos's avatar
Filippos Giannakos committed
49
            print "An error occured. Resolve it and type 'exit 0'"
Nikos Skalkotos's avatar
Nikos Skalkotos committed
50
            tmpbashrc = create_temp_file("bashrc")
Filippos Giannakos's avatar
Filippos Giannakos committed
51
52
53
            f = open(tmpbashrc, 'w')
            f.write("source $HOME/.bashrc ; export PS1=(Conflict)\"$PS1\"")
            f.close()
54
            subprocess.check_call(['bash', '--rcfile', tmpbashrc])
Filippos Giannakos's avatar
Filippos Giannakos committed
55
            os.unlink(tmpbashrc)
56
57
58
        else:
            raise

Nikos Skalkotos's avatar
Nikos Skalkotos committed
59

Filippos Giannakos's avatar
Filippos Giannakos committed
60
61
62
63
64
def get_release_version(develop_version):
    version = develop_version.rstrip('next')
    parts = version.split('.')
    major_version = int(parts[0])
    minor_version = int(parts[1])
Nikos Skalkotos's avatar
Nikos Skalkotos committed
65
    # return str(major_version) + '.' + str(minor_version+1) + 'rc1'
Filippos Giannakos's avatar
Filippos Giannakos committed
66
67
    return str(major_version) + '.' + str(minor_version+1)

Nikos Skalkotos's avatar
Nikos Skalkotos committed
68

Filippos Giannakos's avatar
Filippos Giannakos committed
69
def get_develop_version_from_release(release_version):
Nikos Skalkotos's avatar
Nikos Skalkotos committed
70
    # version = re.sub('rc[0-9]+$', '', release_version)
Filippos Giannakos's avatar
Filippos Giannakos committed
71
72
73
74
75
76
    version = release_version
    parts = version.split('.')
    major_version = int(parts[0])
    minor_version = int(parts[1])
    return str(major_version) + '.' + str(minor_version+1) + 'next'

Nikos Skalkotos's avatar
Nikos Skalkotos committed
77

Filippos Giannakos's avatar
Filippos Giannakos committed
78
79
80
81
82
83
84
85
86
def get_hotfix_version(version):
    parts = version.split('.')
    major_version = int(parts[0])
    minor_version = int(parts[1])
    if (len(parts) > 2):
        hotfix_version = int(parts[2])
    else:
        hotfix_version = 0

Nikos Skalkotos's avatar
Nikos Skalkotos committed
87
88
89
    return str(major_version) + '.' + str(minor_version) + '.' \
        + str(hotfix_version+1)

90
91
92
93
94
95
96
97
98
99
100

class GitManager(object):
    def __init__(self):
        self.repo = utils.get_repository()
        self.start_branch = self.repo.active_branch.name
        self.start_hex = self.repo.head.log()[-1].newhexsha
        self.log = logging.getLogger("")
        self.log.setLevel(logging.DEBUG)
        self.log.info("Repository: %s. HEAD: %s", self.repo, self.start_hex)
        self.new_branches = []
        self.new_tags = []
Nikos Skalkotos's avatar
Nikos Skalkotos committed
101
        # self.repo.git.pull("origin")
102

103
104
105
106
107
108
109
110
111
112
    def get_branch(self, mode, version):
        if mode not in ["release", "hotfix"]:
            raise ValueError("Unknown mode: %s" % mode)
        return "%s-%s" % (mode, version)

    def get_debian_branch(self, mode, version):
        if mode not in ["release", "hotfix"]:
            raise ValueError("Unknown mode: %s" % mode)
        return "debian-%s-%s" % (mode, version)

Nikos Skalkotos's avatar
Nikos Skalkotos committed
113
114
    def doit(self, action_yes=None, action_no=None, question="Do it",
             args=None, default=False):
Filippos Giannakos's avatar
Filippos Giannakos committed
115
        if not args.defaults:
Nikos Skalkotos's avatar
Nikos Skalkotos committed
116
            ret = query_yes_no(question, default="yes" if default else "no")
Filippos Giannakos's avatar
Filippos Giannakos committed
117
118
        else:
            ret = default
119

Filippos Giannakos's avatar
Filippos Giannakos committed
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
        if ret and action_yes:
            action_yes()
        elif not ret and action_no:
            action_no()

    def __print_cleanup(self, branches):
        print "To remove obsolete branches run:"
        for b in branches:
            print "git branch -D %s" % b

    def __cleanup_branches(self, branches):
        repo = self.repo
        for b in branches:
            repo.git.branch("-D", b)

    def cleanup_branches(self, branches, args, default=False):
        if args.cleanup is not None:
            if args.cleanup:
                self.__cleanup_branches(branches)
            else:
                self.__print_cleanup(branches)
            return

Nikos Skalkotos's avatar
Nikos Skalkotos committed
143
        question = "Remove branches %s" % branches
Filippos Giannakos's avatar
Filippos Giannakos committed
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
        action_yes = partial(self.__cleanup_branches, branches)
        action_no = partial(self.__print_cleanup, branches)
        self.doit(action_yes=action_yes, action_no=action_no,
                  question=question, args=args, default=default)

    def check_edit_changelog(self, edit_action, args, default=True):
        if args.edit_changelog is not None:
            if args.edit_changelog:
                edit_action()
            return
        question = "Edit changelog ?"
        self.doit(action_yes=edit_action, question=question, args=args,
                  default=default)

    def _merge_branches(self, branch_to, branch_from):
        repo = self.repo
        cur_branch = repo.active_branch.name
        repo.git.checkout(branch_to)
        with conflicts():
            repo.git.merge("--no-ff", branch_from)
        repo.git.checkout(cur_branch)

    def merge_branches(self, branch_to, branch_from, args, default=True):
        action = partial(self._merge_branches, branch_to, branch_from)
        question = "Merge branch %s to %s ?" % (branch_from, branch_to)
        self.doit(action_yes=action, question=question, args=args,
                  default=default)

    def edit_changelog(self, branch, base_branch=None):
        repo = self.repo
Nikos Skalkotos's avatar
Nikos Skalkotos committed
174
        if branch not in repo.branches:
Filippos Giannakos's avatar
Filippos Giannakos committed
175
            raise ValueError("Branch %s does not exist." % branch)
Nikos Skalkotos's avatar
Nikos Skalkotos committed
176
        if base_branch and base_branch not in repo.branches:
Filippos Giannakos's avatar
Filippos Giannakos committed
177
178
179
180
181
182
183
184
185
            raise ValueError("Branch %s does not exist." % base_branch)

        repo.git.checkout(branch)
        topdir = repo.working_dir
        changelog = os.path.join(topdir, "Changelog")

        lines = []
        lines.append("#Changelog for %s\n" % branch)
        if base_branch:
Nikos Skalkotos's avatar
Nikos Skalkotos committed
186
187
            commits = repo.git.rev_list(
                "%s..%s" % (base_branch, branch)).split("\n")
Filippos Giannakos's avatar
Filippos Giannakos committed
188
189
            for c in commits:
                commit = repo.commit(c)
190
                lines.append("* " + commit.message.split("\n")[0])
Filippos Giannakos's avatar
Filippos Giannakos committed
191
192
193
194
195
196
197
198
199
        lines.append("\n")

        f = open(changelog, 'rw+')
        lines.extend(f.readlines())
        f.seek(0)
        f.truncate(0)
        f.writelines(lines)
        f.close()

200
        subprocess.check_call(['editor', changelog])
Filippos Giannakos's avatar
Filippos Giannakos committed
201
202
203
        repo.git.add(changelog)
        repo.git.commit(m="Update changelog")
        print "Updated changelog on branch %s" % branch
204
205

    @cleanup
Filippos Giannakos's avatar
Filippos Giannakos committed
206
    def start_release(self, args):
207
208
209
        repo = self.repo
        upstream = "develop"
        debian = "debian-develop"
Filippos Giannakos's avatar
Filippos Giannakos committed
210
211
212
213
214
215
216
217
218
        repo.git.checkout(upstream)

        vcs = utils.get_vcs_info()
        develop_version = versioning.get_base_version(vcs)
        if not args.version:
            version = get_release_version(develop_version)
            if not args.defaults:
                version = query_user("Release version", default=version)
        else:
Nikos Skalkotos's avatar
Nikos Skalkotos committed
219
            # validate version?
Filippos Giannakos's avatar
Filippos Giannakos committed
220
221
222
223
            pass
        rc_version = "%src1" % version
        new_develop_version = "%snext" % version

224
225
        upstream_branch = self.get_branch("release", version)
        debian_branch = self.get_debian_branch("release", version)
Filippos Giannakos's avatar
Filippos Giannakos committed
226

Nikos Skalkotos's avatar
Nikos Skalkotos committed
227
        # create release branch
Filippos Giannakos's avatar
Filippos Giannakos committed
228
229
230
231
232
        repo.git.branch(upstream_branch, upstream)
        self.new_branches.append(upstream_branch)
        repo.git.checkout(upstream_branch)
        versioning.bump_version(rc_version)

Nikos Skalkotos's avatar
Nikos Skalkotos committed
233
        # create debian release branch
234
        repo.git.checkout(debian)
Filippos Giannakos's avatar
Filippos Giannakos committed
235
236
        repo.git.branch(debian_branch, debian)
        self.new_branches.append(debian_branch)
237

Filippos Giannakos's avatar
Filippos Giannakos committed
238
239
        repo.git.checkout(upstream_branch)
        repo.git.checkout(debian)
240

Nikos Skalkotos's avatar
Nikos Skalkotos committed
241
        # bump develop version
242
        repo.git.checkout(upstream)
Filippos Giannakos's avatar
Filippos Giannakos committed
243
244
245
246
        versioning.bump_version(new_develop_version)

        repo.git.checkout(upstream_branch)

247
    @cleanup
Filippos Giannakos's avatar
Filippos Giannakos committed
248
    def start_hotfix(self, args):
249
250
251
252
        repo = self.repo
        upstream = "master"
        debian = "debian"
        repo.git.checkout(upstream)
Nikos Skalkotos's avatar
Nikos Skalkotos committed
253
254
        # maybe provide major.minor version, find the latest release/hotfix and
        # branch from there ?
Filippos Giannakos's avatar
Filippos Giannakos committed
255
256
257
258
259
260
261
262

        vcs = utils.get_vcs_info()
        version = versioning.get_base_version(vcs)
        if not args.version:
            version = get_hotfix_version(version)
            if not args.defaults:
                version = query_user("Hotfix version", default=version)
        else:
Nikos Skalkotos's avatar
Nikos Skalkotos committed
263
            # validate version?
Filippos Giannakos's avatar
Filippos Giannakos committed
264
            pass
265

Filippos Giannakos's avatar
Filippos Giannakos committed
266
267
        rc_version = "%src1" % version
        new_develop_version = "%snext" % version
268

Filippos Giannakos's avatar
Filippos Giannakos committed
269
270
271
        upstream_branch = self.get_branch("hotfix", version)
        debian_branch = self.get_debian_branch("hotfix", version)

Nikos Skalkotos's avatar
Nikos Skalkotos committed
272
        # create hotfix branch
273
274
275
        repo.git.branch(upstream_branch, upstream)
        self.new_branches.append(upstream_branch)
        repo.git.checkout(upstream_branch)
Filippos Giannakos's avatar
Filippos Giannakos committed
276
277
        versioning.bump_version(rc_version)

Nikos Skalkotos's avatar
Nikos Skalkotos committed
278
        # create debian hotfix branch
279
280
281
        repo.git.checkout(debian)
        repo.git.branch(debian_branch, debian)
        self.new_branches.append(debian_branch)
Filippos Giannakos's avatar
Filippos Giannakos committed
282

283
284
285
        repo.git.checkout(upstream_branch)
        repo.git.checkout(debian)

Nikos Skalkotos's avatar
Nikos Skalkotos committed
286
287
288
289
        # bump develop version. Ask first or verify we have the same
        # major.minornext?
        # repo.git.checkout(upstream)
        # versioning.bump_version(new_develop_version)
Filippos Giannakos's avatar
Filippos Giannakos committed
290
291
292
293
294
295
296
297
298
299
300
301
302
303

        repo.git.checkout(upstream_branch)

    @cleanup
    def end_release(self, args):
        version = args.version
        repo = self.repo
        master = "master"
        debian_master = "debian"
        upstream = "develop"
        debian = "debian-develop"
        upstream_branch = self.get_branch("release", version)
        debian_branch = self.get_debian_branch("release", version)
        tag = upstream_branch
304
        debian_tag = "debian/" + tag
Filippos Giannakos's avatar
Filippos Giannakos committed
305
306
307
308

        edit_action = partial(self.edit_changelog, upstream_branch, "develop")
        self.check_edit_changelog(edit_action, args, default=True)

309
310
311
312
313
314
        vcs = utils.get_vcs_info()
        release_version = versioning.get_base_version(vcs)
        if re.match('.*'+RC_RE, release_version):
            new_version = re.sub(RC_RE, '', release_version)
            versioning._bump_version(new_version, vcs)

Nikos Skalkotos's avatar
Nikos Skalkotos committed
315
        # merge to master
Filippos Giannakos's avatar
Filippos Giannakos committed
316
317
318
        self._merge_branches(master, upstream_branch)
        self._merge_branches(debian_master, debian_branch)

Nikos Skalkotos's avatar
Nikos Skalkotos committed
319
        # create tags
Filippos Giannakos's avatar
Filippos Giannakos committed
320
321
322
        repo.git.checkout(master)
        repo.git.tag("%s" % tag)
        repo.git.checkout(debian)
323
        repo.git.tag("%s" % debian_tag)
Filippos Giannakos's avatar
Filippos Giannakos committed
324

Nikos Skalkotos's avatar
Nikos Skalkotos committed
325
        # merge release changes to upstream
Filippos Giannakos's avatar
Filippos Giannakos committed
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
        self.merge_branches(upstream, upstream_branch, args, default=True)
        self.merge_branches(debian, debian_branch, args, default=True)

        repo.git.checkout(upstream)

        branches = [upstream_branch, debian_branch]
        self.cleanup_branches(branches, args, default=True)

    @cleanup
    def end_hotfix(self, args):
        version = args.version

        repo = self.repo
        upstream = "master"
        debian = "debian"
        upstream_branch = self.get_branch("hotfix", version)
        debian_branch = self.get_debian_branch("hotfix", version)

Nikos Skalkotos's avatar
Nikos Skalkotos committed
344
        # create tags?
Filippos Giannakos's avatar
Filippos Giannakos committed
345
346
347
348
349
350
351
352
353

        self._merge_branches(upstream, upstream_branch)
        self._merge_branches(debian, debian_branch)

        repo.git.checkout(upstream)

        branches = [upstream_branch, debian_branch]
        self.cleanup_branches(branches, args, default=True)

354
    @cleanup
Filippos Giannakos's avatar
Filippos Giannakos committed
355
356
    def start_feature(self, args):
        feature_name = args.feature_name
357
358
359
360
361
362
363
364
365
        repo = self.repo
        feature_upstream = "feature-%s" % feature_name
        feature_debian = "debian-%s" % feature_upstream
        repo.git.branch(feature_upstream, "develop")
        self.new_branches.append(feature_upstream)
        repo.git.branch(feature_debian, "debian-develop")
        self.new_branches.append(feature_debian)

    @cleanup
Filippos Giannakos's avatar
Filippos Giannakos committed
366
367
    def end_feature(self, args):
        feature_name = args.feature_name
368
369
        repo = self.repo
        feature_upstream = "feature-%s" % feature_name
Nikos Skalkotos's avatar
Nikos Skalkotos committed
370
        if feature_upstream not in repo.branches:
371
372
            raise ValueError("Branch %s does not exist." % feature_upstream)
        feature_debian = "debian-%s" % feature_upstream
Filippos Giannakos's avatar
Filippos Giannakos committed
373
374
375
376

        edit_action = partial(self.edit_changelog, feature_upstream, "develop")
        self.check_edit_changelog(edit_action, args, default=True)

Nikos Skalkotos's avatar
Nikos Skalkotos committed
377
        # merge to develop
Filippos Giannakos's avatar
Filippos Giannakos committed
378
        self._merge_branches("develop", feature_upstream)
379
        if feature_debian in repo.branches:
Filippos Giannakos's avatar
Filippos Giannakos committed
380
            self._merge_branches("debian-develop", feature_debian)
381
        repo.git.checkout("develop")
Filippos Giannakos's avatar
Filippos Giannakos committed
382
383

        branches = [feature_upstream]
384
        if feature_debian in repo.branches:
Filippos Giannakos's avatar
Filippos Giannakos committed
385
386
            branches.append(feature_debian)
        self.cleanup_branches(branches, args, default=True)
387

388

Filippos Giannakos's avatar
Filippos Giannakos committed
389
390
def refhead(repo):
    return repo.head.log[-1].newhexsha
391
392


Filippos Giannakos's avatar
Filippos Giannakos committed
393
394
395
def main():
    parser = ArgumentParser(description="Devflow tool")
    parser.add_argument('-V', '--version', action='version',
Nikos Skalkotos's avatar
Nikos Skalkotos committed
396
397
398
399
                        version='devflow-flow %s' % __version__)
    parser.add_argument(
        '-d', '--defaults', action='store_true', default=False,
        help="Assume default on every choice, unless a value is provided")
400

Filippos Giannakos's avatar
Filippos Giannakos committed
401
    subparsers = parser.add_subparsers()
402

Filippos Giannakos's avatar
Filippos Giannakos committed
403
    init_parser = subparsers.add_parser('init',
Nikos Skalkotos's avatar
Nikos Skalkotos committed
404
                                        help="Initialize a new devflow repo")
Filippos Giannakos's avatar
Filippos Giannakos committed
405
    init_parser.add_argument('-m', '--master', type=str, nargs='?',
Nikos Skalkotos's avatar
Nikos Skalkotos committed
406
                             help="Master branch")
Filippos Giannakos's avatar
Filippos Giannakos committed
407
    init_parser.add_argument('-d', '--develop', type=str, nargs='?',
Nikos Skalkotos's avatar
Nikos Skalkotos committed
408
                             help="Develop branch")
Filippos Giannakos's avatar
Filippos Giannakos committed
409
    init_parser.set_defaults(func='init_repo')
410

Filippos Giannakos's avatar
Filippos Giannakos committed
411
412
413
    feature_parser = subparsers.add_parser('feature', help="Feature options")
    feature_subparsers = feature_parser.add_subparsers()

Nikos Skalkotos's avatar
Nikos Skalkotos committed
414
415
    feature_start_parser = feature_subparsers.add_parser(
        'start', help="Start a new feature")
Filippos Giannakos's avatar
Filippos Giannakos committed
416
417
    feature_start_parser.set_defaults(func='start_feature')
    feature_start_parser.add_argument('feature_name', type=str,
Nikos Skalkotos's avatar
Nikos Skalkotos committed
418
                                      help="Name of the feature")
Filippos Giannakos's avatar
Filippos Giannakos committed
419

Nikos Skalkotos's avatar
Nikos Skalkotos committed
420
421
    feature_finish_parser = feature_subparsers.add_parser(
        'finish', help="Finish a feature")
Filippos Giannakos's avatar
Filippos Giannakos committed
422
423
    feature_finish_parser.set_defaults(func='end_feature')
    feature_finish_parser.add_argument('feature_name', type=str,
Nikos Skalkotos's avatar
Nikos Skalkotos committed
424
425
426
427
428
429
430
                                       help="Name of the feature")
    feature_finish_parser.add_argument(
        '--no-edit-changelog', action='store_const', const=False,
        dest='edit_changelog', help="Do not edit the changelog")
    feature_finish_parser.add_argument(
        '--no-cleanup', action='store_const', const=True, dest='cleanup',
        help="Do not cleanup branches")
Filippos Giannakos's avatar
Filippos Giannakos committed
431
432
433
434

    release_parser = subparsers.add_parser('release', help="release options")
    release_subparsers = release_parser.add_subparsers()

Nikos Skalkotos's avatar
Nikos Skalkotos committed
435
436
437
438
439
440
    release_start_parser = release_subparsers.add_parser(
        'start', help="Start a new release")
    release_start_parser.add_argument(
        '--version', type=str, help="Version of the release")
    release_start_parser.add_argument(
        '--develop-version', type=str, help="New develop version")
Filippos Giannakos's avatar
Filippos Giannakos committed
441
442
    release_start_parser.set_defaults(func='start_release')

Nikos Skalkotos's avatar
Nikos Skalkotos committed
443
444
445
446
447
448
449
450
451
452
    release_finish_parser = release_subparsers.add_parser(
        'finish', help="Finish a release")
    release_finish_parser.add_argument(
        'version', type=str, help="Version of the release")
    release_finish_parser.add_argument(
        '--no-edit-changelog', action='store_const', const=False,
        dest='edit_changelog', help="Do not edit the changelog")
    release_finish_parser.add_argument(
        '--no-cleanup', action='store_const', const=True, dest='cleanup',
        help="Do not cleanup branches")
Filippos Giannakos's avatar
Filippos Giannakos committed
453
454
455
456
457
458

    release_finish_parser.set_defaults(func='end_release')

    hotfix_parser = subparsers.add_parser('hotfix', help="hotfix options")
    hotfix_subparsers = hotfix_parser.add_subparsers()

Nikos Skalkotos's avatar
Nikos Skalkotos committed
459
460
461
462
463
464
    hotfix_start_parser = hotfix_subparsers.add_parser(
        'start', help="Start a new hotfix")
    hotfix_start_parser.add_argument(
        '--version', type=str, help="Version of the hotfix")
    hotfix_start_parser.add_argument(
        '--develop-version', type=str, help="New develop version")
Filippos Giannakos's avatar
Filippos Giannakos committed
465
466
    hotfix_start_parser.set_defaults(func='start_hotfix')

Nikos Skalkotos's avatar
Nikos Skalkotos committed
467
468
469
470
471
472
473
474
475
476
    hotfix_finish_parser = hotfix_subparsers.add_parser(
        'finish', help="Finish a hotfix")
    hotfix_finish_parser.add_argument(
        'version', type=str, help="Version of the hotfix")
    hotfix_finish_parser.add_argument(
        '--no-edit-changelog', action='store_const', const=False,
        dest='edit_changelog', help="Do not edit the changelog")
    hotfix_finish_parser.add_argument(
        '--no-cleanup', action='store_const', const=True, dest='cleanup',
        help="Do not cleanup branches")
Filippos Giannakos's avatar
Filippos Giannakos committed
477
478
479
    hotfix_finish_parser.set_defaults(func='end_hotfix')

    args = parser.parse_args()
480
481

    gm = GitManager()
Filippos Giannakos's avatar
Filippos Giannakos committed
482
483
    getattr(gm, args.func)(args)

484
485
486

if __name__ == "__main__":
    main()