#!/usr/bin/env python3 import argparse from dataclasses import dataclass import re import subprocess from pathlib import Path REMOTE_TYPES = { "github": { "match": re.compile(r'git@github.com:(?P<username>[\w\.-]+)/(?P<project>[\w\.-]+).git'), "format-branch": lambda g: f"https://github.com/{g.username}/{g.project}/tree/{g.branch}/", "format-branch-file": lambda g: f"https://github.com/{g.username}/{g.project}/blob/{g.branch}/{g.file}", "format-branch-dir": lambda g: f"https://github.com/{g.username}/{g.project}/tree/{g.branch}/{g.dir}", "format-commit": lambda g: f"https://github.com/{g.username}/{g.project}/commit/{g.commit}/", "format-commit-file": lambda g: f"https://github.com/{g.username}/{g.project}/blob/{g.commit}/{g.file}", "format-commit-dir": lambda g: f"https://github.com/{g.username}/{g.project}/tree/{g.commit}/{g.dir}", }, "gitea": { "match": re.compile(r'(?P<gituser>[\w\.-]+)@(?P<host>[\w\.-]+):(?P<username>[\w\.-]+)/(?P<project>[\w\.-]+).git'), "format-branch": lambda g: f"https://{g.host}/{g.username}/{g.project}/src/branch/{g.branch}/", "format-branch-file": lambda g: f"https://{g.host}/{g.username}/{g.project}/src/branch/{g.branch}/{g.file}", "format-branch-dir": lambda g: f"https://{g.host}/{g.username}/{g.project}/src/branch/{g.branch}/{g.dir}", "format-commit": lambda g: f"https://{g.host}/{g.username}/{g.project}/commit/{g.commit}/", "format-commit-file": lambda g: f"https://{g.host}/{g.username}/{g.project}/src/commit/{g.commit}/{g.file}", "format-commit-dir": lambda g: f"https://{g.host}/{g.username}/{g.project}/src/commit/{g.commit}/{g.dir}", }, } @dataclass class FormatArgs: gituser: str = None host: str = None username: str = None project: str = None commit: str = None branch: str = None file: str = None dir: str = None def is_git_repo(): s = subprocess.run(["git", "rev-parse"], capture_output=True, text=True) return s.returncode == 0 def get_git_dir(): s = subprocess.run(["git", "rev-parse", "--show-toplevel"], capture_output=True, text=True) return Path(s.stdout.strip()) def get_remote_branch(): s = subprocess.run(["git", "status", "--porcelain", "-uno", "-b", "--no-ahead-behind"], capture_output=True, text=True) if s.stdout.startswith("## HEAD (no branch)"): print("Detached head, can't link") exit(1) git_status_branch_info = s.stdout.splitlines()[0][3:].split()[0] branches = git_status_branch_info.split("...") if len(branches) != 2: raise Exception("no branch name found") local_branch, remote_branch = branches remote, branch = remote_branch.split("/", maxsplit=1) return { "remote": remote, "branch": branch, } def get_remote_url(remote): s = subprocess.run(["git", "remote", "get-url", remote], capture_output=True, text=True) remote_url = s.stdout.strip() return remote_url def get_last_commit(): s = subprocess.run(["git", "rev-parse", "HEAD"], capture_output=True, text=True) commit = s.stdout.strip() return commit def main(): parser = argparse.ArgumentParser( prog='git-show-link', ) parser.add_argument("path", nargs="?", default=None, help="Path to link to specific file or directory") parser.add_argument("--branch", dest="display_branch", action='store_true', help="Display link to branch, instead to commit") parser.add_argument("--remote-type", dest="remote_type", choices=REMOTE_TYPES.keys(), help="Specify remote type") args = parser.parse_args() if not is_git_repo(): print("Not a git repo") exit(1) git_dir_path = get_git_dir() r = get_remote_branch() remote_url = get_remote_url(r["remote"]) selected_remote_types = REMOTE_TYPES if args.remote_type is not None: selected_remote_types = { args.remote_type: REMOTE_TYPES[args.remote_type], } remote_type_found = False for remote_type_name, remote_type in selected_remote_types.items(): m = remote_type["match"].match(remote_url) if m is None: continue remote_type_found = True g = FormatArgs(**m.groupdict()) if args.path is not None: path = Path(args.path).absolute() path = path.relative_to(git_dir_path) if path.is_dir(): path = str(path) if path == ".": path = "" else: path += "/" g.dir = path else: g.file = str(path) if g.file is not None: if args.display_branch: g.branch = r["branch"] print(remote_type["format-branch-file"](g)) else: commit = get_last_commit() g.commit = commit print(remote_type["format-commit-file"](g)) elif g.dir is not None: if args.display_branch: g.branch = r["branch"] print(remote_type["format-branch-dir"](g)) else: commit = get_last_commit() g.commit = commit print(remote_type["format-commit-dir"](g)) else: if args.display_branch: g.branch = r["branch"] print(remote_type["format-branch"](g)) else: commit = get_last_commit() g.commit = commit print(remote_type["format-commit"](g)) break if not remote_type_found: print("No remote type matched") exit(1) if __name__ == "__main__": main()