feature_votes.sh 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. #!/bin/bash
  2. # Rank open enhancement issues by community thumbs-up votes.
  3. #
  4. # A vote is a 👍 on the issue itself or on the "gauge community interest"
  5. # comment. Each person counts once, and the issue author and the maintainer
  6. # are left out. Read-only: it only queries the GitHub API through gh.
  7. #
  8. # Usage: ./feature_votes.sh [limit] (default: all issues)
  9. # ./feature_votes.sh --md [limit] (Markdown table)
  10. set -euo pipefail
  11. FORMAT=text
  12. if [[ "${1:-}" == "--md" ]]; then
  13. FORMAT=md
  14. shift
  15. fi
  16. LIMIT="${1:-0}"
  17. command -v gh >/dev/null || { echo "gh (GitHub CLI) is required" >&2; exit 1; }
  18. gh auth status >/dev/null 2>&1 || { echo "gh is not logged in; run: gh auth login" >&2; exit 1; }
  19. FORMAT="$FORMAT" LIMIT="$LIMIT" python3 - <<'PY'
  20. import json
  21. import os
  22. import subprocess
  23. REPO = "maziggy/bambuddy"
  24. MAINTAINER = "maziggy"
  25. SEARCH = f"repo:{REPO} is:issue state:open label:enhancement -label:contrib"
  26. POLL_MARKER = "I'd like to gauge community interest"
  27. QUERY = """
  28. query($q: String!, $after: String) {
  29. search(query: $q, type: ISSUE, first: 30, after: $after) {
  30. issueCount
  31. pageInfo { hasNextPage endCursor }
  32. nodes {
  33. ... on Issue {
  34. number title url author { login }
  35. reactions(content: THUMBS_UP, first: 100) { nodes { user { login } } }
  36. comments(first: 100) {
  37. totalCount
  38. nodes { body reactions(content: THUMBS_UP, first: 100) { nodes { user { login } } } }
  39. }
  40. }
  41. }
  42. }
  43. }
  44. """
  45. def voters(reactions):
  46. return {r["user"]["login"] for r in reactions["nodes"] if r["user"]}
  47. rows, after, total, truncated = [], None, 0, 0
  48. while True:
  49. args = ["gh", "api", "graphql", "-f", f"query={QUERY}", "-f", f"q={SEARCH}"]
  50. if after:
  51. args += ["-f", f"after={after}"]
  52. page = json.loads(subprocess.check_output(args))["data"]["search"]
  53. total = page["issueCount"]
  54. for issue in page["nodes"]:
  55. body = voters(issue["reactions"])
  56. poll, has_poll = set(), False
  57. for comment in issue["comments"]["nodes"]:
  58. if POLL_MARKER in (comment["body"] or ""):
  59. has_poll = True
  60. poll |= voters(comment["reactions"])
  61. if issue["comments"]["totalCount"] > 100:
  62. truncated += 1
  63. author = (issue["author"] or {}).get("login")
  64. votes = len((body | poll) - {author, MAINTAINER})
  65. rows.append((votes, len(body), len(poll) if has_poll else None, issue["number"], issue["title"], issue["url"]))
  66. if not page["pageInfo"]["hasNextPage"]:
  67. break
  68. after = page["pageInfo"]["endCursor"]
  69. rows.sort(key=lambda r: (-r[0], -r[1], r[3]))
  70. limit = int(os.environ["LIMIT"])
  71. shown = rows[:limit] if limit > 0 else rows
  72. print(f"{total} open enhancement issues, {sum(r[2] is not None for r in rows)} with a poll comment, "
  73. f"{sum(r[0] == 0 for r in rows)} without outside votes")
  74. if truncated:
  75. print(f"Note: {truncated} issues have more than 100 comments; only the first 100 were checked")
  76. print()
  77. if os.environ["FORMAT"] == "md":
  78. print("| votes | on issue | on poll | issue |\n|---:|---:|---:|---|")
  79. for votes, body, poll, number, title, url in shown:
  80. print(f"| {votes} | {body} | {'-' if poll is None else poll} | [#{number}]({url}) {title.replace('|', '/')} |")
  81. else:
  82. print(f"{'votes':>5} {'on issue':>8} {'on poll':>7} issue")
  83. for votes, body, poll, number, title, _ in shown:
  84. print(f"{votes:>5} {body:>8} {'-' if poll is None else poll:>7} #{number} {title}")
  85. PY