src\wily\archivers\git.py
-- -- --- --- --- --- --- --- ------- ------- ------- | """
-- -- --- --- --- --- --- --- ------- ------- ------- | Git Archiver.
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- | Implementation of the archiver API for the gitpython module.
-- -- --- --- --- --- --- --- ------- ------- ------- | """
-- -- --- --- --- --- --- --- ------- ------- ------- | import logging
-- -- --- --- --- --- --- --- ------- ------- ------- | from typing import Any, Dict, List, Tuple
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- | import git.exc
-- -- --- --- --- --- --- --- ------- ------- ------- | from git import Commit
-- -- --- --- --- --- --- --- ------- ------- ------- | from git.repo import Repo
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- | from wily.archivers import BaseArchiver, Revision
-- -- --- --- --- --- --- --- ------- ------- ------- | from wily.config.types import WilyConfig
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- | logger = logging.getLogger(__name__)
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
01 -- --- --- --- --- --- --- ------- ------- ------- | class InvalidGitRepositoryError(Exception):
01 -- --- --- --- --- --- --- ------- ------- ------- | """Error for when a folder is not a git repo."""
01 -- --- --- --- --- --- --- ------- ------- ------- |
01 -- --- --- --- --- --- --- ------- ------- ------- | pass
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
02 -- --- --- --- --- --- --- ------- ------- ------- | class DirtyGitRepositoryError(Exception):
02 -- --- --- --- --- --- --- ------- ------- ------- | """Error for a dirty git repository (untracked files)."""
02 -- --- --- --- --- --- --- ------- ------- ------- |
02 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | def __init__(self, untracked_files: List[str]):
02 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | """
02 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | Raise error for untracked files.
02 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
02 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | :param untracked_files: List of untracked files
02 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | :param untracked_files: ``list``
02 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | """
02 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | self.untracked_files = untracked_files
02 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | self.message = "Dirty repository, make sure you commit/stash files first"
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | def get_tracked_files_dirs(repo: Repo, commit: Commit) -> Tuple[List[str], List[str]]:
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | """Get tracked files in a repo for a commit hash using ls-tree."""
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | paths = repo.git.execute(
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | ["git", "ls-tree", "--name-only", "--full-tree", "-r", commit.hexsha],
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | with_extended_output=False,
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | as_process=False,
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | stdout_as_string=True,
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | ).split("\n")
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | dirs = [""] + repo.git.execute(
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | ["git", "ls-tree", "--name-only", "--full-tree", "-r", "-d", commit.hexsha],
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | with_extended_output=False,
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | as_process=False,
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | stdout_as_string=True,
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | ).split("\n")
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | return paths, dirs
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | def whatchanged(
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | commit_a: Commit, commit_b: Commit
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | ) -> Tuple[List[str], List[str], List[str]]:
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | """Get files added, modified and deleted between commits."""
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | diffs = commit_b.diff(commit_a)
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | added_files = []
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | modified_files = []
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | deleted_files = []
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | for diff in diffs:
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | if diff.new_file:
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | added_files.append(diff.b_path)
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | elif diff.deleted_file:
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | deleted_files.append(diff.a_path)
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | elif diff.renamed_file:
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | added_files.append(diff.b_path)
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | deleted_files.append(diff.a_path)
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | elif diff.change_type == "M":
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | modified_files.append(diff.a_path)
-- 06 001 002 001 002 003 003 0004.75 0002.38 0000.50 | return added_files, modified_files, deleted_files
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
03 -- --- --- --- --- --- --- ------- ------- ------- | class GitArchiver(BaseArchiver):
03 -- --- --- --- --- --- --- ------- ------- ------- | """Gitpython implementation of the base archiver."""
03 -- --- --- --- --- --- --- ------- ------- ------- |
03 -- --- --- --- --- --- --- ------- ------- ------- | name = "git"
03 -- --- --- --- --- --- --- ------- ------- ------- |
03 03 001 001 001 001 002 002 0002.00 0001.00 0000.50 | def __init__(self, config: "WilyConfig"):
03 -- 001 001 001 001 002 002 0002.00 0001.00 0000.50 | """
03 -- 001 001 001 001 002 002 0002.00 0001.00 0000.50 | Instantiate a new Git Archiver.
03 -- 001 001 001 001 002 002 0002.00 0001.00 0000.50 |
03 -- 001 001 001 001 002 002 0002.00 0001.00 0000.50 | :param config: The wily configuration
03 -- 001 001 001 001 002 002 0002.00 0001.00 0000.50 | """
03 -- 001 001 001 001 002 002 0002.00 0001.00 0000.50 | try:
03 -- 001 001 001 001 002 002 0002.00 0001.00 0000.50 | self.repo = Repo(config.path)
03 -- 001 001 001 001 002 002 0002.00 0001.00 0000.50 | except git.exc.InvalidGitRepositoryError as e:
03 -- 001 001 001 001 002 002 0002.00 0001.00 0000.50 | raise InvalidGitRepositoryError from e
03 -- 001 001 001 001 002 002 0002.00 0001.00 0000.50 |
03 -- 001 001 001 001 002 002 0002.00 0001.00 0000.50 | self.config = config
03 -- 001 001 001 001 002 002 0002.00 0001.00 0000.50 | if self.repo.head.is_detached:
03 -- 001 001 001 001 002 002 0002.00 0001.00 0000.50 | self.current_branch = self.repo.head.object.hexsha
03 -- 001 001 001 001 002 002 0002.00 0001.00 0000.50 | else:
03 -- 001 001 001 001 002 002 0002.00 0001.00 0000.50 | self.current_branch = self.repo.active_branch
03 -- 001 001 001 001 002 002 0002.00 0001.00 0000.50 | assert not self.repo.bare, "Not a Git repository"
03 -- --- --- --- --- --- --- ------- ------- ------- |
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | def revisions(self, path: str, max_revisions: int) -> List[Revision]:
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | """
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | Get the list of revisions.
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 |
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | :param path: the path to target.
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | :param max_revisions: the maximum number of revisions.
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 |
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | :return: A list of revisions.
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | """
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | if self.repo.is_dirty():
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | raise DirtyGitRepositoryError(self.repo.untracked_files)
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 |
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | revisions: List[Revision] = []
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | for commit in self.repo.iter_commits(
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | self.current_branch, max_count=max_revisions, reverse=True
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | ):
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | tracked_files, tracked_dirs = get_tracked_files_dirs(self.repo, commit)
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | if not commit.parents or not revisions:
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | added_files = tracked_files
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | modified_files = []
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | deleted_files = []
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | else:
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | added_files, modified_files, deleted_files = whatchanged(
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | commit, self.repo.commit(commit.hexsha + "~1")
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | )
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 |
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | logger.debug(f"For revision {commit.name_rev.split(' ')[0]} found:")
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | logger.debug(f"Tracked files: {tracked_files}")
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | logger.debug(f"Tracked directories: {tracked_dirs}")
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | logger.debug(f"Added files: {added_files}")
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | logger.debug(f"Modified files: {modified_files}")
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | logger.debug(f"Deleted files: {deleted_files}")
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 |
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | rev = Revision(
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | key=commit.name_rev.split(" ")[0],
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | author_name=commit.author.name,
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | author_email=commit.author.email,
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | date=commit.committed_date,
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | message=str(commit.message),
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | tracked_files=tracked_files,
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | tracked_dirs=tracked_dirs,
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | added_files=added_files,
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | modified_files=modified_files,
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | deleted_files=deleted_files,
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | )
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | revisions.append(rev)
03 05 004 007 005 007 011 012 0041.51 0083.03 0002.00 | return revisions[::-1]
03 -- --- --- --- --- --- --- ------- ------- ------- |
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | def checkout(self, revision: Revision, options: Dict[Any, Any]) -> None:
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | """
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | Checkout a specific revision.
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | :param revision: The revision identifier.
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | :param options: Any additional options.
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | """
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | rev = revision.key
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | self.repo.git.checkout(rev)
03 -- --- --- --- --- --- --- ------- ------- ------- |
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | def finish(self):
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | """
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | Clean up any state if processing completed/failed.
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | For git, will checkout HEAD on the original branch when finishing
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | """
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | self.repo.git.checkout(self.current_branch)
03 01 000 000 000 000 000 000 0000.00 0000.00 0000.00 | self.repo.close()
03 -- --- --- --- --- --- --- ------- ------- ------- |
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | def find(self, search: str) -> Revision:
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | """
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | Search a string and return a single revision.
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 |
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | :param search: The search term.
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 |
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | :return: An instance of revision.
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | """
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | commit = self.repo.commit(search)
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | tracked_files, tracked_dirs = get_tracked_files_dirs(self.repo, commit)
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | if not commit.parents:
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | added_files = tracked_files
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | modified_files = []
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | deleted_files = []
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | else:
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | added_files, modified_files, deleted_files = whatchanged(
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | commit, self.repo.commit(commit.hexsha + "~1")
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | )
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 |
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | return Revision(
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | key=commit.name_rev.split(" ")[0],
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | author_name=commit.author.name,
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | author_email=commit.author.email,
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | date=commit.committed_date,
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | message=str(commit.message),
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | tracked_files=tracked_files,
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | tracked_dirs=tracked_dirs,
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | added_files=added_files,
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | modified_files=modified_files,
03 02 002 003 002 003 005 005 0011.61 0011.61 0001.00 | deleted_files=deleted_files,
-- -- 002 003 002 003 005 005 0011.61 0011.61 0001.00 | )