Skip to content

Instantly share code, notes, and snippets.

@samhemelryk
Last active January 4, 2016 15:29
Show Gist options
  • Save samhemelryk/8640601 to your computer and use it in GitHub Desktop.
Save samhemelryk/8640601 to your computer and use it in GitHub Desktop.
This script pulls all of the branches associated with an issue on tracker.moodle.org
#! /usr/bin/python
"""
This script gets the branches used on a tracker.moodle.org issue and uses them to create/merge and stuff like that.
git moodle integration-pull MDL-43581
- Gets the PULL branches from a tracker.moodle.org issue and merges them to the local upstream
branch equivilants automatically fixing any shifter or less conflicts.
I use this during integration.
git moodle ipull 43581
- Shorthand for the above command.
git moodle branch MDL-43581
- Gets the PULL branches from a tracker.moodle.org issue and creates local branches on them.
During the creation of the local branch the upstream repository gets set to the remote upstream
repository the branch was built upon.
Shifter and less conflicts are automatically addressed.
Installation:
- Requires Python 2 (I think - sorry if I'm wrong)
- Requires lxml; see http://lxml.de/installation.html for installation details, its easy.
- Plonk this script somewhere and then symlink it into your path as git-moodle (no extension)
"""
import sys, os, urllib2, re, git, collections, subprocess
from lxml import etree
# The MoodleGit class.
# This class simply maps out git commands used throughout this script.
# If you are altering this script and need to add a git command please ensure that you work through
# this class, creating methods for new commands.
# This way if we find better ways of doing something we can easily modify here.
class MoodleGit:
# Initialises the class.
def __init__(self):
path = os.getcwd()
self.repo = git.Repo(path)
self.git = git.Git(path)
# Returns true if the git repo has outstanding changes (unstaged or staged changes).
def is_dirty(self):
return self.repo.is_dirty()
# Returns the name of the active branch in this git repo.
def active_branch(self):
return self.repo.active_branch.name
# Checks out the requested branch.
def checkout(self, branch):
return self.repo.git.checkout(branch)
# Fetchs a branch from a remote repository.
def fetch(self, url, branch):
return self.git.execute(["git", "fetch", url, branch])
# Merges FETCH_HEAD into the currently checked out branch.
def merge_fetchhead(self):
return self.git.execute(["git", "merge", "FETCH_HEAD"])
# Commits any staged changes with the pre-built commit message.
def merge_commit_after_conflict_fixed(self):
return self.git.execute(["git", "commit", "--no-edit"])
# Aborts a currently running merge, useful if there are conflicts.
def merge_abort(self):
return self.git.execute(["git", "merge", "--abort"])
# Creates and checks out a new branch.
def checkout_new_branch(self, name, upstream):
return self.git.execute(["git", "checkout", "-b", name, upstream])
# Performs a hard reset on the currently checkout branch, setting it back to the given branch.
def reset_hard(self, branch):
return self.git.execute(["git", "reset", "--hard", branch])
# Returns an array containing the unmerged files. Identifies files with conflicts after a merge.
def get_unmerged_files(self):
return self.git.execute(["git", "diff", "--name-only", "--diff-filter=U"]).splitlines()
# Stages a changed file.
def stage_file(self, file):
return self.git.execute(["git", "add", file])
# Returns true if a local branch exists, false otherwise.
def branch_exists(self, branch):
try:
self.git.execute(['git', 'show-ref', '--verify', 'refs/heads/'+branch])
except git.exc.GitCommandError:
return False
else:
return True
def rebase(self):
return self.git.execute(["git", "rebase"])
def rebase_continue(self):
return self.git.execute(["git", "rebase", "--continue"])
def rebase_abort(self):
return self.git.execute(["git", "rebase", "--abort"])
# Gets the PULL branches from a Moodle tracker issue and merges them into the corrosponding HEAD branches
# in the current repository.
#
# This is perfect for preparing branches for integration review.
#
# @param string issue The Moodle tracker issue
# @param string|bool Set this to a specific HEAD (MOODLE_26_STABLE) to limit the pull to just that one head.
def moodleIntegrationPull(issue, specificbranch = False):
repo = MoodleGit()
if repo.is_dirty():
print("The git repository isn't clean, this script can't proceed")
exit(2)
issue = validateMoodleTrackerIssueNumber(issue)
if (issue):
print('Pulling changes for ' + issue)
else:
print("An invalid issue was given")
exit(4)
currentbranch = repo.active_branch()
xml = downloadIssueXML(issue)
pullurl = getRepositoryURLFromXML(xml)
branches = getBranchesFromXML(xml)
if specificbranch:
branches = getSpecificBranch(branches, specificbranch)
if branches == False:
print('Requested branch does not exist for the given issue')
exit(5)
for branch, pullbranch in branches.iteritems():
# Check the local branch exists first up, if it doesn't skip it.
if repo.branch_exists(branch):
# Checkout the branch
repo.checkout(branch)
else:
print("Cannot merge changes on local branch `" + branch + "` as it couldn't be found")
continue
# Attempt to fetch the remove branch
try:
repo.fetch(pullurl, pullbranch)
except git.exc.GitCommandError:
print("Could not fetch branch '" + pullbranch + "' from " + pullurl)
result = "Branch could not be pulled as it did not exist."
else:
try:
result = repo.merge_fetchhead()
except git.exc.GitCommandError:
if attemptConflictFixes(repo):
print("Conflicts found and automatically fixed. Yay!")
repo.merge_commit_after_conflict_fixed()
result = "Changes pulled"
else:
print("Conflicts found that can't be automatically fixed. Sorry.")
repo.merge_abort()
result = "Branch could not be pulled due to conflicts."
print(branch + ": " + result)
# Checkout the branch the user originally had checked out.
repo.checkout(currentbranch)
# Given an issue number this function creates local branches from the PULL branches detailed on the issue.
# This function goes to tracker.moodle.org, gets the XML, fetchs the PULL branches and makes local branches.
def moodleBranch(issue, specificbranch):
repo = MoodleGit()
if repo.is_dirty():
print("The git repository isn't clean, this script can't proceed")
exit(2)
issue = validateMoodleTrackerIssueNumber(issue)
if (issue):
print('Pulling changes for ' + issue)
else:
print("An invalid issue was given")
exit(4)
currentbranch = repo.active_branch()
xml = downloadIssueXML(issue)
pullurl = getRepositoryURLFromXML(xml)
branches = getBranchesFromXML(xml)
if specificbranch:
branches = getSpecificBranch(branches, specificbranch)
if branches == False:
print('Requested branch does not exist for the given issue')
exit(5)
for branch, pullbranch in branches.iteritems():
try:
repo.fetch(pullurl, pullbranch)
except git.exc.GitCommandError:
print("Could not fetch branch '" + pullbranch + "' from " + pullurl)
result = "Branch could not be pulled as it did not exist."
else:
try:
result = repo.checkout_new_branch(pullbranch, "origin/" + branch)
except git.exc.GitCommandError:
print("Cannot create local branch " + pullbranch + " perhaps it already exists?")
continue;
try:
result = result + "\n" + repo.reset_hard("FETCH_HEAD")
except git.exc.GitCommandError:
print("Cannot reset the local branch to FETCH_HEAD... wtf?!")
continue;
print(branch + ": " + result)
# Checkout the branch the user originally had checked out.
repo.checkout(currentbranch)
def moodleRebase(branch):
repo = MoodleGit()
if repo.is_dirty():
print("The git repository isn't clean, this script can't proceed")
exit(2)
if repo.branch_exists(branch):
# Checkout the branch
repo.checkout(branch)
else:
print("Cannot merge changes on local branch `" + branch + "` as it couldn't be found")
exit(2)
result = branch + " rebased"
try:
result = repo.rebase()
except git.exc.GitCommandError:
if attemptConflictFixes(repo):
result = result + ": conflicts automatically fixed. Yay!"
tryRebaseConflictFixRecursive(repo)
else:
result = result + ": conflicts found that can't be automatically fixed. Sorry."
repo.rebase_abort()
print(result)
def tryRebaseConflictFixRecursive(repo):
try:
repo.rebase_continue()
except git.exc.GitCommandError:
if attemptConflictFixes(repo):
tryRebaseConflictFixRecursive(repo)
else:
repo.rebase_abort()
print("Conflicts found that can't be automatically fixed. Sorry.")
exit(2)
# Validates an issue number, making sure it is in fact a tracker.moodle.org issue number
def validateMoodleTrackerIssueNumber(issue):
shorthand = re.compile('^\d+$')
validissue = re.compile('^[A-Z]+-\d+$')
if shorthand.match(issue):
return "MDL-" + issue
elif validissue.match(issue):
return issue;
return False
# This function reduces a dict containing all branches to just the requested branch if it exists.
def getSpecificBranch(branches, specificbranch):
if specificbranch in branches:
pullbranch = branches[specificbranch]
return {specificbranch : pullbranch}
return False
# Attempts to fix any outstanding merge conflicts.
# This function can automatically deal with bootstrapbase less build conflict, and shifter build conflicts.
# Any other conflicts cannot be automatically fixed - sorry.
# @param MoodleGit repo
# @return Boolean True if all conflicts could be fixed, false otherwise.
def attemptConflictFixes(repo):
print("Merge conflicts hit - assessing now")
fixable = True
less = False
shifter = False
lesspattern = re.compile("theme/bootstrapbase/style/moodle\.css")
unmergedfiles = repo.get_unmerged_files()
for conflict in unmergedfiles:
if lesspattern.match(conflict):
less = True
elif conflict.find('/yui/build/') != -1:
shifter = True
else:
fixable = False
if fixable:
print("Conflicts can be automatically fixed. Yay!")
if shifter:
runShifter()
if less:
runRecess()
for conflict in unmergedfiles:
p = subprocess.Popen(["grep", "-c", "<<<<<<|>>>>>>", conflict], stdout=subprocess.PIPE)
out, err = p.communicate()
if int(out) == 0:
repo.stage_file(conflict)
else:
fixable = False
break
if fixable:
return True
else:
return False
else:
return False
# Runs shifter to across the whole of Moodle to blanket corrects any build conflicts.
def runShifter():
subprocess.call(["shifter", "--walk", "--recursive"])
# Runs Recess to build the bootstrapbase theme.
def runRecess():
subprocess.call("recess --compress --compile theme/bootstrapbase/less/moodle.less > theme/bootstrapbase/style/moodle.css", shell=True)
# Gets the issue XML from tracker.moodle.org.
# @param string issue The MDL... issue number
# @return string The XML.
def downloadIssueXML(issue):
url = "http://tracker.moodle.org/si/jira.issueviews:issue-xml/" + issue + "/" + issue + ".xml"
try:
response = urllib2.urlopen(url)
except urllib2.URLError as e:
if e.code != 200:
print("Could not get the XML for the requested issue [" + str(e.code) + "]")
exit(3)
else:
xml = response.read()
return xml
# Gets the remote repository URL from a tracker issues XML.
# @param string xml The tracker issues XML.
# @return string
def getRepositoryURLFromXML(xml):
root = etree.fromstring(xml)
return stripTags(getCustomFieldValue(root, "Pull from Repository"))
# Gets a dict of branches to pull for the selected issue.
# The key is the local upstream branch
# The value is the remote branch
# @param string xml The issue XML as it was returned from teh web request.
# @return dict
def getBranchesFromXML(xml):
# First match all branches by their expected name.
branchexpr = re.compile("(Pull (\d+).(\d+) Branch)")
branches = branchexpr.findall(xml)
# Build an XML tree from which we can easily extract information.
root = etree.fromstring(xml)
# Prepare the results dict.
result = collections.OrderedDict({})
# Specifically look for master first, it breaks the expected naming we are relying on and we want it to
# always come first.
branchmaster = getCustomFieldValue(root, "Pull Master Branch")
if branchmaster != None:
result['master'] = stripTags(branchmaster)
# Now work on the branches we matched with our regex earlier.
for branch in branches:
# Foreach branch get the remote branch name from the XML
remotebranch = getCustomFieldValue(root, branch[0])
if (remotebranch != None):
# There is a remote branch (surprise surprise) lets strip any tags Jira may have wrapped it in.
# Jira typically wraps a branch name in an <a> so that it is linkable... stupid when you
# request XML of course.
result['MOODLE_' + branch[1] + branch[2] + '_STABLE'] = stripTags(remotebranch)
return result
# Strips any HTML tags from the content.
# This function is aggressive, any <...> get removed.
# @param string content
# @return string The cleaned content.
def stripTags(content):
patt = re.compile("<[^>]*>")
result = patt.sub('', content)
return result
# Gets the custom field value for a custom field with the given name
# @param etree root The XML tree to work on.
# @param string name The name of the custom field whos value we are interested in.
def getCustomFieldValue(root, name):
customfield = root.xpath('//channel/item/customfields/customfield/customfieldname[text()[contains(., \'' + name + '\')]]/ancestor::customfield//customfieldvalue')
if len(customfield) == 0:
return None
return customfield[0].text
# Prints help.
def printHelp():
print("Moodle command aid.")
exit(0)
# The following code determines the command being run and calls the desired function with its expected arguments.
if __name__ == "__main__":
arglength = len(sys.argv)
if arglength < 2:
print("moodle expects at least one argument")
sys.exit(2)
process = sys.argv[1]
if process in ("integration-pull", "ipull"):
specificbranch = False
if arglength == 4:
specificbranch = sys.argv[3]
elif arglength != 3:
print("Moodle integration-pull expects one argument the issue number to merge in")
moodleIntegrationPull(sys.argv[2], specificbranch)
elif process in ("branch"):
specificbranch = False
if arglength == 4:
specificbranch = sys.argv[3]
elif arglength != 3:
print("Moodle branch expects one argument the issue number to create local branches for")
moodleBranch(sys.argv[2], specificbranch)
elif process in ("rebase"):
specificbranch = False
if arglength != 3:
print("Moodle branch expects one argument the issue number to create local branches for")
moodleRebase(sys.argv[2])
else:
printHelp()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment