Skip to content

Commit 36bcfdc

Browse files
committed
feat: print the change report, either nicely formatted or JSON
1 parent 4a078f5 commit 36bcfdc

File tree

5 files changed

+216
-38
lines changed

5 files changed

+216
-38
lines changed

gh_org_mgr/_gh_org.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from ._helpers import (
2828
compare_two_dicts,
2929
compare_two_lists,
30-
pretty_print_dict,
30+
dict_to_pretty_string,
3131
sluggify_teamname,
3232
)
3333
from ._stats import OrgChanges
@@ -122,7 +122,7 @@ def ratelimit(self):
122122

123123
def pretty_print_dataclass(self) -> str:
124124
"""Convert this dataclass to a pretty-printed output"""
125-
return pretty_print_dict(asdict(self), sensible_keys=["gh_token", "gh_app_private_key"])
125+
return dict_to_pretty_string(asdict(self), sensible_keys=["gh_token", "gh_app_private_key"])
126126

127127
def _resolve_gh_username(self, username: str, teamname: str) -> NamedUser | None:
128128
"""Turn a username into a proper GitHub user object"""
@@ -418,7 +418,7 @@ def sync_current_teams_settings(self, dry: bool = False) -> None:
418418
logging.critical(
419419
"Team '%s' settings could not be edited. Error: \n%s",
420420
team.name,
421-
pretty_print_dict(exc.data),
421+
dict_to_pretty_string(exc.data),
422422
)
423423
sys.exit(1)
424424
else:

gh_org_mgr/_helpers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def compare_two_dicts(dict1: dict, dict2: dict) -> dict[str, dict[str, str | int
6666
return differences
6767

6868

69-
def pretty_print_dict(dictionary: dict, sensible_keys: None | list[str] = None) -> str:
69+
def dict_to_pretty_string(dictionary: dict, sensible_keys: None | list[str] = None) -> str:
7070
"""Convert a dict to a pretty-printed output"""
7171

7272
# Censor sensible fields
@@ -99,7 +99,7 @@ def pretty(d, indent=0):
9999

100100
def pretty_print_dataclass(dc):
101101
"""Convert dataclass to a pretty-printed output"""
102-
pretty_print_dict(asdict(dc))
102+
dict_to_pretty_string(asdict(dc))
103103

104104

105105
def implement_changes_into_class(dc_object, **changes: bool | str | list[str]):

gh_org_mgr/_stats.py

Lines changed: 131 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55
"""Dataclasses and functions for statistics"""
66

7+
import json
78
from dataclasses import dataclass, field
89

10+
from ._helpers import implement_changes_into_class
11+
912

1013
@dataclass
1114
class TeamChanges: # pylint: disable=too-many-instance-attributes
@@ -35,12 +38,13 @@ class RepoChanges: # pylint: disable=too-many-instance-attributes
3538
class OrgChanges: # pylint: disable=too-many-instance-attributes
3639
"""Dataclass holding general statistics about the changes made to the organization"""
3740

41+
dry: bool = False
3842
added_owners: list[str] = field(default_factory=list)
3943
degraded_owners: list[str] = field(default_factory=list)
40-
teams: dict[str, TeamChanges] = field(default_factory=dict)
41-
repos: dict[str, RepoChanges] = field(default_factory=dict)
4244
members_without_team: list[str] = field(default_factory=list)
4345
removed_members: list[str] = field(default_factory=list)
46+
teams: dict[str, TeamChanges] = field(default_factory=dict)
47+
repos: dict[str, RepoChanges] = field(default_factory=dict)
4448

4549
# --------------------------------------------------------------------------
4650
# Owners
@@ -62,7 +66,7 @@ def update_team(self, team_name: str, **changes: bool | str | list[str]) -> None
6266
if team_name not in self.teams:
6367
self.teams[team_name] = TeamChanges()
6468

65-
implement_changes(dc_object=self.teams[team_name], **changes)
69+
implement_changes_into_class(dc_object=self.teams[team_name], **changes)
6670

6771
def create_team(self, team: str) -> None:
6872
"""Team has been created"""
@@ -110,7 +114,7 @@ def update_repo(self, repo_name: str, **changes: bool | str | list[str]) -> None
110114
if repo_name not in self.teams:
111115
self.repos[repo_name] = RepoChanges()
112116

113-
implement_changes(dc_object=self.repos[repo_name], **changes)
117+
implement_changes_into_class(dc_object=self.repos[repo_name], **changes)
114118

115119
def change_repo_team_permissions(self, repo: str, team: str, perm: str) -> None:
116120
"""Team permissions have been changed for a repo"""
@@ -128,21 +132,127 @@ def remove_repo_collaborator(self, repo: str, user: str) -> None:
128132
"""Remove collaborator"""
129133
self.update_repo(repo_name=repo, removed_collaborators=user)
130134

131-
132-
def implement_changes(dc_object, **changes: bool | str | list[str]):
133-
"""Smartly add changes to a dataclass object"""
134-
for attribute, value in changes.items():
135-
current_value = getattr(dc_object, attribute)
136-
# attribute is list
137-
if isinstance(current_value, list):
138-
# input change is list
139-
if isinstance(value, list):
140-
current_value.extend(value)
141-
# input change is not list
142-
else:
143-
current_value.append(value)
144-
# All other cases, bool
135+
# --------------------------------------------------------------------------
136+
# Output
137+
# --------------------------------------------------------------------------
138+
def changes_into_dict(self) -> dict:
139+
"""Convert dataclass to dict, and only use classes that are not empty/False"""
140+
changes_dict: dict[str, list[str] | dict] = {
141+
key: value # type: ignore
142+
for key, value in {
143+
"dry": self.dry,
144+
"added_owners": self.added_owners,
145+
"degraded_owners": self.degraded_owners,
146+
"members_without_team": self.members_without_team,
147+
"removed_members": self.removed_members,
148+
"teams": self.teams,
149+
"repos": self.repos,
150+
}.items()
151+
if value # Exclude empty values
152+
}
153+
154+
team_changes: dict[str, TeamChanges] = changes_dict.get("teams", {}) # type: ignore
155+
repo_changes: dict[str, RepoChanges] = changes_dict.get("repos", {}) # type: ignore
156+
157+
for team, tchanges in team_changes.items():
158+
new_changes = {
159+
key: value
160+
for key, value in tchanges.__dict__.items()
161+
if value # Exclude empty values
162+
}
163+
team_changes[team] = new_changes # type: ignore
164+
changes_dict["teams"] = team_changes
165+
166+
for repo, rchanges in repo_changes.items():
167+
new_changes = {
168+
key: value
169+
for key, value in rchanges.__dict__.items()
170+
if value # Exclude empty values
171+
}
172+
repo_changes[repo] = new_changes # type: ignore
173+
changes_dict["repos"] = repo_changes
174+
175+
return changes_dict
176+
177+
def print_changes( # pylint: disable=too-many-branches, too-many-statements
178+
self, orgname: str, output: str, dry: bool
179+
) -> None:
180+
"""Print the changes, either in pretty format or as JSON"""
181+
182+
# Add dry run information to stats dataclass
183+
self.dry = dry
184+
185+
# Output in the requested format
186+
if output == "json":
187+
changes_dict = self.changes_into_dict()
188+
print(json.dumps(changes_dict, indent=2))
145189
else:
146-
setattr(dc_object, attribute, value)
147-
148-
return dc_object
190+
output = (
191+
f"#-------------------------------------{len(orgname)*'-'}\n"
192+
f"# Changes made to GitHub organisation {orgname}\n"
193+
f"#-------------------------------------{len(orgname)*'-'}\n\n"
194+
)
195+
if dry:
196+
output += "⚠️ Dry-run mode, no changes executed\n\n"
197+
if self.added_owners:
198+
output += f"➕ Added owners: {', '.join(self.added_owners)}\n"
199+
if self.degraded_owners:
200+
output += f"🔻 Degraded owners: {', '.join(self.degraded_owners)}\n"
201+
if self.members_without_team:
202+
output += f"⚠️ Members without team: {', '.join(self.members_without_team)}\n"
203+
if self.removed_members:
204+
output += (
205+
f"❌ Members removed from organisation: {', '.join(self.removed_members)}\n"
206+
)
207+
if self.teams:
208+
output += "\n🤝 Team Changes:\n"
209+
for team, tchanges in self.teams.items():
210+
output += f" 🔹 {team}:\n"
211+
if tchanges.unconfigured:
212+
output += " ⚠️ Is/was unconfigured\n"
213+
if tchanges.newly_created:
214+
output += " 🆕 Has been created\n"
215+
if tchanges.deleted:
216+
output += " ❌ Has been deleted\n"
217+
if tchanges.changed_config:
218+
output += " 🔧 Changed config:\n"
219+
for item in tchanges.changed_config:
220+
output += f" - {item}\n"
221+
if tchanges.added_members:
222+
output += " ➕ Added members:\n"
223+
for item in tchanges.added_members:
224+
output += f" - {item}\n"
225+
if tchanges.changed_members_role:
226+
output += ' 🔄 Changed members role:"'
227+
for item in tchanges.changed_members_role:
228+
output += f" - {item}\n"
229+
if tchanges.removed_members:
230+
output += " ❌ Removed members:\n"
231+
for item in tchanges.removed_members:
232+
output += f" - {item}\n"
233+
if tchanges.pending_members:
234+
output += " ⏳ Pending members:\n"
235+
for item in tchanges.pending_members:
236+
output += f" - {item}\n"
237+
if self.repos:
238+
output += "\n📂 Repository Changes:\n"
239+
for repo, rchanges in self.repos.items():
240+
output += f" 🔹 {repo}:\n"
241+
if rchanges.changed_permissions_for_teams:
242+
output += " 🔧 Changed permissions for teams:\n"
243+
for item in rchanges.changed_permissions_for_teams:
244+
output += f" - {item}\n"
245+
if rchanges.removed_teams:
246+
output += " ❌ Removed teams:\n"
247+
for item in rchanges.removed_teams:
248+
output += f" - {item}\n"
249+
if rchanges.unconfigured_teams_with_permissions:
250+
output += " ⚠️ Unconfigured teams with permissions:\n"
251+
for item in rchanges.unconfigured_teams_with_permissions:
252+
output += f" - {item}\n"
253+
if rchanges.removed_collaborators:
254+
output += " ❌ Removed collaborators:\n"
255+
for item in rchanges.removed_collaborators:
256+
output += f" - {item}\n"
257+
258+
print(output.strip())

gh_org_mgr/manage.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@
4040
required=True,
4141
help="Path to the directory in which the configuration of an GitHub organisation is located",
4242
)
43+
parser_sync.add_argument(
44+
"-o",
45+
"--output",
46+
help="Output format for report",
47+
choices=["json", "text"],
48+
default="text",
49+
)
4350
parser_sync.add_argument("--dry", action="store_true", help="Do not make any changes at GitHub")
4451
parser_sync.add_argument(
4552
"-A",
@@ -149,7 +156,9 @@ def main():
149156
logging.debug("Final dataclass:\n%s", org.pretty_print_dataclass())
150157
org.ratelimit()
151158

152-
print(org.stats)
159+
org.stats.print_changes(
160+
orgname=cfg_org.get("org_name", ""), output=args.output, dry=args.dry
161+
)
153162

154163
# Setup Team command
155164
elif args.command == "setup-team":

0 commit comments

Comments
 (0)