Skip to content

Instantly share code, notes, and snippets.

@mordonez
Last active January 16, 2026 13:39
Show Gist options
  • Select an option

  • Save mordonez/31d8bdcb8c8915539344ac9ceefac5b3 to your computer and use it in GitHub Desktop.

Select an option

Save mordonez/31d8bdcb8c8915539344ac9ceefac5b3 to your computer and use it in GitHub Desktop.
GitHub Issues Time Calculator
#!/usr/bin/env python3
"""
GitHub Issues Time Calculator
==============================
Calculate total time spent from GitHub issues based on a custom time label pattern.
Perfect for teams tracking time in issue descriptions using formats like:
"⏲️ Time Spent: 6h 30m" or custom labels like "Tiempo consumido: 2h"
Requirements:
- gh CLI installed and authenticated (https://cli.github.com/)
- Python 3.8+ (uses walrus operator :=)
Default Pattern:
Matches: "⏲️ Time Spent: Xh Ym", "⏲️ Time Spent: Xh", "⏲️ Time Spent: Ym"
Examples: "6h 30m", "6h", "30m"
Usage:
python3 gh_time_calculator.py <github_issues_url> [time_label]
Examples:
# All issues in a repository
python3 gh_time_calculator.py 'https://github.com/owner/repo/issues?q=is:issue+state:all'
# Open issues only
python3 gh_time_calculator.py 'https://github.com/owner/repo/issues?q=is:issue+state:open'
# Specific issues (e.g., #167)
python3 gh_time_calculator.py 'https://github.com/owner/repo/issues?q=is:issue+%23167'
# Issues with specific labels
python3 gh_time_calculator.py 'https://github.com/owner/repo/issues?q=is:issue+label:bug'
# Custom time labels (script auto-generates regex)
python3 gh_time_calculator.py <url> 'Total time:'
python3 gh_time_calculator.py <url> 'Tiempo consumido:'
How it works:
1. Parses the GitHub issues URL to extract repository and query filters
2. Uses gh CLI to fetch all matching issues via GitHub API (with pagination)
3. Extracts time values from issue bodies using regex pattern
4. Calculates and displays total time summary
Note:
The script automatically chooses between GitHub's list API (faster, for simple
queries) or search API (for complex filters like labels, text search, etc.)
Author: mordonez
License: MIT
"""
import subprocess, json, re, os, sys
from urllib.parse import urlparse, parse_qs, unquote, quote
ANSI_RE = re.compile(r'\x1b\[[0-9;]*m')
def usage():
print("Usage: python3 gh_time_calculator.py <github_issues_url> [time_label]")
print("Example: python3 gh_time_calculator.py 'https://github.com/owner/repo/issues?q=is:issue+state:open'")
print("Custom label: python3 gh_time_calculator.py <url> 'Total time:'")
def build_pattern(label):
time_label = re.escape(label) if label else r'⏲️ Time Spent:'
return re.compile(rf'{time_label}\s*(?:(\d+)h)?(?:\s*(\d+)m)?')
def parse_url(issues_url):
url = urlparse(issues_url)
repo = '/'.join(url.path.split('/')[1:3])
query = unquote(parse_qs(url.query).get('q', ['is:issue state:all'])[0])
return repo, query
def build_api_path(repo, query):
clean = re.sub(r'\bis:issue\b|\bstate:\w+\b', '', query).strip()
if clean:
return f"search/issues?q={quote(f'repo:{repo} {query}')}" , True
state = re.search(r'state:(\w+)', query)
return f"repos/{repo}/issues?state={state.group(1) if state else 'all'}", False
def fetch_issues(api_path, use_search):
env = os.environ.copy()
env.update({'NO_COLOR': '1', 'GH_PAGER': ''})
issues, page = [], 1
while True:
proc = subprocess.Popen(
["gh", "api", f"{api_path}&per_page=100&page={page}"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
env=env,
)
stdout, _ = proc.communicate()
if proc.returncode != 0 or not stdout.strip():
break
try:
data = json.loads(ANSI_RE.sub('', stdout))
items = data.get('items', data) if use_search else data
if not items:
break
issues.extend(items)
if len(items) < 100:
break
page += 1
except json.JSONDecodeError:
break
return issues
def total_minutes(issues, pattern):
total = 0
count = 0
for issue in issues:
body = issue.get('body') or ''
match = pattern.search(body)
if not match:
continue
hours = int(match.group(1) or 0)
minutes = int(match.group(2) or 0)
total += hours * 60 + minutes
count += 1
return total, count
def main():
if len(sys.argv) < 2:
usage()
sys.exit(1)
pattern = build_pattern(sys.argv[2] if len(sys.argv) > 2 else None)
repo, query = parse_url(sys.argv[1])
api_path, use_search = build_api_path(repo, query)
print("\nFetching issues...", end=" ", flush=True)
issues = fetch_issues(api_path, use_search)
print("OK\n")
total, count = total_minutes(issues, pattern)
print("=" * 60)
print(f" TOTAL TIME: {total // 60}h {total % 60}m ({total:,} minutes)")
print(f" Issues with time: {count}/{len(issues)}")
print("=" * 60 + "\n")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment