aboutsummaryrefslogtreecommitdiffstats
path: root/build/bin/mergetool.py
diff options
context:
space:
mode:
Diffstat (limited to 'build/bin/mergetool.py')
-rwxr-xr-xbuild/bin/mergetool.py682
1 files changed, 682 insertions, 0 deletions
diff --git a/build/bin/mergetool.py b/build/bin/mergetool.py
new file mode 100755
index 0000000000..bc1b23b090
--- /dev/null
+++ b/build/bin/mergetool.py
@@ -0,0 +1,682 @@
+#!/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> <trg> [<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)
+
+ # 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):
+ for change in self.changes:
+ change.merge(trunkURI, dryrun=dryrun)
+
+ 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):
+ cfg = Configuration(cfgfilename, startfrom=startfrom)
+ cfg.massMerge(sourceuri, dryrun=dryrun)
+
+# 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
+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]
+
+ 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)
+
+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)