diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd32419..da749dc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -116,35 +116,19 @@ jobs: echo ::set-output name=docker_tags::devel echo ::set-output name=snap_channel::edge fi + echo ::set-output name=tag::${GITHUB_REF#refs/tags/} git log --pretty='format:%d%n- %s%n%b---' $(git tag --sort=v:refname | tail -n2 | head -n1)..HEAD > _CHANGES.md - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') - id: create_release - uses: actions/create-release@v1 + uses: softprops/action-gh-release@v1 env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} with: - tag_name: ${{ github.ref }} - release_name: git-fame ${{ github.ref }} stable + name: git-fame ${{ steps.collect_assets.outputs.tag }} stable body_path: _CHANGES.md draft: true - - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: dist/${{ steps.dist.outputs.whl }} - asset_name: ${{ steps.dist.outputs.whl }} - asset_content_type: application/zip - - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: dist/${{ steps.dist.outputs.whl_asc }} - asset_name: ${{ steps.dist.outputs.whl_asc }} - asset_content_type: text/plain + files: | + dist/${{ steps.dist.outputs.whl }} + dist/${{ steps.dist.outputs.whl_asc }} - uses: snapcore/action-build@v1 id: snap_build - if: github.event_name == 'push' && steps.collect_assets.outputs.snap_channel diff --git a/.gitignore b/.gitignore index 76836fb..c5eecc9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,17 @@ *.py[cod] - -# C extensions *.so +__pycache__/ # Packages /gitfame/_dist_ver.py -/.eggs/ -/*.egg-info +/*.egg*/ /build/ /dist/ /git-fame_*_amd64.snap /.dockerignore # Unit test / coverage reports -.tox/ -.coverage -__pycache__/ +/.tox/ +/.coverage* +/coverage.xml nosetests.xml - -# Translations -*.mo - -# Mr Developer -.mr.developer.cfg -.project -.pydevproject - -# PyCharm -.idea diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3071409..0513a89 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.0.1 hooks: - id: check-added-large-files - id: check-case-conflict @@ -38,16 +38,16 @@ repos: - tabulate - tqdm - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.4 + rev: 3.9.2 hooks: - id: flake8 - args: ['-j8'] + args: [-j8] additional_dependencies: - flake8-bugbear - flake8-comprehensions - flake8-debugger - flake8-string-format - repo: https://github.com/PyCQA/isort - rev: 5.7.0 + rev: 5.9.2 hooks: - id: isort diff --git a/README.rst b/README.rst index a81ce0e..3458386 100644 --- a/README.rst +++ b/README.rst @@ -202,26 +202,31 @@ Documentation May be multiple comma-separated values. Alters `--loc` default to imply 'ins' (COCOMO) or 'ins,del' (hours). + -R, --recurse Recursively find repositories & submodules within . -n, --no-regex Assume are comma-separated exact matches rather than regular expressions [default: False]. NB: if regex is enabled ',' is equivalent to '|'. -s, --silent-progress Suppress `tqdm` [default: False]. - --warn-binary Don't silently skip files which appear to be binary data - [default: False]. + --warn-binary Don't silently skip files which appear to be binary data + [default: False]. -e, --show-email Show author email instead of name [default: False]. - --enum Show row numbers [default: False]. + --enum Show row numbers [default: False]. -t, --bytype Show stats per file extension [default: False]. -w, --ignore-whitespace Ignore whitespace when comparing the parent's version and the child's to find where the lines came from [default: False]. - -M Detect intra-file line moves and copies [default: False]. - -C Detect inter-file line moves and copies [default: False]. + -M Detect intra-file line moves and copies [default: False]. + -C Detect inter-file line moves and copies [default: False]. + --ignore-rev= Ignore changes made by the given revision + (requires `--loc=surviving`). + --ignore-revs-file= Ignore revisions listed in the given file + (requires `--loc=surviving`). --format= Table format [default: pipe]|md|markdown|yaml|yml|json|csv|tsv|tabulate. May require `git-fame[]`, e.g. `pip install git-fame[yaml]`. Any `tabulate.tabulate_formats` is also accepted. --manpath= Directory in which to install git-fame man pages. - --log= FATAL|CRITICAL|ERROR|WARN(ING)|[default: INFO]|DEBUG|NOTSET. + --log= FATAL|CRITICAL|ERROR|WARN(ING)|[default: INFO]|DEBUG|NOTSET. If multiple user names and/or emails correspond to the same user, aggregate diff --git a/git-fame_completion.bash b/git-fame_completion.bash index 067e24b..e8e8394 100644 --- a/git-fame_completion.bash +++ b/git-fame_completion.bash @@ -5,7 +5,7 @@ _git_fame() prv="${COMP_WORDS[COMP_CWORD-1]}" case ${COMP_CWORD} in 1) - COMPREPLY=($(compgen -W "fame" ${cur})) + COMPREPLY=($(compgen -W "fame" "${cur}")) ;; *) case ${prv} in @@ -27,15 +27,18 @@ _git_fame() --branch) COMPREPLY=($(compgen -W "$(git branch | sed 's/*/ /')" -- ${cur})) ;; + --ignore-revs-file) + COMPREPLY=($(compgen -f -- "${cur}")) + ;; --manpath) - COMPREPLY=($(compgen -d -- ${cur})) + COMPREPLY=($(compgen -d -- "${cur}")) ;; - --incl|--excl|--since) + --incl|--excl|--since|--ignore-rev) COMPREPLY=( ) ;; *) if [ ${COMP_WORDS[1]} == fame ]; then - COMPREPLY=($(compgen -dW '-h --help -v --version --cost --branch --since --sort --loc --incl --excl -n --no-regex -s --silent-progress --warn-binary -t --bytype -w --ignore-whitespace -e --show-email --enum -M -C --format --manpath --log' -- ${cur})) + COMPREPLY=($(compgen -dW '-h --help -v --version --cost --branch --since --sort --loc --incl --excl -R --recurse -n --no-regex -s --silent-progress --warn-binary -t --bytype -w --ignore-whitespace -e --show-email --enum -M -C --ignore-rev --ignore-revs-file --format --manpath --log' -- ${cur})) fi ;; esac diff --git a/gitfame/_gitfame.py b/gitfame/_gitfame.py index 59916e9..873a27e 100755 --- a/gitfame/_gitfame.py +++ b/gitfame/_gitfame.py @@ -27,26 +27,31 @@ May be multiple comma-separated values. Alters `--loc` default to imply 'ins' (COCOMO) or 'ins,del' (hours). + -R, --recurse Recursively find repositories & submodules within . -n, --no-regex Assume are comma-separated exact matches rather than regular expressions [default: False]. NB: if regex is enabled ',' is equivalent to '|'. -s, --silent-progress Suppress `tqdm` [default: False]. - --warn-binary Don't silently skip files which appear to be binary data - [default: False]. + --warn-binary Don't silently skip files which appear to be binary data + [default: False]. -e, --show-email Show author email instead of name [default: False]. - --enum Show row numbers [default: False]. + --enum Show row numbers [default: False]. -t, --bytype Show stats per file extension [default: False]. -w, --ignore-whitespace Ignore whitespace when comparing the parent's version and the child's to find where the lines came from [default: False]. - -M Detect intra-file line moves and copies [default: False]. - -C Detect inter-file line moves and copies [default: False]. + -M Detect intra-file line moves and copies [default: False]. + -C Detect inter-file line moves and copies [default: False]. + --ignore-rev= Ignore changes made by the given revision + (requires `--loc=surviving`). + --ignore-revs-file= Ignore revisions listed in the given file + (requires `--loc=surviving`). --format= Table format [default: pipe]|md|markdown|yaml|yml|json|csv|tsv|tabulate. May require `git-fame[]`, e.g. `pip install git-fame[yaml]`. Any `tabulate.tabulate_formats` is also accepted. --manpath= Directory in which to install git-fame man pages. - --log= FATAL|CRITICAL|ERROR|WARN(ING)|[default: INFO]|DEBUG|NOTSET. + --log= FATAL|CRITICAL|ERROR|WARN(ING)|[default: INFO]|DEBUG|NOTSET. """ from __future__ import division, print_function @@ -223,7 +228,7 @@ def _get_auth_stats( gitdir, branch="HEAD", since=None, include_files=None, exclude_files=None, silent_progress=False, ignore_whitespace=False, M=False, C=False, warn_binary=False, bytype=False, show_email=False, - prefix_gitdir=False, churn=None): + prefix_gitdir=False, churn=None, ignore_rev="", ignore_revs_file=None): """Returns dict: {"": {"loc": int, "files": {}, "commits": int, "ctimes": [int]}}""" since = ["--since", since] if since else [] @@ -244,6 +249,10 @@ def _get_auth_stats( if churn & CHURN_SLOC: base_cmd = git_cmd + ["blame", "--line-porcelain"] + since + if ignore_rev: + base_cmd.extend(["--ignore-rev", ignore_rev]) + if ignore_revs_file: + base_cmd.extend(["--ignore-revs-file", ignore_revs_file]) else: base_cmd = git_cmd + ["log", "--format=aN%aN ct%ct", "--numstat"] + since @@ -377,7 +386,28 @@ def run(args): if isinstance(args.gitdir, string_types): args.gitdir = [args.gitdir] + # strip `/`, `.git` gitdirs = [i.rstrip(os.sep) for i in args.gitdir] + gitdirs = [path.join(*path.split(i)[:-1]) if path.split(i)[-1] == '.git' else i + for i in args.gitdir] + # remove duplicates + for i, d in reversed(list(enumerate(gitdirs))): + if d in gitdirs[:i]: + gitdirs.pop(i) + # recurse + if args.recurse: + nDirs = len(gitdirs) + i = 0 + while i < nDirs: + if path.isdir(gitdirs[i]): + for root, dirs, fns in tqdm(os.walk(gitdirs[i]), desc="Recursing", unit="dir", + disable=args.silent_progress, leave=False): + if '.git' in fns + dirs: + if root not in gitdirs: + gitdirs.append(root) + if '.git' in dirs: + dirs.remove('.git') + i += 1 exclude_files = None include_files = None @@ -423,7 +453,8 @@ def run(args): ignore_whitespace=args.ignore_whitespace, M=args.M, C=args.C, warn_binary=args.warn_binary, bytype=args.bytype, show_email=args.show_email, prefix_gitdir=len(gitdirs) > 1, - churn=churn) + churn=churn, ignore_rev=args.ignore_rev, + ignore_revs_file=args.ignore_revs_file) # concurrent multi-repo processing if len(gitdirs) > 1: