vaadin-framework/build/bin/mergetool.py
Artur Signell 8c740b94ba Removed svn:executable property to avoid problems with Git on Windows
svn changeset:21944/svn branch:6.7
2011-11-08 14:29:40 +00:00

705 lines
21 KiB
Python

#!/usr/bin/python
import sys,re,os,string,urllib,httplib
################################################################################
# Configuration
################################################################################
# Determine repository root
pin = os.popen("svn info|grep 'Repository Root'|sed -e 's/^.\+: //'", "r")
REPOSITORY = pin.read().rstrip()+"/"
pin.close()
print "Repository: %s" % (REPOSITORY)
################################################################################
# Parse command-line arguments
################################################################################
def help(exitvalue = 0):
print "Usage: batchmerge [options] <command>\n"
print "Options:"
print "\t-m\tOnly merge. (For 'single' command.)"
print "\t-c\tOnly commit. (For 'single' command.)"
print "\t-html\tHTML output. (For 'log' command.)"
print "\t-author\tHTML output. (Include author in HTML log.)"
print "\t-milestone <ms>\tList tickets in milestone (For 'log' command.)"
print "\nCommands:"
print "massmerge <cfg> <src> [<from>] "
print " - Merges changesets listed in the configuration"
print " file. The file is in svn log (text) format."
print " You can comment out unwanted changesets."
print " Merge is stopped on conflict."
print " If you give the optional <from> parameter,"
print " merge is started from the changeset number. "
print "single <chg#> <target> - Merges a single changeset. If -m is given,"
print " only merge is done. If -c is given, only commit is done."
print "revert - Reverts all changes made to repository except"
print " changes in this program and the merge files."
print "log <cfg> <src> <trg> - Prints a ChangeLog as it will appear in the"
print " commit log. If -html option is given,"
print " the log is printed in a HTML format"
print " suitable for the Release Notes."
print "commit <cfg> <src> <trg> - Commits all changes, except changes to this"
print " program and merge files. The commit log"
print " comment includes list of all changesets"
print " listed in the configuration. The <target>"
print " is the branch name, e.g., \"5.2\".\n"
print "Common parameters:"
print " <cfg> - Configuration file (svn text log format)."
print " <src> - Source branch relative to repository URI."
print " <trg> - Target branch relative to repository URI."
print "You must run the command in the root directory of the branch."
print "The program file contains some basic configuration parameters."
sys.exit(exitvalue)
################################################################################
# Globals
################################################################################
tickets = {}
################################################################################
# Utility Functions
################################################################################
def command(cmd, dryrun=0):
print cmd
if not dryrun:
if os.system(cmd):
print "Command failed, exiting."
sys.exit(1)
else:
print "Dry run - not executing."
def listChangedFiles():
# Get Svn status
pin = os.popen("svn st", "r")
lines = pin.readlines()
pin.close()
changed = []
for line in lines:
# Remove trailing newline
line = line[:-1]
# Extract the file state and name
(filestate, filename) = re.split(r'[ \+]+', line.lstrip())
# Ignore files in build directory
if (filename.startswith("build/merge/") \
or filename.startswith("build/bin/mergetool.py") \
or filename.startswith("build/testing")) \
and filestate == "M":
continue
# File is changed if it is not local
if filestate != "?":
changed.append(filename)
return changed
# Retrieves ticket summary string with HTTP
# Returns: (summary, milestone)
def fetchSummary(ticketno):
params = urllib.urlencode({'format': 'tab'})
conn = httplib.HTTPConnection("dev.itmill.com")
conn.request("GET", "/ticket/%d?%s" % (ticketno, params) )
response = conn.getresponse()
data = response.read()
conn.close()
lines = data.split("\n")
data = reduce(lambda x,y: x+"\n"+y, lines[1:])
#cols = lines[1].split("\t")
cols = data.split("\t")
return (cols[1],cols[8])
# Adds summary to ticket number, unless the context already has it
# Returns: (summary, milestone)
def addSummary(m):
ticketnum = int(m.group(1))
context = m.group(2)
if re.match(" *\(", context):
# The context already has ticket summary
return "#%d%s" % (ticketnum, context)
(summary,milestone) = fetchSummary(ticketnum)
# Remove possible " quotation from the summary
if summary.startswith('"'):
summary = summary.strip('"')
summary = summary.replace('""', '"')
# Add summaries to further ticket numbers recursively
context = re.sub(r'#([0-9]+)(.*)', addSummary, context)
return "#%s (<i>%s</i>) %s" % (ticketnum, summary, context)
################################################################################
# Change
################################################################################
class Ticket:
def __init__(self, id, summary=None, milestone=None):
self.id = id
self.summary = summary
self.milestone = milestone
def fetchData(self):
(summary, milestone) = fetchSummary(self.id)
self.summary = summary
self.milestone = milestone
################################################################################
# Change
################################################################################
class Change:
def __init__(self, id, undo=0, author=""):
self.id = id
self.author = author
self.comment = ""
self.undo = undo
self.tickets = []
def addCommentLine(self, line):
self.comment += line
def merge(self, trunkURI, dryrun=0):
drycmd = ""
if dryrun:
drycmd = "--dry-run"
# Handle negative merge
mergesign = ""
if self.undo:
mergesign = "-"
# Build the merge command
cmd = "svn merge --non-interactive %s -c %s%d %s" % (drycmd, mergesign, self.id, trunkURI)
print cmd
# Run the merge command
pin = os.popen(cmd, "r")
lines = pin.readlines()
pin.close()
# Parse the lines for conflicts
conflicts = 0
for line in lines:
print line[:-1]
# Check for conflict
if line.startswith("C"):
conflicts += 1
# Check for skipped file
elif line.startswith("Skipped"):
conflicts += 1
filename = line[8:-1]
# Simply exit if there was any problem
if conflicts > 0:
print "Problems detected. Exiting."
sys.exit(1)
def fetchComment(self, trunkURI):
cmd = "svn log -r %d %s" % (self.id, trunkURI)
# Run the log command
pin = os.popen(cmd, "r")
lines = pin.readlines()
pin.close()
STATE_START = 0
STATE_COMMENT = 1
comment = None
state = STATE_START
for line in lines:
if state == STATE_START:
if line == "\n":
state = STATE_COMMENT
elif state == STATE_COMMENT:
if line.startswith("-----------------"):
self.comment = comment
return comment
elif comment:
comment += "\n" + line.rstrip("\n")
else:
comment = line.rstrip("\n")
self.comment = comment
return comment
def commit(self):
# Write the log comment to a temporary file
logtmpname = "/tmp/merge-single-%d.log" % (os.getpid())
fout = open(logtmpname, "w")
fout.write(self.comment)
fout.close()
# Get listo
files = listChangedFiles()
if len(files) == 0:
print "Error: Will not do empty commit."
sys.exit(1)
# Write the list of files to be committed to a temporary file
changedfiles = ("\n".join(files)) + "\n"
targettmpname = "/tmp/merge-targets-%d.txt" % (os.getpid())
fout = open(targettmpname, "w")
fout.write(changedfiles)
fout.close()
print changedfiles,
command("svn commit --file %s --targets %s" % (logtmpname, targettmpname))
command("rm %s %s" % (logtmpname, targettmpname))
def getNumber(self):
return self.id
def getComment(self):
return self.comment
def getUndo(self):
return self.undo
def getAuthor(self):
return self.author
def isForMilestone(self, milestone):
return self.author
def addSummary(self, m, target_milestone=None):
ticketnum = int(m.group(1))
context = m.group(2)
if re.match(" *\(", context):
# The context already has ticket summary
return "#%d%s" % (ticketnum, context)
# Check for cached ticket
if tickets.has_key(ticketnum):
summary = tickets[ticketnum].summary
ticket_milestone = tickets[ticketnum].milestone
else:
# Fetch ticket from server and add to cache
(summary,ticket_milestone) = fetchSummary(ticketnum)
tickets[ticketnum] = Ticket(ticketnum,summary,ticket_milestone)
self.tickets.append(ticketnum);
# Remove possible " quotation from the summary
if summary.startswith('"'):
summary = summary.strip('"')
summary = summary.replace('""', '"')
ticketnum = "#%s" % (ticketnum)
# Emphasize tickets matching the target milestone
if target_milestone:
if ticket_milestone.find(target_milestone) != -1:
ticketnum = "<b>%s</b>" % (ticketnum)
# Add summaries to further ticket numbers recursively
context = re.sub(r'#([0-9]+)(.*)', lambda m: self.addSummary(m, target_milestone=target_milestone), context)
return "%s (<i>%s</i>) %s" % (ticketnum, summary, context)
def registerTicket(self, m, ticketNumbers):
ticketNumbers[int(m.group(1))] = 1
return ""
# Returns a list of ticket numbers referenced by this change
def listTickets(self):
ticketNumbers = {}
re.sub(r'#([0-9]+)', lambda m: self.registerTicket(m,ticketNumbers=ticketNumbers), self.comment)
return ticketNumbers.keys()
################################################################################
# Read configuration file
################################################################################
class Configuration:
def __init__(self, cfgfilename, startfrom=0):
self.changes = []
self.readConfig(cfgfilename, startfrom)
def readConfig(self, cfgfilename, startfrom=0):
fin = open(cfgfilename, "r")
content = fin.readlines()
fin.close()
# Parse configuration
currentChange = None
skipChange = 0
for line in content:
m_changestart = re.match(r'(-?)r([0-9]+)', line)
m_endofchange = re.match(r'------+', line)
m_emptyline = re.match(r'^$', line)
if m_changestart:
# Parse negative merge
undo = 0
if m_changestart.group(1) == "-":
undo = 1
# Get changeset number
id = int(m_changestart.group(2))
# Skip the target if its number is too small
if startfrom != 0 and id < startfrom:
skipChange = 1
# Get the author
author = re.sub(r'\@.+', '', line.split("|")[1].strip())
currentChange = Change(id, undo=undo, author=author)
elif m_endofchange:
# Register changeset, unless it is marked
# for skipping.
if currentChange:
if skipChange:
skipChange = 0
else:
self.changes.append(currentChange)
currentChange = None
elif m_emptyline:
pass
else:
if currentChange:
currentChange.addCommentLine(line)
def massMerge(self, trunkURI, dryrun=0, allatonce=0):
if not allatonce:
# Merge one changeset at a time
for change in self.changes:
change.merge(trunkURI, dryrun=dryrun)
else:
# What is the first changeset in the merge?
smallest = 99999999
for change in self.changes:
if change.getNumber() < smallest:
smallest = change.getNumber()
drycmd = ""
if dryrun:
drycmd = "--dry-run"
# Merge from the first changeset to HEAD
cmd = "svn merge --non-interactive %s -r %d:HEAD %s" % (drycmd, smallest, trunkURI)
print cmd
command(cmd)
def createLogComment(self):
# Create a log comment that lists all merged changesets with
# comments
logcomment = "Merge from %s to %s:\n" % (sourcebranch, targetbranch)
for change in self.changes:
changeno = change.getNumber()
changecomment = change.getComment().rstrip("\n")
if change.getUndo():
logcomment += "Reverted [%d] merge: %s\n" % (changeno, changecomment)
else:
logcomment += "Merged [%d]: %s\n" % (changeno, changecomment)
return logcomment
def logHtml(self, author=0, milestone=None):
# In author inclusion mode, include some styles to make a printout look better
if author:
print "<head>\n<style type=\"text/css\">\n"+ \
"tr {\n vertical-align: top;\n}\ntd {\n font-size: 8pt;\n}\n</style>\n</head>\n"
# Print header
print "<table id=\"changelog-table\">"
authorcolumnheader = ""
if author:
authorcolumnheader = "<td>Author</td>"
print " <tr><td>#</td><td>Changeset Comment</td>%s</tr>" % (authorcolumnheader)
# Print body: the changes
for change in self.changes:
changeno = change.getNumber()
changecomment = change.getComment().rstrip("\n")
changeref = "[%d]" % (changeno)
# Handle merge undo markup
if change.getUndo():
changeref = "<font class=\"changeset-merge-undone\">%s</font>" % (changeref)
changecomment = "<font class=\"changeset-merge-undone\">%s</font>" % (changecomment)
changecomment = "Reverted a change: "+changecomment
authorcolumn = ""
if author:
authorcolumn = "<td>%s</td>" % (change.getAuthor())
# Add ticket summary after ticket number, if missing
# TODO: this handles only one
changecomment = re.sub(r'#([0-9]+)(.*)', lambda m: change.addSummary(m, target_milestone=milestone), changecomment, 100)
# item = re.sub(r'#([0-9]+)(.*)', '#\\1 (SUMMARY) \\2', item)
# Change ticket numbers to references to tickets
changecomment = re.sub(r'#([0-9]+)', '#<a href="http://dev.itmill.com/ticket/\\1">\\1</a>', changecomment)
# Change changeset numbers to references to changesets
changecomment = re.sub(r'\[([0-9]+)\]', '[<a href="http://dev.itmill.com/changeset/\\1">\\1</a>]', changecomment)
changeref = re.sub(r'\[([0-9]+)\]', '[<a href="http://dev.itmill.com/changeset/\\1">\\1</a>]', changeref)
# See if any of the tickets have milestone under work.
if milestone:
for ticketnum in change.tickets:
ticket = tickets[ticketnum]
if ticket.milestone.find(milestone) != -1:
changeref = "<b>%s</b>" % (changeref)
# Make basic HTML formatting
item = " <tr><td>%s:</td><td>%s</td>%s</tr>" % (changeref, changecomment, authorcolumn)
print item
sys.stdout.flush()
print "</table>"
# Prints a commit log to standard output
def log(self, sourcebranch, targetbranch, html=0, author=0, milestone=None):
if html:
self.logHtml(author=author,milestone=milestone)
return
sys.stdout.write(self.createLogComment())
def commit(self, sourcebranch, targetbranch, dryrun=0):
logcomment = self.createLogComment()
# Write the log comment to a temporary file
logtmpname = "/tmp/massmerge-pid-%d.log" % (os.getpid())
fout = open(logtmpname, "w")
fout.write(logcomment)
fout.close()
# Write the list of files to be committed to a temporary file
changedfiles = "\n".join(listChangedFiles())
targettmpname = "/tmp/massmerge-targets-%d.txt" % (os.getpid())
fout = open(targettmpname, "w")
fout.write(changedfiles)
fout.close()
if dryrun:
print "Log:"
os.system("cat %s" % (logtmpname))
print "\nChanged Files:"
os.system("cat %s" % (targettmpname))
print "\n"
command("svn commit --file %s --targets %s" % (logtmpname, targettmpname), dryrun=dryrun)
command("rm %s %s" % (logtmpname, targettmpname))
def listTickets(self):
fixed = {}
for change in self.changes:
changeno = change.getNumber()
changeTickets = change.listTickets()
if len(changeTickets)>0 and change.comment.lower().find("fix") != -1:
for ticket in changeTickets:
fixed[ticket] = 1
fixedlist = fixed.keys()
fixedlist.sort()
# print "Fixed:", fixedlist
print "<ul>"
for ticketNum in fixedlist:
if not tickets.has_key(ticketNum):
ticket = Ticket(ticketNum)
ticket.fetchData()
tickets[ticketNum] = ticket
ticket = tickets[ticketNum]
# print "%d: %s" % (ticket.id, ticket.summary)
print " <li><a href=\"http://dev.itmill.com/ticket/%d\">#%d</a>: %s</li>" % (ticket.id, ticket.id, ticket.summary)
sys.stdout.flush()
print "</ul>"
################################################################################
# Commands
################################################################################
# Command: revert
def commandRevert():
# Get Svn status
pin = os.popen("svn st", "r")
lines = pin.readlines()
pin.close()
reverted = []
removed = []
for line in lines:
# Remove trailing newline
line = line[:-1]
# Extract the file state and name
(filestate, filename) = re.split(r'[ \+]+', line)
# Ignore files in build directory
if (filename.startswith("build/merge/") \
or filename.startswith("build/bin/")) \
and filestate == "M":
continue
# Added files are simply deleted
if filestate != "?":
reverted.append(filename)
# Added files have to be removed in addition to reverting
if filestate == "A":
removed.append(filename)
# Remove conflict choises
elif filestate == "?" and \
(filename.find(".merge-left.r") != -1 or \
filename.find(".merge-right.r") != -1):
removed.append(filename)
# Revert files marked for reverting
donework = 0
if len(reverted) > 0:
files = " ".join(reverted)
command("svn revert -R %s" % (files))
donework = 1
# Remove files marked for deletion
if len(removed) > 0:
files = " ".join(removed)
command("rm %s" % (files))
donework = 1
if not donework:
print "Nothing to do."
# Command: massmerge
def commandMassmerge(cfgfilename, sourceuri, startfrom, dryrun=0, allatonce=0):
cfg = Configuration(cfgfilename, startfrom=startfrom)
cfg.massMerge(sourceuri, dryrun=dryrun, allatonce=allatonce)
# Command: single
def commandSingle(trunkuri, changeset, sourcebranch, targetbranch, onlymerge=0, onlycommit=0):
change = Change(changeset)
print "Found changeset with log comment:\n "+change.fetchComment(trunkuri) + "\n"
change.merge(trunkuri, dryrun=onlycommit)
if onlycommit:
print "Got file list."
else:
print "Merge successful."
# Change the comment
change.comment = "Merged [%d] from %s to %s branch: %s" % (change.id, sourcebranch, branchname, change.comment)
print "\nLog comment: \"%s\"" % (change.comment)
if not onlymerge:
print "Committing."
change.commit()
# Command: commit
def commandCommit(cfgfilename, sourcebranch, targetbranch, dryrun=0):
cfg = Configuration(cfgfilename)
cfg.commit(sourcebranch, targetbranch, dryrun=dryrun)
# Command: log
def commandLog(cfgfilename, sourcebranch, targetbranch, html=0, author=0, milestone=None):
cfg = Configuration(cfgfilename)
cfg.log(sourcebranch, targetbranch, html=html, author=author, milestone=milestone)
# Command: tickets
def commandTickets(cfgfilename):
cfg = Configuration(cfgfilename)
cfg.listTickets()
################################################################################
# Main Program
################################################################################
# Handle switches
dryrun = 0
html = 0
html_author = 0
html_milestone = None
onlymerge = 0
onlycommit = 0
all = 0
while len(sys.argv)>1 and sys.argv[1][0] == '-':
if sys.argv[1] == "-d":
dryrun = 1
del sys.argv[1:2]
elif sys.argv[1] == "-html":
html = 1
del sys.argv[1:2]
elif sys.argv[1] == "-author":
html_author = 1
del sys.argv[1:2]
elif sys.argv[1] == "-milestone":
html_milestone = sys.argv[2]
del sys.argv[1:3]
elif sys.argv[1] == "-m":
onlymerge = 1
del sys.argv[1:2]
elif sys.argv[1] == "-c":
onlycommit = 1
del sys.argv[1:2]
elif sys.argv[1] == "-all":
all = 1
del sys.argv[1:2]
else:
print "Invalid option '%s'." % (sys.argv[1])
sys.exit(1)
if len(sys.argv) < 2:
help(1)
# Handle commands
if sys.argv[1] == "revert":
commandRevert()
elif (len(sys.argv) == 4 or len(sys.argv)==5) and sys.argv[1] == "massmerge":
cfgfilename = sys.argv[2]
sourcebranch = sys.argv[3]
startfrom = None
if len(sys.argv)>4:
startfrom = int(sys.argv[4])
commandMassmerge(cfgfilename, sourceuri=REPOSITORY+sourcebranch, startfrom=startfrom, dryrun=dryrun, allatonce=all)
elif len(sys.argv) == 5 and sys.argv[1] == "single":
changeset = int(sys.argv[2])
sourcebranch = sys.argv[3]
targetbranch = sys.argv[4]
commandSingle(REPOSITORY+sourcebranch, changeset, targetbranch, onlymerge=onlymerge, onlycommit=onlycommit)
elif len(sys.argv) == 5 and sys.argv[1] == "commit":
cfgfilename = sys.argv[2]
sourcebranch = sys.argv[3]
targetbranch = sys.argv[4]
commandCommit(cfgfilename, sourcebranch, targetbranch, dryrun=dryrun)
elif len(sys.argv) == 5 and sys.argv[1] == "log":
cfgfilename = sys.argv[2]
sourcebranch = sys.argv[3]
targetbranch = sys.argv[4]
commandLog(cfgfilename, sourcebranch, targetbranch, html=html, author=html_author, milestone=html_milestone)
elif len(sys.argv) == 3 and sys.argv[1] == "tickets":
cfgfilename = sys.argv[2]
commandTickets(cfgfilename)
else:
help(1)