Statistics
| Branch: | Tag: | Revision:

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()