From: Frederic Massart Date: Tue, 1 Apr 2014 16:19:37 +0000 (+0800) Subject: Patch can be pulled from the tracker on demand X-Git-Tag: v1.0~10 X-Git-Url: https://git.cameron1729.xyz/?a=commitdiff_plain;h=6e9ab0895d743d4cb33dd3817aa18cb99851274e;p=mdk.git Patch can be pulled from the tracker on demand This also includes a massive rewrite of the pull command which was getting too distusting to be true... Now the logic is abstracted in a class, which style does not entirely convince me, but that is still a lot neater than before! --- diff --git a/extra/bash_completion b/extra/bash_completion index 6a56a2e..7e03045 100644 --- a/extra/bash_completion +++ b/extra/bash_completion @@ -167,7 +167,11 @@ function _mdk() { fi ;; pull) - OPTS="--fetch-only --integration --no-merge --testing" + if [[ "${PREV}" == "-m" ]] || "${PREV}" == "--mode"; then + OPTS="checkout fetch integration pull testing" + else + OPTS="--fetch-only --integration --no-merge --testing --mode --prompt" + fi ;; push) if [[ "$PREV" != "--branch" && "$PREV" != "-b" && "$PREV" != "--remote" && "$PREV" != "-r" ]]; then diff --git a/lib/commands/pull.py b/lib/commands/pull.py index 12d8cab..2432b80 100644 --- a/lib/commands/pull.py +++ b/lib/commands/pull.py @@ -25,8 +25,7 @@ http://github.com/FMCorz/mdk 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 @@ -38,7 +37,7 @@ class PullCommand(Command): ['-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.' } ), ( @@ -46,7 +45,7 @@ class PullCommand(Command): { '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.' } ), ( @@ -54,14 +53,30 @@ class PullCommand(Command): { '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.' } ), ( @@ -82,8 +97,19 @@ class PullCommand(Command): 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 @@ -101,69 +127,29 @@ class PullCommand(Command): # 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 @@ -171,100 +157,40 @@ class PullCommand(Command): 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!! diff --git a/lib/fetch.py b/lib/fetch.py new file mode 100644 index 0000000..b253ec0 --- /dev/null +++ b/lib/fetch.py @@ -0,0 +1,257 @@ +#!/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://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 diff --git a/lib/git.py b/lib/git.py index f6add12..da7ee3b 100644 --- a/lib/git.py +++ b/lib/git.py @@ -211,6 +211,12 @@ class Git(object): return stdout + def merge(self, ref): + """Wrapper for the merge command""" + cmd = 'merge %s' % (ref) + (returncode, stdout, stderr) = self.execute(cmd) + return returncode == 0 + def messages(self, count=10, since=None, path=None): """Return the latest titles of the commit messages""" messages = self.log(count=count, since=since, path=path, format='%s')