From f3a578d8ac358b7cc156346f1fae00a9eb78be81 Mon Sep 17 00:00:00 2001
From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
Date: Wed, 5 Apr 2023 11:28:44 -0400
Subject: [PATCH] misc: migrate source formatting script to python

Devs will need to remove the existing pre-commit hook or regenerate the hook with `build.py --init`.

Gitlab: #1059
Change-Id: I1cec9150c7781d769cb229dfd170a9dcfc819edb
---
 build.py                 |   5 +-
 extras/scripts/format.py | 127 +++++++++++++++++++++++++++++++++++++++
 extras/scripts/format.sh | 103 -------------------------------
 3 files changed, 130 insertions(+), 105 deletions(-)
 create mode 100755 extras/scripts/format.py
 delete mode 100755 extras/scripts/format.sh

diff --git a/build.py b/build.py
index 556a631bb..fb487d5c4 100755
--- a/build.py
+++ b/build.py
@@ -312,8 +312,9 @@ def run_init():
     for hooks_dir in hooks_directories:
         if not os.path.exists(hooks_dir):
             os.makedirs(hooks_dir)
-        copy_file("./extras/scripts/commit-msg", hooks_dir + "/commit-msg")
-        execute_script(['./extras/scripts/format.sh --install %(path)s'],
+        copy_file("./extras/scripts/commit-msg",
+                  os.path.join(hooks_dir, "commit-msg"))
+        execute_script(['./extras/scripts/format.py --install %(path)s'],
                        {"path": hooks_dir})
 
 
diff --git a/extras/scripts/format.py b/extras/scripts/format.py
new file mode 100755
index 000000000..4676c592a
--- /dev/null
+++ b/extras/scripts/format.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+"""
+Clang format C/C++ source files with clang-format.
+Also optionally installs a pre-commit hook to run this script.
+
+Usage:
+    format.py [-a | --all] [-i | --install PATH]
+
+    -a | --all
+        Format all files instead of only committed ones
+
+    -i | --install PATH
+        Install a pre-commit hook to run this script in PATH
+"""
+
+import os
+import sys
+import subprocess
+import argparse
+import shutil
+
+CFVERSION = "9"
+CLANGFORMAT = ""
+
+
+def command_exists(cmd):
+    """ Check if a command exists """
+    return shutil.which(cmd) is not None
+
+
+def clang_format_file(filename):
+    """ Format a file using clang-format """
+    if os.path.isfile(filename):
+        subprocess.call([CLANGFORMAT, "-i", "-style=file", filename])
+
+
+def clang_format_files(files):
+    """ Format a list of files """
+    for filename in files:
+        print(f"Formatting: {filename}", end='\r')
+        clang_format_file(filename)
+
+
+def exit_if_no_files():
+    """ Exit if no files to format """
+    print("No files to format")
+    sys.exit(0)
+
+
+def install_hook(hooks_path):
+    """ Install a pre-commit hook to run this script """
+    if not os.path.isdir(hooks_path):
+        print(f"{hooks_path} path does not exist")
+        sys.exit(1)
+    print(f"Installing pre-commit hook in {hooks_path}")
+    with open(os.path.join(hooks_path, "pre-commit"),
+              "w", encoding="utf-8") as file:
+        file.write(os.path.realpath(sys.argv[0]))
+    os.chmod(os.path.join(hooks_path, "pre-commit"), 0o755)
+
+
+def get_files(file_types, recursive=True, committed_only=False):
+    """
+    Get a list of files in the src directory [and subdirectories].
+    Filters by file types and whether the file is committed.
+    """
+    file_list = []
+    committed_files = []
+    if committed_only:
+        committed_files = subprocess.check_output(
+            "git diff-index --cached --name-only HEAD",
+            shell=True).decode().strip().split('\n')
+    for dirpath, _, filenames in os.walk('src'):
+        for filename in filenames:
+            file_path = os.path.join(dirpath, filename)
+            if file_types and not any(file_path.endswith(file_type)
+                                      for file_type in file_types):
+                continue  # Skip files that don't match any file types.
+            if committed_only:
+                if file_path not in committed_files:
+                    continue  # Skip uncommitted files.
+            file_list.append(file_path)
+        if not recursive:
+            break  # Stop searching if not recursive.
+    return file_list
+
+
+def main():
+    """Check if clang-format is installed, and format files."""
+    global CLANGFORMAT  # pylint: disable=global-statement
+    parser = argparse.ArgumentParser(
+        description="Format source filess with a clang-format")
+    parser.add_argument("-a", "--all", action="store_true",
+                        help="format all files instead of only committed ones")
+    parser.add_argument("-i", "--install", metavar="PATH",
+                        help="install a pre-commit hook to run this script")
+    args = parser.parse_args()
+
+    if not command_exists("clang-format-" + CFVERSION):
+        if not command_exists("clang-format"):
+            print("Required version of clang-format not found")
+            sys.exit(1)
+        else:
+            CLANGFORMAT = "clang-format"
+    else:
+        CLANGFORMAT = "clang-format-" + CFVERSION
+
+    if args.install:
+        install_hook(args.install)
+        sys.exit(0)
+
+    if args.all:
+        print("Formatting all files...")
+        # Find all files in the recursively in the current directory.
+        clang_format_files(get_files((".cpp", ".cxx", ".cc", ".h", ".hpp"),
+                                     committed_only=False))
+    else:
+        files = get_files((".cpp", ".cxx", ".cc", ".h", ".hpp"),
+                          committed_only=True)
+        if not files:
+            exit_if_no_files()
+        print("Formatting committed source files...")
+        clang_format_files(files)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/extras/scripts/format.sh b/extras/scripts/format.sh
deleted file mode 100755
index d33b506c0..000000000
--- a/extras/scripts/format.sh
+++ /dev/null
@@ -1,103 +0,0 @@
-#!/usr/bin/env bash
-# format.sh --- set up clang-format for source files
-
-# Copyright (C) 2020-2023 Savoir-faire Linux Inc.
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
-
-set -e
-
-command_exists ()
-{
-    type "$1" &> /dev/null ;
-}
-
-CFVERSION="9"
-CLANGFORMAT=""
-if command_exists clang-format-${CFVERSION}; then
-    CLANGFORMAT=clang-format-${CFVERSION}
-else
-    if command_exists clang-format; then
-        CLANGFORMAT=clang-format
-    fi
-fi
-
-if ! command -v $CLANGFORMAT &> /dev/null; then
-    echo "Required version of clang-format not found"
-    exit 1
-fi
-
-format_file()
-{
-    if [ -f "${1}" ]; then
-        $CLANGFORMAT -i -style=file "${1}" || true
-    fi
-}
-
-format_files()
-{
-    for file in $1; do
-        echo -ne "Formatting: ${file}\\033[0K\\r"
-        format_file "${file}"
-    done
-}
-
-exit_if_no_files()
-{
-    echo No files to format
-    exit 0
-}
-
-install_hook()
-{
-    hooks_path=$1
-    if [ ! -d "$hooks_path" ]; then
-        echo "$hooks_path" path does not exist
-        exit 1
-    fi
-    echo Installing pre-commit hook in "$hooks_path"
-    echo "$(realpath $0)" > "$hooks_path"/pre-commit
-    chmod +x "$hooks_path"/pre-commit
-}
-
-display_help()
-{
-    echo "Usage: $0 [OPTION...] -- Clang format source files with a .clang-format file" >&2
-    echo
-    echo "   --all             format all files instead of only committed ones"
-    echo "   --install <path>  install a pre-commit hook to run this script"
-    echo
-}
-
-if [ "$1" == "--help" ]; then
-    display_help
-    exit 0
-fi
-
-case "${1}" in
-  --all )
-    files=$(find src -regex '.*\.\(cpp\|hpp\|cc\|cxx\|h\)$') || true
-    echo Formatting all source files...
-    format_files "$files"
-    ;;
-  --install )
-    install_hook "${2}"
-    ;;
-  * )
-    files=$(git diff-index --cached --name-only HEAD | grep -iE '\.(cpp|cxx|cc|h|hpp)$') || exit_if_no_files
-    echo Formatting committed source files...
-    format_files "$files"
-    ;;
-esac
-- 
GitLab