Procházet zdrojové kódy

add a simple diffstat implementation

kevinhendricks před 5 roky
rodič
revize
ff06a7fc73
1 změnil soubory, kde provedl 325 přidání a 0 odebrání
  1. 325 0
      dulwich/diffstat.py

+ 325 - 0
dulwich/diffstat.py

@@ -0,0 +1,325 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
+
+# Copyright (c) 2020 Kevin B. Hendricks, Stratford Ontario Canada
+# All rights reserved.
+#
+# This diffstat code was extracted and heavily modified from:
+#
+#    https://github.com/techtonik/python-patch
+#        Under the following license:
+#
+#    Patch utility to apply unified diffs
+#    Brute-force line-by-line non-recursive parsing
+#
+#    Copyright (c) 2008-2016 anatoly techtonik
+#    Available under the terms of MIT license
+#
+# and falls under the exact same MIT license
+
+import sys
+import re
+
+# only needs to detect git style diffs as this is for
+# use with dulwich
+
+_git_header_name = re.compile(br'diff --git a/(.*) b/(.*)')
+
+_GIT_HEADER_START = b'diff --git a/'
+_GIT_BINARY_START = b'Binary file'
+_GIT_RENAMEFROM_START = b'rename from'
+_GIT_RENAMETO_START = b'rename to'
+_GIT_CHUNK_START = b'@@'
+_GIT_ADDED_START = b'+'
+_GIT_DELETED_START = b'-'
+_GIT_UNCHANGED_START = b' '
+
+# emulate original full Patch class by just extracting
+# filename and minimal chunk added/deleted information to
+# properly interface with diffstat routine
+
+
+def _parse_patch(lines):
+    """An internal routine to parse a git style diff or patch to generate
+       diff stats
+    Args:
+      lines: list of byte strings "lines" from the diff to be parsed
+    Returns: A tuple (names, nametypes, counts) of three lists:
+             names = list of repo relative file paths
+             nametypes - list of booolean values indicating if file
+                         is binary (True means binary file)
+             counts = list of tuples of (added, deleted) counts for that file
+    """
+    names = []
+    nametypes = []
+    counts = []
+    in_patch_chunk = in_git_header = binaryfile = False
+    currentfile = None
+    added = deleted = 0
+    for line in lines:
+        if line.startswith(_GIT_HEADER_START):
+            if currentfile is not None:
+                names.append(currentfile)
+                nametypes.append(binaryfile)
+                counts.append((added, deleted))
+            currentfile = _git_header_name.search(line).group(2)
+            binaryfile = False
+            added = deleted = 0
+            in_git_header = True
+            in_patch_chunk = False
+        elif line.startswith(_GIT_BINARY_START) and in_git_header:
+            binaryfile = True
+            in_git_header = False
+        elif line.startswith(_GIT_RENAMEFROM_START) and in_git_header:
+            currentfile = line[12:]
+        elif line.startswith(_GIT_RENAMETO_START) and in_git_header:
+            currentfile += b' => %s' % line[10:]
+        elif line.startswith(_GIT_CHUNK_START) and \
+                (in_patch_chunk or in_git_header):
+            in_patch_chunk = True
+            in_git_header = False
+        elif line.startswith(_GIT_ADDED_START) and in_patch_chunk:
+            added += 1
+        elif line.startswith(_GIT_DELETED_START) and in_patch_chunk:
+            deleted += 1
+        elif not line.startswith(_GIT_UNCHANGED_START) and in_patch_chunk:
+            in_patch_chunk = False
+    # handle end of input
+    if currentfile is not None:
+        names.append(currentfile)
+        nametypes.append(binaryfile)
+        counts.append((added, deleted))
+    return names, nametypes, counts
+
+
+# note must all done using bytes not string because on linux filenames
+# may not be encodable even to utf-8
+def diffstat(lines, max_width=80):
+    """Generate summary statistics from a git style diff ala
+       (git diff tag1 tag2 --stat)
+    Args:
+      lines: list of byte string "lines" from the diff to be parsed
+      max_width: maximum line length for generating the summary
+                 statistics (default 80)
+    Returns: A byte string that lists the changed files with change
+             counts and histogram
+    """
+    names, nametypes, counts = _parse_patch(lines)
+    insert = []
+    delete = []
+    namelen = 0
+    maxdiff = 0  # max changes for any file used for histogram width calc
+    for i, filename in enumerate(names):
+        i, d = counts[i]
+        insert.append(i)
+        delete.append(d)
+        namelen = max(namelen, len(filename))
+        maxdiff = max(maxdiff, i+d)
+    output = b''
+    statlen = len(str(maxdiff))  # stats column width
+    for i, n in enumerate(names):
+        binaryfile = nametypes[i]
+        # %-19s | %-4d %s
+        # note b'%d' % namelen is not supported until Python 3.5
+        # To convert an int to a format width specifier for byte
+        # strings use str(namelen).encode('ascii')
+        format = b' %-' + str(namelen).encode('ascii') + \
+            b's | %' + str(statlen).encode('ascii') + b's %s\n'
+        binformat = b' %-' + str(namelen).encode('ascii') + b's | %s\n'
+        if not binaryfile:
+            hist = b''
+            # -- calculating histogram --
+            width = len(format % (b'', b'', b''))
+            histwidth = max(2, max_width - width)
+            if maxdiff < histwidth:
+                hist = b'+'*insert[i] + b'-'*delete[i]
+            else:
+                iratio = (float(insert[i]) / maxdiff) * histwidth
+                dratio = (float(delete[i]) / maxdiff) * histwidth
+                iwidth = dwidth = 0
+                # make sure every entry that had actual insertions gets
+                # at least one +
+                if insert[i] > 0:
+                    iwidth = int(iratio)
+                    if iwidth == 0 and 0 < iratio < 1:
+                        iwidth = 1
+                # make sure every entry that had actual deletions gets
+                # at least one -
+                if delete[i] > 0:
+                    dwidth = int(dratio)
+                    if dwidth == 0 and 0 < dratio < 1:
+                        dwidth = 1
+                hist = b'+'*int(iwidth) + b'-'*int(dwidth)
+            output += (format % (bytes(names[i]),
+                                 str(insert[i] + delete[i]).encode('ascii'),
+                                 hist))
+        else:
+            output += (binformat % (bytes(names[i]), b'Bin'))
+
+    output += (b' %d files changed, %d insertions(+), %d deletions(-)'
+               % (len(names), sum(insert), sum(delete)))
+    return output
+
+
+def main():
+    argv = sys.argv
+    # allow diffstat.py to also be used from the comand line
+    if len(sys.argv) > 1:
+        diffpath = argv[1]
+        data = b''
+        with open(diffpath, 'rb') as f:
+            data = f.read()
+        lines = data.split(b'\n')
+        result = diffstat(lines)
+        print(result.decode('utf-8'))
+        return 0
+
+    # if no path argument to a diff file is passed in, run
+    # a self test. The test case includes tricky things like
+    # a diff of diff, binary files, renames with futher changes
+    # added files and removed files.
+    # All extracted from Sigil-Ebook/Sigil's github repo with
+    # full permission to use under this license.
+    selftest = b"""
+diff --git a/docs/qt512.7_remove_bad_workaround.patch b/docs/qt512.7_remove_bad_workaround.patch
+new file mode 100644
+index 00000000..64e34192
+--- /dev/null
++++ b/docs/qt512.7_remove_bad_workaround.patch
+@@ -0,0 +1,15 @@
++--- qtbase/src/gui/kernel/qwindow.cpp.orig     2019-12-12 09:15:59.000000000 -0500
+++++ qtbase/src/gui/kernel/qwindow.cpp  2020-01-10 10:36:53.000000000 -0500
++@@ -218,12 +218,6 @@
++     QGuiApplicationPrivate::window_list.removeAll(this);
++     if (!QGuiApplicationPrivate::is_app_closing)
++         QGuiApplicationPrivate::instance()->modalWindowList.removeOne(this);
++-
++-    // focus_window is normally cleared in destroy(), but the window may in
++-    // some cases end up becoming the focus window again. Clear it again
++-    // here as a workaround. See QTBUG-75326.
++-    if (QGuiApplicationPrivate::focus_window == this)
++-        QGuiApplicationPrivate::focus_window = 0;
++ }
++
++ void QWindowPrivate::init(QScreen *targetScreen)
+diff --git a/docs/testplugin_v017.zip b/docs/testplugin_v017.zip
+new file mode 100644
+index 00000000..a4cf4c4c
+Binary files /dev/null and b/docs/testplugin_v017.zip differ
+diff --git a/ci_scripts/macgddeploy.py b/ci_scripts/gddeploy.py
+similarity index 73%
+rename from ci_scripts/macgddeploy.py
+rename to ci_scripts/gddeploy.py
+index a512d075..f9dacd33 100644
+--- a/ci_scripts/macgddeploy.py
++++ b/ci_scripts/gddeploy.py
+@@ -1,19 +1,32 @@
+ #!/usr/bin/env python3
+
+ import os
++import sys
+ import subprocess
+ import datetime
+ import shutil
++import glob
+
+ gparent = os.path.expandvars('$GDRIVE_DIR')
+ grefresh_token = os.path.expandvars('$GDRIVE_REFRESH_TOKEN')
+
+-travis_branch = os.path.expandvars('$TRAVIS_BRANCH')
+-travis_commit = os.path.expandvars('$TRAVIS_COMMIT')
+-travis_build_number = os.path.expandvars('$TRAVIS_BUILD_NUMBER')
++if sys.platform.lower().startswith('darwin'):
++    travis_branch = os.path.expandvars('$TRAVIS_BRANCH')
++    travis_commit = os.path.expandvars('$TRAVIS_COMMIT')
++    travis_build_number = os.path.expandvars('$TRAVIS_BUILD_NUMBER')
++
++    origfilename = './bin/Sigil.tar.xz'
++    newfilename = './bin/Sigil-{}-{}-build_num-{}.tar.xz'.format(travis_branch, travis_commit[:7],travis_build_numbe\
+r)
++else:
++    appveyor_branch = os.path.expandvars('$APPVEYOR_REPO_BRANCH')
++    appveyor_commit = os.path.expandvars('$APPVEYOR_REPO_COMMIT')
++    appveyor_build_number = os.path.expandvars('$APPVEYOR_BUILD_NUMBER')
++    names = glob.glob('.\\installer\\Sigil-*-Setup.exe')
++    if not names:
++        exit(1)
++    origfilename = names[0]
++    newfilename = '.\\installer\\Sigil-{}-{}-build_num-{}-Setup.exe'.format(appveyor_branch, appveyor_commit[:7], ap\
+pveyor_build_number)
+
+-origfilename = './bin/Sigil.tar.xz'
+-newfilename = './bin/Sigil-{}-{}-build_num-{}.tar.xz'.format(travis_branch, travis_commit[:7],travis_build_number)
+ shutil.copy2(origfilename, newfilename)
+
+ folder_name = datetime.date.today()
+diff --git a/docs/qt512.6_backport_009abcd_fix.patch b/docs/qt512.6_backport_009abcd_fix.patch
+deleted file mode 100644
+index f4724347..00000000
+--- a/docs/qt512.6_backport_009abcd_fix.patch
++++ /dev/null
+@@ -1,26 +0,0 @@
+---- qtbase/src/widgets/kernel/qwidget.cpp.orig 2019-11-08 10:57:07.000000000 -0500
+-+++ qtbase/src/widgets/kernel/qwidget.cpp      2019-12-11 12:32:24.000000000 -0500
+-@@ -8934,6 +8934,23 @@
+-         }
+-     }
+-     switch (event->type()) {
+-+    case QEvent::PlatformSurface: {
+-+        // Sync up QWidget's view of whether or not the widget has been created
+-+        switch (static_cast<QPlatformSurfaceEvent*>(event)->surfaceEventType()) {
+-+        case QPlatformSurfaceEvent::SurfaceCreated:
+-+            if (!testAttribute(Qt::WA_WState_Created))
+-+                create();
+-+            break;
+-+        case QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed:
+-+            if (testAttribute(Qt::WA_WState_Created)) {
+-+                // Child windows have already been destroyed by QWindow,
+-+                // so we skip them here.
+-+                destroy(false, false);
+-+            }
+-+            break;
+-+        }
+-+        break;
+-+    }
+-     case QEvent::MouseMove:
+-         mouseMoveEvent((QMouseEvent*)event);
+-         break;
+diff --git a/docs/Building_Sigil_On_MacOSX.txt b/docs/Building_Sigil_On_MacOSX.txt
+index 3b41fd80..64914c78 100644
+--- a/docs/Building_Sigil_On_MacOSX.txt
++++ b/docs/Building_Sigil_On_MacOSX.txt
+@@ -113,7 +113,7 @@ install_name_tool -add_rpath @loader_path/../../Frameworks ./bin/Sigil.app/Content
+ 
+ # To test if the newly bundled python 3 version of Sigil is working properly ypou can do the following:
+ 
+-1. download testplugin_v014.zip from https://github.com/Sigil-Ebook/Sigil/tree/master/docs
++1. download testplugin_v017.zip from https://github.com/Sigil-Ebook/Sigil/tree/master/docs
+ 2. open Sigil.app to the normal nearly blank template epub it generates when opened
+ 3. use Plugins->Manage Plugins menu and make sure the "Use Bundled Python" checkbox is checked
+ 4. use the "Add Plugin" button to navigate to and add testplugin.zip and then hit "Okay" to exit the Manage Plugins Dialog
+"""     # noqa: E501 W293
+
+    testoutput = b""" docs/qt512.7_remove_bad_workaround.patch            | 15 ++++++++++++
+ docs/testplugin_v017.zip                            | Bin
+ ci_scripts/macgddeploy.py => ci_scripts/gddeploy.py |  0 
+ docs/qt512.6_backport_009abcd_fix.patch             | 26 ---------------------
+ docs/Building_Sigil_On_MacOSX.txt                   |  2 +-
+ 5 files changed, 16 insertions(+), 27 deletions(-)"""  # noqa: W291
+
+    # return 0 on success otherwise return -1
+    result = diffstat(selftest.split(b'\n'))
+    if result == testoutput:
+        print("self test passed")
+        return 0
+    print("self test failed")
+    print("Received:")
+    print(result.decode('utf-8'))
+    print("Expected:")
+    print(testoutput.decode('utf-8'))
+    return -1
+
+
+if __name__ == '__main__':
+    sys.exit(main())