import re
import os
import logging
-from datetime import datetime
-from lib import tools, jira
+from lib import tools, jira, fetch
from lib.command import Command
from lib.tools import question
['-i', '--integration'],
{
'action': 'store_true',
- 'help': 'checkout the stable branch before proceeding to the pull (Integration mode)'
+ 'help': 'checkout the stable branch before proceeding to the pull. Short for --mode integration.'
}
),
(
{
'action': 'store_true',
'dest': 'nomerge',
- 'help': 'checkout the remote branch without merging. Also this does not work with patch files. (No merge mode)'
+ 'help': 'checkout the remote branch without merging. Short for --mode checkout.'
}
),
(
{
'action': 'store_true',
'dest': 'fetchonly',
- 'help': 'only fetches the remote branch, you can then use FETCH_HEAD. Does not work with patch files. (Fetch mode)'
+ 'help': 'only fetches the remote branch, you can then use FETCH_HEAD. Short for --mode fetch.'
}
),
(
['-t', '--testing'],
{
'action': 'store_true',
- 'help': 'checkout a testing branch before proceeding to the pull (Testing mode)'
+ 'help': 'checkout a testing branch before proceeding to the pull. Short for --mode testing.'
+ }
+ ),
+ (
+ ['-m', '--mode'],
+ {
+ 'action': 'store',
+ 'choices': ['checkout', 'fetch', 'integration', 'pull', 'testing'],
+ 'default': 'pull',
+ 'help': 'define the mode to use'
+ }
+ ),
+ (
+ ['-p', '--prompt'],
+ {
+ 'action': 'store_true',
+ 'help': 'prompts the user to choose the patch to download.'
}
),
(
if not M:
raise Exception('This is not a Moodle instance')
- if (args.testing and args.integration) or (args.testing and args.nomerge) or (args.integration and args.nomerge):
- raise Exception('You cannot combine --integration, --testing or --no-merge')
+ # Get the mode.
+ mode = args.mode
+ if args.fetchonly:
+ mode = 'fetch'
+ elif args.nomerge:
+ mode = 'checkout'
+ elif args.testing:
+ mode = 'testing'
+ elif args.integration:
+ mode = 'integration'
+
+ # Prompt?
+ prompt = args.prompt
# Tracker issue number.
issuenb = args.issue
# Get information from Tracker
logging.info('Retrieving information about %s from Moodle Tracker' % (mdl))
- J = jira.Jira()
- issueInfo = J.getIssue(mdl)
-
- mode = 'pull' if not args.fetchonly else 'fetchonly'
- remoteUrl = issueInfo.get('named').get(self.C.get('tracker.fieldnames.repositoryurl'))
- remoteBranch = issueInfo.get('named').get(self.C.get('tracker.fieldnames.%s.branch' % (branch)))
- patchesToApply = []
-
- if (args.nomerge or args.fetchonly) and (not remoteUrl or not remoteBranch):
- # No merge and Fetch only require valid URL and branch
- mode = None
-
- elif (not args.nomerge and not args.fetchonly) and (not remoteUrl or not remoteBranch):
- # Attempting to find a patch
- mode = None
- attachments = issueInfo.get('fields').get('attachment')
- patches = {}
- for attachment in attachments:
- if attachment['filename'].endswith('.patch'):
- patches[attachment['filename']] = attachment
+ fetcher = fetch.FetchTracker(M)
- if len(patches) > 0:
- mapping = {}
- i = 1
- for key in sorted(patches.keys()):
- patch = patches[key]
- mapping[i] = patch
- date = jira.Jira.parseDate(patch['created'])
- print '{0:<2}: {1:<60} {2}'.format(i, patch['filename'][:60], datetime.strftime(date, '%Y-%m-%d %H:%M'))
- i += 1
+ try:
+ if not prompt:
+ fetcher.setFromTracker(mdl, branch)
+ except (fetch.FetchTrackerRepoException, fetch.FetchTrackerBranchException) as e:
+ prompt = True
- ids = question('What patches would you like to apply?')
- if ids:
- ids = re.split(r'\s*[, ]\s*', ids)
- for i in ids:
- i = int(i)
- if not i in mapping.keys():
- continue
- j = 0
- while True:
- mapping[i]['mdk-filename'] = mapping[i]['filename'] + (('.' + str(j)) if j > 0 else '')
- j += 1
- if not os.path.isfile(mapping[i]['mdk-filename']):
- break
- patchesToApply.append(mapping[i])
- mode = 'patch'
- else:
- mode = False
-
- if not mode:
- raise Exception('Did not find enough information to pull a patch.')
+ if prompt:
+ patches = self.pickPatches(mdl)
+ if not patches:
+ raise Exception('Could not find any relevant information for a successful pull')
+ fetcher.usePatches(patches)
- # Stash
- stash = None
- if mode != 'fetchonly':
- stash = M.git().stash(untracked=True)
- if stash[0] != 0:
- raise Exception('Error while trying to stash your changes. Exiting...')
- elif not stash[1].startswith('No local changes'):
- logging.info('Stashed your local changes')
-
- # Create a testing branch
- if args.testing:
+ if mode == 'pull':
+ fetcher.pull()
+ elif mode == 'checkout':
+ fetcher.checkout()
+ elif mode == 'fetch':
+ fetcher.fetch()
+ elif mode == 'integration':
+ fetcher.pull(into=M.get('stablebranch'))
+ elif mode == 'testing':
i = 0
while True:
i += 1
newBranch = M.generateBranchName(issue, suffix=suffix, version=branch)
if not M.git().hasBranch(newBranch):
break
- track = '%s/%s' % (self.C.get('upstreamRemote'), M.get('stablebranch'))
- if not M.git().createBranch(newBranch, track=track):
- raise Exception('Could not create branch %s tracking %s' % (newBranch, track))
- if not M.git().checkout(newBranch):
- raise Exception('Could not checkout branch %s' % (newBranch))
- logging.info('Checked out branch %s' % (newBranch))
+ fetcher.pull(into=newBranch)
- # Checkout the stable branch
- elif args.integration:
- if not M.git().checkout(M.get('stablebranch')):
- logging.error('Could not checkout branch %s' % (M.get('stablebranch')))
- logging.info('Checked out branch %s' % (M.get('stablebranch')))
+ def pickPatches(self, mdl):
+ """Prompts the user to pick a patch"""
- # Create a no-merge branch
- elif args.nomerge:
- i = 0
- while True:
- i += 1
- suffix = 'nomerge' if i <= 1 else 'nomerge' + str(i)
- newBranch = M.generateBranchName(issue, suffix=suffix, version=branch)
- if not M.git().hasBranch(newBranch):
- break
- track = '%s/%s' % (self.C.get('upstreamRemote'), M.get('stablebranch'))
- if not M.git().createBranch(newBranch, track=track):
- raise Exception('Could not create branch %s tracking %s' % (newBranch, track))
- if not M.git().checkout(newBranch):
- raise Exception('Could not checkout branch %s' % (newBranch))
- logging.info('Checked out branch %s' % (newBranch))
- mode = 'nomerge'
-
- # Let's pretend everything was fine at the start.
- result = True
- unstash = True
-
- if mode == 'pull':
- # Pull branch from tracker
- logging.info('Pulling branch %s from %s into %s' % (remoteBranch, remoteUrl, M.currentBranch()))
- result = M.git().pull(remote=remoteUrl, ref=remoteBranch)
- if result[0] != 0:
- logging.warning('Merge failed, please solve the conflicts and commit')
- result = False
- unstash = False
-
- elif mode == 'fetchonly':
- # Only fetches the branch from the remote
- logging.info('Fetching branch %s from %s' % (remoteBranch, remoteUrl))
- fetch = M.git().fetch(remote=remoteUrl, ref=remoteBranch)
- if fetch[0] != 0:
- logging.warning('Failed to fetch the remote branch')
- else:
- logging.info('Fetch successful, you can now use FETCH_HEAD')
-
- elif mode == 'patch':
- # Apply a patch from tracker
- files = []
- for patch in patchesToApply:
- dest = patch['mdk-filename']
- logging.info('Downloading %s' % (patch['filename']))
- if not J.download(patch['content'], dest):
- logging.error('Failed to download. Aborting...')
- result = False
- files = []
- break
- files.append(dest)
-
- if len(files) > 0:
- logging.info('Applying patch(es)...')
- if not M.git().apply(files):
- logging.warning('Could not apply the patch(es), please apply manually')
- result = False
- else:
- for f in files:
- os.remove(f)
-
- elif mode == 'nomerge':
- # Checking out the patch without merging it.
- logging.info('Fetching %s %s' % (remoteUrl, remoteBranch))
- M.git().fetch(remote=remoteUrl, ref=remoteBranch)
- logging.info('Hard reset to FETCH_HEAD')
- M.git().reset('FETCH_HEAD', hard=True)
-
- # Stash pop
- if unstash and stash and not stash[1].startswith('No local changes'):
- pop = M.git().stash(command='pop')
- if pop[0] != 0:
- logging.error('An error ocured while unstashing your changes')
- else:
- logging.info('Popped the stash')
- elif not unstash and stash:
- logging.warning('Note that some files have been left in your stash')
-
- if result:
- logging.info('Done.')
+ J = jira.Jira()
+ patches = J.getAttachments(mdl)
+ patches = {k: v for k, v in patches.items() if v.get('filename').endswith('.patch')}
+ toApply = []
+
+ if len(patches) < 1:
+ return False
+
+ mapping = {}
+ i = 1
+ for key in sorted(patches.keys()):
+ patch = patches[key]
+ mapping[i] = patch
+ print '{0:<2}: {1:<60} {2}'.format(i, key[:60], datetime.strftime(patch.get('date'), '%Y-%m-%d %H:%M'))
+ i += 1
+
+ while True:
+ try:
+ ids = question('What patches would you like to apply?')
+ if ids.lower() == 'ankit':
+ logging.warning('Sorry, I am unable to punch a child at the moment...')
+ continue
+ elif ids:
+ ids = re.split(r'\s*[, ]\s*', ids)
+ toApply = [mapping[int(i)] for i in ids if int(i) in mapping.keys()]
+ except ValueError:
+ logging.warning('Error while parsing the list of patches, try a little harder.')
+ continue
+ break
+
+ return toApply
- # TODO Tidy up the messy logic above!
- # TODO Really, this needs some good tidy up!
- # TODO I'm being serious... this needs to crazy clean up!!
--- /dev/null
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+Moodle Development Kit
+
+Copyright (c) 2014 Frédéric Massart - FMCorz.net
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+http://github.com/FMCorz/mdk
+"""
+
+import os
+import jira
+import logging
+
+
+class Fetch(object):
+ """Holds the logic and processing to fetch a remote and following actions into an instance"""
+
+ _M = None
+ _ref = None
+ _repo = None
+
+ _canCreateBranch = True
+ _hasstashed = False
+
+ def __init__(self, M, repo=None, ref=None):
+ self._M = M
+ self._repo = repo
+ self._ref = ref
+
+ def checkout(self):
+ """Fetch and checkout the fetched branch"""
+ self.fetch()
+ logging.info('Checking out branch as FETCH_HEAD')
+ if not self.M.git().checkout('FETCH_HEAD'):
+ raise FetchException('Could not checkout FETCH_HEAD')
+
+ def fetch(self):
+ """Perform the fetch"""
+ if not self.repo:
+ raise FetchException('The repository to fetch from is unknown')
+ elif not self.ref:
+ raise FetchException('The ref to fetch is unknown')
+
+ git = self.M.git()
+ logging.info('Fetching %s from %s' % (self.ref, self.repo))
+ result = git.fetch(remote=self.repo, ref=self.ref)
+ if result[0] != 0:
+ raise FetchException('Error while fetching %s from %s' % (self.ref, self.repo))
+
+ def _merge(self):
+ """Protected method to merge FETCH_HEAD into the current branch"""
+ logging.info('Merging into current branch')
+ if not self.M.git().merge('FETCH_HEAD'):
+ raise FetchException('Merge failed, resolve the conflicts and commit')
+
+ def pull(self, into=None):
+ """Fetch and merge the fetched branch into a branch passed as param"""
+ self._stash()
+
+ try:
+ self.fetch()
+ git = self.M.git()
+
+ if into:
+ logging.info('Switching to branch %s' % (into))
+
+ if not git.hasBranch(into):
+ if self.canCreateBranch:
+ if not git.createBranch(into):
+ raise FetchException('Could not create the branch %s' % (into))
+ else:
+ raise FetchException('Branch %s does not exist and create branch is forbidden' % (into))
+
+ if not git.checkout(into):
+ raise FetchException('Could not checkout branch %s' % (into))
+
+ self._merge()
+
+ except FetchException as e:
+ if self._hasstashed:
+ logging.warning('An error occured. Some files may have been left in your stash.')
+ raise e
+
+ self._unstash()
+
+ def setRef(self, ref):
+ """Set the reference to fetch"""
+ self._ref = ref
+
+ def setRepo(self, repo):
+ """Set the repository to fetch from"""
+ self._repo = repo
+
+ def _stash(self):
+ """Protected method to stash"""
+ stash = self.M.git().stash(untracked=True)
+ if stash[0] != 0:
+ raise FetchException('Error while trying to stash your changes')
+ elif not stash[1].startswith('No local changes'):
+ logging.info('Stashed your local changes')
+ self._hasstashed = True
+
+ def _unstash(self):
+ """Protected method to unstash"""
+ if self._hasstashed:
+ pop = self.M.git().stash(command='pop')
+ if pop[0] != 0:
+ logging.error('An error ocured while unstashing your changes')
+ else:
+ logging.info('Popped the stash')
+ self._hasstashed = False
+
+ @property
+ def canCreateBranch(self):
+ return self._canCreateBranch
+
+ @property
+ def into(self):
+ return self._into
+
+ @property
+ def M(self):
+ return self._M
+
+ @property
+ def ref(self):
+ return self._ref
+
+ @property
+ def repo(self):
+ return self._repo
+
+
+class FetchTracker(Fetch):
+ """Pretty dodgy implementation of Fetch to work with the tracker.
+
+ If a list of patches is set, we override the git methods to fetch from a remote
+ to use the patches instead. I am not super convinced by this design, but at
+ least the logic to fetch/pull/merge is more or less self contained.
+ """
+
+ _J = None
+ _cache = None
+ _patches = None
+
+ def __init__(self, *args, **kwargs):
+ super(FetchTracker, self).__init__(*args, **kwargs)
+ self._J = jira.Jira()
+ self._cache = {}
+
+ def checkout(self):
+ if not self.patches:
+ return super(FetchTracker, self).checkout()
+
+ self.fetch()
+
+ def fetch(self):
+ if not self.patches:
+ return super(FetchTracker, self).fetch()
+
+ for patch in self.patches:
+ j = 0
+ dest = None
+ while True:
+ downloadedTo = patch.get('filename') + (('.' + str(j)) if j > 0 else '')
+ dest = os.path.join(self.M.get('path'), downloadedTo)
+ j += 1
+ if not os.path.isfile(dest):
+ patch['downloadedTo'] = downloadedTo
+ break
+
+ logging.info('Downloading patch as %s' % (patch.get('downloadedTo')))
+ if not dest or not self.J.download(patch.get('url'), dest):
+ raise FetchTrackerException('Failed to download the patch to %s' % (dest))
+
+
+ def _merge(self):
+ if not self.patches:
+ return super(FetchTracker, self)._merge()
+
+ patchList = [patch.get('downloadedTo') for patch in self.patches]
+ git = self.M.git()
+ if not git.apply(patchList):
+ raise FetchTrackerException('Could not apply the patch(es), please apply manually')
+ else:
+ for f in patchList:
+ os.remove(f)
+ logging.info('Patches applied successfully')
+
+ def getPullInfo(self, mdl):
+ """Return the pull information
+
+ This implements its own local cache because we could potentially
+ call it multiple times during the same request. This is bad though.
+ """
+ if not self._cache.has_key(mdl):
+ issueInfo = self.J.getPullInfo(mdl)
+ self._cache[mdl] = issueInfo
+ return self._cache[mdl]
+
+ def setFromTracker(self, mdl, branch):
+ """Sets the repo and ref according to the tracker information"""
+ issueInfo = self.getPullInfo(mdl)
+
+ repo = issueInfo.get('repo', None)
+ if not repo:
+ raise FetchTrackerRepoException('Missing information about the repository to pull from on %s' % (mdl))
+
+ ref = issueInfo.get('branches').get(str(branch), None)
+ if not ref:
+ raise FetchTrackerBranchException('Could not find branch info on %s' % (str(branch), mdl))
+
+ self.setRepo(repo)
+ self.setRef(ref.get('branch'))
+
+ def usePatches(self, patches):
+ """List of patches (returned by jira.Jira.getAttachments) to work with instead of the standard repo and ref"""
+ self._patches = patches
+
+ @property
+ def J(self):
+ return self._J
+
+ @property
+ def patches(self):
+ return self._patches
+
+
+class FetchException(Exception):
+ pass
+
+
+class FetchTrackerException(FetchException):
+ pass
+
+
+class FetchTrackerBranchException(FetchTrackerException):
+ pass
+
+
+class FetchTrackerRepoException(FetchTrackerException):
+ pass
\ No newline at end of file