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 -ttextTo 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.