88GITHUB_TOKEN = os .getenv ("GITHUB_TOKEN" )
99GITHUB_USERNAME = os .getenv ("GITHUB_USERNAME" )
1010
11- # GitHub API URL
12- GITHUB_API_URL = (
13- f"https://api.github.com/users/{ GITHUB_USERNAME } /repos?per_page=100&type=all&page="
11+ # GitHub API URLs
12+ GITHUB_API_BASE = "https://api.github.com"
13+ USER_REPOS_URL = (
14+ f"{ GITHUB_API_BASE } /users/{ GITHUB_USERNAME } /repos?per_page=100&type=all&page="
1415)
16+ USER_ORGS_URL = f"{ GITHUB_API_BASE } /users/{ GITHUB_USERNAME } /orgs"
17+ ORG_REPOS_URL = f"{ GITHUB_API_BASE } /orgs/{{org}}/repos?per_page=100&page="
1518
1619# Output directories
1720TEXT_FOLDER = "output/text"
@@ -30,83 +33,215 @@ def check_folders(log_file):
3033 """Check if output directories exist, create if not, and log status."""
3134 log_message ("[INFO] Checking output directories..." , log_file )
3235
33- if not os .path .exists (TEXT_FOLDER ):
34- os .makedirs (TEXT_FOLDER )
36+ for folder in [TEXT_FOLDER , MARKDOWN_FOLDER ]:
37+ if not os .path .exists (folder ):
38+ os .makedirs (folder )
39+ log_message (
40+ f"[CREATED] Folder '{ folder } ' was missing and has been created." ,
41+ log_file ,
42+ )
43+ else :
44+ log_message (f"[FOUND] Folder '{ folder } ' exists." , log_file )
45+
46+ log_message ("[SUCCESS] All necessary folders are ready.\n " , log_file )
47+
48+
49+ def make_api_request (url , headers , log_file ):
50+ """Make an API request and handle errors."""
51+ global response
52+ try :
53+ response = requests .get (url , headers = headers )
54+ response .raise_for_status ()
55+ return response .json ()
56+ except requests .exceptions .RequestException as e :
3557 log_message (
36- f"[CREATED] Folder ' { TEXT_FOLDER } ' was missing and has been created. " ,
58+ f"[ERROR] API request failed: { url } - { str ( e ) } " ,
3759 log_file ,
3860 )
39- else :
40- log_message (f"[FOUND] Folder '{ TEXT_FOLDER } ' exists." , log_file )
61+ try :
62+ log_message (str (response .json ()), log_file ) # Print error details
63+ except :
64+ pass
65+ return []
66+
67+
68+ def fetch_user_repos (headers , log_file ):
69+ """Fetch user's personal GitHub repositories."""
70+ log_message ("[INFO] Fetching personal repositories from GitHub..." , log_file )
71+ repos = []
72+ page = 1
4173
42- if not os .path .exists (MARKDOWN_FOLDER ):
43- os .makedirs (MARKDOWN_FOLDER )
74+ while True :
75+ page_repos = make_api_request (USER_REPOS_URL + str (page ), headers , log_file )
76+ if not page_repos :
77+ break
78+
79+ repos .extend (page_repos )
80+ page += 1
81+
82+ log_message (f"[SUCCESS] Retrieved { len (repos )} personal repositories." , log_file )
83+ return repos
84+
85+
86+ def fetch_organizations (headers , log_file ):
87+ """Fetch organizations that the user is a member of."""
88+ log_message ("[INFO] Fetching user's organizations..." , log_file )
89+ orgs = make_api_request (USER_ORGS_URL , headers , log_file )
90+
91+ if orgs :
92+ org_names = [org ["login" ] for org in orgs ]
4493 log_message (
45- f"[CREATED] Folder ' { MARKDOWN_FOLDER } ' was missing and has been created. " ,
94+ f"[SUCCESS] Found { len ( orgs ) } organizations: { ', ' . join ( org_names ) } " ,
4695 log_file ,
4796 )
4897 else :
49- log_message (f"[FOUND] Folder '{ MARKDOWN_FOLDER } ' exists." , log_file )
98+ log_message (
99+ "[INFO] No organizations found or unable to fetch organizations." , log_file
100+ )
50101
51- log_message ( "[SUCCESS] All necessary folders are ready. \n " , log_file )
102+ return orgs
52103
53104
54- def fetch_repos (log_file ):
55- """Fetch GitHub repositories and return a list of repo details."""
56- log_message ("[INFO] Fetching repositories from GitHub..." , log_file )
57- headers = {"Authorization" : f"token { GITHUB_TOKEN } " }
105+ def fetch_org_repos (org_name , headers , log_file ):
106+ """Fetch repositories from a specific organization."""
107+ log_message (
108+ f"[INFO] Fetching repositories from organization: { org_name } ..." , log_file
109+ )
58110 repos = []
59111 page = 1
60112
61113 while True :
62- response = requests .get (GITHUB_API_URL + str (page ), headers = headers )
63-
64- if response .status_code != 200 :
65- log_message (
66- f"[ERROR] Failed to fetch repositories. Status Code: { response .status_code } " ,
67- log_file ,
68- )
69- log_message (str (response .json ()), log_file ) # Print error details
70- return []
71-
72- page_repos = response .json ()
114+ url = ORG_REPOS_URL .format (org = org_name ) + str (page )
115+ page_repos = make_api_request (url , headers , log_file )
73116 if not page_repos :
74117 break
75118
76119 repos .extend (page_repos )
77120 page += 1
78121
79- log_message (f"[SUCCESS] Retrieved { len (repos )} repositories.\n " , log_file )
122+ log_message (
123+ f"[SUCCESS] Retrieved { len (repos )} repositories from { org_name } ." , log_file
124+ )
80125 return repos
81126
82127
128+ def fetch_all_repos (log_file ):
129+ """Fetch all repositories: personal and from organizations."""
130+ log_message ("[INFO] Beginning repository collection process..." , log_file )
131+ headers = {"Authorization" : f"token { GITHUB_TOKEN } " }
132+
133+ # Get personal repos
134+ user_repos = fetch_user_repos (headers , log_file )
135+
136+ # Get organization repos
137+ orgs = fetch_organizations (headers , log_file )
138+ org_repos = []
139+
140+ for org in orgs :
141+ org_name = org ["login" ]
142+ org_repos_list = fetch_org_repos (org_name , headers , log_file )
143+
144+ # Add organization info to each repo
145+ for repo in org_repos_list :
146+ repo ["organization" ] = org_name
147+
148+ org_repos .extend (org_repos_list )
149+
150+ # Add source info to personal repos
151+ for repo in user_repos :
152+ repo ["organization" ] = "personal"
153+
154+ # Combine all repos
155+ all_repos = user_repos + org_repos
156+
157+ log_message (f"[SUCCESS] Total repositories collected: { len (all_repos )} \n " , log_file )
158+ return all_repos
159+
160+
83161def format_repo_list (repos , log_file ):
84162 """Format repo data and generate both text and markdown outputs."""
85163 total_repos = len (repos )
86164 public_repos = sum (1 for repo in repos if not repo ["private" ])
87165 private_repos = total_repos - public_repos
88166
167+ # Count repos by source (personal vs organizations)
168+ personal_repos = sum (1 for repo in repos if repo ["organization" ] == "personal" )
169+ org_repos = total_repos - personal_repos
170+
171+ # Get unique organizations
172+ orgs = {
173+ repo ["organization" ] for repo in repos if repo ["organization" ] != "personal"
174+ }
175+
89176 timestamp = datetime .datetime .now ().strftime ("%Y-%m-%d_%H-%M-%S" )
90- text_filename = f"{ TEXT_FOLDER } /text_output_ { timestamp } .txt"
91- markdown_filename = f"{ MARKDOWN_FOLDER } /markdown_output_ { timestamp } .txt "
177+ text_filename = f"{ TEXT_FOLDER } /github_inventory_ { timestamp } .txt"
178+ markdown_filename = f"{ MARKDOWN_FOLDER } /github_inventory_ { timestamp } .md "
92179
93- log_message (f"[INFO] Formatting repository data...\n " , log_file )
180+ log_message (f"[INFO] Formatting repository data..." , log_file )
94181
95182 # Stats Header
96- header_text = f"GitHub Inventory Report\n Total Repositories: { total_repos } \n Public Repos: { public_repos } \n Private Repos: { private_repos } \n \n "
97- header_markdown = f"# GitHub Inventory Report\n **Total Repositories:** { total_repos } \n **Public Repos:** { public_repos } \n **Private Repos:** { private_repos } \n \n "
183+ header_text = (
184+ f"GitHub Inventory Report\n "
185+ f"Generated on: { datetime .datetime .now ().strftime ('%Y-%m-%d %H:%M:%S' )} \n "
186+ f"User: { GITHUB_USERNAME } \n \n "
187+ f"Repository Statistics:\n "
188+ f"- Total Repositories: { total_repos } \n "
189+ f"- Personal Repositories: { personal_repos } \n "
190+ f"- Organization Repositories: { org_repos } \n "
191+ f"- Public Repositories: { public_repos } \n "
192+ f"- Private Repositories: { private_repos } \n "
193+ f"- Organizations: { len (orgs )} \n \n "
194+ )
195+
196+ header_markdown = (
197+ f"# GitHub Inventory Report\n "
198+ f"**Generated on:** { datetime .datetime .now ().strftime ('%Y-%m-%d %H:%M:%S' )} \n "
199+ f"**User:** { GITHUB_USERNAME } \n \n "
200+ f"## Repository Statistics\n "
201+ f"- **Total Repositories:** { total_repos } \n "
202+ f"- **Personal Repositories:** { personal_repos } \n "
203+ f"- **Organization Repositories:** { org_repos } \n "
204+ f"- **Public Repositories:** { public_repos } \n "
205+ f"- **Private Repositories:** { private_repos } \n "
206+ f"- **Organizations:** { len (orgs )} \n \n "
207+ )
98208
99209 # Repo List
100210 text_output = header_text
101211 markdown_output = header_markdown
102212
103- for index , repo in enumerate (repos , 1 ): # Adding numbering here
213+ # Sort repos by source (personal first, then by org name) and then by repo name
214+ sorted_repos = sorted (
215+ repos ,
216+ key = lambda r : (r ["organization" ] != "personal" , r ["organization" ], r ["name" ]),
217+ )
218+
219+ # Group by source (personal/organization)
220+ current_source = None
221+
222+ for index , repo in enumerate (sorted_repos , 1 ):
104223 name = repo ["name" ]
105224 desc = repo ["description" ] or "No description provided."
106225 visibility = "Public" if not repo ["private" ] else "Private"
226+ source = repo ["organization" ]
227+
228+ # Add section headers when source changes
229+ if source != current_source :
230+ current_source = source
231+
232+ if source == "personal" :
233+ text_output += f"PERSONAL REPOSITORIES\n { '=' * 20 } \n \n "
234+ markdown_output += f"## Personal Repositories\n \n "
235+ else :
236+ text_output += f"\n ORGANIZATION: { source } \n { '=' * 20 } \n \n "
237+ markdown_output += f"## Organization: { source } \n \n "
107238
108- text_output += f"{ index } . { name } ({ visibility } )\n { desc } \n \n "
239+ # Add repository information
240+ text_output += f"{ index } . { name } ({ visibility } )\n { desc } \n \n "
109241 markdown_output += f"### { index } . { name } ({ visibility } )\n { desc } \n \n "
242+ # Add URL to markdown version
243+ if "html_url" in repo :
244+ markdown_output += f"[View on GitHub]({ repo ['html_url' ]} )\n \n "
110245
111246 # Writing to files
112247 with open (text_filename , "w" , encoding = "utf-8" ) as text_file :
@@ -126,11 +261,30 @@ def main():
126261 os .makedirs (LOGS_FOLDER )
127262 print (f"[INFO] Logs folder created at '{ LOGS_FOLDER } '" )
128263
129- log_filename = f"{ LOGS_FOLDER } /log.txt"
264+ log_filename = (
265+ f"{ LOGS_FOLDER } /log_{ datetime .datetime .now ().strftime ('%Y-%m-%d_%H-%M-%S' )} .txt"
266+ )
130267 with open (log_filename , "a" , encoding = "utf-8" ) as log_file :
131- log_message ("🔹 Starting GitHub Inventory Tool 🔹\n " , log_file )
268+ log_message (
269+ "🔹 Starting GitHub Inventory Tool (with Organizations) 🔹\n " , log_file
270+ )
132271 check_folders (log_file )
133- repos = fetch_repos (log_file )
272+
273+ if not GITHUB_TOKEN :
274+ log_message (
275+ "[ERROR] GitHub token not found. Please set GITHUB_TOKEN in your .env file." ,
276+ log_file ,
277+ )
278+ return
279+
280+ if not GITHUB_USERNAME :
281+ log_message (
282+ "[ERROR] GitHub username not found. Please set GITHUB_USERNAME in your .env file." ,
283+ log_file ,
284+ )
285+ return
286+
287+ repos = fetch_all_repos (log_file )
134288 if repos :
135289 format_repo_list (repos , log_file )
136290 log_message ("✅ Done. Exiting program.\n " , log_file )
0 commit comments