e3b36b080462b790e35083226baf9b543f02f190
[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 ['-i', '--integration'],
55 {
56 'action': 'store_true',
57 'help': 'backport to integration instances'
58 }
59 ),
60 (
61 ['-p', '--push'],
62 {
63 'action': 'store_true',
64 'help': 'push the branch after successful backport'
65 }
66 ),
67 (
68 ['--push-to'],
69 {
70 'dest': 'pushremote',
71 'help': 'the remote to push the branch to. Default is %s.' % self.C.get('myRemote'),
72 'metavar': 'remote'
73 }
74 ),
75 (
76 ['--patch'],
77 {
78 'action': 'store_true',
79 'dest': 'patch',
80 '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.',
81 }
82 ),
83 (
84 ['-t', '--update-tracker'],
85 {
86 'const': True,
87 'dest': 'updatetracker',
88 'help': 'to use with --push, also add the diff information to the tracker issue',
89 'metavar': 'gitref',
90 'nargs': '?'
91 }
92 ),
93 (
94 ['-v', '--versions'],
95 {
96 'choices': [str(x) for x in range(13, int(self.C.get('masterBranch')))] + ['master'],
97 'help': 'versions to backport to',
98 'metavar': 'version',
99 'nargs': '+',
100 'required': True
101 }
102 ),
103 (
104 ['name'],
105 {
106 'default': None,
107 'help': 'name of the instance to backport from. Can be omitted if branch is specified.',
108 'metavar': 'name',
109 'nargs': '?'
110 }
111 )
112 ]
113
114 def run(self, args):
115 M = None
116 branch = args.branch
117 versions = args.versions
118 integration = args.integration
119
120 # If we don't have a branch, we need an instance
121 M = self.Wp.resolve(args.name)
122 if not M and not branch:
123 raise Exception('This is not a Moodle instance')
124
125 # Getting issue number
126 if M and not branch:
127 branch = M.currentBranch()
128
129 # Parsing the branch
130 parsedbranch = tools.parseBranch(branch)
131 if not parsedbranch:
132 raise Exception('Could not extract issue number from %s' % branch)
133 issue = parsedbranch['issue']
134 suffix = parsedbranch['suffix']
135 version = parsedbranch['version']
136
137 if args.push and not args.patch:
138 mdlIssue = 'MDL-%s' % (issue)
139 J = jira.Jira()
140 args.patch = J.isSecurityIssue(mdlIssue)
141 if args.patch:
142 args.push = False
143 logging.info('%s appears to be a security issue, switching to patch mode...' % (mdlIssue))
144
145 # Original track
146 originaltrack = tools.stableBranch(version)
147
148 # Integration?
149 if M:
150 integration = M.isIntegration()
151
152 def stashPop(stash):
153 """Small helper to pop the stash has we have to do it in some different places"""
154 if not stash[1].startswith('No local changes'):
155 pop = M2.git().stash(command='pop')
156 if pop[0] != 0:
157 logging.error('An error ocured while unstashing your changes')
158 else:
159 logging.info('Popped the stash')
160
161 # Begin backport
162 for v in versions:
163
164 # Gets the instance to cherry-pick to
165 name = self.Wp.generateInstanceName(v, integration=integration)
166 if not self.Wp.isMoodle(name):
167 logging.warning('Could not find instance %s for version %s' % (name, v))
168 continue
169 M2 = self.Wp.get(name)
170
171 logging.info("Preparing cherry-pick of %s/%s in %s" % (M.get('identifier'), branch, name))
172
173 # Get hash list
174 cherry = '%s/%s..%s' % (self.C.get('upstreamRemote'), originaltrack, branch)
175 hashes = M.git().hashes(cherry)
176 hashes.reverse()
177
178 # Stash
179 stash = M2.git().stash(untracked=False)
180 if stash[0] != 0:
181 logging.error('Error while trying to stash your changes. Skipping %s.' % M2.get('identifier'))
182 logging.debug(stash[2])
183 continue
184 elif not stash[1].startswith('No local changes'):
185 logging.info('Stashed your local changes')
186
187 # Fetch the remote to get reference to the branch to backport
188 logging.info("Fetching remote %s..." % (M.get('path')))
189 M2.git().fetch(M.get('path'), branch)
190
191 # Creates a new branch if necessary
192 newbranch = M2.generateBranchName(issue, suffix=suffix)
193 track = '%s/%s' % (self.C.get('upstreamRemote'), M2.get('stablebranch'))
194 if not M2.git().hasBranch(newbranch):
195 logging.info('Creating branch %s' % newbranch)
196 if not M2.git().createBranch(newbranch, track=track):
197 logging.error('Could not create branch %s tracking %s in %s' % (newbranch, track, name))
198 stashPop(stash)
199 continue
200 M2.git().checkout(newbranch)
201 else:
202 M2.git().checkout(newbranch)
203 logging.info('Hard reset %s to %s' % (newbranch, track))
204 M2.git().reset(to=track, hard=True)
205
206 # Picking the diff upstream/MOODLE_23_STABLE..github/MDL-12345-master
207 logging.info('Cherry-picking %s' % (cherry))
208 result = M2.git().pick(hashes)
209 if result[0] != 0:
210
211 # Try to resolve the conflicts if any.
212 resolveConflicts = True
213 conflictsResolved = False
214 while resolveConflicts:
215
216 # Check the list of possible conflicting files.
217 conflictingFiles = M2.git().conflictingFiles()
218 if conflictingFiles and len(conflictingFiles) == 1 and 'theme/bootstrapbase/style/moodle.css' in conflictingFiles:
219 logging.info('Conflicts found in bootstrapbase moodle CSS, trying to auto resolve...')
220 cssCompiler = css.Css(M2)
221 if cssCompiler.compile(theme='bootstrapbase', sheets=['moodle']):
222 M2.git().add('theme/bootstrapbase/style/moodle.css')
223 # We need to commit manually to prevent the editor to open.
224 M2.git().commit(filepath='.git/MERGE_MSG')
225 result = M2.git().pick(continu=True)
226 if result[0] == 0:
227 resolveConflicts = False
228 conflictsResolved = True
229 else:
230 resolveConflicts = False
231
232 # We still have a dirty repository.
233 if not conflictsResolved:
234 logging.error('Error while cherry-picking %s in %s.' % (cherry, name))
235 logging.debug(result[2])
236 if yesOrNo('The cherry-pick might still be in progress, would you like to abort it?'):
237 result = M2.git().pick(abort=True)
238 if result[0] > 0 and result[0] != 128:
239 logging.error('Could not abort the cherry-pick!')
240 else:
241 stashPop(stash)
242 logging.info('')
243 continue
244
245 # Pushing branch
246 if args.push:
247 pushremote = args.pushremote
248 if pushremote == None:
249 pushremote = self.C.get('myRemote')
250 logging.info('Pushing %s to %s' % (newbranch, pushremote))
251 result = M2.git().push(remote=pushremote, branch=newbranch, force=args.forcepush)
252 if result[0] != 0:
253 logging.warning('Error while pushing to remote %s' % (pushremote))
254 logging.debug(result[2])
255 stashPop(stash)
256 continue
257
258 # Update the tracker
259 if args.updatetracker != None:
260 ref = None if args.updatetracker == True else args.updatetracker
261 M2.updateTrackerGitInfo(branch=newbranch, ref=ref)
262
263 elif args.patch:
264 if not M2.pushPatch(newbranch):
265 continue
266
267 stashPop(stash)
268
269 logging.info('Instance %s successfully patched!' % name)
270 logging.info('')
271
272 logging.info('Done.')