Skip to content

Instantly share code, notes, and snippets.

@graingert-coef
Created March 10, 2025 15:33
Show Gist options
  • Save graingert-coef/62f49452f7ea8eef78e3479ca0bc1b67 to your computer and use it in GitHub Desktop.
Save graingert-coef/62f49452f7ea8eef78e3479ca0bc1b67 to your computer and use it in GitHub Desktop.
import argparse
import git
import requirements
def get_version(req):
"""Extracts version from requirement or raises an error if not specified."""
if req.specs:
return req.specs[0][1]
else:
raise ValueError(f"Package {req.name} does not specify a version with '=='.")
def parse_requirements(file_content):
"""Parses content from a requirements.txt file and returns a dictionary of packages and their versions."""
packages = {
req.name: get_version(req)
for req in requirements.parse(file_content)
}
return packages
def compare_requirements(before_io, after_io):
"""Compares two requirements files from IO streams and prints a list of version changes or 'no changes'."""
before_requirements = parse_requirements(before_io)
after_requirements = parse_requirements(after_io)
changes = False
for package in after_requirements:
before_version = before_requirements.get(package, None)
after_version = after_requirements.get(package)
if before_version != after_version:
changes = True
print(f"{package} {before_version} -> {after_version}")
if not changes:
print("no changes")
def get_file_at_commit(repo, commit_hash, file_path):
"""Retrieves file content from a specific commit in a Git repository."""
try:
content = repo.commit(commit_hash).tree[file_path].data_stream.read().decode('utf-8')
except KeyError:
content = "" # File does not exist at this commit
return content
def main():
parser = argparse.ArgumentParser(description='Compare requirements.txt file between two Git commits.')
parser.add_argument('commit_range', nargs='?', default='HEAD', help='The commit range to compare, defaults to HEAD. Formats accepted: <commit> or <start_commit>...<end_commit>')
parser.add_argument('file_path', nargs='?', default='requirements.txt', help='Path to the requirements.txt file within the repository, defaults to "requirements.txt"')
args = parser.parse_args()
repo = git.Repo() # Assumes the script is run within a git repository
if '...' in args.commit_range:
commits = list(repo.iter_commits(args.commit_range))
if not commits:
raise ValueError("No commits found in the specified range.")
start_commit = commits[-1] if len(commits) > 1 else commits[0].parents[0]
end_commit = commits[0]
else:
commit = repo.commit(args.commit_range)
start_commit = commit.parents[0] if commit.parents else None # Use the parent commit if available
end_commit = commit
start_content = get_file_at_commit(repo, start_commit, args.file_path) if start_commit else ""
end_content = get_file_at_commit(repo, end_commit, args.file_path)
compare_requirements(start_content, end_content)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment