Show a patch.

GET /api/patches/53/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 53,
    "url": "https://patchwork.libcamera.org/api/patches/53/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/53/",
    "project": {
        "id": 1,
        "url": "https://patchwork.libcamera.org/api/projects/1/?format=api",
        "name": "libcamera",
        "link_name": "libcamera",
        "list_id": "libcamera_core",
        "list_email": "libcamera-devel@lists.libcamera.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": ""
    },
    "msgid": "<20181214133216.18375-1-laurent.pinchart@ideasonboard.com>",
    "date": "2018-12-14T13:32:15",
    "name": "[libcamera-devel,v2,1/2] utils: Add Python-based commit style checker script",
    "commit_ref": "8b30bb318538fe5b44168ceb62cc47f450546b22",
    "pull_url": null,
    "state": "accepted",
    "archived": false,
    "hash": "668a19b969295df5a81700f96e9496b806631c19",
    "submitter": {
        "id": 2,
        "url": "https://patchwork.libcamera.org/api/people/2/?format=api",
        "name": "Laurent Pinchart",
        "email": "laurent.pinchart@ideasonboard.com"
    },
    "delegate": null,
    "mbox": "https://patchwork.libcamera.org/patch/53/mbox/",
    "series": [
        {
            "id": 27,
            "url": "https://patchwork.libcamera.org/api/series/27/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=27",
            "date": "2018-12-14T13:32:16",
            "name": "[libcamera-devel,v2,1/2] utils: Add Python-based commit style checker script",
            "version": 2,
            "mbox": "https://patchwork.libcamera.org/series/27/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/53/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/53/checks/",
    "tags": {},
    "headers": {
        "Return-Path": "<laurent.pinchart@ideasonboard.com>",
        "Received": [
            "from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 6B05060B17\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 14 Dec 2018 14:31:36 +0100 (CET)",
            "from avalon.bb.dnainternet.fi\n\t(dfj612ybrt5fhg77mgycy-3.rev.dnainternet.fi\n\t[IPv6:2001:14ba:21f5:5b00:2e86:4862:ef6a:2804])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id EAD9F549\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 14 Dec 2018 14:31:32 +0100 (CET)"
        ],
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1544794293;\n\tbh=MlPIxC82VBT5x3T9lmHHtGs04IfSmdPobNdRKFO7UAU=;\n\th=From:To:Subject:Date:From;\n\tb=q6OecqPRgAZPyVR+1S0RXtoetWNV0VG6/QR26epxPjX5r9NkBkWxCjasVoHpnv0fu\n\tDa4r+OUpbJ7SVlmmk1eC3eufBgANNW9O2FsILpCQ9P1AFddhEjqnQprDGGlb5oDMVQ\n\tAmD626sS2ZmGibDmcO4ANaMDYD145kUiu+mBtolc=",
        "From": "Laurent Pinchart <laurent.pinchart@ideasonboard.com>",
        "To": "libcamera-devel@lists.libcamera.org",
        "Date": "Fri, 14 Dec 2018 15:32:15 +0200",
        "Message-Id": "<20181214133216.18375-1-laurent.pinchart@ideasonboard.com>",
        "X-Mailer": "git-send-email 2.19.2",
        "MIME-Version": "1.0",
        "Content-Transfer-Encoding": "8bit",
        "Subject": "[libcamera-devel] [PATCH v2 1/2] utils: Add Python-based commit\n\tstyle checker script",
        "X-BeenThere": "libcamera-devel@lists.libcamera.org",
        "X-Mailman-Version": "2.1.23",
        "Precedence": "list",
        "List-Id": "<libcamera-devel.lists.libcamera.org>",
        "List-Unsubscribe": "<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>",
        "List-Archive": "<https://lists.libcamera.org/pipermail/libcamera-devel/>",
        "List-Post": "<mailto:libcamera-devel@lists.libcamera.org>",
        "List-Help": "<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>",
        "List-Subscribe": "<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>",
        "X-List-Received-Date": "Fri, 14 Dec 2018 13:31:36 -0000"
    },
    "content": "checkstyle.py is a reimplementation of checkstyle.sh in Python, that\nshould be easier to extend with additional features.\n\nThree additional features and enhancements are already implemented:\n\n- While retaining the default behaviour of operating on the HEAD commit,\n  a list of commits can also be specified on the command line.\n\n- Correct line numbers are printed in the diff output.\n\n- The index and working tree are not touched, they can be dirty.\n\nSigned-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n---\n utils/checkstyle.py | 267 ++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 267 insertions(+)\n create mode 100755 utils/checkstyle.py",
    "diff": "diff --git a/utils/checkstyle.py b/utils/checkstyle.py\nnew file mode 100755\nindex 000000000000..6e07ffdc19e6\n--- /dev/null\n+++ b/utils/checkstyle.py\n@@ -0,0 +1,267 @@\n+#!/usr/bin/python3\n+# SPDX-License-Identifier: GPL-2.0-or-later\n+# Copyright (C) 2018, Google Inc.\n+#\n+# Author: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n+#\n+# checkstyle.py - A patch style checker script based on astyle\n+#\n+# TODO:\n+#\n+# - Support other formatting tools (clang-format, ...)\n+# - Split large hunks to minimize context noise\n+# - Improve style issues counting\n+#\n+\n+import argparse\n+import difflib\n+import re\n+import shutil\n+import subprocess\n+import sys\n+\n+astyle_options = (\n+    '-n',\n+    '--style=linux',\n+    '--indent=force-tab=8',\n+    '--attach-namespaces',\n+    '--attach-extern-c',\n+    '--pad-oper',\n+    '--align-pointer=name',\n+    '--align-reference=name',\n+    '--max-code-length=120'\n+)\n+\n+source_extensions = (\n+    '.c',\n+    '.cpp',\n+    '.h'\n+)\n+\n+class Colours:\n+    Default = 0\n+    Red = 31\n+    Green = 32\n+    Cyan = 36\n+\n+for attr in Colours.__dict__.keys():\n+    if attr.startswith('_'):\n+        continue\n+\n+    if sys.stdout.isatty():\n+        setattr(Colours, attr, '\\033[0;%um' % getattr(Colours, attr))\n+    else:\n+        setattr(Colours, attr, '')\n+\n+\n+class DiffHunkSide(object):\n+    \"\"\"A side of a diff hunk, recording line numbers\"\"\"\n+    def __init__(self, start):\n+        self.start = start\n+        self.touched = []\n+        self.untouched = []\n+\n+    def __len__(self):\n+        return len(self.touched) + len(self.untouched)\n+\n+\n+class DiffHunk(object):\n+    diff_header_regex = re.compile('@@ -([0-9]+),([0-9]+) \\+([0-9]+),([0-9]+) @@')\n+\n+    def __init__(self, line):\n+        match = DiffHunk.diff_header_regex.match(line)\n+        if not match:\n+            raise RuntimeError(\"Malformed diff hunk header '%s'\" % line)\n+\n+        self.__from_line = int(match.group(1))\n+        self.__to_line = int(match.group(3))\n+        self.__from = DiffHunkSide(self.__from_line)\n+        self.__to = DiffHunkSide(self.__to_line)\n+\n+        self.lines = []\n+\n+    def __repr__(self):\n+        s = '%s@@ -%u,%u +%u,%u @@\\n' % \\\n+                (Colours.Cyan,\n+                 self.__from.start, len(self.__from),\n+                 self.__to.start, len(self.__to))\n+\n+        for line in self.lines:\n+            if line[0] == '-':\n+                s += Colours.Red\n+            elif line[0] == '+':\n+                s += Colours.Green\n+            else:\n+                s += Colours.Default\n+            s += line\n+\n+        s += Colours.Default\n+        return s\n+\n+    def append(self, line):\n+        if line[0] == ' ':\n+            self.__from.untouched.append(self.__from_line)\n+            self.__from_line += 1\n+            self.__to.untouched.append(self.__to_line)\n+            self.__to_line += 1\n+        elif line[0] == '-':\n+            self.__from.touched.append(self.__from_line)\n+            self.__from_line += 1\n+        elif line[0] == '+':\n+            self.__to.touched.append(self.__to_line)\n+            self.__to_line += 1\n+\n+        self.lines.append(line)\n+\n+    def intersects(self, lines):\n+        for line in lines:\n+            if line in self.__from.touched:\n+                return True\n+        return False\n+\n+    def side(self, side):\n+        if side == 'from':\n+            return self.__from\n+        else:\n+            return self.__to\n+\n+\n+def parse_diff(diff):\n+    hunks = []\n+    hunk = None\n+    for line in diff:\n+        if line.startswith('@@'):\n+            if hunk:\n+                hunks.append(hunk)\n+            hunk = DiffHunk(line)\n+\n+        elif hunk is not None:\n+            hunk.append(line)\n+\n+    if hunk:\n+        hunks.append(hunk)\n+\n+    return hunks\n+\n+\n+def check_file(commit, filename):\n+    # Extract the line numbers touched by the commit.\n+    diff = subprocess.run(['git', 'diff', '%s~..%s' % (commit, commit), '--', filename],\n+                            stdout=subprocess.PIPE).stdout\n+    diff = diff.decode('utf-8').splitlines(True)\n+    commit_diff = parse_diff(diff)\n+\n+    lines = []\n+    for hunk in commit_diff:\n+        lines.extend(hunk.side('to').touched)\n+\n+    # Skip commits that don't add any line.\n+    if len(lines) == 0:\n+        return 0\n+\n+    # Format the file after the commit with astyle and compute the diff between\n+    # the two files.\n+    after = subprocess.run(['git', 'show', '%s:%s' % (commit, filename)],\n+                           stdout=subprocess.PIPE).stdout\n+    formatted = subprocess.run(['astyle', *astyle_options],\n+                               input=after, stdout=subprocess.PIPE).stdout\n+\n+    after = after.decode('utf-8').splitlines(True)\n+    formatted = formatted.decode('utf-8').splitlines(True)\n+\n+    diff = difflib.unified_diff(after, formatted)\n+\n+    # Split the diff in hunks, recording line number ranges for each hunk.\n+    formatted_diff = parse_diff(diff)\n+\n+    # Filter out hunks that are not touched by the commit.\n+    formatted_diff = [hunk for hunk in formatted_diff if hunk.intersects(lines)]\n+    if len(formatted_diff) == 0:\n+        return 0\n+\n+    print('%s---' % Colours.Red, filename)\n+    print('%s+++' % Colours.Green, filename)\n+    for hunk in formatted_diff:\n+        print(hunk)\n+\n+    return len(formatted_diff)\n+\n+\n+def check_style(commit):\n+    # Get the commit title and list of files.\n+    ret = subprocess.run(['git', 'show', '--pretty=oneline','--name-only', commit],\n+                         stdout=subprocess.PIPE)\n+    files = ret.stdout.decode('utf-8').splitlines()\n+    title = files[0]\n+    files = files[1:]\n+\n+    separator = '-' * len(title)\n+    print(separator)\n+    print(title)\n+    print(separator)\n+\n+    # Filter out non C/C++ files.\n+    files = [f for f in files if f.endswith(source_extensions)]\n+    if len(files) == 0:\n+        print(\"Commit doesn't touch source files, skipping\")\n+        return\n+\n+    issues = 0\n+    for f in files:\n+        issues += check_file(commit, f)\n+\n+    if issues == 0:\n+        print(\"No style issue detected\")\n+    else:\n+        print('---')\n+        print(\"%u potential style %s detected, please review\" % \\\n+                (issues, 'issue' if issues == 1 else 'issues'))\n+\n+\n+def extract_revlist(revs):\n+    \"\"\"Extract a list of commits on which to operate from a revision or revision\n+    range.\n+    \"\"\"\n+    ret = subprocess.run(['git', 'rev-parse', revs], stdout=subprocess.PIPE,\n+                         stderr=subprocess.PIPE)\n+    if ret.returncode != 0:\n+        print(ret.stderr.decode('utf-8').splitlines()[0])\n+        return []\n+\n+    revlist = ret.stdout.decode('utf-8').splitlines()\n+\n+    # If the revlist contains more than one item, pass it to git rev-list to list\n+    # each commit individually.\n+    if len(revlist) > 1:\n+        ret = subprocess.run(['git', 'rev-list', *revlist], stdout=subprocess.PIPE)\n+        revlist = ret.stdout.decode('utf-8').splitlines()\n+        revlist.reverse()\n+\n+    return revlist\n+\n+\n+def main(argv):\n+\n+    # Parse command line arguments\n+    parser = argparse.ArgumentParser()\n+    parser.add_argument('revision_range', type=str, default='HEAD', nargs='?',\n+                        help='Revision range (as defined by git rev-parse). Defaults to HEAD if not specified.')\n+    args = parser.parse_args(argv[1:])\n+\n+    # Check for required dependencies.\n+    dependencies = ('astyle', 'git')\n+\n+    for dependency in dependencies:\n+        if not shutil.which(dependency):\n+            print(\"Executable %s not found\" % dependency)\n+            return 1\n+\n+    revlist = extract_revlist(args.revision_range)\n+\n+    for commit in revlist:\n+        check_style(commit)\n+        print('')\n+\n+\n+if __name__ == '__main__':\n+    sys.exit(main(sys.argv))\n",
    "prefixes": [
        "libcamera-devel",
        "v2",
        "1/2"
    ]
}