[libcamera-devel] [PATCH 2/2] checkstyle: Add support for pre-commit hook

Kieran Bingham kieran.bingham at ideasonboard.com
Thu Jan 16 23:25:31 CET 2020


Hi Nicolas,

On 16/01/2020 18:26, nicolas at ndufresne.ca wrote:
> From: Nicolas Dufresne <nicolas.dufresne at collabora.com>
> 
> This adds support for pre-commit hook workflow. In pre-commit hook we
> check the style on the changes currently staged. Note that this patch
> also includes a bit of sugar to support --amend.
> 
> Signed-off-by: Nicolas Dufresne <nicolas.dufresne at collabora.com>
> ---
>  utils/checkstyle.py    | 55 +++++++++++++++++++++++++++++-------------
>  utils/hooks/pre-commit | 16 ++++++++++++
>  2 files changed, 54 insertions(+), 17 deletions(-)
>  mode change 100644 => 100755 utils/checkstyle.py
>  create mode 100755 utils/hooks/pre-commit
> 
> diff --git a/utils/checkstyle.py b/utils/checkstyle.py
> old mode 100644
> new mode 100755
> index 7edea25..23eb3f6
> --- a/utils/checkstyle.py
> +++ b/utils/checkstyle.py
> @@ -458,11 +458,16 @@ class StripTrailingSpaceFormatter(Formatter):
>  # Style checking
>  #
>  
> -def check_file(top_level, commit, filename):
> +def check_file(top_level, commit, filename, staged):
>      # Extract the line numbers touched by the commit.
> -    diff = subprocess.run(['git', 'diff', '%s~..%s' % (commit, commit), '--',
> -                           '%s/%s' % (top_level, filename)],
> -                          stdout=subprocess.PIPE).stdout

I wonder if we could/should automate this by detecting staged changes,
and then setting staged based on that?

	"git status --porcelain | grep ^M"

Could detect if there are staged changes.

But equally it might be better to allow the caller to be explicit.


> +    if staged:
> +        diff = subprocess.run(['git', 'diff', '--staged', commit, '--',
> +                               '%s/%s' % (top_level, filename)],
> +                              stdout=subprocess.PIPE).stdout
> +    else:
> +        diff = subprocess.run(['git', 'diff', '%s~..%s' % (commit, commit), '--',
> +                               '%s/%s' % (top_level, filename)],
> +                              stdout=subprocess.PIPE).stdout
>      diff = diff.decode('utf-8').splitlines(True)
>      commit_diff = parse_diff(diff)
>  
> @@ -476,8 +481,12 @@ def check_file(top_level, commit, filename):
>  
>      # Format the file after the commit with all formatters and compute the diff
>      # between the unformatted and formatted contents.
> -    after = subprocess.run(['git', 'show', '%s:%s' % (commit, filename)],
> -                           stdout=subprocess.PIPE).stdout
> +    if staged:
> +        after = subprocess.run(['git', 'show', ':%s' % (filename)],
> +                               stdout=subprocess.PIPE).stdout
> +    else:
> +        after = subprocess.run(['git', 'show', '%s:%s' % (commit, filename)],
> +                               stdout=subprocess.PIPE).stdout
>      after = after.decode('utf-8')
>  
>      formatted = after
> @@ -521,13 +530,20 @@ def check_file(top_level, commit, filename):
>      return len(formatted_diff) + len(issues)
>  
>  
> -def check_style(top_level, commit):
> -    # Get the commit title and list of files.
> -    ret = subprocess.run(['git', 'show', '--pretty=oneline','--name-only', commit],
> -                         stdout=subprocess.PIPE)
> -    files = ret.stdout.decode('utf-8').splitlines()
> -    title = files[0]
> -    files = files[1:]
> +def check_style(top_level, commit, staged):
> +    if staged:
> +        # Get list of staged changed files
> +        ret = subprocess.run(['git', 'diff', '--staged', '--name-only', commit],
> +                             stdout=subprocess.PIPE)
> +        files = ret.stdout.decode('utf-8').splitlines()
> +        title = "Pre-commit"

This is not necessarily a "Pre-commit" (hook). It's just staged.
A user could run the tool directly to validate some staged changes
before committing...

So I'd perhaps make this
	title = "Staged"


> +    else:
> +        # Get the commit title and list of files.
> +        ret = subprocess.run(['git', 'show', '--pretty=oneline', '--name-only', commit],
> +                             stdout=subprocess.PIPE)
> +        files = ret.stdout.decode('utf-8').splitlines()
> +        title = files[0]
> +        files = files[1:]
>  
>      separator = '-' * len(title)
>      print(separator)
> @@ -541,11 +557,11 @@ def check_style(top_level, commit):
>      files = [f for f in files if len([p for p in patterns if fnmatch.fnmatch(os.path.basename(f), p)])]
>      if len(files) == 0:
>          print("Commit doesn't touch source files, skipping")
> -        return
> +        return 0
>  
>      issues = 0
>      for f in files:
> -        issues += check_file(top_level, commit, f)
> +        issues += check_file(top_level, commit, f, staged)
>  
>      if issues == 0:
>          print("No style issue detected")
> @@ -554,6 +570,8 @@ def check_style(top_level, commit):
>          print("%u potential style %s detected, please review" % \
>                  (issues, 'issue' if issues == 1 else 'issues'))
>  
> +    return issues
> +
>  
>  def extract_revlist(revs):
>      """Extract a list of commits on which to operate from a revision or revision
> @@ -595,6 +613,8 @@ def main(argv):
>      parser = argparse.ArgumentParser()
>      parser.add_argument('--formatter', '-f', type=str, choices=['astyle', 'clang-format'],
>                          help='Code formatter. Default to clang-format if not specified.')
> +    parser.add_argument('--staged', '-s', action='store_true',
> +                        help='Looks at the staged changes, used for pre-commit, defaults to False')
>      parser.add_argument('revision_range', type=str, default='HEAD', nargs='?',
>                          help='Revision range (as defined by git rev-parse). Defaults to HEAD if not specified.')
>      args = parser.parse_args(argv[1:])
> @@ -632,11 +652,12 @@ def main(argv):
>  
>      revlist = extract_revlist(args.revision_range)
>  
> +    issues = 0
>      for commit in revlist:
> -        check_style(top_level, commit)
> +        issues += check_style(top_level, commit, args.staged)
>          print('')
>  
> -    return 0
> +    return issues

I'd be tempted to split the
  "checkstyle: Report issue count as a failure"
and
  "checkstyle: Support validating staged git state"

to two patches, but perhaps that's not necessary unless you see benefit
in splitting.


>  
>  
>  if __name__ == '__main__':
> diff --git a/utils/hooks/pre-commit b/utils/hooks/pre-commit
> new file mode 100755
> index 0000000..57d23ef
> --- /dev/null
> +++ b/utils/hooks/pre-commit

Though I really think this deserves it's own commit.

 "utils/hooks: Provide a pre-commit checkstyle hook"



> @@ -0,0 +1,16 @@
> +#!/bin/sh
> +
> +# Execute the checkstyle script before committing any code, failing the commit
> +# if needed. With this workflow, false positive can be ignored using:
> +#   git commit -n
> +#
> +# To utilise this hook, install this file with:
> +#   cp utils/hooks/pre-commit .git/hooks/pre-commit

One of the things I had toyed with is installing the hook by setting the
git hook directory to utils/hooks/ :

 git config core.hooksPath utils/hooks

But doing so now would invoke both the pre-commit and the post-commit hook.

I think it's good to provide the option of how someone might install
their hook

> +
> +commit=
> +if ps -ocommand= -p $PPID | grep -- "--amend"
> +then
> +   commit="HEAD~"

Is this really needed?

Edit: Yes, I've just played around with it - and I see what's happening.
Good catch to find that something extra was required here.


> +fi
> +
> +./utils/checkstyle.py --staged $commit


I wonder if the checkstyle.py could detect if we had staged changes if
we could fall back to needing only a single variant of the hook which
the user could choose to install as either a pre-commit or a post-commit
hook.

But perhaps it's more straight-forward to have two hooks to keep it
easier to support any future differences too.


-- 
Regards
--
Kieran


More information about the libcamera-devel mailing list