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) |
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
Updated the script with some minor modifications. Tested working on Ubuntu 20.04.6 LTS.