February 12, 2008

Python Annotate

Surround SCM

Works with Surround SCM 2008

People sometimes want to see a view of a file which shows who edited each line. This is sometimes called annotate, blame or praise. Below is a (relatively) simple script written in python to produce such a view. You basically type in your command prompt:
python annotate.py SurroundFile -oMyOutput.txt -ttext
To get an annotated view of your file. Your output will look something like:
Version	       User	   Date	                      Code

1              dev1       12/20/2007               //I hope this works
2              dev2       1/1/2007                 //That didn't work
etc.
Save the following code in a file called annotate.py and save it somewhere in your path:
#!/usr/bin/python

import os,sys,getopt

class LineList:
	def __init__(self):
		self.data = []
		self.firstVersion = -1

	def InsertLine(self,newStartNum,newNumLines,newVersion):
		currentLength = len(self.data)
		if currentLength < newStartNum:
			self.data.extend([self.firstVersion]*(newStartNum-currentLength))
		for i in range(newNumLines):
			self.data.insert(newStartNum+i-1,newVersion)

	def DeleteLines(self,oldStartNum,oldNumLines):
		for i in range(oldNumLines):
			if len(self.data) >= oldStartNum:
				del self.data[oldStartNum-1]

	def UpdateLine(self,startNum,numLines,newVersion):
		currentLength = len(self.data)
		if currentLength < startNum+numLines:
			self.data.extend([self.firstVersion]*(startNum+numLines-currentLength))
		for i in range(numLines):
			self.data[startNum+i-1]=newVersion

	def CreateVersionList(self,fileName):
		for line in os.popen("sscm diffreport " + fileName + " -n0"):
			if line.startswith("Record not found"):
				raise Usage("File does not appear to be in Surround")
			elif  line.startswith("---"):
				oldVersion = int(line.split("version")[1].split(" in ")[0])
				if self.firstVersion == -1:
					self.firstVersion = oldVersion
			elif line.startswith("+++"):
				newVersion = int(line.split("version")[1].split(" in ")[0])
			elif line.startswith("@@"):
				changeLine = line.split("-")[1].split("+")
				lhs = changeLine[0].strip().split(",")
				oldStartNum = int(lhs[0])

				if len(lhs) == 2:
					oldNumLines = int(lhs[1])
				else:
					oldNumLines = 1

				rhs = changeLine[1].strip().rstrip("@").split(",")
				newStartNum = int(rhs[0])
				if len(rhs) == 2:
					newNumLines = int(rhs[1])
				else:
					newNumLines = 1

				if oldNumLines == 0:
					self.InsertLine(newStartNum,newNumLines,newVersion)
				elif oldNumLines == 1:
					if newNumLines == 0:
						self.DeleteLines(newStartNum,1)
					elif newNumLines == 1:
						self.UpdateLine(newStartNum,1,newVersion)
					else:
						self.UpdateLine(newStartNum,1,newVersion)
						self.InsertLine(newStartNum+1,newNumLines-1,newVersion)
				else:
					if oldStartNum == newStartNum and oldNumLines == newNumLines:
						self.UpdateLine(newStartNum,newNumLines,newVersion)
					else:
						if oldNumLines > newNumLines:
							self.UpdateLine(newStartNum,newNumLines,newVersion)
							self.DeleteLines(newStartNum+newNumLines,oldNumLines-newNumLines)
						else:
							self.UpdateLine(newStartNum,oldNumLines,newVersion)
							self.InsertLine(newStartNum+oldNumLines,newNumLines-oldNumLines,newVersion)

class HistoryList:
	def __init__(self):
		self.data = {}

	def CreateHistoryList(self,fileName):
		foundAction = False
		foundWholeLine = False
		tempLine = ""
		for line in os.popen("sscm history " + fileName):
			if line.startswith("add") or line.startswith("checkin") or line.startswith("promote") or line.startswith("rebase") or line.startswith("attach") or line.startswith("label") or line.startswith("rollback"):
				if len(line) > 60:
					tempLine = line.strip()
					foundWholeLine = True
				else:
					tempLine = line
			elif line.strip().startswith("Comments"):
				foundWholeLine = True
			else:
				tempLine = tempLine.strip() + "  " + line.strip()
			if foundWholeLine:
				items = tempLine.split("  ")
				user = ""
				version = ""
				for col in items[1:]:
					if col != "":
						if user == "":
							user = col
						elif version == "":
							version = col
						else:
							date = col
							break
				foundWholeLine = False
				if len(version) > 0 and int(version) not in self.data:
					self.data[int(version)] = user,date
				tempLine = ""

class Usage(Exception):
	def __init__(self, msg):
		self.msg = msg

def main(argv=None):
	if argv is None:
		argv = sys.argv
	try:
		try:
			if len(argv) < 2:
				raise Usage("usage: annotate FileName [-oOutputFileName] [-thtml-ttext]")
			fileName = sys.argv[1]
			if not os.path.isfile(fileName):
				raise Usage("usage: annotate FileName [-oOutputFileName] [-thtml-ttext]")

			cwd = os.getcwd()
			os.chdir(os.path.dirname(os.path.realpath(fileName)))
			fileName = os.path.basename(fileName)

			opts, args = getopt.getopt(argv[2:], "o:t:")
			outputName = ""
			htmlOutput = False
			for o,a in opts:
				if o == "-o":
					outputName = a
				if o == "-t":
					if a == "html":
						htmlOutput = True
			if outputName == "":
				if htmlOutput:
					outputName = "annotate.html"
				else:
					outputName = "annotate.txt"
		except getopt.error, msg:
			raise Usage(msg)

		lineVersions = LineList()
		lineVersions.CreateVersionList(fileName)

		historyData = HistoryList()
		historyData.CreateHistoryList(fileName)

		f = open(fileName)
		os.chdir(cwd)
		outFile = open(outputName,"w")
		try:
			if htmlOutput:
				outFile.writelines('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">m')
				outFile.writelines('<html xmlns="http://www.w3.org/1999/xhtml" ><head><meta content="text/html; charset=UTF-8" http-equiv="content-type"></head><body>')
				outFile.writelines('<table cellpadding="0" cellspacing="0" style="border-bottom:solid 1px black;width:100%; border-collapse: collapse;
									 font-family: 'Courier New', Courier, monospace; font-size: small;">')
				outFile.writelines('<thead><tr><th style="width:7%"></th><th style="width:7%"></th><th style="width:15%"></th><th style="width:71%"></th></tr></thead>')
			else:
				outFile.writelines("VersiontUsertDatetCoden")
			lineCount=1
			totalNum = len(lineVersions.data)
			lastVersion = -1
			for line in f:
				if lineCount<totalNum:
					currentVersion = lineVersions.data[lineCount-1]
				else:
					currentVersion = lineVersions.firstVersion
				if htmlOutput:
					if lastVersion == currentVersion:
						border = "border-left:solid 1px black;border-right:solid 1px black"
						versionText = "<td></td><td></td><td></td>"
					else:
						border = "border-left:solid 1px black;border-right:solid 1px black;border-top:solid 1px black"
						lastVersion = currentVersion
						versionData = historyData.data[currentVersion]
						versionText = "<td>Version %s</td><td>User %s</td><td>Date %s</td>" % (lastVersion, versionData[0], versionData[1])
					print >> outFile, "<tr style='white-space:nowrap;text-overflow: ellipsis;%s'>%s<td style='%s'>%s</td></tr>" % (border, versionText, border, line)
				else:
					try:
						 versionData = historyData.data[currentVersion]
						 print >> outFile, "%st%st%st%s" % (currentVersion, versionData[0], versionData[1], line.expandtabs())
					except KeyError, e:
						print "Missing version: " + str(currentVersion)
				lineCount += 1
			if htmlOutput:
				print >> outFile, '</table>'
				print >> outFile, '</body></html>'
		finally:
			 f.close()
		outFile.close()

	except Usage, err:
		print >>sys.stderr, err.msg
		return 2

if __name__ == "__main__":
	 sys.exit(main())
Note: Seapine does not provide support for sample scripts.