diff --git a/.gitignore b/.gitignore index 5afb9d6..906c18c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ config.json *.csv .repos +.logs # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/README.md b/README.md index 30bc1c9..7fdb93d 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,9 @@ Create [Gource animations](https://gource.io/) for an entire organization's repositories! -## Setup +
+ Installation + 1. Install Python 3.8 or later 2. Create a virtual environment ```bash @@ -36,10 +38,17 @@ Create [Gource animations](https://gource.io/) for an entire organization's repo ``` 5. Install `gource` ``` - wget https://github.com/acaudwell/Gource/releases/download/gource-0.54/gource-0.54.tar.gz - tar -xzvf gource-0.54.tar.gz + # Ubuntu 20.04 + sudo apt install gource + ``` +6. Install `ffmpeg` ``` -6. Create a GitHub Personal Access Token (classic) with the `repo` scope. + # Ubuntu 20.04 + sudo apt install ffmpeg + ``` +7. Create a GitHub Personal Access Token (classic) with the `repo` scope. + +
## Usage diff --git a/gource_org.py b/gource_org.py index eb9650a..fadb506 100644 --- a/gource_org.py +++ b/gource_org.py @@ -4,7 +4,8 @@ from pathlib import Path import requests from git import Repo -config = json.load(open('config.json')) +config = json.load(open("config.json")) + def list_repositories(organization_name: str) -> list: response = requests.get( @@ -13,16 +14,78 @@ def list_repositories(organization_name: str) -> list: "Accept": "application/vnd.github+json", "Authorization": f"Bearer {config['token']}", "X-GitHub-Api-Version": "2022-11-28", - } + }, ) return json.loads(response.text) + def clone_repos(repositories: list, dst: Path = Path("./")) -> None: dst.mkdir(exist_ok=True, parents=True) for repository in repositories: print(f"Cloning {repository['name']}") Repo.clone_from(repository["clone_url"], dst / repository["name"]) + +def create_gource_logs(repos_dir: Path, dst: Path = Path("./.logs")) -> None: + if not repos_dir.is_dir(): + raise Exception(f"Invalid repos_dir {repos_dir}. Must be a directory.") + dst.mkdir(exist_ok=True, parents=True) + for repo_dir in repos_dir.iterdir(): + print(f"Creating log for {repo_dir.name}") + os.system(f"gource {repo_dir} --output-custom-log {dst / repo_dir.name}.log") + + +def combine_gource_logs(logs_dir: Path, dst: Path = Path("./gource.log")) -> None: + if not logs_dir.is_dir(): + raise Exception(f"Invalid logs_dir {logs_dir}. Must be a directory.") + logs = [] + for log_file in logs_dir.iterdir(): + with open(log_file) as f: + for line in f: + parts = line.split("|") + edited_file_path = f"{log_file.name[:-len('.log')]}{parts[-1]}" + log_line = "|".join(parts[:-1] + [edited_file_path]) + logs.append(log_line) + dst.write_text("".join(sorted(logs))) + + +def create_gource_visualization( + log_file: Path, dst: Path = Path("./visualization.mp4") +) -> None: + tmp_file = Path("./tmp.ppm") + gource_command = f""" + gource {log_file} \ + --key \ + --viewport 1280x720 \ + --seconds-per-day 0.05 \ + --highlight-users \ + --hide filenames,dirnames,progress \ + --file-idle-time 0 \ + --hide-root \ + -o {tmp_file} + """ + ffmpeg_command = f""" + ffmpeg \ + -y \ + -r 60 \ + -f image2pipe \ + -vcodec ppm \ + -i {tmp_file} \ + -vcodec libx264 \ + -preset veryslow \ + -pix_fmt yuv420p \ + -crf 17 \ + -threads 0 \ + -bf 0 {dst} + """ + os.system(gource_command) + os.system(ffmpeg_command) + os.remove(tmp_file) + + if __name__ == "__main__": repos = list_repositories(config["organization"]) - clone_repos(repos, dst=Path("./.repos")) \ No newline at end of file + clone_repos(repos, dst=Path("./.repos")) + create_gource_logs(Path("./.repos"), dst=Path("./.logs")) + combine_gource_logs(Path("./.logs"), dst=Path("./gource.log")) + create_gource_visualization(Path("./gource.log"), dst=Path("./visualization.mp4"))