diffstat.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. #!/usr/bin/env python
  2. # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
  3. # SPDX-License-Identifier: MIT
  4. # Copyright (c) 2020 Kevin B. Hendricks, Stratford Ontario Canada
  5. # All rights reserved.
  6. #
  7. # This diffstat code was extracted and heavily modified from:
  8. #
  9. # https://github.com/techtonik/python-patch
  10. # Under the following license:
  11. #
  12. # Patch utility to apply unified diffs
  13. # Brute-force line-by-line non-recursive parsing
  14. #
  15. # Copyright (c) 2008-2016 anatoly techtonik
  16. #
  17. # Permission is hereby granted, free of charge, to any person obtaining a copy
  18. # of this software and associated documentation files (the "Software"), to deal
  19. # in the Software without restriction, including without limitation the rights
  20. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  21. # copies of the Software, and to permit persons to whom the Software is
  22. # furnished to do so, subject to the following conditions:
  23. #
  24. # The above copyright notice and this permission notice shall be included in
  25. # all copies or substantial portions of the Software.
  26. #
  27. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  28. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  29. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  30. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  31. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  32. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  33. # THE SOFTWARE.
  34. """Generate diff statistics similar to git's --stat option.
  35. This module provides functionality to parse unified diff output and generate
  36. statistics about changes, including:
  37. - Number of lines added and removed per file
  38. - Binary file detection
  39. - File rename detection
  40. - Formatted output similar to git diff --stat
  41. """
  42. __all__ = [
  43. "diffstat",
  44. "main",
  45. "parse_patch",
  46. ]
  47. import re
  48. import sys
  49. from collections.abc import Sequence
  50. # only needs to detect git style diffs as this is for
  51. # use with dulwich
  52. _git_header_name = re.compile(rb"diff --git a/(.*) b/(.*)")
  53. _GIT_HEADER_START = b"diff --git a/"
  54. _GIT_BINARY_START = b"Binary file"
  55. _GIT_RENAMEFROM_START = b"rename from"
  56. _GIT_RENAMETO_START = b"rename to"
  57. _GIT_CHUNK_START = b"@@"
  58. _GIT_ADDED_START = b"+"
  59. _GIT_DELETED_START = b"-"
  60. _GIT_UNCHANGED_START = b" "
  61. # emulate original full Patch class by just extracting
  62. # filename and minimal chunk added/deleted information to
  63. # properly interface with diffstat routine
  64. def parse_patch(
  65. lines: Sequence[bytes],
  66. ) -> tuple[list[bytes], list[bool], list[tuple[int, int]]]:
  67. """Parse a git style diff or patch to generate diff stats.
  68. Args:
  69. lines: list of byte string lines from the diff to be parsed
  70. Returns: A tuple (names, is_binary, counts) of three lists
  71. """
  72. names = []
  73. nametypes = []
  74. counts = []
  75. in_patch_chunk = in_git_header = binaryfile = False
  76. currentfile: bytes | None = None
  77. added = deleted = 0
  78. for line in lines:
  79. if line.startswith(_GIT_HEADER_START):
  80. if currentfile is not None:
  81. names.append(currentfile)
  82. nametypes.append(binaryfile)
  83. counts.append((added, deleted))
  84. m = _git_header_name.search(line)
  85. assert m
  86. currentfile = m.group(2)
  87. binaryfile = False
  88. added = deleted = 0
  89. in_git_header = True
  90. in_patch_chunk = False
  91. elif line.startswith(_GIT_BINARY_START) and in_git_header:
  92. binaryfile = True
  93. in_git_header = False
  94. elif line.startswith(_GIT_RENAMEFROM_START) and in_git_header:
  95. currentfile = line[12:]
  96. elif line.startswith(_GIT_RENAMETO_START) and in_git_header:
  97. assert currentfile
  98. currentfile += b" => %s" % line[10:]
  99. elif line.startswith(_GIT_CHUNK_START) and (in_patch_chunk or in_git_header):
  100. in_patch_chunk = True
  101. in_git_header = False
  102. elif line.startswith(_GIT_ADDED_START) and in_patch_chunk:
  103. added += 1
  104. elif line.startswith(_GIT_DELETED_START) and in_patch_chunk:
  105. deleted += 1
  106. elif not line.startswith(_GIT_UNCHANGED_START) and in_patch_chunk:
  107. in_patch_chunk = False
  108. # handle end of input
  109. if currentfile is not None:
  110. names.append(currentfile)
  111. nametypes.append(binaryfile)
  112. counts.append((added, deleted))
  113. return names, nametypes, counts
  114. # note must all done using bytes not string because on linux filenames
  115. # may not be encodable even to utf-8
  116. def diffstat(lines: Sequence[bytes], max_width: int = 80) -> bytes:
  117. """Generate summary statistics from a git style diff ala (git diff tag1 tag2 --stat).
  118. Args:
  119. lines: list of byte string "lines" from the diff to be parsed
  120. max_width: maximum line length for generating the summary
  121. statistics (default 80)
  122. Returns: A byte string that lists the changed files with change
  123. counts and histogram.
  124. """
  125. names, nametypes, counts = parse_patch(lines)
  126. insert = []
  127. delete = []
  128. namelen = 0
  129. maxdiff = 0 # max changes for any file used for histogram width calc
  130. for i, filename in enumerate(names):
  131. i, d = counts[i]
  132. insert.append(i)
  133. delete.append(d)
  134. namelen = max(namelen, len(filename))
  135. maxdiff = max(maxdiff, i + d)
  136. output = b""
  137. statlen = len(str(maxdiff)) # stats column width
  138. for i, n in enumerate(names):
  139. binaryfile = nametypes[i]
  140. # %-19s | %-4d %s
  141. # note b'%d' % namelen is not supported until Python 3.5
  142. # To convert an int to a format width specifier for byte
  143. # strings use str(namelen).encode('ascii')
  144. format = (
  145. b" %-"
  146. + str(namelen).encode("ascii")
  147. + b"s | %"
  148. + str(statlen).encode("ascii")
  149. + b"s %s\n"
  150. )
  151. binformat = b" %-" + str(namelen).encode("ascii") + b"s | %s\n"
  152. if not binaryfile:
  153. hist = b""
  154. # -- calculating histogram --
  155. width = len(format % (b"", b"", b""))
  156. histwidth = max(2, max_width - width)
  157. if maxdiff < histwidth:
  158. hist = b"+" * insert[i] + b"-" * delete[i]
  159. else:
  160. iratio = (float(insert[i]) / maxdiff) * histwidth
  161. dratio = (float(delete[i]) / maxdiff) * histwidth
  162. iwidth = dwidth = 0
  163. # make sure every entry that had actual insertions gets
  164. # at least one +
  165. if insert[i] > 0:
  166. iwidth = int(iratio)
  167. if iwidth == 0 and 0 < iratio < 1:
  168. iwidth = 1
  169. # make sure every entry that had actual deletions gets
  170. # at least one -
  171. if delete[i] > 0:
  172. dwidth = int(dratio)
  173. if dwidth == 0 and 0 < dratio < 1:
  174. dwidth = 1
  175. hist = b"+" * int(iwidth) + b"-" * int(dwidth)
  176. output += format % (
  177. bytes(names[i]),
  178. str(insert[i] + delete[i]).encode("ascii"),
  179. hist,
  180. )
  181. else:
  182. output += binformat % (bytes(names[i]), b"Bin")
  183. output += b" %d files changed, %d insertions(+), %d deletions(-)" % (
  184. len(names),
  185. sum(insert),
  186. sum(delete),
  187. )
  188. return output
  189. def main() -> int:
  190. """Main entry point for diffstat command line tool.
  191. Returns:
  192. Exit code (0 for success)
  193. """
  194. argv = sys.argv
  195. # allow diffstat.py to also be used from the command line
  196. if len(sys.argv) > 1:
  197. diffpath = argv[1]
  198. data = b""
  199. with open(diffpath, "rb") as f:
  200. data = f.read()
  201. lines = data.split(b"\n")
  202. result = diffstat(lines)
  203. print(result.decode("utf-8"))
  204. return 0
  205. # if no path argument to a diff file is passed in, run
  206. # a self test. The test case includes tricky things like
  207. # a diff of diff, binary files, renames with further changes
  208. # added files and removed files.
  209. # All extracted from Sigil-Ebook/Sigil's github repo with
  210. # full permission to use under this license.
  211. selftest = b"""
  212. diff --git a/docs/qt512.7_remove_bad_workaround.patch b/docs/qt512.7_remove_bad_workaround.patch
  213. new file mode 100644
  214. index 00000000..64e34192
  215. --- /dev/null
  216. +++ b/docs/qt512.7_remove_bad_workaround.patch
  217. @@ -0,0 +1,15 @@
  218. +--- qtbase/src/gui/kernel/qwindow.cpp.orig 2019-12-12 09:15:59.000000000 -0500
  219. ++++ qtbase/src/gui/kernel/qwindow.cpp 2020-01-10 10:36:53.000000000 -0500
  220. +@@ -218,12 +218,6 @@
  221. + QGuiApplicationPrivate::window_list.removeAll(this);
  222. + if (!QGuiApplicationPrivate::is_app_closing)
  223. + QGuiApplicationPrivate::instance()->modalWindowList.removeOne(this);
  224. +-
  225. +- // focus_window is normally cleared in destroy(), but the window may in
  226. +- // some cases end up becoming the focus window again. Clear it again
  227. +- // here as a workaround. See QTBUG-75326.
  228. +- if (QGuiApplicationPrivate::focus_window == this)
  229. +- QGuiApplicationPrivate::focus_window = 0;
  230. + }
  231. +
  232. + void QWindowPrivate::init(QScreen *targetScreen)
  233. diff --git a/docs/testplugin_v017.zip b/docs/testplugin_v017.zip
  234. new file mode 100644
  235. index 00000000..a4cf4c4c
  236. Binary files /dev/null and b/docs/testplugin_v017.zip differ
  237. diff --git a/ci_scripts/macgddeploy.py b/ci_scripts/gddeploy.py
  238. similarity index 73%
  239. rename from ci_scripts/macgddeploy.py
  240. rename to ci_scripts/gddeploy.py
  241. index a512d075..f9dacd33 100644
  242. --- a/ci_scripts/macgddeploy.py
  243. +++ b/ci_scripts/gddeploy.py
  244. @@ -1,19 +1,32 @@
  245. #!/usr/bin/env python3
  246. import os
  247. +import sys
  248. import subprocess
  249. import datetime
  250. import shutil
  251. +import glob
  252. gparent = os.path.expandvars('$GDRIVE_DIR')
  253. grefresh_token = os.path.expandvars('$GDRIVE_REFRESH_TOKEN')
  254. -travis_branch = os.path.expandvars('$TRAVIS_BRANCH')
  255. -travis_commit = os.path.expandvars('$TRAVIS_COMMIT')
  256. -travis_build_number = os.path.expandvars('$TRAVIS_BUILD_NUMBER')
  257. +if sys.platform.lower().startswith('darwin'):
  258. + travis_branch = os.path.expandvars('$TRAVIS_BRANCH')
  259. + travis_commit = os.path.expandvars('$TRAVIS_COMMIT')
  260. + travis_build_number = os.path.expandvars('$TRAVIS_BUILD_NUMBER')
  261. +
  262. + origfilename = './bin/Sigil.tar.xz'
  263. + newfilename = './bin/Sigil-{}-{}-build_num-{}.tar.xz'.format(travis_branch, travis_commit[:7],travis_build_numbe\
  264. r)
  265. +else:
  266. + appveyor_branch = os.path.expandvars('$APPVEYOR_REPO_BRANCH')
  267. + appveyor_commit = os.path.expandvars('$APPVEYOR_REPO_COMMIT')
  268. + appveyor_build_number = os.path.expandvars('$APPVEYOR_BUILD_NUMBER')
  269. + names = glob.glob('.\\installer\\Sigil-*-Setup.exe')
  270. + if not names:
  271. + exit(1)
  272. + origfilename = names[0]
  273. + newfilename = '.\\installer\\Sigil-{}-{}-build_num-{}-Setup.exe'.format(appveyor_branch, appveyor_commit[:7], ap\
  274. pveyor_build_number)
  275. -origfilename = './bin/Sigil.tar.xz'
  276. -newfilename = './bin/Sigil-{}-{}-build_num-{}.tar.xz'.format(travis_branch, travis_commit[:7],travis_build_number)
  277. shutil.copy2(origfilename, newfilename)
  278. folder_name = datetime.date.today()
  279. diff --git a/docs/qt512.6_backport_009abcd_fix.patch b/docs/qt512.6_backport_009abcd_fix.patch
  280. deleted file mode 100644
  281. index f4724347..00000000
  282. --- a/docs/qt512.6_backport_009abcd_fix.patch
  283. +++ /dev/null
  284. @@ -1,26 +0,0 @@
  285. ---- qtbase/src/widgets/kernel/qwidget.cpp.orig 2019-11-08 10:57:07.000000000 -0500
  286. -+++ qtbase/src/widgets/kernel/qwidget.cpp 2019-12-11 12:32:24.000000000 -0500
  287. -@@ -8934,6 +8934,23 @@
  288. - }
  289. - }
  290. - switch (event->type()) {
  291. -+ case QEvent::PlatformSurface: {
  292. -+ // Sync up QWidget's view of whether or not the widget has been created
  293. -+ switch (static_cast<QPlatformSurfaceEvent*>(event)->surfaceEventType()) {
  294. -+ case QPlatformSurfaceEvent::SurfaceCreated:
  295. -+ if (!testAttribute(Qt::WA_WState_Created))
  296. -+ create();
  297. -+ break;
  298. -+ case QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed:
  299. -+ if (testAttribute(Qt::WA_WState_Created)) {
  300. -+ // Child windows have already been destroyed by QWindow,
  301. -+ // so we skip them here.
  302. -+ destroy(false, false);
  303. -+ }
  304. -+ break;
  305. -+ }
  306. -+ break;
  307. -+ }
  308. - case QEvent::MouseMove:
  309. - mouseMoveEvent((QMouseEvent*)event);
  310. - break;
  311. diff --git a/docs/Building_Sigil_On_MacOSX.txt b/docs/Building_Sigil_On_MacOSX.txt
  312. index 3b41fd80..64914c78 100644
  313. --- a/docs/Building_Sigil_On_MacOSX.txt
  314. +++ b/docs/Building_Sigil_On_MacOSX.txt
  315. @@ -113,7 +113,7 @@ install_name_tool -add_rpath @loader_path/../../Frameworks ./bin/Sigil.app/Content
  316. # To test if the newly bundled python 3 version of Sigil is working properly ypou can do the following:
  317. -1. download testplugin_v014.zip from https://github.com/Sigil-Ebook/Sigil/tree/master/docs
  318. +1. download testplugin_v017.zip from https://github.com/Sigil-Ebook/Sigil/tree/master/docs
  319. 2. open Sigil.app to the normal nearly blank template epub it generates when opened
  320. 3. use Plugins->Manage Plugins menu and make sure the "Use Bundled Python" checkbox is checked
  321. 4. use the "Add Plugin" button to navigate to and add testplugin.zip and then hit "Okay" to exit the Manage Plugins Dialog
  322. """
  323. testoutput = b""" docs/qt512.7_remove_bad_workaround.patch | 15 ++++++++++++
  324. docs/testplugin_v017.zip | Bin
  325. ci_scripts/macgddeploy.py => ci_scripts/gddeploy.py | 0
  326. docs/qt512.6_backport_009abcd_fix.patch | 26 ---------------------
  327. docs/Building_Sigil_On_MacOSX.txt | 2 +-
  328. 5 files changed, 16 insertions(+), 27 deletions(-)"""
  329. # return 0 on success otherwise return -1
  330. result = diffstat(selftest.split(b"\n"))
  331. if result == testoutput:
  332. print("self test passed")
  333. return 0
  334. print("self test failed")
  335. print("Received:")
  336. print(result.decode("utf-8"))
  337. print("Expected:")
  338. print(testoutput.decode("utf-8"))
  339. return -1
  340. if __name__ == "__main__":
  341. sys.exit(main())