#!/usr/bin/env python
#
# git-bbdiff
#
# Created by Dan Preston on 4/09/08.
#
import sys, commands, os, tempfile, filecmp
from optparse import OptionParser
gToolName = "git-bbdiff"
gdiffVersion = gToolName + " version 1.2"
gTempDir = "/tmp/"
gConflictMarker = ".CONFLICT"
gTheirsMarker = ".THEIRS"
#----------------------------------------------------------------------------------------------------------------------------
# fileNotPresentError
#----------------------------------------------------------------------------------------------------------------------------
def fileNotPresentError(repositoryPath, revision, chatty=True):
if chatty:
print "\tFile \"%s\" does not exist in repository at revision \"%s\"." % (repositoryPath, revision)
#----------------------------------------------------------------------------------------------------------------------------
# createTempFileForRevision
#----------------------------------------------------------------------------------------------------------------------------
def createTempFileForRevision( revision, repositoryPath, chatty=True ):
# Escape the path with quotes to handle the case where we have spaces.
command = "git show " + revision + ':"' + repositoryPath + '"'
# Use git show to get the contents of the head of the file.
status, output = commands.getstatusoutput( command )
if status != 0:
fileNotPresentError( repositoryPath, revision, chatty )
return None
# git show doesn't seem to append the final carriage return.
output = output + "\n"
# Create the temp file name and write out the temp file.
nameRoot, extension = os.path.splitext( repositoryPath )
_, fileName = os.path.split( nameRoot )
temp = tempfile.mktemp( "_" + fileName + extension, gToolName + "-" + revision + "_", gTempDir )
f = open( temp, 'w' )
f.write( output )
f.close()
return temp
#----------------------------------------------------------------------------------------------------------------------------
# performDiff
#----------------------------------------------------------------------------------------------------------------------------
def performDiff(compareFile, revision, chatty=True):
# Check that we have enough arguments.
revision, _, revision2 = revision.partition("..")
realpath = os.path.abspath( compareFile )
head, relativePath = os.path.split( realpath )
fileName = relativePath
if os.path.isdir( realpath ):
if chatty:
print "\t\"" + relativePath + "\" is a directory. " + gToolName + " only compares files."
return
found = False;
# Figure out what the relative path of the file is from the .git folder.
while (found == False) and (head != "/"):
gitPath = os.path.join( head, ".git" )
found = os.path.exists( gitPath )
if found == False:
head, tail = os.path.split( head )
relativePath = os.path.join( tail, relativePath )
# Make sure we are actually in a git repository.
if (found == False) and (head == "/"):
if chatty:
print "\tThat file is not in a git repository."
return
file1 = createTempFileForRevision( revision, relativePath, chatty )
if revision2:
file2 = createTempFileForRevision( revision2, relativePath, chatty )
else:
file2 = realpath
if os.path.exists(file2) == False:
fileNotPresentError( relativePath, "HEAD", chatty )
file2 = None
if (file1 == None) or (file2 == None):
return
same = filecmp.cmp( file2, file1 )
if same:
if chatty:
print "\tThe file \"" + fileName + "\" has no differences."
else:
# Escape the files with quotes to allow for files with spaces.
command = 'bbdiff "%s" "%s"' % ( file2, file1 )
status, output = commands.getstatusoutput( command )
#----------------------------------------------------------------------------------------------------------------------------
# cleanTmpDirectory
#----------------------------------------------------------------------------------------------------------------------------
def cleanTmpDirectory(chatty=True):
tempContents = os.listdir( gTempDir )
for tmpFile in tempContents:
if tmpFile.startswith( gToolName + "-" ):
filePath = gTempDir + tmpFile
if chatty:
print "\tRemoving temp file:\"%s\"" % filePath
os.remove( filePath )
#----------------------------------------------------------------------------------------------------------------------------
# numConflicts
#----------------------------------------------------------------------------------------------------------------------------
def numConflicts(conflictFile):
foundStart = False
foundMiddle = False
conflicts = 0
f = open(conflictFile, "r")
line = f.readline()
while line:
if line.startswith("<<<<<<<") and (foundStart == False) and (foundMiddle == False):
foundStart = True
if line.startswith("=======") and foundStart and (foundMiddle == False):
foundMiddle = True
if line.startswith(">>>>>>>") and foundStart and foundMiddle:
conflicts = conflicts + 1
foundStart = False
foundMiddle = False
line = f.readline()
f.close()
return conflicts
#----------------------------------------------------------------------------------------------------------------------------
# renameConflictFile
#----------------------------------------------------------------------------------------------------------------------------
def uniqueName(path, uniqueLabel):
base, extension = os.path.splitext(path)
digit = 2
uniqueDigit = ""
if os.path.exists(base + uniqueLabel + uniqueDigit + extension):
uniqueDigit = str(digit)
digit = digit + 1
newPath = base + uniqueLabel + uniqueDigit + extension
return newPath
#----------------------------------------------------------------------------------------------------------------------------
# createConflictFiles
#----------------------------------------------------------------------------------------------------------------------------
def createConflictFiles(minePath, theirPath, conflictFile):
orig = open(conflictFile, "r")
mine = open(minePath, "w")
theirs = open(theirPath, "w")
placeInTheirs = True
placeInMine = True
line = orig.readline()
while line:
if line.startswith("<<<<<<<") and placeInTheirs and placeInMine:
placeInMine = False
elif line.startswith("=======") and placeInTheirs and (placeInMine == False):
placeInTheirs = False
placeInMine = True
elif line.startswith(">>>>>>>") and (placeInTheirs == False) and placeInMine:
placeInTheirs = True
else:
if placeInMine:
mine.write(line)
if placeInTheirs:
theirs.write(line)
line = orig.readline()
orig.close()
mine.close()
theirs.close()
#----------------------------------------------------------------------------------------------------------------------------
# diffConflict
#----------------------------------------------------------------------------------------------------------------------------
def diffConflict(conflictFile, chatty=True):
conflicts = numConflicts(conflictFile)
if (conflicts == 0) and chatty:
basePath, fileName = os.path.split( conflictFile )
print "\tThe file \"%s\" does not contain any conflicts. SKIPPING." % fileName
else:
newConflictPath = uniqueName(conflictFile, gConflictMarker)
os.rename(conflictFile, newConflictPath)
theirPath = uniqueName(conflictFile, gTheirsMarker)
createConflictFiles(conflictFile, theirPath, newConflictPath)
command = 'bbdiff "%s" "%s"' % ( conflictFile, theirPath )
status, output = commands.getstatusoutput( command )
#----------------------------------------------------------------------------------------------------------------------------
# cleanConflictRelatedFiles
#----------------------------------------------------------------------------------------------------------------------------
def cleanConflictRelatedFiles(conflictFile, chatty=True):
filePath, extension = os.path.splitext( conflictFile )
# Remove .CONFLICT files.
tempfile = filePath + gConflictMarker + extension
if os.path.exists( tempfile ):
if chatty:
_, base = os.path.split( filePath )
base = base + gConflictMarker + extension
print "\tDeleting file \"%s\"." % base
command = "rm " + tempfile
os.remove( tempfile )
#remove .THEIRS files.
tempfile = filePath + gTheirsMarker + extension
if os.path.exists( tempfile ):
if chatty:
_, base = os.path.split( filePath )
base = base + gTheirsMarker + extension
print "\tDeleting file \"%s\"." % base
command = "rm " + tempfile
os.remove( tempfile )
#----------------------------------------------------------------------------------------------------------------------------
# main
#----------------------------------------------------------------------------------------------------------------------------
def main(argv=None):
if argv is None:
argv = sys.argv
# Set up options for the command line that we support.
description="A utility to compare files in a git repository using bbedit."
usage = "usage: %prog [--version] | [-h | --help] | [-q | --quiet] [--clean] [[-c | --conflict] | [-r | --revision <revision(s)>]] file1 [file2 ...]"
parser = OptionParser(version=gdiffVersion, description=description, usage=usage)
parser.add_option("-r", "--revision", dest="revision", help="Pass a revision or a revision range using the git syntax (ie: 98d4cf..bfced5) for " + gToolName + " to compare. This option is mutually exclusive to the '-c' option.")
parser.add_option("--clean", action="store_true", dest="cleanUp", default=False, help="Delete all of the temp files that " + gToolName + " has created. If any files are passed as arguments, their --conflict related files (\"" + gConflictMarker + "\", \"" + gTheirsMarker + "\") will be deleted.")
parser.add_option("-q", "--quiet", action="store_false", dest="chatty", default=True, help="Reduce the chatter of " + gToolName + ".")
parser.add_option("-c", "--conflict", action="store_true", dest="conflict", default=False, help="The files being diffed have conflict markers that need to be resolved. This option is mutually exclusive to the '-r' option.")
printUsage = True
(options, args) = parser.parse_args(argv[1:])
if options.cleanUp:
cleanTmpDirectory(options.chatty)
for gitFile in args:
cleanConflictRelatedFiles(gitFile, options.chatty)
return 0
if len(args) > 0:
printUsage = False
# These options are mutually exclusive.
if options.revision and options.conflict:
printUsage = True
# See if we can see any reason that we need to print the usage for the app.
if printUsage:
parser.print_help()
return 2
else:
if options.conflict:
for gitFile in args:
if options.chatty:
print "Diffing Conflict \"%s\"." % gitFile
diffConflict(gitFile, options.chatty)
else:
if options.revision:
rev = options.revision
else:
rev = "HEAD"
for gitFile in args:
if options.chatty:
print "Comparing \"%s\"." % gitFile
performDiff(gitFile, rev, options.chatty)
return 0
if __name__ == "__main__":
sys.exit(main())