#!/usr/bin/env python
#
# git-bbdiff
#
# Created by Dan Preston on 4/09/08.
#
import sys, commands, os, tempfile, filecmp
import thread, threading
from optparse import OptionParser
kToolName = "git-bbdiff"
kdiffVersion = kToolName + " version 1.4"
kTempDir = "/tmp/"
kConflictMarker = ".CONFLICT"
kTheirsMarker = ".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, kToolName + "-" + revision + "_", kTempDir )
f = open( temp, 'w' )
f.write( output )
f.close()
return temp
#----------------------------------------------------------------------------------------------------------------------------
# signalDiffProcessed
#----------------------------------------------------------------------------------------------------------------------------
def signalDiffProcessed( condition, diffsLeft ):
condition.acquire()
diffsLeft[0] = diffsLeft[0] - 1
condition.notifyAll()
condition.release()
#----------------------------------------------------------------------------------------------------------------------------
# performDiff
#----------------------------------------------------------------------------------------------------------------------------
def performDiff( compareFile, revision, condition, diffsLeft, chatty=True ):
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. " + kToolName + " only compares files."
signalDiffProcessed( condition, diffsLeft )
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."
signalDiffProcessed( condition, diffsLeft )
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 ):
signalDiffProcessed( condition, diffsLeft )
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" --wait' % ( file2, file1 )
output = commands.getoutput( command )
os.remove( file1 )
if revision2:
os.remove( file2 )
signalDiffProcessed( condition, diffsLeft )
#----------------------------------------------------------------------------------------------------------------------------
# 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
#----------------------------------------------------------------------------------------------------------------------------
# uniqueName
#----------------------------------------------------------------------------------------------------------------------------
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, kConflictMarker )
os.rename( conflictFile, newConflictPath )
theirPath = uniqueName( conflictFile, kTheirsMarker )
createConflictFiles( conflictFile, theirPath, newConflictPath )
command = 'bbdiff "%s" "%s"' % ( conflictFile, theirPath )
output = commands.getoutput( command )
#----------------------------------------------------------------------------------------------------------------------------
# cleanConflictRelatedFiles
#----------------------------------------------------------------------------------------------------------------------------
def cleanConflictRelatedFiles( conflictFile, chatty=True ):
filePath, extension = os.path.splitext( conflictFile )
# Remove .CONFLICT files.
tempfile = filePath + kConflictMarker + extension
if os.path.exists( tempfile ):
if chatty:
_, base = os.path.split( filePath )
base = base + kConflictMarker + extension
print "\tDeleting file \"%s\"." % base
command = "rm " + tempfile
os.remove( tempfile )
#remove .THEIRS files.
tempfile = filePath + kTheirsMarker + extension
if os.path.exists( tempfile ):
if chatty:
_, base = os.path.split( filePath )
base = base + kTheirsMarker + extension
print "\tDeleting file \"%s\"." % base
command = "rm " + tempfile
os.remove( tempfile )
#----------------------------------------------------------------------------------------------------------------------------
# compareModified
#----------------------------------------------------------------------------------------------------------------------------
def compareModified( chatty=True ):
command = "git status"
output = commands.getoutput( command )
lines = output.split( "\n" )
modifiedFiles = []
for line in lines:
modifiedFile = line.partition( "modified:" )[2].strip()
if modifiedFile != "":
modifiedFiles.append( modifiedFile )
if len( modifiedFiles ) > 0:
compareFiles( modifiedFiles, "HEAD", chatty )
elif chatty:
print( "There are no modified files." )
#----------------------------------------------------------------------------------------------------------------------------
# compareFiles
#----------------------------------------------------------------------------------------------------------------------------
def compareFiles( files, revision, chatty=True ):
diffsLeft = [len( files )];
# Create a condition lock to ensure that we wait until all the diffs are done since we use the --wait option in bbdiff.
condition = threading.Condition()
for gitFile in files:
if chatty:
print "Comparing \"%s\"." % gitFile
thread.start_new_thread( performDiff, (gitFile, revision, condition, diffsLeft, chatty) )
condition.acquire()
while diffsLeft[0] > 0:
try:
condition.wait()
except KeyboardInterrupt, e:
print( "\nProcess terminated.")
condition.release()
#----------------------------------------------------------------------------------------------------------------------------
# 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] | [-q | --quiet] [--clean file1 [file2 ...]] [[-c | --conflict] | [-r | --revision <revision(s)>]] file1 [file2 ...]"
parser = OptionParser(version=kdiffVersion, 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 " + kToolName + " to compare. This option is mutually exclusive to the '-c' option." )
parser.add_option( "-m", "--modified", action="store_true", dest="modified", help="Compare files that have been modified against HEAD." )
parser.add_option( "--clean", action="store_true", dest="cleanUp", default=False, help="Conflict (--conflict) related files (\"" + kConflictMarker + "\", \"" + kTheirsMarker + "\") will be deleted for any files that are passed as arguments. Requires at least one file path as an argument." )
parser.add_option( "-q", "--quiet", action="store_false", dest="chatty", default=True, help="Reduce the chatter of " + kToolName + "." )
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:
if len( args ) > 0:
for gitFile in args:
cleanConflictRelatedFiles( gitFile, options.chatty )
return 0
elif options.modified:
compareModified( options.chatty )
return 0
elif 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"
compareFiles( args, rev, options.chatty )
return 0
if __name__ == "__main__":
sys.exit(main())