2 # -*- coding: utf-8 -*-
7 Copyright (c) 2013 Frédéric Massart - FMCorz.net
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.
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.
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/>.
22 http://github.com/FMCorz/mdk
26 from .. import tools
, css
, jira
27 from ..command
import Command
28 from ..tools
import yesOrNo
31 class BackportCommand(Command
):
33 _description
= 'Backports a branch'
35 def __init__(self
, *args
, **kwargs
):
36 super(BackportCommand
, self
).__init__(*args
, **kwargs
)
41 'help': 'the branch to backport if not the current one. If omitted, guessed from instance name.',
46 ['-f', '--force-push'],
48 'action': 'store_true',
50 'help': 'force the push'
57 'choices': ['integration', 'review', 'stable'],
59 'help': 'which instances to backport to',
60 'metavar': 'instances'
66 'action': 'store_true',
67 'help': 'push the branch after successful backport'
74 'help': 'the remote to push the branch to. Default is %s.' % self
.C
.get('myRemote'),
81 'action': 'store_true',
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.',
87 ['-t', '--update-tracker'],
90 'dest': 'updatetracker',
91 'help': 'to use with --push, also add the diff information to the tracker issue',
99 'choices': [str(x
) for x
in range(13, int(self
.C
.get('masterBranch')))] + ['master'],
100 'help': 'versions to backport to',
101 'metavar': 'version',
110 'help': 'name of the instance to backport from. Can be omitted if branch is specified.',
120 versions
= args
.versions
121 backportto
= args
.backport_to
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')
128 # Getting issue number
130 branch
= M
.currentBranch()
133 parsedbranch
= tools
.parseBranch(branch
)
135 raise Exception('Could not extract issue number from %s' % branch
)
136 issue
= parsedbranch
['issue']
137 suffix
= parsedbranch
['suffix']
138 version
= parsedbranch
['version']
140 if args
.push
and not args
.patch
:
141 mdlIssue
= 'MDL-%s' %
(issue
)
143 args
.patch
= J
.isSecurityIssue(mdlIssue
)
146 logging
.info('%s appears to be a security issue, switching to patch mode...' %
(mdlIssue
))
149 originaltrack
= tools
.stableBranch(version
)
153 if M
.isIntegration():
154 backportto
= 'integration'
157 backportto
= 'stable'
160 backportto
= 'review'
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')
167 logging
.error('An error ocured while unstashing your changes')
169 logging
.info('Popped the stash')
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
))
179 M2
= self
.Wp
.get(name
)
181 logging
.info("Preparing cherry-pick of %s/%s in %s" %
(M
.get('identifier'), branch
, name
))
184 cherry
= '%s/%s..%s' %
(self
.C
.get('upstreamRemote'), originaltrack
, branch
)
185 hashes
= M
.git().hashes(cherry
)
189 stash
= M2
.git().stash(untracked
=False)
191 logging
.error('Error while trying to stash your changes. Skipping %s.' % M2
.get('identifier'))
192 logging
.debug(stash
[2])
194 elif not stash
[1].startswith('No local changes'):
195 logging
.info('Stashed your local changes')
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
)
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
))
210 M2
.git().checkout(newbranch
)
212 M2
.git().checkout(newbranch
)
213 logging
.info('Hard reset %s to %s' %
(newbranch
, track
))
214 M2
.git().reset(to
=track
, hard
=True)
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
)
221 # Try to resolve the conflicts if any.
222 resolveConflicts
= True
223 conflictsResolved
= False
224 while resolveConflicts
:
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)
237 resolveConflicts
= False
238 conflictsResolved
= True
240 resolveConflicts
= False
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!')
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
)
263 logging
.warning('Error while pushing to remote %s' %
(pushremote
))
264 logging
.debug(result
[2])
269 if args
.updatetracker
!= None:
270 ref
= None if args
.updatetracker
== True else args
.updatetracker
271 M2
.updateTrackerGitInfo(branch
=newbranch
, ref
=ref
)
274 if not M2
.pushPatch(newbranch
):
279 logging
.info('Instance %s successfully patched!' % name
)
282 logging
.info('Done.')