diffstat.py 13 KB

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