Changes to instance naming:
[mdk.git] / mdk / commands / backport.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 """
5 Moodle Development Kit
6
7 Copyright (c) 2013 Frédéric Massart - FMCorz.net
8
9 This program is free software: you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation, either version 3 of the License, or
12 (at your option) any later version.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
21
22 http://github.com/FMCorz/mdk
23 """
24
25 import logging
26 from .. import tools, css, jira
27 from ..command import Command
28 from ..tools import yesOrNo
29
30
31 class BackportCommand(Command):
32
33 _description = 'Backports a branch'
34
35 def __init__(self, *args, **kwargs):
36 super(BackportCommand, self).__init__(*args, **kwargs)
37 self._arguments = [
38 (
39 ['-b', '--branch'],
40 {
41 'help': 'the branch to backport if not the current one. If omitted, guessed from instance name.',
42 'metavar': 'branch'
43 }
44 ),
45 (
46 ['-f', '--force-push'],
47 {
48 'action': 'store_true',
49 'dest': 'forcepush',
50 'help': 'force the push'
51 }
52 ),
53 (
54 ['--backport-to'],
55 {
56 'action': 'store',
57 'choices': ['integration', 'review', 'stable'],
58 'default': 'stable',
59 'help': 'which instances to backport to',
60 'metavar': 'instances'
61 }
62 ),
63 (
64 ['-p', '--push'],
65 {
66 'action': 'store_true',
67 'help': 'push the branch after successful backport'
68 }
69 ),
70 (
71 ['--push-to'],
72 {
73 'dest': 'pushremote',
74 'help': 'the remote to push the branch to. Default is %s.' % self.C.get('myRemote'),
75 'metavar': 'remote'
76 }
77 ),
78 (
79 ['--patch'],
80 {
81 'action': 'store_true',
82 'dest': 'patch',
83 'help': 'instead of pushing to a remote, this will upload a patch file to the tracker. Security issues use this by default if --push is set. This option discards most other flags.',
84 }
85 ),
86 (
87 ['-t', '--update-tracker'],
88 {
89 'const': True,
90 'dest': 'updatetracker',
91 'help': 'to use with --push, also add the diff information to the tracker issue',
92 'metavar': 'gitref',
93 'nargs': '?'
94 }
95 ),
96 (
97 ['-v', '--versions'],
98 {
99 'choices': [str(x) for x in range(13, int(self.C.get('masterBranch')))] + ['master'],
100 'help': 'versions to backport to',
101 'metavar': 'version',
102 'nargs': '+',
103 'required': True
104 }
105 ),
106 (
107 ['name'],
108 {
109 'default': None,
110 'help': 'name of the instance to backport from. Can be omitted if branch is specified.',
111 'metavar': 'name',
112 'nargs': '?'
113 }
114 )
115 ]
116
117 def run(self, args):
118 M = None
119 branch = args.branch
120 versions = args.versions
121 backportto = args.backport_to
122
123 # If we don't have a branch, we need an instance
124 M = self.Wp.resolve(args.name)
125 if not M and not branch:
126 raise Exception('This is not a Moodle instance')
127
128 # Getting issue number
129 if M and not branch:
130 branch = M.currentBranch()
131
132 # Parsing the branch
133 parsedbranch = tools.parseBranch(branch)
134 if not parsedbranch:
135 raise Exception('Could not extract issue number from %s' % branch)
136 issue = parsedbranch['issue']
137 suffix = parsedbranch['suffix']
138 version = parsedbranch['version']
139
140 if args.push and not args.patch:
141 mdlIssue = 'MDL-%s' % (issue)
142 J = jira.Jira()
143 args.patch = J.isSecurityIssue(mdlIssue)
144 if args.patch:
145 args.push = False
146 logging.info('%s appears to be a security issue, switching to patch mode...' % (mdlIssue))
147
148 # Original track
149 originaltrack = tools.stableBranch(version)
150
151 # Integration?
152 if M:
153 if M.isIntegration():
154 backportto = 'integration'
155
156 if M.isStable():
157 backportto = 'stable'
158
159 if M.isReview():
160 backportto = 'review'
161
162 def stashPop(stash):
163 """Small helper to pop the stash has we have to do it in some different places"""
164 if not stash[1].startswith('No local changes'):
165 pop = M2.git().stash(command='pop')
166 if pop[0] != 0:
167 logging.error('An error ocured while unstashing your changes')
168 else:
169 logging.info('Popped the stash')
170
171 # Begin backport
172 for v in versions:
173
174 # Gets the instance to cherry-pick to
175 name = self.Wp.generateInstanceName(v, purpose=backportto)
176 if not self.Wp.isMoodle(name):
177 logging.warning('Could not find instance %s for version %s' % (name, v))
178 continue
179 M2 = self.Wp.get(name)
180
181 logging.info("Preparing cherry-pick of %s/%s in %s" % (M.get('identifier'), branch, name))
182
183 # Get hash list
184 cherry = '%s/%s..%s' % (self.C.get('upstreamRemote'), originaltrack, branch)
185 hashes = M.git().hashes(cherry)
186 hashes.reverse()
187
188 # Stash
189 stash = M2.git().stash(untracked=False)
190 if stash[0] != 0:
191 logging.error('Error while trying to stash your changes. Skipping %s.' % M2.get('identifier'))
192 logging.debug(stash[2])
193 continue
194 elif not stash[1].startswith('No local changes'):
195 logging.info('Stashed your local changes')
196
197 # Fetch the remote to get reference to the branch to backport
198 logging.info("Fetching remote %s..." % (M.get('path')))
199 M2.git().fetch(M.get('path'), branch)
200
201 # Creates a new branch if necessary
202 newbranch = M2.generateBranchName(issue, suffix=suffix)
203 track = '%s/%s' % (self.C.get('upstreamRemote'), M2.get('stablebranch'))
204 if not M2.git().hasBranch(newbranch):
205 logging.info('Creating branch %s' % newbranch)
206 if not M2.git().createBranch(newbranch, track=track):
207 logging.error('Could not create branch %s tracking %s in %s' % (newbranch, track, name))
208 stashPop(stash)
209 continue
210 M2.git().checkout(newbranch)
211 else:
212 M2.git().checkout(newbranch)
213 logging.info('Hard reset %s to %s' % (newbranch, track))
214 M2.git().reset(to=track, hard=True)
215
216 # Picking the diff upstream/MOODLE_23_STABLE..github/MDL-12345-master
217 logging.info('Cherry-picking %s' % (cherry))
218 result = M2.git().pick(hashes)
219 if result[0] != 0:
220
221 # Try to resolve the conflicts if any.
222 resolveConflicts = True
223 conflictsResolved = False
224 while resolveConflicts:
225
226 # Check the list of possible conflicting files.
227 conflictingFiles = M2.git().conflictingFiles()
228 if conflictingFiles and len(conflictingFiles) == 1 and 'theme/bootstrapbase/style/moodle.css' in conflictingFiles:
229 logging.info('Conflicts found in bootstrapbase moodle CSS, trying to auto resolve...')
230 cssCompiler = css.Css(M2)
231 if cssCompiler.compile(theme='bootstrapbase', sheets=['moodle']):
232 M2.git().add('theme/bootstrapbase/style/moodle.css')
233 # We need to commit manually to prevent the editor to open.
234 M2.git().commit(filepath='.git/MERGE_MSG')
235 result = M2.git().pick(continu=True)
236 if result[0] == 0:
237 resolveConflicts = False
238 conflictsResolved = True
239 else:
240 resolveConflicts = False
241
242 # We still have a dirty repository.
243 if not conflictsResolved:
244 logging.error('Error while cherry-picking %s in %s.' % (cherry, name))
245 logging.debug(result[2])
246 if yesOrNo('The cherry-pick might still be in progress, would you like to abort it?'):
247 result = M2.git().pick(abort=True)
248 if result[0] > 0 and result[0] != 128:
249 logging.error('Could not abort the cherry-pick!')
250 else:
251 stashPop(stash)
252 logging.info('')
253 continue
254
255 # Pushing branch
256 if args.push:
257 pushremote = args.pushremote
258 if pushremote == None:
259 pushremote = self.C.get('myRemote')
260 logging.info('Pushing %s to %s' % (newbranch, pushremote))
261 result = M2.git().push(remote=pushremote, branch=newbranch, force=args.forcepush)
262 if result[0] != 0:
263 logging.warning('Error while pushing to remote %s' % (pushremote))
264 logging.debug(result[2])
265 stashPop(stash)
266 continue
267
268 # Update the tracker
269 if args.updatetracker != None:
270 ref = None if args.updatetracker == True else args.updatetracker
271 M2.updateTrackerGitInfo(branch=newbranch, ref=ref)
272
273 elif args.patch:
274 if not M2.pushPatch(newbranch):
275 continue
276
277 stashPop(stash)
278
279 logging.info('Instance %s successfully patched!' % name)
280 logging.info('')
281
282 logging.info('Done.')