Last active
March 18, 2024 12:05
-
-
Save jacobabrahamb4/a60624d6274ece7a0bd2d141b53407bc to your computer and use it in GitHub Desktop.
Display diff between two commits. Tested working on ubuntu and Windows. Install beyond compare or meld before use. Works well with small-medium projects.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
""" | |
How to use this script? | |
----------------------- | |
Prerequisites: | |
1. python3, git and Beyond Compare/meld should be installed. | |
LINUX: | |
1. Create a directory ~/bin in your home directory. | |
2. Copy the script 'bdiff.py' to the ~/bin directory | |
3. Open the shell go to the directory ~/bin and do 'chmod +x bdiff.py' | |
4. Open the ~/.bashrc file and add the ~/bin directory to the PATH variable, | |
add the following line at the end of the file and save the ~/.bashrc file | |
export PATH=$PATH:~/bin | |
5. Execute the following command to source the ~.bashrc file. | |
source ~/.bashrc | |
6. Go to the project directory in shell. | |
Examples: | |
Usage: bdiff.py <project_folder> <commit_id_one> <commit_id_two> | |
Example: bdiff.py . 0 1 | |
Example: bdiff.py . fhejk7fe d78ewg9we | |
Example: bdiff.py . 0 d78ewg9we | |
Example: bdiff.py . HEAD d78ewg9we | |
Example: bdiff.py . HEAD-4 87 | |
WINDOWS: | |
1. Create a directory C:\bin\ | |
2. Copy the script bdiff.py to the C:\bin\ directory. | |
3. Add the paths C:\Program Files\Beyond Compare\ and C:\Program Files\Meld\ to the PATH environment variable | |
4. Open the project directory in windows cmd. | |
Examples: | |
Usage: py.exe C:\bin\bdiff.py <project_folder> <commit_id_one> <commit_id_two> | |
Example: py.exe C:\bin\bdiff.py . 0 1 | |
Example: py.exe C:\bin\bdiff.py . fhejk7fe d78ewg9we | |
Example: py.exe C:\bin\bdiff.py . 0 d78ewg9we | |
Example: py.exe C:\bin\bdiff.py . HEAD d78ewg9we | |
Example: py.exe C:\bin\bdiff.py . HEAD-4 87 | |
""" | |
import sys, subprocess, os, shutil | |
DIFF_TOOLS = {'posix' : ['bcompare', 'meld'], 'nt' : ['BComp.exe', 'Meld.exe']} | |
WHICH = {'posix' : 'which', 'nt' : 'where'} | |
DU = {'posix' : ['du', '-sh'], 'nt' : None} | |
RM = {'posix' : ['rm', '-rf'], 'nt' : ['rmdir']} | |
SLASH = {'posix' : '/', 'nt' : '\\'} | |
CMD_TOOLS = [] | |
HEAD = 'HEAD' | |
TMP = {'posix': '/tmp/', 'nt' : 'C:\Temp\\'} | |
COMMIT_ID_LENGTH = 7 | |
PACKAGES = ['shutil'] | |
class OS(object): | |
_instance = None | |
def __init__(self): | |
raise RuntimeError('Call instance() instead!') | |
@classmethod | |
def instance(cls): | |
if cls._instance is None: | |
print('Creating new instance') | |
cls._instance = cls.__new__(cls) | |
return cls._instance | |
def get_diff_tools(self): | |
return DIFF_TOOLS[os.name] | |
def which(self): | |
return WHICH[os.name] | |
def tmp(self): | |
return TMP[os.name] | |
def du(self): | |
return DU[os.name] | |
def rm(self): | |
return RM[os.name] | |
def slash(self): | |
return SLASH[os.name] | |
def isWindows(self): | |
return os.name == 'nt' | |
def isLinux(self): | |
return os.name == 'posix' | |
class Shell(object): | |
_instance = None | |
def __init__(self): | |
raise RuntimeError('Call instance() instead!') | |
@classmethod | |
def instance(cls): | |
if cls._instance is None: | |
print('Creating new instance') | |
cls._instance = cls.__new__(cls) | |
return cls._instance | |
def execute_command(self, command): | |
print("Executing command: " + str(command)) | |
if command: return subprocess.check_output(command).decode() | |
return None | |
def which(self, tool): | |
return self.execute_command([OS.instance().which(), tool]).strip() | |
def dir_name(self, commit): | |
return commit if commit else None | |
def tmp(self, name): | |
return OS.instance().tmp() + self.dir_name(name) + OS.instance().slash() | |
def get_dir_size(self, name): | |
du = OS.instance().du() | |
if du and isinstance(du, list): du.extend([name]) | |
else: | |
return '' | |
return self.execute_command(du).strip() | |
def cleanup(self, commit): | |
dir = self.tmp(commit) | |
if not os.path.exists(dir): return | |
if OS.instance().isWindows(): | |
shutil.rmtree(dir) | |
else: | |
rm = OS.instance().rm() | |
if rm and isinstance(rm, list): | |
rm.extend([self.tmp(commit)]) | |
try: | |
self.execute_command(rm) | |
except FileNotFoundError: | |
pass | |
def print_usage(): | |
if OS.instance().isLinux(): | |
print('Usage: bdiff.py <project_folder> <commit_id_one> <commit_id_two>\n' | |
'Example: bdiff.py . 0 1\n' | |
'Example: bdiff.py . fhejk7fe d78ewg9we\n' | |
'Example: bdiff.py . 0 d78ewg9we\n' | |
'Example: bdiff.py . HEAD d78ewg9we\n' | |
'Example: bdiff.py . HEAD-4 87\n') | |
else: | |
print('Usage: py.exe C:\bin\bdiff.py <project_folder> <commit_id_one> <commit_id_two>\n' | |
'Example: py.exe C:\bin\bdiff.py . 0 1\n' | |
'Example: py.exe C:\bin\bdiff.py . fhejk7fe d78ewg9we\n' | |
'Example: py.exe C:\bin\bdiff.py . 0 d78ewg9we\n' | |
'Example: py.exe C:\bin\bdiff.py . HEAD d78ewg9we\n' | |
'Example: py.exe C:\bin\bdiff.py . HEAD-4 87\n') | |
class Env(object): | |
def __init__(self): | |
self.__diff_tool = None | |
self.shell = Shell.instance() | |
self.os = OS.instance() | |
def check_dependencies(self): | |
status = True | |
found = False | |
for tool in self.os.get_diff_tools(): | |
if self.__check_tool(tool): | |
print('Diff tool found: ' + tool) | |
found = True | |
self.__diff_tool = tool | |
break | |
if not found: | |
self.__print_install('commandline tool ' + self.os.get_diff_tools()[0] + ' or ' + self.os.get_diff_tools()[1]) | |
status = False | |
for tool in CMD_TOOLS: | |
if self.__check_tool(tool): | |
print('Commandline tool already installed: ' + tool) | |
else: | |
self.__print_install('commandline tool ' + tool) | |
status = False | |
for package in PACKAGES: | |
if self.__check_package(package): | |
print('Python package already installed: ' + package) | |
else: | |
self.__print_install('python package ' + package) | |
status = False | |
return status | |
def get_diff_tool(self): | |
return self.__diff_tool | |
def __check_tool(self, tool): | |
try: | |
output = self.shell.which(tool) | |
if tool in output: | |
print("Already installed: " + tool) | |
except subprocess.CalledProcessError: | |
return False | |
return True | |
def __print_install(self, str): | |
print('Please install ' + str + ' before use!') | |
def __check_package(self, package): | |
try: | |
__import__(package) | |
except ImportError as e: | |
print("Unable to check package: " + package) | |
return False | |
return True | |
def parse_opt(self, args): | |
args_length = len(args) | |
cwd = os.getcwd() | |
if args_length == 2: | |
return cwd, args[1] + '-1', args[1] | |
elif args_length == 3: | |
return cwd, args[1], args[2] | |
elif args_length == 4: | |
return args[1], args[2], args[3] | |
else: | |
return None | |
class Executor(object): | |
def __init__(self, name, first, second): | |
self.shell = Shell.instance() | |
self.name = name | |
self.first = first | |
self.second = second | |
self.commit1 = None | |
self.commit2 = None | |
def __get_commit_id(self, position): | |
print('Get commit id: ' + position) | |
index = -1 | |
command = ['git', '-C', self.name, 'log', '--pretty=format:"%h"', '--abbrev=' + str(COMMIT_ID_LENGTH)] | |
#reverse = ['--reverse'] | |
if position == HEAD: | |
return self.__rev_parse(position) | |
elif HEAD in position and ('-' in position or '~' in position): | |
try: | |
return self.__rev_parse(position) | |
except ValueError: | |
print("Error parsing commit ids!") | |
return None | |
elif HEAD in position: | |
print("Error parsing commit ids. Wrong commandline arguments!") | |
return None | |
elif '-' in position: | |
commitid = position.split()[0][:COMMIT_ID_LENGTH] | |
index = self.shell.execute_command(command).splitlines().index(commitid) | |
try: | |
index -= int(position.split()[1]) | |
except ValueError: | |
print("Unable to parse the input!") | |
return None | |
elif position.isdigit(): | |
#command.extend(reverse) | |
index = int(position) | |
else: | |
return position[:COMMIT_ID_LENGTH] | |
if index >= 0: | |
logs = self.shell.execute_command(command).splitlines() | |
commitid = logs[index].strip().replace('"', '') | |
print('Commit id: ----------->' + commitid) | |
return commitid | |
else: | |
return None | |
def validate(self): | |
self.commit1, self.commit2 = self.__get_commit_id(first), self.__get_commit_id(second) | |
if not self.commit1 and self.commit2: | |
print("Unable to get the commit ids!") | |
return False | |
if self.__validate(self.commit1) and self.__validate(self.commit2) is False: | |
return False | |
return True | |
def execute(self, tool): | |
if not self.__checkout(): | |
print('Unable to checkout the project. May be you are running out of free space!') | |
return False | |
if not self.__compare(tool): | |
return False | |
return True | |
def __checkout(self): | |
return self.__checkout_commit(self.commit1) and self.__checkout_commit(self.commit2) | |
def __checkout_commit(self, commit): | |
if commit: | |
print('Cloning the project to temp directory to compare. Project size: ' + self.shell.get_dir_size(self.name) + | |
' Please wait. This may take time!') | |
self.shell.execute_command(['git', 'clone', self.name, self.shell.tmp(commit)]) | |
self.shell.execute_command(['git', '-C', self.shell.tmp(commit), 'checkout', commit]) | |
else: | |
#if not self.shell.execute_command(['mkdir', self.shell.tmp('0')]): return False | |
return False | |
return True | |
def __validate(self, commit): | |
if not commit: | |
print("Invalid commit id!") | |
return False | |
try: | |
self.shell.execute_command(['git', '-C', self.name, 'cat-file', '-t', commit]) | |
except subprocess.CalledProcessError: | |
return False | |
return True | |
def __rev_parse(self, position): | |
if '-' in position: | |
position.replace('-', '~') | |
command = ['git', '-C', self.name, 'rev-parse', position] | |
return self.shell.execute_command(command).strip() | |
def cleanup(self): | |
if not self.__cleanup(self.commit1) and self.__cleanup(self.commit2): | |
print('Unable to remove temporary files!') | |
def __cleanup(self, commit): | |
return self.shell.cleanup(commit) | |
def __compare(self, tool): | |
return self.shell.execute_command([tool, self.shell.tmp(self.commit1), self.shell.tmp(self.commit2)]) | |
if __name__ == '__main__': | |
env = Env() | |
if not env.check_dependencies(): | |
Shell.instance().print_usage() | |
sys.exit(1) | |
name, first, second = env.parse_opt(sys.argv) | |
if not name or not first or not second: | |
print('Unable to parse the commandline!') | |
Shell.instance().print_usage() | |
sys.exit(1) | |
executor = Executor(name, first, second) | |
if not executor.validate(): | |
print('Validation failed. Exiting!') | |
sys.exit(1) | |
executor.cleanup() | |
try: | |
executor.execute(env.get_diff_tool()) | |
except KeyboardInterrupt: | |
pass | |
finally: | |
executor.cleanup() | |
sys.exit(0) |
Python3 update
#!/usr/bin/env python3 import sys, subprocess, os TOOLS = ['bcompare', 'meld'] HEAD = 'HEAD' TMP = '/tmp/' def execute (command): return subprocess.check_output(command).decode() def which(tool): return execute(['which', tool]).strip() def getTool(): for tool in TOOLS: try: out = which(tool) if tool in out: return tool except subprocess.CalledProcessError: pass return None def printUsageAndExit(): print('Usage: python bdiff.py <project> <commit_one> <commit_two>\n' 'Example: python bdiff.py <project> 0 1\n' 'Example: python bdiff.py <project> fhejk7fe d78ewg9we\n' 'Example: python bdiff.py <project> 0 d78ewg9we\n' 'Example: python bdiff.py <project> HEAD d78ewg9we\n' 'Example: python bdiff.py <project> HEAD-4 87\n') sys.exit(0) def revParse(name, position): if '-' in position: position.replace('-', '~') command = ['git', '-C', name, 'rev-parse', position] return execute(command).strip() def getCommitId(name, position): index = -1; command = ['git', '-C', name, 'log', '--pretty=format:"%h"', '--abbrev=7'] reverse = ['--reverse'] if position == HEAD: return revParse(name, position) elif HEAD in position and ('-' in position or '~' in position): try: return revParse(name, position) except ValueError: print("Error in parsing commit ids!") sys.exit(0) elif HEAD in position: print("Error in parsing commit ids!") sys.exit(-1) elif '-' in position: commitid = position.split()[0][:7] index = execute(command).splitlines().index(commitid) try: index -= int(position.split()[1]) except ValueError: print("Unable to paser the input!") sys.exit(-1) elif position.isdigit(): command.extend(reverse) index = int(position) else: return position if index >= 0: logs = execute(command).splitlines() return logs[index].strip() else: return None def dir_name(commit): return commit if commit else '0' def tmp(name): return TMP + dir_name(name) def validate(name, commit): if not commit: print("Nothing to do, exit!") return False try: if commit: execute(['git', '-C', name, 'cat-file', '-t', commit]) except subprocess.CalledProcessError: return False return True cleanup = lambda commit: execute(['rm', '-rf', tmp(commit)]) def checkoutCommit(name, commit): if commit: execute(['git', 'clone', name, tmp(commit)]) execute(['git', '-C', tmp(commit), 'checkout', commit]) else: execute(['mkdir', tmp('0')]) def compare(tool, commit1, commit2): execute([tool, tmp(commit1), tmp(commit2)]) def parseOpt(): if len(sys.argv) == 2: return os.getcwd(), sys.argv[1] + '-1', sys.argv[1] elif len(sys.argv) == 3: return os.getcwd(), sys.argv[1], sys.argv[2] elif len(sys.argv) == 4: return sys.argv[1], sys.argv[2], sys.argv[3] else: printUsageAndExit() if __name__ == '__main__': tool = getTool() if not tool: print("No GUI diff tools, install bcompare or meld") sys.exit(0) name, first, second = parseOpt() commit1, commit2 = getCommitId(name, first), getCommitId(name, second) if validate(name, commit1) and validate(name, commit2) is False: sys.exit(0) cleanup(commit1), cleanup(commit2) try: checkoutCommit(name, commit1), checkoutCommit(name, commit2) compare(tool, commit1, commit2) except KeyboardInterrupt: pass finally: cleanup(commit1), cleanup(commit2) sys.exit(0)(github won't let me just attach file, that is why I am adding this as comment)
Please fork and make changes.
How to use it??
Please refer the printUsageAndExit function.
Updated the script to support python 3. Works on ubuntu. Not built for Windows.
Updated the script with some minor modifications. Tested working on Ubuntu 20.04.6 LTS.
Tested working on Ubuntu 20.04.6 LTS and Windows 11.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
It is assumed 'meld' is installed:
local_git_dir$ bdiff.py . 15d3c2ec3d007a5f0a6387a4998dc6fbcd9d93f5 c182600f5ff2491964af59c32933efa73575949d
or
local_git_dir$ bdiff.py . HEAD c182600f5ff2491964af59c32933efa73575949d
or
$ bdiff.py local_git_dir HEAD c182600f5ff2491964af59c32933efa73575949d
The following will all error out (even after replacing "catch" with "except"):
$ bdiff.py local_git_dir 0 1
$ bdiff.py local_git_dir HEAD HEAD-1