Skip to content

Instantly share code, notes, and snippets.

@bneijt
Created September 16, 2016 09:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bneijt/cf7ec98ffd97b5bed12503a17faa6844 to your computer and use it in GitHub Desktop.
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
#!/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