diffstat.py 13 KB

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