Skip to content

Commit 375449b

Browse files
committed
Attempt org stats
1 parent fdb1f0a commit 375449b

File tree

3 files changed

+118
-22
lines changed

3 files changed

+118
-22
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ For more information on inaccuracies, see issue
9696
[main
9797
workflow](https://github.com/jstrieb/github-stats/blob/master/.github/workflows/main.yml))
9898
called `EXCLUDE_FORKED_REPOS` with a value of `true`.
99+
- To generate statistics for a GitHub organization, add a new secret called
100+
`GITHUB_ORG` with the name of the organization. Note that when this is set,
101+
the statistics will be for the organization and not the user. Also, the
102+
`total_contributions`, `lines_changed`, and `views` statistics are not
103+
displayed for organizations.
99104
- These other values are added as secrets by default to prevent leaking
100105
information about private repositories. If you're not worried about that,
101106
you can change the values directly [in the Actions workflow

generate_images.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,17 @@ async def generate_overview(s: Stats) -> None:
3838
output = re.sub("{{ name }}", await s.name, output)
3939
output = re.sub("{{ stars }}", f"{await s.stargazers:,}", output)
4040
output = re.sub("{{ forks }}", f"{await s.forks:,}", output)
41-
output = re.sub("{{ contributions }}", f"{await s.total_contributions:,}", output)
42-
changed = (await s.lines_changed)[0] + (await s.lines_changed)[1]
43-
output = re.sub("{{ lines_changed }}", f"{changed:,}", output)
44-
output = re.sub("{{ views }}", f"{await s.views:,}", output)
41+
42+
# Conditional replacement for organization stats
43+
if s.org_name:
44+
output = re.sub(r"{{ contributions }}", "", output)
45+
output = re.sub(r"{{ lines_changed }}", "", output)
46+
output = re.sub(r"{{ views }}", "", output)
47+
else:
48+
output = re.sub("{{ contributions }}", f"{await s.total_contributions:,}", output)
49+
changed = (await s.lines_changed)[0] + (await s.lines_changed)[1]
50+
output = re.sub("{{ lines_changed }}", f"{changed:,}", output)
51+
output = re.sub("{{ views }}", f"{await s.views:,}", output)
4552
output = re.sub("{{ repos }}", f"{len(await s.repos):,}", output)
4653

4754
generate_output_folder()
@@ -106,6 +113,7 @@ async def main() -> None:
106113
user = os.getenv("GITHUB_ACTOR")
107114
if user is None:
108115
raise RuntimeError("Environment variable GITHUB_ACTOR must be set.")
116+
org = os.getenv("GITHUB_ORG")
109117
exclude_repos = os.getenv("EXCLUDED")
110118
excluded_repos = (
111119
{x.strip() for x in exclude_repos.split(",")} if exclude_repos else None
@@ -128,6 +136,7 @@ async def main() -> None:
128136
exclude_repos=excluded_repos,
129137
exclude_langs=excluded_langs,
130138
ignore_forked_repos=ignore_forked_repos,
139+
org_name=org,
131140
)
132141
await asyncio.gather(generate_languages(s), generate_overview(s))
133142

github_stats.py

Lines changed: 100 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,52 @@ def repos_overview(
250250
}}
251251
}}
252252
}}
253+
"""
254+
255+
@staticmethod
256+
def repos_overview_org(
257+
org_name: str, owned_cursor: Optional[str] = None
258+
) -> str:
259+
"""
260+
:param org_name: GitHub organization name
261+
:param owned_cursor: GraphQL cursor for pagination
262+
:return: GraphQL query with overview of organization repositories
263+
"""
264+
return f"""query {{
265+
organization(login: "{org_name}") {{
266+
login,
267+
name,
268+
repositories(
269+
first: 100,
270+
orderBy: {{
271+
field: UPDATED_AT,
272+
direction: DESC
273+
}},
274+
after: {"null" if owned_cursor is None else '"'+ owned_cursor +'"'}
275+
) {{
276+
pageInfo {{
277+
hasNextPage
278+
endCursor
279+
}}
280+
nodes {{
281+
nameWithOwner
282+
stargazers {{
283+
totalCount
284+
}}
285+
forkCount
286+
languages(first: 10, orderBy: {{field: SIZE, direction: DESC}}) {{
287+
edges {{
288+
size
289+
node {{
290+
name
291+
color
292+
}}
293+
}}
294+
}}
295+
}}
296+
}}
297+
}}
298+
}}
253299
"""
254300

255301
@staticmethod
@@ -317,6 +363,7 @@ def __init__(
317363
cache_ttl: int = 3600,
318364
include_views: bool = True,
319365
include_lines_changed: bool = True,
366+
org_name: Optional[str] = None,
320367
):
321368
self.username = username
322369
self._ignore_forked_repos = ignore_forked_repos
@@ -326,6 +373,7 @@ def __init__(
326373
self._include_lines_changed = include_lines_changed
327374
self.queries = Queries(username, access_token, session)
328375
self.cache = SimpleCache(ttl=cache_ttl) if enable_cache else None
376+
self.org_name = org_name
329377

330378
self._name: Optional[str] = None
331379
self._stargazers: Optional[int] = None
@@ -371,29 +419,52 @@ async def get_stats(self) -> None:
371419
next_owned = None
372420
next_contrib = None
373421
while True:
374-
raw_results = await self.queries.query(
375-
Queries.repos_overview(
376-
owned_cursor=next_owned, contrib_cursor=next_contrib
422+
if self.org_name:
423+
raw_results = await self.queries.query(
424+
Queries.repos_overview_org(
425+
org_name=self.org_name, owned_cursor=next_owned
426+
)
427+
)
428+
else:
429+
raw_results = await self.queries.query(
430+
Queries.repos_overview(
431+
owned_cursor=next_owned, contrib_cursor=next_contrib
432+
)
377433
)
378-
)
379434
raw_results = raw_results if raw_results is not None else {}
380435

381-
self._name = raw_results.get("data", {}).get("viewer", {}).get("name", None)
382-
if self._name is None:
383-
self._name = (
436+
if self.org_name:
437+
self._name = raw_results.get("data", {}).get("organization", {}).get("name", None)
438+
if self._name is None:
439+
self._name = (
440+
raw_results.get("data", {})
441+
.get("organization", {})
442+
.get("login", "No Name")
443+
)
444+
445+
owned_repos = (
384446
raw_results.get("data", {})
385-
.get("viewer", {})
386-
.get("login", "No Name")
447+
.get("organization", {})
448+
.get("repositories", {})
387449
)
450+
contrib_repos = {}
451+
else:
452+
self._name = raw_results.get("data", {}).get("viewer", {}).get("name", None)
453+
if self._name is None:
454+
self._name = (
455+
raw_results.get("data", {})
456+
.get("viewer", {})
457+
.get("login", "No Name")
458+
)
388459

389-
contrib_repos = (
390-
raw_results.get("data", {})
391-
.get("viewer", {})
392-
.get("repositoriesContributedTo", {})
393-
)
394-
owned_repos = (
395-
raw_results.get("data", {}).get("viewer", {}).get("repositories", {})
396-
)
460+
contrib_repos = (
461+
raw_results.get("data", {})
462+
.get("viewer", {})
463+
.get("repositoriesContributedTo", {})
464+
)
465+
owned_repos = (
466+
raw_results.get("data", {}).get("viewer", {}).get("repositories", {})
467+
)
397468

398469
repos = owned_repos.get("nodes", [])
399470
if not self._ignore_forked_repos:
@@ -424,7 +495,16 @@ async def get_stats(self) -> None:
424495
"color": lang.get("node", {}).get("color"),
425496
}
426497

427-
if owned_repos.get("pageInfo", {}).get(
498+
if self.org_name:
499+
if owned_repos.get("pageInfo", {}).get(
500+
"hasNextPage", False
501+
):
502+
next_owned = owned_repos.get("pageInfo", {}).get(
503+
"endCursor", next_owned
504+
)
505+
else:
506+
break
507+
elif owned_repos.get("pageInfo", {}).get(
428508
"hasNextPage", False
429509
) or contrib_repos.get("pageInfo", {}).get("hasNextPage", False):
430510
next_owned = owned_repos.get("pageInfo", {}).get(
@@ -515,6 +595,8 @@ async def total_contributions(self) -> int:
515595
"""
516596
if self._total_contributions is not None:
517597
return self._total_contributions
598+
if self.org_name:
599+
return 0
518600

519601
# Check cache first
520602
cache_key = f"total_contributions_{self.username}"

0 commit comments

Comments
 (0)