cli.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. import argparse
  2. import os
  3. import subprocess
  4. import sys
  5. import time
  6. import numpy
  7. from labours.burndown import import_pandas, load_burndown
  8. from labours.cors_web_server import web_server
  9. from labours.embeddings import train_embeddings, write_embeddings
  10. from labours.modes.burndown import plot_burndown, plot_many_burndown
  11. from labours.modes.devs import show_devs, show_devs_efforts
  12. from labours.modes.devs_parallel import load_devs_parallel, show_devs_parallel
  13. from labours.modes.languages import show_languages
  14. from labours.modes.old_vs_new import show_old_vs_new
  15. from labours.modes.overwrites import load_overwrites_matrix, plot_overwrites_matrix
  16. from labours.modes.ownership import load_ownership, plot_ownership
  17. from labours.modes.sentiment import show_sentiment_stats
  18. from labours.modes.shotness import show_shotness_stats
  19. from labours.readers import read_input
  20. def list_matplotlib_styles():
  21. script = "import sys; from matplotlib import pyplot; " \
  22. "sys.stdout.write(repr(pyplot.style.available))"
  23. styles = eval(subprocess.check_output([sys.executable, "-c", script]))
  24. styles.remove("classic")
  25. return ["default", "classic"] + styles
  26. def parse_args():
  27. parser = argparse.ArgumentParser()
  28. parser.add_argument("-o", "--output", default="",
  29. help="Path to the output file/directory (empty for display). "
  30. "If the extension is JSON, the data is saved instead of "
  31. "the real image.")
  32. parser.add_argument("-i", "--input", default="-",
  33. help="Path to the input file (- for stdin).")
  34. parser.add_argument("-f", "--input-format", default="auto", choices=["yaml", "pb", "auto"])
  35. parser.add_argument("--font-size", default=12, type=int,
  36. help="Size of the labels and legend.")
  37. parser.add_argument("--style", default="ggplot", choices=list_matplotlib_styles(),
  38. help="Plot style to use.")
  39. parser.add_argument("--backend", help="Matplotlib backend to use.")
  40. parser.add_argument("--background", choices=["black", "white"], default="white",
  41. help="Plot's general color scheme.")
  42. parser.add_argument("--size", help="Axes' size in inches, for example \"12,9\"")
  43. parser.add_argument("--relative", action="store_true",
  44. help="Occupy 100%% height for every measurement.")
  45. parser.add_argument("--tmpdir", help="Temporary directory for intermediate files.")
  46. parser.add_argument("-m", "--mode", dest="modes", default=[], action="append",
  47. choices=["burndown-project", "burndown-file", "burndown-person",
  48. "overwrites-matrix", "ownership", "couples-files",
  49. "couples-people", "couples-shotness", "shotness", "sentiment",
  50. "devs", "devs-efforts", "old-vs-new", "run-times",
  51. "languages", "devs-parallel", "all"],
  52. help="What to plot. Can be repeated, e.g. "
  53. "-m burndown-project -m run-times")
  54. parser.add_argument(
  55. "--resample", default="year",
  56. help="The way to resample the time series. Possible values are: "
  57. "\"month\", \"year\", \"no\", \"raw\" and pandas offset aliases ("
  58. "http://pandas.pydata.org/pandas-docs/stable/timeseries.html"
  59. "#offset-aliases).")
  60. dateutil_url = "https://dateutil.readthedocs.io/en/stable/parser.html#dateutil.parser.parse"
  61. parser.add_argument("--start-date",
  62. help="Start date of time-based plots. Any format is accepted which is "
  63. "supported by %s" % dateutil_url)
  64. parser.add_argument("--end-date",
  65. help="End date of time-based plots. Any format is accepted which is "
  66. "supported by %s" % dateutil_url)
  67. parser.add_argument("--disable-projector", action="store_true",
  68. help="Do not run Tensorflow Projector on couples.")
  69. parser.add_argument("--max-people", default=20, type=int,
  70. help="Maximum number of developers in overwrites matrix and people plots.")
  71. parser.add_argument("--order-ownership-by-time", action="store_true",
  72. help="Sort developers in the ownership plot according to their first "
  73. "appearance in the history. The default is sorting by the number of "
  74. "commits.")
  75. args = parser.parse_args()
  76. return args
  77. def main():
  78. args = parse_args()
  79. reader = read_input(args)
  80. header = reader.get_header()
  81. name = reader.get_name()
  82. burndown_warning = "Burndown stats were not collected. Re-run hercules with --burndown."
  83. burndown_files_warning = \
  84. "Burndown stats for files were not collected. Re-run hercules with " \
  85. "--burndown --burndown-files."
  86. burndown_people_warning = \
  87. "Burndown stats for people were not collected. Re-run hercules with " \
  88. "--burndown --burndown-people."
  89. couples_warning = "Coupling stats were not collected. Re-run hercules with --couples."
  90. shotness_warning = "Structural hotness stats were not collected. Re-run hercules with " \
  91. "--shotness. Also check --languages - the output may be empty."
  92. sentiment_warning = "Sentiment stats were not collected. Re-run hercules with --sentiment."
  93. devs_warning = "Devs stats were not collected. Re-run hercules with --devs."
  94. def run_times():
  95. rt = reader.get_run_times()
  96. pandas = import_pandas()
  97. series = pandas.to_timedelta(pandas.Series(rt).sort_values(ascending=False), unit="s")
  98. df = pandas.concat([series, series / series.sum()], axis=1)
  99. df.columns = ["time", "ratio"]
  100. print(df)
  101. def project_burndown():
  102. try:
  103. full_header = header + reader.get_burndown_parameters()
  104. except KeyError:
  105. print("project: " + burndown_warning)
  106. return
  107. plot_burndown(args, "project",
  108. *load_burndown(full_header, *reader.get_project_burndown(),
  109. resample=args.resample, interpolation_progress=True))
  110. def files_burndown():
  111. try:
  112. full_header = header + reader.get_burndown_parameters()
  113. except KeyError:
  114. print(burndown_warning)
  115. return
  116. try:
  117. plot_many_burndown(args, "file", full_header, reader.get_files_burndown())
  118. except KeyError:
  119. print("files: " + burndown_files_warning)
  120. def people_burndown():
  121. try:
  122. full_header = header + reader.get_burndown_parameters()
  123. except KeyError:
  124. print(burndown_warning)
  125. return
  126. try:
  127. plot_many_burndown(args, "person", full_header, reader.get_people_burndown())
  128. except KeyError:
  129. print("people: " + burndown_people_warning)
  130. def overwrites_matrix():
  131. try:
  132. plot_overwrites_matrix(args, name, *load_overwrites_matrix(
  133. *reader.get_people_interaction(), max_people=args.max_people))
  134. people, matrix = load_overwrites_matrix(
  135. *reader.get_people_interaction(), max_people=1000000, normalize=False)
  136. from scipy.sparse import csr_matrix
  137. matrix = matrix[:, 1:]
  138. matrix = numpy.triu(matrix) + numpy.tril(matrix).T
  139. matrix = matrix + matrix.T
  140. matrix = csr_matrix(matrix)
  141. try:
  142. write_embeddings("overwrites", args.output, not args.disable_projector,
  143. *train_embeddings(people, matrix, tmpdir=args.tmpdir))
  144. except AttributeError as e:
  145. print("Training the embeddings is not possible: %s: %s", type(e).__name__, e)
  146. except KeyError:
  147. print("overwrites_matrix: " + burndown_people_warning)
  148. def ownership_burndown():
  149. try:
  150. full_header = header + reader.get_burndown_parameters()
  151. except KeyError:
  152. print(burndown_warning)
  153. return
  154. try:
  155. plot_ownership(args, name, *load_ownership(
  156. full_header, *reader.get_ownership_burndown(), max_people=args.max_people,
  157. order_by_time=args.order_ownership_by_time))
  158. except KeyError:
  159. print("ownership: " + burndown_people_warning)
  160. def couples_files():
  161. try:
  162. write_embeddings("files", args.output, not args.disable_projector,
  163. *train_embeddings(*reader.get_files_coocc(),
  164. tmpdir=args.tmpdir))
  165. except KeyError:
  166. print(couples_warning)
  167. def couples_people():
  168. try:
  169. write_embeddings("people", args.output, not args.disable_projector,
  170. *train_embeddings(*reader.get_people_coocc(),
  171. tmpdir=args.tmpdir))
  172. except KeyError:
  173. print(couples_warning)
  174. def couples_shotness():
  175. try:
  176. write_embeddings("shotness", args.output, not args.disable_projector,
  177. *train_embeddings(*reader.get_shotness_coocc(),
  178. tmpdir=args.tmpdir))
  179. except KeyError:
  180. print(shotness_warning)
  181. def shotness():
  182. try:
  183. data = reader.get_shotness()
  184. except KeyError:
  185. print(shotness_warning)
  186. return
  187. show_shotness_stats(data)
  188. def sentiment():
  189. try:
  190. data = reader.get_sentiment()
  191. except KeyError:
  192. print(sentiment_warning)
  193. return
  194. show_sentiment_stats(args, reader.get_name(), args.resample, reader.get_header()[0], data)
  195. def devs():
  196. try:
  197. data = reader.get_devs()
  198. except KeyError:
  199. print(devs_warning)
  200. return
  201. show_devs(args, reader.get_name(), *reader.get_header(), *data,
  202. max_people=args.max_people)
  203. def devs_efforts():
  204. try:
  205. data = reader.get_devs()
  206. except KeyError:
  207. print(devs_warning)
  208. return
  209. show_devs_efforts(args, reader.get_name(), *reader.get_header(), *data,
  210. max_people=args.max_people)
  211. def old_vs_new():
  212. try:
  213. data = reader.get_devs()
  214. except KeyError:
  215. print(devs_warning)
  216. return
  217. show_old_vs_new(args, reader.get_name(), *reader.get_header(), *data)
  218. def languages():
  219. try:
  220. data = reader.get_devs()
  221. except KeyError:
  222. print(devs_warning)
  223. return
  224. show_languages(args, reader.get_name(), *reader.get_header(), *data)
  225. def devs_parallel():
  226. try:
  227. ownership = reader.get_ownership_burndown()
  228. except KeyError:
  229. print(burndown_people_warning)
  230. return
  231. try:
  232. couples = reader.get_people_coocc()
  233. except KeyError:
  234. print(couples_warning)
  235. return
  236. try:
  237. devs = reader.get_devs()
  238. except KeyError:
  239. print(devs_warning)
  240. return
  241. show_devs_parallel(args, reader.get_name(), *reader.get_header(),
  242. load_devs_parallel(ownership, couples, devs, args.max_people))
  243. modes = {
  244. "run-times": run_times,
  245. "burndown-project": project_burndown,
  246. "burndown-file": files_burndown,
  247. "burndown-person": people_burndown,
  248. "overwrites-matrix": overwrites_matrix,
  249. "ownership": ownership_burndown,
  250. "couples-files": couples_files,
  251. "couples-people": couples_people,
  252. "couples-shotness": couples_shotness,
  253. "shotness": shotness,
  254. "sentiment": sentiment,
  255. "devs": devs,
  256. "devs-efforts": devs_efforts,
  257. "old-vs-new": old_vs_new,
  258. "languages": languages,
  259. "devs-parallel": devs_parallel,
  260. }
  261. if "all" in args.modes:
  262. all_mode = True
  263. args.modes = [
  264. "burndown-project",
  265. "overwrites-matrix",
  266. "ownership",
  267. "couples-files",
  268. "couples-people",
  269. "couples-shotness",
  270. "shotness",
  271. "devs",
  272. "devs-efforts",
  273. ]
  274. else:
  275. all_mode = False
  276. for mode in args.modes:
  277. if mode not in modes:
  278. print("Unknown mode: %s" % mode)
  279. continue
  280. print("Running: %s" % mode)
  281. # `args.mode` is required for path determination in the mode functions
  282. args.mode = ("all" if all_mode else mode)
  283. try:
  284. modes[mode]()
  285. except ImportError as ie:
  286. print("A module required by the %s mode was not found: %s" % (mode, ie))
  287. if not all_mode:
  288. raise
  289. if web_server.running:
  290. secs = int(os.getenv("COUPLES_SERVER_TIME", "60"))
  291. print("Sleeping for %d seconds, safe to Ctrl-C" % secs)
  292. sys.stdout.flush()
  293. try:
  294. time.sleep(secs)
  295. except KeyboardInterrupt:
  296. pass
  297. web_server.stop()