about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--flake.nix6
-rw-r--r--nix/flake/packages.nix12
-rw-r--r--packages/git-blame-stats/default.nix26
-rwxr-xr-xpackages/git-blame-stats/git-blame-stats.py90
-rw-r--r--packages/git-broom/default.nix26
-rwxr-xr-xpackages/git-broom/git-broom.py313
6 files changed, 1 insertions, 472 deletions
diff --git a/flake.nix b/flake.nix
index 33da294..2cfe728 100644
--- a/flake.nix
+++ b/flake.nix
@@ -43,10 +43,6 @@
         "x86_64-linux"
       ];
 
-      imports = [
-        ./nix/flake/devshell.nix
-        ./nix/flake/packages.nix
-        ./nix/flake/hosts.nix
-      ];
+      imports = [ ./nix/flake/devshell.nix ./nix/flake/hosts.nix ];
     };
 }
diff --git a/nix/flake/packages.nix b/nix/flake/packages.nix
deleted file mode 100644
index 16c5ec4..0000000
--- a/nix/flake/packages.nix
+++ /dev/null
@@ -1,12 +0,0 @@
-{ inputs, self, ... }: {
-  imports = [
-    inputs.flake-parts.flakeModules.easyOverlay
-  ];
-
-  perSystem = { pkgs, ... }: {
-    packages = {
-      git-blame-stats = pkgs.callPackage "${self}/packages/git-blame-stats" { };
-      git-broom = pkgs.callPackage "${self}/packages/git-broom" { };
-    };
-  };
-}
diff --git a/packages/git-blame-stats/default.nix b/packages/git-blame-stats/default.nix
deleted file mode 100644
index aab7cfb..0000000
--- a/packages/git-blame-stats/default.nix
+++ /dev/null
@@ -1,26 +0,0 @@
-{ lib, python3, stdenvNoCC, pkgs }:
-
-stdenvNoCC.mkDerivation rec {
-  pname = "git-blame-stats";
-  src = ./git-blame-stats.py;
-  version = "0.1.1";
-
-  nativeBuildInputs = with pkgs; [ python3 ];
-  propagatedBuildInputs = with pkgs; [ python3 ];
-
-  dontUnpack = true;
-  dontBuild = true;
-
-  installPhase = ''
-    mkdir -p $out/bin
-    cp $src $out/bin/${pname}
-  '';
-
-
-  meta = with pkgs.lib; {
-    description = "CLI to reports git blame statistics per author.";
-    license = licenses.mit;
-    platforms = platforms.unix;
-    maintainers = [ ];
-  };
-}
diff --git a/packages/git-blame-stats/git-blame-stats.py b/packages/git-blame-stats/git-blame-stats.py
deleted file mode 100755
index 5f2a43f..0000000
--- a/packages/git-blame-stats/git-blame-stats.py
+++ /dev/null
@@ -1,90 +0,0 @@
-#!/usr/bin/env python3
-
-import argparse
-import subprocess
-from typing import Any
-
-parser = argparse.ArgumentParser()
-parser.add_argument("rev", metavar="revision", type=str, help="the revision", default="HEAD", nargs="?")
-args = parser.parse_args()
-
-authors: dict[str, Any] = dict()
-max_lenght_author = 0
-max_lenght_email = 0
-
-
-def get_files(rev):
-    """Returns a list of files for the repository, at the given path, for the given revision."""
-    tree = subprocess.run(
-        ["git", "ls-tree", "--name-only", "-r", rev],
-        capture_output=True,
-        check=True,
-        encoding="utf-8",
-    )
-    return tree.stdout.splitlines()
-
-
-def line_info(filename, rev):
-    """Generates a set of commit blocks using `git blame` for a file.
-
-    Each block corresponds to the information about a single line of code."""
-    blame = subprocess.run(
-        ["git", "blame", "-w", "--line-porcelain", rev, "--", filename],
-        capture_output=True,
-        encoding="utf-8",
-        check=True,
-    )
-    block = []
-    for line in blame.stdout.splitlines():
-        block.append(line)
-        if line.startswith("\t"):
-            yield block
-            block = []
-
-
-files = get_files(args.rev)
-
-for filename in files:
-    try:
-        for block in line_info(filename.rstrip(), args.rev):
-            author = ""
-            author_email = ""
-            commit = ""
-            skip = False
-            for i, val in enumerate(block):
-                if i == 0:
-                    commit = val.split()[0]
-                    continue
-                if val.startswith("author "):
-                    author = " ".join(val.split()[1:])
-                    continue
-                if val.startswith("author-mail"):
-                    author_email = " ".join(val.split()[1:])
-                    continue
-                if val.startswith("\t") and val == "\t":
-                    skip = True
-            if skip:
-                continue
-            if authors.get(author, None) is None:
-                authors[author] = {
-                    "email": author_email,
-                    "commits": set(),
-                    "files": set(),
-                    "lines": 0,
-                }
-            authors[author]["commits"].add(commit)
-            authors[author]["files"].add(filename)
-            authors[author]["lines"] += 1
-            if len(author) > max_lenght_author:
-                max_lenght_author = len(author)
-            if len(author_email) > max_lenght_email:
-                max_lenght_email = len(author_email)
-    except Exception:
-        continue
-
-for author, stats in authors.items():
-    email = stats["email"]
-    lines = stats["lines"]
-    commits = len(stats["commits"])
-    files = len(stats["files"])
-    print(f"{author:{max_lenght_author}} {email:{max_lenght_email}} {lines:6} {commits:6} {files:6}")
diff --git a/packages/git-broom/default.nix b/packages/git-broom/default.nix
deleted file mode 100644
index fea555f..0000000
--- a/packages/git-broom/default.nix
+++ /dev/null
@@ -1,26 +0,0 @@
-{ lib, python3, stdenvNoCC, pkgs }:
-
-stdenvNoCC.mkDerivation rec {
-  pname = "git-broom";
-  src = ./git-broom.py;
-  version = "0.1.0";
-
-  nativeBuildInputs = with pkgs; [ python3 ];
-  propagatedBuildInputs = with pkgs; [ python3 ];
-
-  dontUnpack = true;
-  dontBuild = true;
-
-  installPhase = ''
-    mkdir -p $out/bin
-    cp $src $out/bin/${pname}
-  '';
-
-
-  meta = with pkgs.lib; {
-    description = "CLI to delete local and remote git branches that have been merged.";
-    license = licenses.mit;
-    platforms = platforms.unix;
-    maintainers = [ ];
-  };
-}
diff --git a/packages/git-broom/git-broom.py b/packages/git-broom/git-broom.py
deleted file mode 100755
index 27b97c6..0000000
--- a/packages/git-broom/git-broom.py
+++ /dev/null
@@ -1,313 +0,0 @@
-#!/usr/bin/env python3
-
-import os
-import re
-import sys
-import logging
-import argparse
-import subprocess
-from typing import Dict, List
-
-logging.basicConfig(format="[%(asctime)s]%(levelname)s:%(message)s", level=logging.INFO)
-
-# regular expression to find the name of the main branch on the remote
-re_match_remote_branch = re.compile(r"ref: refs/heads/(?P<branch>\S+)\tHEAD")
-
-# never delete any branches or references with one of these names
-immortal_ref = ["main", "master", "HEAD"]
-
-# that's how my remotes are usually named, and in that order of preference.
-preferred_remotes = ["origin", "github", "work"]
-
-
-class GitConfig(object):
-    """Represent the configuration for the git repository."""
-
-    def __init__(self) -> None:
-        self.guess_remote()
-        self.guess_primary_branch()
-        self.remote_ref = f"{self.remote_name}/{self.primary_branch}"
-        self.me = os.getenv("USER")
-
-    def guess_remote(self) -> None:
-        """Guess the name and URL for the remote repository.
-
-        If the name of the remote is from the list of preferred remote, we
-        return the name and URL.
-
-        If we don't have a remote set, throw an exception.
-        If we don't find any remote, throw an exception.
-        """
-        candidates = subprocess.run(
-            ["git", "config", "--get-regexp", "remote\.[a-z0-9]+.url"],
-            capture_output=True,
-            check=True,
-            encoding="utf-8",
-        ).stdout.splitlines()
-
-        if len(candidates) == 0:
-            raise ValueError("No remote is defined.")
-
-        remotes = dict()
-
-        for candidate in candidates:
-            parts = candidate.split(" ")
-            remote = parts[0].split(".")[1]
-            url = parts[1]
-            remotes[remote] = url
-
-        for remote in preferred_remotes:
-            if remote in remotes:
-                self.remote_name = remote
-                self.remote_url = remotes[remote]
-                return
-
-        raise ValueError("can't find the preferred remote.")
-
-    def guess_primary_branch(self) -> None:
-        """Guess the primary branch on the remote.
-
-        If we can't figure out the default branch, thrown an exception.
-        """
-        remote_head = subprocess.run(
-            ["git", "ls-remote", "--symref", self.remote_name, "HEAD"],
-            capture_output=True,
-            check=True,
-            encoding="utf-8",
-        ).stdout.splitlines()
-
-        for rh in remote_head:
-            m = re_match_remote_branch.match(rh)
-            if m:
-                self.primary_branch = m.group("branch")
-                return
-
-        raise ValueError(f"can't find the name of the remote branch for {self.remote_name}")
-
-
-def is_git_repository() -> bool:
-    """Check if we are inside a git repository.
-
-    Return True if we are, false otherwise."""
-    res = subprocess.run(["git", "rev-parse", "--show-toplevel"], check=False, capture_output=True)
-    return not res.returncode
-
-
-def fetch(remote: str):
-    """Fetch updates from the remote repository."""
-    subprocess.run(["git", "fetch", remote, "--prune"], capture_output=True, check=True)
-
-
-def ref_sha(ref: str) -> str:
-    """Get the sha from a ref."""
-    res = subprocess.run(["git", "show-ref", ref], capture_output=True, check=True, encoding="utf-8")
-    return res.stdout.rstrip()
-
-
-def get_branches(options: List[str]) -> List[str]:
-    """Get a list of branches."""
-    return subprocess.run(
-        ["git", "branch", "--format", "%(refname:short)"] + options,
-        capture_output=True,
-        check=True,
-        encoding="utf-8",
-    ).stdout.splitlines()
-
-
-def ref_tree(ref: str) -> str:
-    """Get the reference from a tree."""
-    return subprocess.run(
-        ["git", "rev-parse", f"{ref}^{{tree}}"],
-        check=True,
-        capture_output=True,
-        encoding="utf-8",
-    ).stdout.rstrip()
-
-
-def rebase_local_branches(config: GitConfig, local_rebase_tree_id: dict) -> None:
-    """Try to rebase the local branches that have been not been merged."""
-    for branch in get_branches(["--list", "--no-merged"]):
-        _rebase_local_branch(branch, config, local_rebase_tree_id)
-
-
-def _rebase_local_branch(branch: str, config: GitConfig, local_rebase_tree_id: dict) -> None:
-    res = subprocess.run(
-        [
-            "git",
-            "merge-base",
-            "--is-ancestor",
-            config.remote_ref,
-            branch,
-        ],
-        check=False,
-        capture_output=True,
-    )
-    if res.returncode == 0:
-        logging.info(f"local branch {branch} is already a descendant of {config.remote_ref}.")
-        local_rebase_tree_id[branch] = ref_tree(branch)
-        return
-
-    logging.info(f"local branch {branch} will be rebased on {config.remote_ref}.")
-    subprocess.run(["git", "checkout", "--force", branch], check=True, capture_output=True)
-    res = subprocess.run(["git", "rebase", config.remote_ref], check=True, capture_output=True)
-    if res.returncode == 0:
-        logging.info(f"local branch {branch} has been rebased")
-        local_rebase_tree_id[branch] = ref_tree(branch)
-    else:
-        logging.error(f"failed to rebase local branch {branch}.")
-        subprocess.run(["git", "rebase", "--abort"], check=True)
-        subprocess.run(["git", "checkout", "--force", config.primary_branch], check=True)
-        subprocess.run(["git", "reset", "--hard"], check=True)
-
-
-def rebase_remote_branches(config: GitConfig, local_rebase_tree_id: dict, main_sha: str) -> None:
-    for branch in get_branches(["--list", "-r", f"{config.me}/*", "--no-merged", config.remote_ref]):
-        _rebase_remote_branches(branch, config, local_rebase_tree_id, main_sha)
-
-
-def _rebase_remote_branches(branch: str, config: GitConfig, local_rebase_tree_id: dict, main_sha: str) -> None:
-    remote, head = branch.split("/")
-    if head in immortal_ref:
-        return
-
-    res = subprocess.run(
-        ["git", "merge-base", "--is-ancestor", config.remote_ref, branch],
-        check=False,
-        capture_output=True,
-    )
-    if res.returncode == 0:
-        logging.info(f"local branch {branch} is already a descendant of {config.remote_ref}.")
-        return
-
-    logging.info(f"remote branch {branch} will be rebased on {config.remote_ref}.")
-
-    sha = ref_sha(branch)
-    subprocess.run(["git", "checkout", "--force", sha], capture_output=True, check=True)
-    res = subprocess.run(
-        ["git", "rebase", config.remote_ref],
-        capture_output=True,
-        check=True,
-    )
-    if res.returncode == 0:
-        new_sha = ref_sha("--head")
-        short_sha = new_sha[0:8]
-        logging.info(f"remote branch {branch} at {sha} rebased to {new_sha}.")
-        if new_sha == main_sha:
-            logging.info(f"remote branch {branch}, when rebased, is already merged!")
-            logging.info(f"would run `git push {remote} :{head}'")
-        elif new_sha == sha:
-            logging.info(f"remote branch {branch}, when rebased, is unchanged!")
-        elif ref_tree(new_sha) == local_rebase_tree_id.get(head, ""):
-            logging.info(f"remote branch {branch}, when rebased, same as local branch!")
-            logging.info(f"would run `git push --force-with-lease {remote} {head}'")
-        else:
-            logging.info(f"remote branch {branch} has been rebased to create {short_sha}!")
-            logging.info(f"would run `git push --force-with-lease {remote} {new_sha}:{head}'")
-    else:
-        logging.error(f"failed to rebase remote branch {branch}.")
-        subprocess.run(["git", "rebase", "--abort"], check=True)
-        subprocess.run(["git", "checkout", "--force", config.primary_branch], check=True)
-        subprocess.run(["git", "reset", "--hard"], check=True)
-
-
-def destroy_remote_merged_branches(config: GitConfig, dry_run: bool) -> None:
-    """Destroy remote branches that have been merged."""
-    for branch in get_branches(["--list", "-r", f"{config.me}/*", "--merged", config.remote_ref]):
-        remote, head = branch.split("/")
-        if head in immortal_ref:
-            continue
-        logging.info(f"remote branch {branch} has been merged")
-        if dry_run:
-            logging.info(f"would have run git push {remote} :{head}")
-        else:
-            subprocess.run(["git", "push", remote, f":{head}"], check=True, encoding="utf-8")
-
-
-def destroy_local_merged_branches(config: GitConfig, dry_run: bool) -> None:
-    """Destroy local branches that have been merged."""
-    for branch in get_branches(["--list", "--merged", config.remote_ref]):
-        if branch in immortal_ref:
-            continue
-
-        logging.info(f"local branch {branch} has been merged")
-        if dry_run:
-            logging.info(f"would have run git branch --delete --force {branch}")
-        else:
-            subprocess.run(
-                ["git", "branch", "--delete", "--force", branch],
-                check=True,
-                encoding="utf-8",
-            )
-
-
-def workdir_is_clean() -> bool:
-    """Check the git workdir is clean."""
-    res = subprocess.run(
-        ["git", "status", "--porcelain"],
-        check=True,
-        capture_output=True,
-        encoding="utf-8",
-    ).stdout.splitlines()
-    return not len(res)
-
-
-def main(dry_run: bool) -> bool:
-    if not is_git_repository():
-        logging.error("error: run this inside a git repository")
-        return False
-
-    if not workdir_is_clean():
-        logging.error("the git workdir is not clean, commit or stash your changes.")
-        return False
-
-    config = GitConfig()
-
-    # what's our current sha ?
-    origin_main_sha = ref_sha(config.remote_ref)
-
-    # let's get everything up to date
-    fetch(config.remote_name)
-
-    # let's get the new sha
-    main_sha = ref_sha(config.remote_ref)
-
-    if origin_main_sha != main_sha:
-        logging.info(f"we started with {origin_main_sha} and now we have {main_sha}")
-
-    local_rebase_tree_id: Dict[str, str] = dict()
-
-    # try to rebase local branches that have been not been merged
-    rebase_local_branches(config, local_rebase_tree_id)
-
-    # try to rebase remote branches that have been not been merged
-    rebase_remote_branches(config, local_rebase_tree_id, main_sha)
-
-    # let's checkout to main now and see what left to do
-    subprocess.run(
-        ["git", "checkout", "--force", config.primary_branch],
-        check=True,
-        capture_output=True,
-    )
-
-    # branches on the remote that have been merged can be destroyed.
-    destroy_remote_merged_branches(config, dry_run)
-
-    # local branches that have been merged can be destroyed.
-    destroy_local_merged_branches(config, dry_run)
-
-    # TODO: restore to the branch I was on before ?
-    return True
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="delete local and remote branches that have been merged.")
-    parser.add_argument(
-        "--dry-run",
-        action=argparse.BooleanOptionalAction,
-        help="when set to True, do not execute the destructive actions",
-        default=True,
-    )
-    args = parser.parse_args()
-
-    if not main(args.dry_run):
-        sys.exit(1)