Skip to content

Instantly share code, notes, and snippets.

@jacobabrahamb4
Last active March 18, 2024 12:05
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save jacobabrahamb4/a60624d6274ece7a0bd2d141b53407bc to your computer and use it in GitHub Desktop.
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.
#!/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)
@jacobabrahamb4
Copy link
Author

Updated the script with some minor modifications. Tested working on Ubuntu 20.04.6 LTS.

@jacobabrahamb4
Copy link
Author

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