Created
September 16, 2016 09:44
-
-
Save bneijt/cf7ec98ffd97b5bed12503a17faa6844 to your computer and use it in GitHub Desktop.
Simple git java maven project release script: remove SNAPSHOT, test build, generate random tag, commit, introduce SNAPSHOT again
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python2 | |
import sys | |
import pygit2 as git | |
import os | |
import re | |
import logging | |
import xml.etree.ElementTree as ET | |
import optparse | |
import subprocess | |
logging.basicConfig(level = logging.INFO, format = '%(levelname)s %(message)s') | |
logger = logging.getLogger(__name__) | |
def repoTags(repo): | |
tagRegex = re.compile("^refs/tags/") | |
for reference in repo.listall_references(): | |
if tagRegex.match(reference): | |
tag = repo.lookup_reference(reference).resolve() | |
yield tag | |
def lastTag(repo): | |
tags = list(repoTags(repo)) | |
if len(tags) == 0: | |
return None | |
latest = tags[0] | |
latestTime = tags[0].get_object().commit_time | |
for tag in tags: | |
thisTime = tag.get_object().commit_time | |
if latestTime < thisTime: | |
latestTime = thisTime | |
latest = tag | |
return latest | |
def changesSince(lastTag, repo): | |
head = repo.revparse_single('HEAD') | |
changes = repo.diff('HEAD', lastTag.get_object()) | |
for change in changes: | |
yield change.delta.old_file.path | |
yield change.delta.new_file.path | |
def pomsWithChangesBelowThemSinceLastTag(repo): | |
assert repo.is_bare == False | |
assert repo.is_empty == False | |
changesSinceLastTag = changesSince(lastTag(repo), repo) | |
poms = set() | |
for change in changesSinceLastTag: | |
(directory, fname) = os.path.split(change) | |
if fname == "pom.xml": | |
logger.debug("Ignoring change in pom.xml: %s", change) | |
continue | |
# Find pom in directory | |
logger.debug("Going up the directory tree looking for pom.xml for change of " + change) | |
while True: | |
logger.debug(" directory is '%s'", directory) | |
pomCandidate = os.path.join(directory, "pom.xml") | |
if os.path.exists(pomCandidate): | |
logger.debug("Adding pom: %s", pomCandidate) | |
poms.add(pomCandidate) | |
break | |
if len(directory) <= 0: | |
break | |
(directory, fname) = os.path.split(directory) | |
return poms | |
def pomsToRelease(repo): | |
pomCandidates = pomsWithChangesBelowThemSinceLastTag(repo) | |
for pomCandidate in pomCandidates: | |
if os.path.exists(pomCandidate): | |
yield pomCandidate | |
def replaceSnapshotVersions(pomFile): | |
snapshotPattern = re.compile("[a-zA-Z+0-9._-]+-SNAPSHOT") | |
with open(pomFile) as pom: | |
pomContents = pom.read() | |
for found in re.findall(snapshotPattern, pomContents): | |
yield (found, found[0:len(found) - len("-SNAPSHOT")]) | |
def applyChanges(pomPath, changeList): | |
with open(pomPath, 'r+') as pomFile: | |
logger.info("Updating: " + pomPath) | |
contents = pomFile.read() | |
for (search, replace) in changeList: | |
logger.debug("\treplacing %s with %s", search, replace) | |
contents = contents.replace(search, replace) | |
pomFile.seek(0, os.SEEK_SET) | |
pomFile.write(contents) | |
pomFile.truncate() | |
def upMinorVersion(lowVersion): | |
splitVersion = lowVersion.split(".") | |
minorNumber = int(re.search("[0-9]+", splitVersion[-1]).group(0)) | |
splitVersion[-1] = splitVersion[-1].replace(str(minorNumber), str(minorNumber + 1)) | |
return ".".join(splitVersion) | |
def commitMessageForChanges(changeList): | |
return "\n".join([key + "\n " + "\n ".join([a + " -> " + b for (a,b) in changes]) for key, changes in changeList.items()]) | |
def commitWorkingTree(repo, changeList, commitSummary): | |
index = repo.index | |
for fname in changeList: | |
index.add(fname) | |
index.write() | |
indexId = index.write_tree() | |
author = git.Signature('Release Script', 'software@foreyet.com') | |
# committer = git.Signature('Cecil Committer', 'cecil@committers.tld') | |
committer = author | |
tree = repo.TreeBuilder().write() | |
repo.create_commit( | |
'refs/heads/master', # the name of the reference to update | |
author, committer, commitSummary + "\n\n" + commitMessageForChanges(changeList), | |
indexId, # binary string representing the tree object ID | |
[repo.head.get_object().oid], # list of binary strings representing parents of the new commit | |
'utf-8') | |
def createTagOnHead(repo, tagIdentity): | |
tagger = git.Signature('Release Script', 'software@foreyet.com') | |
repo.create_tag("release-" + tagIdentity[:10], repo.head.get_object().oid, git.GIT_OBJ_COMMIT, tagger, "Release tag on " + tagIdentity) | |
def commitsSinceLastTag(repo): | |
tag = lastTag(repo) | |
if tag == None: | |
logger.warning("No tags found in current repo") | |
return 0 | |
logger.info("Considering %s as last tag", tag.name) | |
count = 0 | |
lastTagCommitTime = tag.get_object().commit_time | |
for commit in repo.walk(repo.head.target, git.GIT_SORT_TIME): | |
if isinstance(commit, git.Commit): | |
if commit.commit_time > lastTagCommitTime: | |
count += 1 | |
return count | |
def repoIsConsideredSane(repo): | |
if repo.is_bare: | |
logger.error("Repo is BARE") | |
return False | |
cslt = commitsSinceLastTag(repo) | |
logger.info("%i commits since last tag", cslt) | |
if cslt < 2: | |
logger.error("Not enough commits since last tag (%i)", cslt) | |
return False | |
return True | |
def determineSnapshotRemoveChangesFor(poms): | |
removeSnapshotChanges = dict() | |
for pom in poms: | |
removeSnapshotChanges[pom] = list(replaceSnapshotVersions(pom)) | |
return removeSnapshotChanges | |
def applyFileChanges(changes): | |
for fileName in changes: | |
applyChanges(fileName, changes[fileName]) | |
def main(): | |
parser = optparse.OptionParser(usage='%prog [[pom to release] ..]') | |
parser.add_option('-V', '--version', action='store_true', dest='version', help='show version and exit') | |
parser.add_option('-s', '--skip-install', action='store_true', dest='skipInstall', help='skip mvn clean install') | |
parser.add_option('-f', '--force', action='store_true', dest='force', help='Force the procedure') | |
parser.add_option('-d', '--dry-run', action='store_true', dest='dryRun', help='exit after detecting the changes') | |
parser.add_option('-m', '--message', dest='message', help='add a release message') | |
(options, args) = parser.parse_args() | |
if options.version: | |
print('Version 1.0.0') | |
return 1 | |
repo = git.Repository('.git') | |
if not options.force: | |
for f, stat in repo.status().items(): | |
if stat == git.GIT_STATUS_WT_MODIFIED: | |
logger.error("Repo contains uncommited changes") | |
logger.error(f + " contains changes") | |
return 1 | |
if options.force or repoIsConsideredSane(repo): | |
poms = set(pomsToRelease(repo)) | |
poms.update(args) | |
removeSnapshotChanges = determineSnapshotRemoveChangesFor(poms) | |
if len(removeSnapshotChanges) == 0: | |
logger.error("No changes found to release since last tag") | |
return 1 | |
if options.dryRun: | |
logger.info("Dry run") | |
logger.info("Changes where:") | |
for pom in removeSnapshotChanges: | |
logger.info(pom) | |
for change in removeSnapshotChanges[pom]: | |
logger.info("\t" + repr(change)) | |
return 0 | |
#Change pom files | |
applyFileChanges(removeSnapshotChanges) | |
#Remember head for later restore | |
originalHead = repo.head.get_object() | |
#Commit changes | |
commitWorkingTree(repo, removeSnapshotChanges, "Release script: removed SNAPSHOT" + ("\n\n" + options.message if options.message else "")) | |
if not options.skipInstall: | |
rstatus = subprocess.call(["mvn", "clean", "install"]) | |
if not rstatus == 0: | |
logger.error("Install failed") | |
logger.info("Reset last commit with: git reset --hard HEAD~1") | |
return 1 | |
#Tag commit without -SNASHOT poms | |
createTagOnHead(repo, originalHead.oid.hex) | |
#Increment minor version and add SNAPSHOT in current development POM only (not dependencies) | |
incrementSnapshotChanges = {} | |
for pomFileName in removeSnapshotChanges: | |
ET.register_namespace("", "http://maven.apache.org/POM/4.0.0") | |
tree = ET.parse(pomFileName) | |
versionNode = tree.getroot().find('{http://maven.apache.org/POM/4.0.0}version') | |
oldVersion = versionNode.text | |
versionNode.text = upMinorVersion(oldVersion) + "-SNAPSHOT" | |
incrementSnapshotChanges[pomFileName] = [(oldVersion, versionNode.text)] | |
tree.write(pomFileName, encoding = "utf-8", xml_declaration = True) | |
commitWorkingTree(repo, incrementSnapshotChanges, "Release script: increment minor version for development") | |
return 0 | |
else: | |
logger.critical("Repo is not considered sane") | |
return 1 | |
if __name__ == "__main__": | |
sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment