Statistics
| Branch: | Tag: | Revision:

root / devflow / flow.py @ 2e9bddbf

History | View | Annotate | Download (16.6 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
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)
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
        debial_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
        #merge to master
311
        self._merge_branches(master, upstream_branch)
312
        self._merge_branches(debian_master, debian_branch)
313

    
314
        #create tags
315
        repo.git.checkout(master)
316
        repo.git.tag("%s" % tag)
317
        repo.git.checkout(debian)
318
        repo.git.tag("%s" % debian)
319

    
320
        #merge release changes to upstream
321
        self.merge_branches(upstream, upstream_branch, args, default=True)
322
        self.merge_branches(debian, debian_branch, args, default=True)
323

    
324
        repo.git.checkout(upstream)
325

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

    
329
    @cleanup
330
    def end_hotfix(self, args):
331
        version = args.version
332

    
333
        repo = self.repo
334
        upstream = "master"
335
        debian = "debian"
336
        upstream_branch = self.get_branch("hotfix", version)
337
        debian_branch = self.get_debian_branch("hotfix", version)
338

    
339
        #create tags?
340

    
341
        self._merge_branches(upstream, upstream_branch)
342
        self._merge_branches(debian, debian_branch)
343

    
344
        repo.git.checkout(upstream)
345

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

    
349
    @cleanup
350
    def start_feature(self, args):
351
        feature_name = args.feature_name
352
        repo = self.repo
353
        feature_upstream = "feature-%s" % feature_name
354
        feature_debian = "debian-%s" % feature_upstream
355
        repo.git.branch(feature_upstream, "develop")
356
        self.new_branches.append(feature_upstream)
357
        repo.git.branch(feature_debian, "debian-develop")
358
        self.new_branches.append(feature_debian)
359

    
360
    @cleanup
361
    def end_feature(self, args):
362
        feature_name = args.feature_name
363
        repo = self.repo
364
        feature_upstream = "feature-%s" % feature_name
365
        if not feature_upstream in repo.branches:
366
            raise ValueError("Branch %s does not exist." % feature_upstream)
367
        feature_debian = "debian-%s" % feature_upstream
368

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

    
372
        #merge to develop
373
        self._merge_branches("develop", feature_upstream)
374
        if feature_debian in repo.branches:
375
            self._merge_branches("debian-develop", feature_debian)
376
        repo.git.checkout("develop")
377

    
378
        branches = [feature_upstream]
379
        if feature_debian in repo.branches:
380
            branches.append(feature_debian)
381
        self.cleanup_branches(branches, args, default=True)
382

    
383

    
384
def refhead(repo):
385
    return repo.head.log[-1].newhexsha
386

    
387

    
388
def main():
389
    parser = ArgumentParser(description="Devflow tool")
390
    parser.add_argument('-V', '--version', action='version',
391
            version='devflow-flow %s' % __version__)
392
    parser.add_argument('-d', '--defaults', action='store_true', default=False,
393
            help="Assume default on every choice, unless a value is provided")
394

    
395
    subparsers = parser.add_subparsers()
396

    
397

    
398
    init_parser = subparsers.add_parser('init',
399
            help="Initialize a new devflow repo")
400
    init_parser.add_argument('-m', '--master', type=str, nargs='?',
401
            help="Master branch")
402
    init_parser.add_argument('-d', '--develop', type=str, nargs='?',
403
            help="Develop branch")
404
    init_parser.set_defaults(func='init_repo')
405

    
406

    
407
    feature_parser = subparsers.add_parser('feature', help="Feature options")
408
    feature_subparsers = feature_parser.add_subparsers()
409

    
410
    feature_start_parser = feature_subparsers.add_parser('start',
411
            help="Start a new feature")
412
    feature_start_parser.set_defaults(func='start_feature')
413
    feature_start_parser.add_argument('feature_name', type=str,
414
            help="Name of the feature")
415

    
416
    feature_finish_parser = feature_subparsers.add_parser('finish',
417
            help="Finish a feature")
418
    feature_finish_parser.set_defaults(func='end_feature')
419
    feature_finish_parser.add_argument('feature_name', type=str,
420
            help="Name of the feature")
421
    feature_finish_parser.add_argument('--no-edit-changelog',
422
            action='store_const', const=False, dest='edit_changelog',
423
            help="Do not edit the changelog")
424
    feature_finish_parser.add_argument('--no-cleanup', action='store_const',
425
            const=True, dest='cleanup', help="Do not cleanup branches")
426

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

    
430

    
431
    release_start_parser = release_subparsers.add_parser('start',
432
            help="Start a new release")
433
    release_start_parser.add_argument('--version', type=str,
434
            help="Version of the release")
435
    release_start_parser.add_argument('--develop-version', type=str,
436
            help="New develop version")
437
    release_start_parser.set_defaults(func='start_release')
438

    
439

    
440
    release_finish_parser = release_subparsers.add_parser('finish',
441
            help="Finish a release")
442
    release_finish_parser.add_argument('version', type=str,
443
            help="Version of the release")
444
    release_finish_parser.add_argument('--no-edit-changelog',
445
            action='store_const', const=False, dest='edit_changelog',
446
            help="Do not edit the changelog")
447
    release_finish_parser.add_argument('--no-cleanup', action='store_const',
448
            const=True, dest='cleanup', help="Do not cleanup branches")
449

    
450
    release_finish_parser.set_defaults(func='end_release')
451

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

    
455

    
456
    hotfix_start_parser = hotfix_subparsers.add_parser('start',
457
            help="Start a new hotfix")
458
    hotfix_start_parser.add_argument('--version', type=str,
459
            help="Version of the hotfix")
460
    hotfix_start_parser.add_argument('--develop-version', type=str,
461
            help="New develop version")
462
    hotfix_start_parser.set_defaults(func='start_hotfix')
463

    
464

    
465
    hotfix_finish_parser = hotfix_subparsers.add_parser('finish',
466
            help="Finish a hotfix")
467
    hotfix_finish_parser.add_argument('version', type=str,
468
            help="Version of the hotfix")
469
    hotfix_finish_parser.add_argument('--no-edit-changelog',
470
            action='store_const', const=False, dest='edit_changelog',
471
            help="Do not edit the changelog")
472
    hotfix_finish_parser.add_argument('--no-cleanup', action='store_const',
473
            const=True, dest='cleanup', help="Do not cleanup branches")
474
    hotfix_finish_parser.set_defaults(func='end_hotfix')
475

    
476

    
477

    
478
    args = parser.parse_args()
479

    
480
    gm = GitManager()
481
    getattr(gm, args.func)(args)
482

    
483

    
484
if __name__ == "__main__":
485
    main()