codefetch-cli 1.2.3__tar.gz → 1.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codefetch-cli
3
- Version: 1.2.3
3
+ Version: 1.2.4
4
4
  Summary: Quickly fetch code files from GitHub repositories with simple commands
5
5
  Home-page: https://github.com/InzamamulQureshi/Codefetch
6
6
  Author: Inzamamul Qureshi
@@ -16,17 +16,17 @@ Usage in Python:
16
16
  files = cf.list("torvalds/linux")
17
17
  """
18
18
 
19
- __version__ = "1.2.3"
19
+ __version__ = "1.2.4"
20
20
 
21
21
 
22
- def get(target, filename=None, branch="main"):
22
+ def get(target, filename=None, branch=None):
23
23
  """
24
24
  Fetch and return the text content of a file from GitHub.
25
25
 
26
26
  Args:
27
27
  target: 'owner/repo' or full path 'owner/repo/path/to/file' or full GitHub URL.
28
28
  filename: Optional path to file in repo if target is 'owner/repo'.
29
- branch: Branch name (default: 'main', automatically falls back to 'master').
29
+ branch: Branch name (default: None, automatically detects repo default branch).
30
30
 
31
31
  Returns:
32
32
  str: File content.
@@ -37,8 +37,10 @@ def get(target, filename=None, branch="main"):
37
37
  """
38
38
  import urllib.request
39
39
  from .cli import (
40
- fetch_raw_direct,
41
- get_repo_tree_with_branch,
40
+ get_default_branch,
41
+ check_branch_exists,
42
+ fetch_raw_single,
43
+ get_repo_tree,
42
44
  resolve_file_in_tree,
43
45
  parse_repo_identifier,
44
46
  resolve_invocation,
@@ -49,7 +51,7 @@ def get(target, filename=None, branch="main"):
49
51
  file_path = filename.strip("/\\")
50
52
  else:
51
53
  owner, repo, resolved_branch, file_path = resolve_invocation(
52
- [target], None, None, branch, None, None, "main"
54
+ [target], None, None, branch, None, None
53
55
  )
54
56
  if resolved_branch:
55
57
  branch = resolved_branch
@@ -57,24 +59,30 @@ def get(target, filename=None, branch="main"):
57
59
  if not owner or not repo or not file_path:
58
60
  raise ValueError(f"Could not resolve owner, repo, and filename from target='{target}', filename='{filename}'")
59
61
 
62
+ target_branch = branch
63
+ if not target_branch:
64
+ target_branch = get_default_branch(owner, repo)
65
+ elif not check_branch_exists(owner, repo, target_branch):
66
+ target_branch = get_default_branch(owner, repo)
67
+
60
68
  # 1. Try fast direct raw fetch
61
- found_branch, content = fetch_raw_direct(owner, repo, branch, file_path)
69
+ content = fetch_raw_single(owner, repo, target_branch, file_path)
62
70
  if content is not None:
63
71
  return content
64
72
 
65
73
  # 2. Try repository tree search
66
- actual_branch, tree_data = get_repo_tree_with_branch(owner, repo, branch)
74
+ tree_data = get_repo_tree(owner, repo, target_branch)
67
75
  matched = resolve_file_in_tree(tree_data, file_path)
68
76
  if matched:
69
- url = f"https://raw.githubusercontent.com/{owner}/{repo}/{actual_branch}/{matched}"
77
+ url = f"https://raw.githubusercontent.com/{owner}/{repo}/{target_branch}/{matched}"
70
78
  req = urllib.request.Request(url, headers={"User-Agent": "codefetch"})
71
79
  with urllib.request.urlopen(req, timeout=15) as resp:
72
80
  return resp.read().decode("utf-8")
73
81
 
74
- raise FileNotFoundError(f"File '{file_path}' not found in {owner}/{repo} ({actual_branch})")
82
+ raise FileNotFoundError(f"File '{file_path}' not found in {owner}/{repo} ({target_branch})")
75
83
 
76
84
 
77
- def download(target, filename=None, output=None, branch="main"):
85
+ def download(target, filename=None, output=None, branch=None):
78
86
  """
79
87
  Download a file from GitHub and save it locally.
80
88
 
@@ -82,7 +90,7 @@ def download(target, filename=None, output=None, branch="main"):
82
90
  target: 'owner/repo' or full path 'owner/repo/path/to/file'.
83
91
  filename: Optional path to file in repo if target is 'owner/repo'.
84
92
  output: Destination filename/path on local disk (default: original filename).
85
- branch: Branch name (default: 'main').
93
+ branch: Branch name (default: None, automatically detects repo default branch).
86
94
 
87
95
  Returns:
88
96
  str: Path to the downloaded file.
@@ -99,20 +107,22 @@ def download(target, filename=None, output=None, branch="main"):
99
107
  return dest_name
100
108
 
101
109
 
102
- def list(target, branch="main"):
110
+ def list(target, branch=None):
103
111
  """
104
112
  List supported code files in a repository.
105
113
 
106
114
  Args:
107
115
  target: 'owner/repo' or repository URL.
108
- branch: Branch name (default: 'main').
116
+ branch: Branch name (default: None, automatically detects repo default branch).
109
117
 
110
118
  Returns:
111
119
  list[str]: Relative paths of code files in the repository.
112
120
  """
113
121
  from .cli import (
114
122
  parse_repo_identifier,
115
- get_repo_tree_with_branch,
123
+ get_default_branch,
124
+ check_branch_exists,
125
+ get_repo_tree,
116
126
  CODE_EXTENSIONS,
117
127
  NAMED_FILES,
118
128
  )
@@ -121,7 +131,11 @@ def list(target, branch="main"):
121
131
  if not owner or not repo:
122
132
  raise ValueError(f"Invalid repository identifier: '{target}'")
123
133
 
124
- actual_branch, tree_data = get_repo_tree_with_branch(owner, repo, branch)
134
+ target_branch = branch
135
+ if not target_branch or not check_branch_exists(owner, repo, target_branch):
136
+ target_branch = get_default_branch(owner, repo)
137
+
138
+ tree_data = get_repo_tree(owner, repo, target_branch)
125
139
  files = []
126
140
  for item in tree_data.get("tree", []):
127
141
  if item.get("type") != "blob":
@@ -14,11 +14,11 @@ from pathlib import Path
14
14
 
15
15
  DEFAULT_OWNER = None
16
16
  DEFAULT_REPO = None
17
- DEFAULT_BRANCH = "main"
17
+ DEFAULT_BRANCH = None
18
18
 
19
19
  CONFIG_FILE = Path.home() / ".codefetch.json"
20
20
 
21
- __version__ = "1.2.3"
21
+ __version__ = "1.2.4"
22
22
 
23
23
  # Supported code, data, and config extensions
24
24
  CODE_EXTENSIONS = {
@@ -93,7 +93,7 @@ FLAGS & OPTIONS:
93
93
  -R, --repos List public repositories for a GitHub user
94
94
  -r, --repo <repo> Specify repository name or URL
95
95
  -u, --user <user> Specify GitHub username / organization
96
- -b, --branch <branch> Specify branch name (default: main)
96
+ -b, --branch <branch> Specify branch name (default: repository default branch)
97
97
  --set-default <repo> Save default repository (e.g. owner/repo)
98
98
  --set-default-user <user> Save default user/organization
99
99
  --config Show current saved configuration
@@ -102,27 +102,32 @@ FLAGS & OPTIONS:
102
102
  -h, --help Show this help message
103
103
  """
104
104
 
105
+ _REPO_DEFAULT_BRANCH_CACHE = {}
106
+ _REPO_BRANCHES_CACHE = {}
107
+
105
108
 
106
109
  def load_config():
107
110
  """Load user configuration from ~/.codefetch.json."""
108
111
  if CONFIG_FILE.exists():
109
112
  try:
110
113
  with open(CONFIG_FILE, "r", encoding="utf-8") as f:
111
- return json.load(f)
114
+ data = json.load(f)
115
+ return {
116
+ "owner": data.get("owner"),
117
+ "repo": data.get("repo"),
118
+ }
112
119
  except Exception:
113
120
  return {}
114
121
  return {}
115
122
 
116
123
 
117
- def save_config(owner=None, repo=None, branch=None):
118
- """Save user configuration to ~/.codefetch.json."""
124
+ def save_config(owner=None, repo=None):
125
+ """Save user configuration to ~/.codefetch.json (no branch saved in config)."""
119
126
  cfg = load_config()
120
127
  if owner is not None:
121
128
  cfg["owner"] = owner
122
129
  if repo is not None:
123
130
  cfg["repo"] = repo
124
- if branch is not None:
125
- cfg["branch"] = branch
126
131
 
127
132
  try:
128
133
  with open(CONFIG_FILE, "w", encoding="utf-8") as f:
@@ -168,76 +173,99 @@ def parse_repo_identifier(repo_str, default_owner=None):
168
173
  return default_owner, repo_str.removesuffix(".git")
169
174
 
170
175
 
171
- def fetch_raw_direct(owner, repo, branch, clean_filename):
176
+ def get_default_branch(owner, repo):
172
177
  """
173
- Direct raw fetch from raw.githubusercontent.com.
174
- Tries requested branch first. If branch is 'main', also tries 'master',
175
- and vice versa.
176
- Returns (branch_used, content_str) or (None, None).
178
+ Get the default branch of a repository (e.g. 'main', 'master', 'unstable').
179
+ Queries GitHub API and caches the result. Falls back to probing if rate limited.
177
180
  """
178
- branches_to_try = [branch]
179
- if branch == "main":
180
- branches_to_try.append("master")
181
- elif branch == "master":
182
- branches_to_try.append("main")
183
-
184
- for b in branches_to_try:
185
- url = f"https://raw.githubusercontent.com/{owner}/{repo}/{b}/{clean_filename}"
186
- try:
187
- req = urllib.request.Request(url, headers={"User-Agent": "codefetch"})
188
- with urllib.request.urlopen(req, timeout=15) as resp:
189
- return b, resp.read().decode("utf-8")
190
- except urllib.error.HTTPError as e:
191
- if e.code == 404:
192
- continue
193
- raise
194
- return None, None
181
+ key = f"{owner.lower()}/{repo.lower()}"
182
+ if key in _REPO_DEFAULT_BRANCH_CACHE:
183
+ return _REPO_DEFAULT_BRANCH_CACHE[key]
195
184
 
185
+ try:
186
+ url = f"https://api.github.com/repos/{owner}/{repo}"
187
+ data = get_json(url)
188
+ def_b = data.get("default_branch")
189
+ if def_b:
190
+ _REPO_DEFAULT_BRANCH_CACHE[key] = def_b
191
+ return def_b
192
+ except Exception:
193
+ pass
196
194
 
197
- def get_repo_tree_with_branch(owner, repo, branch="main"):
198
- """
199
- Fetch the Git tree for a repository, returning (actual_branch, tree_data).
200
- Tries branch first, then fallback to master/main, then checks repository info for default_branch.
201
- """
202
- branches_to_try = [branch]
203
- if branch == "main":
204
- branches_to_try.append("master")
205
- elif branch == "master":
206
- branches_to_try.append("main")
207
-
208
- for b in branches_to_try:
209
- url = f"https://api.github.com/repos/{owner}/{repo}/git/trees/{b}?recursive=1"
195
+ # Fast fallback probe via CDN if API is unavailable or rate-limited
196
+ for b in ("main", "master"):
210
197
  try:
211
- return b, get_json(url)
212
- except urllib.error.HTTPError as e:
213
- if e.code == 404:
214
- continue
215
- raise
198
+ req = urllib.request.Request(
199
+ f"https://raw.githubusercontent.com/{owner}/{repo}/{b}/README.md",
200
+ headers=get_headers(),
201
+ )
202
+ with urllib.request.urlopen(req, timeout=5):
203
+ _REPO_DEFAULT_BRANCH_CACHE[key] = b
204
+ return b
205
+ except Exception:
206
+ continue
207
+
208
+ return "main"
209
+
210
+
211
+ def get_repo_branches(owner, repo):
212
+ """Fetch the list of all branch names in a repository."""
213
+ key = f"{owner.lower()}/{repo.lower()}"
214
+ if key in _REPO_BRANCHES_CACHE:
215
+ return _REPO_BRANCHES_CACHE[key]
216
216
 
217
- # Query repository info to discover actual default branch (e.g. develop, trunk, etc.)
218
217
  try:
219
- info_url = f"https://api.github.com/repos/{owner}/{repo}"
220
- info = get_json(info_url)
221
- def_branch = info.get("default_branch")
222
- if def_branch and def_branch not in branches_to_try:
223
- url = f"https://api.github.com/repos/{owner}/{repo}/git/trees/{def_branch}?recursive=1"
224
- return def_branch, get_json(url)
218
+ url = f"https://api.github.com/repos/{owner}/{repo}/branches?per_page=100"
219
+ data = get_json(url)
220
+ branches = [b["name"] for b in data if isinstance(b, dict) and b.get("name")]
221
+ if branches:
222
+ _REPO_BRANCHES_CACHE[key] = branches
223
+ return branches
225
224
  except Exception:
226
225
  pass
226
+ return []
227
227
 
228
- raise urllib.error.HTTPError(
229
- f"https://api.github.com/repos/{owner}/{repo}/git/trees/{branch}",
230
- 404,
231
- "Branch tree not found",
232
- {},
233
- None,
234
- )
228
+
229
+ def check_branch_exists(owner, repo, branch):
230
+ """Check if a specific branch exists in a repository."""
231
+ if not branch:
232
+ return False
233
+ branches = get_repo_branches(owner, repo)
234
+ if branches:
235
+ return branch in branches
236
+ try:
237
+ get_json(f"https://api.github.com/repos/{owner}/{repo}/branches/{branch}")
238
+ return True
239
+ except urllib.error.HTTPError as e:
240
+ if e.code == 404:
241
+ return False
242
+ return True
243
+ except Exception:
244
+ return True
235
245
 
236
246
 
237
- def get_repo_tree(owner, repo, branch="main"):
238
- """Fetch the Git tree for a repository (backwards compatible helper)."""
239
- _, tree_data = get_repo_tree_with_branch(owner, repo, branch)
240
- return tree_data
247
+ def fetch_raw_single(owner, repo, branch, clean_filename):
248
+ """
249
+ Direct raw fetch from raw.githubusercontent.com for a specific branch.
250
+ Returns content string or None if 404.
251
+ """
252
+ url = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{clean_filename}"
253
+ try:
254
+ req = urllib.request.Request(url, headers={"User-Agent": "codefetch"})
255
+ with urllib.request.urlopen(req, timeout=15) as resp:
256
+ return resp.read().decode("utf-8")
257
+ except urllib.error.HTTPError as e:
258
+ if e.code == 404:
259
+ return None
260
+ raise
261
+ except UnicodeDecodeError:
262
+ raise
263
+
264
+
265
+ def get_repo_tree(owner, repo, branch):
266
+ """Fetch the Git tree for a repository on a given branch."""
267
+ url = f"https://api.github.com/repos/{owner}/{repo}/git/trees/{branch}?recursive=1"
268
+ return get_json(url)
241
269
 
242
270
 
243
271
  def get_user_repos(owner):
@@ -247,7 +275,7 @@ def get_user_repos(owner):
247
275
 
248
276
 
249
277
  def list_repos(owner=None, default_repo=None):
250
- """List all public repositories for a GitHub user."""
278
+ """List all public repositories for a GitHub user with clean spacing."""
251
279
  if not owner:
252
280
  print("Error: No GitHub username specified.")
253
281
  print("Usage: codefetch --repos <username>")
@@ -259,21 +287,21 @@ def list_repos(owner=None, default_repo=None):
259
287
  print(f"No public repositories found for user '{owner}'.")
260
288
  return
261
289
 
262
- print(f"Public repositories for '{owner}':\n")
290
+ print(f"Public repositories for '{owner}' ({len(repos)} total):\n")
263
291
  for i, r in enumerate(repos, 1):
264
292
  name = r.get("name", "")
265
- desc = r.get("description") or "No description"
293
+ desc = (r.get("description") or "").strip()
266
294
  is_def = " (default)" if default_repo and name.lower() == default_repo.lower() else ""
267
- marker = "*" if is_def else "-"
268
- print(f" {marker} {name:<22}{is_def}")
269
- if desc and desc != "No description":
295
+ print(f" {i:>2}. {name}{is_def}")
296
+ if desc:
270
297
  print(f" {desc}")
298
+ print()
271
299
 
272
- print(f"\nTotal: {len(repos)} repositories")
273
- print("\nCommands:")
274
- print(f" codefetch -l {owner}/<repo> List files in a repository")
275
- print(f" codefetch {owner}/<repo> <filename> Download a file from a repository")
276
- print(f" codefetch --set-default {owner}/<repo> Set default repository")
300
+ print("Commands:")
301
+ print(f" codefetch {owner}/<repo> <filename> Download a file")
302
+ print(f" codefetch -s {owner}/<repo> <filename> Preview file in terminal")
303
+ print(f" codefetch -l {owner}/<repo> List files in repository")
304
+ print(f" codefetch --set-default {owner}/<repo> Set as default repository")
277
305
 
278
306
  except urllib.error.HTTPError as e:
279
307
  print(f"Could not list repositories: HTTP {e.code}")
@@ -283,8 +311,8 @@ def list_repos(owner=None, default_repo=None):
283
311
  print(f"Could not list repositories: {e}")
284
312
 
285
313
 
286
- def list_files(owner, repo, branch="main"):
287
- """List code files in a repository."""
314
+ def list_files(owner, repo, branch=None):
315
+ """List code files in a repository, with automatic branch detection and fallback."""
288
316
  if not owner or not repo:
289
317
  print("Error: No repository specified to list.\n")
290
318
  print("Usage:")
@@ -292,8 +320,25 @@ def list_files(owner, repo, branch="main"):
292
320
  print(" codefetch -u <owner> -r <repo> -l")
293
321
  return
294
322
 
323
+ target_branch = branch
324
+ default_branch = get_default_branch(owner, repo)
325
+
326
+ # If branch was specified by user, verify it first
327
+ if target_branch:
328
+ if not check_branch_exists(owner, repo, target_branch):
329
+ print(f"Notice: Branch '{target_branch}' was not found in {owner}/{repo}.")
330
+ available_branches = get_repo_branches(owner, repo)
331
+ if available_branches:
332
+ shown = available_branches[:15]
333
+ extra = f" (+{len(available_branches) - 15} more)" if len(available_branches) > 15 else ""
334
+ print(f"Available branches in {owner}/{repo}: {', '.join(shown)}{extra}")
335
+ print(f"Listing files from default branch '{default_branch}' instead:\n")
336
+ target_branch = default_branch
337
+ else:
338
+ target_branch = default_branch
339
+
295
340
  try:
296
- actual_branch, data = get_repo_tree_with_branch(owner, repo, branch)
341
+ data = get_repo_tree(owner, repo, target_branch)
297
342
  if data.get("truncated"):
298
343
  print("Warning: GitHub returned a truncated file list.")
299
344
 
@@ -314,10 +359,10 @@ def list_files(owner, repo, branch="main"):
314
359
  files.sort(key=str.lower)
315
360
 
316
361
  if not files:
317
- print(f"No supported code files found in {owner}/{repo} ({actual_branch}).")
362
+ print(f"No supported code files found in {owner}/{repo} ({target_branch}).")
318
363
  return
319
364
 
320
- print(f"Available files in {owner}/{repo} ({actual_branch}):\n")
365
+ print(f"Available files in {owner}/{repo} ({target_branch}):\n")
321
366
  for i, path in enumerate(files, 1):
322
367
  print(f"{i:>3}. {path}")
323
368
 
@@ -387,9 +432,9 @@ def find_file_in_other_repos(owner, current_repo, target_filename):
387
432
  r_name = r.get("name")
388
433
  if not r_name or (current_repo and r_name.lower() == current_repo.lower()):
389
434
  continue
390
- default_branch = r.get("default_branch", "main")
435
+ default_b = r.get("default_branch") or "main"
391
436
  try:
392
- tree_data = get_repo_tree(owner, r_name, default_branch)
437
+ tree_data = get_repo_tree(owner, r_name, default_b)
393
438
  match = resolve_file_in_tree(tree_data, target_filename)
394
439
  if match:
395
440
  return r_name, match
@@ -400,9 +445,14 @@ def find_file_in_other_repos(owner, current_repo, target_filename):
400
445
  return None, None
401
446
 
402
447
 
403
- def fetch_file(filename, owner=None, repo=None, branch=DEFAULT_BRANCH,
448
+ def fetch_file(filename, owner=None, repo=None, branch=None,
404
449
  show=False, output=None):
405
- """Fetch or download a file from GitHub."""
450
+ """
451
+ Fetch or download a file from GitHub.
452
+ - If branch is None: fetches the default branch of the repository.
453
+ - If branch is specified: tries that branch first; if not found, shows message,
454
+ displays all available branches, and falls back to default branch.
455
+ """
406
456
  if not owner or not repo:
407
457
  print("Error: No GitHub repository specified.\n")
408
458
  print("Usage:")
@@ -422,63 +472,158 @@ def fetch_file(filename, owner=None, repo=None, branch=DEFAULT_BRANCH,
422
472
 
423
473
  content = None
424
474
  resolved_path = clean_filename
425
- actual_branch = branch
475
+ actual_branch = None
476
+
477
+ default_branch = get_default_branch(owner, repo)
478
+
479
+ # Case A: Branch parameter was explicitly entered by the user
480
+ if branch:
481
+ # First, check if the specified branch exists
482
+ branch_exists = check_branch_exists(owner, repo, branch)
483
+
484
+ if not branch_exists:
485
+ print(f"Notice: Branch '{branch}' was not found in {owner}/{repo}.")
486
+ available_branches = get_repo_branches(owner, repo)
487
+ if available_branches:
488
+ shown = available_branches[:15]
489
+ extra = f" (+{len(available_branches) - 15} more)" if len(available_branches) > 15 else ""
490
+ print(f"Available branches in {owner}/{repo}: {', '.join(shown)}{extra}")
491
+ print(f"Trying default branch '{default_branch}'...")
492
+
493
+ # Fall back to default branch
494
+ actual_branch = default_branch
495
+ try:
496
+ content = fetch_raw_single(owner, repo, default_branch, clean_filename)
497
+ except UnicodeDecodeError:
498
+ print("The file is not UTF-8 text, so it cannot be displayed or saved as text.")
499
+ return
500
+ except Exception:
501
+ pass
502
+
503
+ if content is None:
504
+ # Try tree on default branch
505
+ try:
506
+ tree_data = get_repo_tree(owner, repo, default_branch)
507
+ matched = resolve_file_in_tree(tree_data, clean_filename)
508
+ if matched:
509
+ resolved_path = matched
510
+ content = download_url(f"https://raw.githubusercontent.com/{owner}/{repo}/{default_branch}/{resolved_path}")
511
+ if resolved_path != clean_filename:
512
+ print(f"(Resolved '{clean_filename}' -> '{resolved_path}')")
513
+ except Exception:
514
+ pass
515
+
516
+ if content is None:
517
+ print(f"File not found: '{clean_filename}' in {owner}/{repo} (checked '{branch}' and default '{default_branch}').")
518
+ print(f"To fetch from a specific branch: codefetch -b <branch> {owner}/{repo} {clean_filename}")
519
+ return
426
520
 
427
- # 1. Direct raw fetch first (fastest, preserves GitHub API rate limit)
428
- try:
429
- found_branch, raw_content = fetch_raw_direct(owner, repo, branch, clean_filename)
430
- if raw_content is not None:
431
- actual_branch = found_branch
432
- content = raw_content
433
- except UnicodeDecodeError:
434
- print("The file is not UTF-8 text, so it cannot be displayed or saved as text.")
435
- return
436
- except urllib.error.HTTPError as e:
437
- print(f"Download failed: HTTP {e.code}")
438
- return
439
- except urllib.error.URLError:
440
- print("Could not connect to GitHub.")
441
- return
521
+ else:
522
+ # The specified branch exists! First try direct fetch from it
523
+ actual_branch = branch
524
+ try:
525
+ content = fetch_raw_single(owner, repo, branch, clean_filename)
526
+ except UnicodeDecodeError:
527
+ print("The file is not UTF-8 text, so it cannot be displayed or saved as text.")
528
+ return
529
+ except Exception:
530
+ pass
531
+
532
+ if content is None:
533
+ # Direct raw 404ed, search tree of the specified branch
534
+ try:
535
+ tree_data = get_repo_tree(owner, repo, branch)
536
+ matched = resolve_file_in_tree(tree_data, clean_filename)
537
+ if matched:
538
+ resolved_path = matched
539
+ content = download_url(f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{resolved_path}")
540
+ if resolved_path != clean_filename:
541
+ print(f"(Resolved '{clean_filename}' -> '{resolved_path}')")
542
+ except Exception:
543
+ pass
544
+
545
+ # If file not in specified branch, check default branch as well
546
+ if content is None and branch.lower() != default_branch.lower():
547
+ print(f"Notice: File '{clean_filename}' not found on branch '{branch}'.")
548
+ available_branches = get_repo_branches(owner, repo)
549
+ if available_branches:
550
+ shown = available_branches[:15]
551
+ extra = f" (+{len(available_branches) - 15} more)" if len(available_branches) > 15 else ""
552
+ print(f"Available branches in {owner}/{repo}: {', '.join(shown)}{extra}")
553
+ print(f"Trying default branch '{default_branch}'...")
554
+
555
+ try:
556
+ content = fetch_raw_single(owner, repo, default_branch, clean_filename)
557
+ if content is not None:
558
+ actual_branch = default_branch
559
+ except Exception:
560
+ pass
561
+
562
+ if content is None:
563
+ try:
564
+ tree_data = get_repo_tree(owner, repo, default_branch)
565
+ matched = resolve_file_in_tree(tree_data, clean_filename)
566
+ if matched:
567
+ resolved_path = matched
568
+ actual_branch = default_branch
569
+ content = download_url(f"https://raw.githubusercontent.com/{owner}/{repo}/{default_branch}/{resolved_path}")
570
+ if resolved_path != clean_filename:
571
+ print(f"(Resolved '{clean_filename}' -> '{resolved_path}')")
572
+ except Exception:
573
+ pass
574
+
575
+ if content is None:
576
+ print(f"File not found: '{clean_filename}' in {owner}/{repo} ({branch}).")
577
+ return
442
578
 
443
- # 2. If direct fetch didn't find the file (404), fall back to smart repository tree search
444
- if content is None:
579
+ # Case B: No branch was specified (branch is None) -> fetch repo default branch
580
+ else:
581
+ actual_branch = default_branch
445
582
  try:
446
- actual_branch, tree_data = get_repo_tree_with_branch(owner, repo, branch)
447
- matched = resolve_file_in_tree(tree_data, clean_filename)
448
- if matched:
449
- resolved_path = matched
450
- raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{actual_branch}/{resolved_path}"
451
- content = download_url(raw_url)
452
- if resolved_path != clean_filename:
453
- print(f"(Resolved '{clean_filename}' -> '{resolved_path}')")
454
- else:
455
- # Check other public repositories for the same user
456
- other_repo, other_match = find_file_in_other_repos(owner, repo, clean_filename)
457
- if other_repo:
458
- print(f"File not found: '{clean_filename}' in {owner}/{repo}.")
459
- print(f"\n[Tip] Found '{other_match}' in repository '{other_repo}'!")
460
- print(f"To fetch it, run:\n codefetch {owner}/{other_repo} {other_match}")
461
- return
462
- else:
463
- print(f"File not found: '{clean_filename}' in {owner}/{repo}.")
464
- print(f"Run 'codefetch -l {owner}/{repo}' to see available files.")
465
- return
466
- except urllib.error.HTTPError as e:
467
- if e.code == 404:
468
- print(f"Repository or branch not found: {owner}/{repo} ({branch})")
469
- else:
470
- print(f"Could not search repository: HTTP {e.code}")
471
- return
472
- except urllib.error.URLError:
473
- print("Could not connect to GitHub.")
474
- return
583
+ content = fetch_raw_single(owner, repo, default_branch, clean_filename)
475
584
  except UnicodeDecodeError:
476
585
  print("The file is not UTF-8 text, so it cannot be displayed or saved as text.")
477
586
  return
478
- except Exception:
479
- print(f"File not found: '{clean_filename}' in {owner}/{repo}.")
587
+ except urllib.error.HTTPError as e:
588
+ if e.code != 404:
589
+ print(f"Download failed: HTTP {e.code}")
590
+ return
591
+ except urllib.error.URLError:
592
+ print("Could not connect to GitHub.")
480
593
  return
481
594
 
595
+ if content is None:
596
+ # Tree search on default branch
597
+ try:
598
+ tree_data = get_repo_tree(owner, repo, default_branch)
599
+ matched = resolve_file_in_tree(tree_data, clean_filename)
600
+ if matched:
601
+ resolved_path = matched
602
+ content = download_url(f"https://raw.githubusercontent.com/{owner}/{repo}/{default_branch}/{resolved_path}")
603
+ if resolved_path != clean_filename:
604
+ print(f"(Resolved '{clean_filename}' -> '{resolved_path}')")
605
+ else:
606
+ # Check other public repositories for the same user
607
+ other_repo, other_match = find_file_in_other_repos(owner, repo, clean_filename)
608
+ if other_repo:
609
+ print(f"File not found: '{clean_filename}' in {owner}/{repo} ({default_branch}).")
610
+ print(f"\n[Tip] Found '{other_match}' in repository '{other_repo}'!")
611
+ print(f"To fetch it, run:\n codefetch {owner}/{other_repo} {other_match}")
612
+ return
613
+ else:
614
+ print(f"File not found: '{clean_filename}' in {owner}/{repo} ({default_branch}).")
615
+ print(f"Run 'codefetch -l {owner}/{repo}' to see available files.")
616
+ return
617
+ except urllib.error.HTTPError as e:
618
+ print(f"Could not search repository: HTTP {e.code}")
619
+ return
620
+ except urllib.error.URLError:
621
+ print("Could not connect to GitHub.")
622
+ return
623
+ except Exception:
624
+ print(f"File not found: '{clean_filename}' in {owner}/{repo} ({default_branch}).")
625
+ return
626
+
482
627
  if content is None:
483
628
  return
484
629
 
@@ -518,7 +663,7 @@ def build_parser():
518
663
  parser.add_argument("-R", "--repos", action="store_true", help="List public repositories for a user")
519
664
  parser.add_argument("-r", "--repo", type=str, help="Repository name or URL")
520
665
  parser.add_argument("-u", "--user", type=str, help="GitHub username")
521
- parser.add_argument("-b", "--branch", type=str, help="Branch name (default: main)")
666
+ parser.add_argument("-b", "--branch", type=str, default=None, help="Branch name (default: repository default branch)")
522
667
  parser.add_argument("-o", "--output", type=str, help="Custom output filename or path")
523
668
  parser.add_argument("--set-default", type=str, help="Save default repository (e.g. owner/repo)")
524
669
  parser.add_argument("--set-default-user", type=str, help="Save default GitHub user")
@@ -531,13 +676,13 @@ def build_parser():
531
676
 
532
677
 
533
678
  def resolve_invocation(positional_args, repo_flag, user_flag, branch_flag,
534
- default_owner, default_repo, default_branch):
679
+ default_owner, default_repo):
535
680
  """
536
681
  Resolve arguments into (owner, repo, branch, filename).
537
682
  """
538
683
  owner = user_flag or default_owner
539
684
  repo = default_repo
540
- branch = branch_flag or default_branch
685
+ branch = branch_flag # None if not provided
541
686
  filename = None
542
687
 
543
688
  if repo_flag:
@@ -596,7 +741,6 @@ def main():
596
741
  config = load_config()
597
742
  default_owner = config.get("owner", DEFAULT_OWNER)
598
743
  default_repo = config.get("repo", DEFAULT_REPO)
599
- default_branch = config.get("branch", DEFAULT_BRANCH)
600
744
 
601
745
  # Handle --reset-config
602
746
  if args.reset_config:
@@ -632,12 +776,12 @@ def main():
632
776
  print("CodeFetch Configuration:")
633
777
  print(f" Default User: {default_owner or 'None (not set)'}")
634
778
  print(f" Default Repository: {default_repo or 'None (not set)'}")
635
- print(f" Default Branch: {default_branch}")
779
+ print(" Default Branch: None (auto-detect repository default)")
636
780
  print(f" Config File: {CONFIG_FILE}")
637
781
  return
638
782
 
639
783
  owner = args.user or default_owner
640
- branch = args.branch or default_branch
784
+ branch = args.branch # None if not provided
641
785
 
642
786
  # Handle --repos (list repositories)
643
787
  if args.repos:
@@ -682,7 +826,6 @@ def main():
682
826
  args.branch,
683
827
  default_owner,
684
828
  default_repo,
685
- default_branch,
686
829
  )
687
830
 
688
831
  if not resolved_filename:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codefetch-cli
3
- Version: 1.2.3
3
+ Version: 1.2.4
4
4
  Summary: Quickly fetch code files from GitHub repositories with simple commands
5
5
  Home-page: https://github.com/InzamamulQureshi/Codefetch
6
6
  Author: Inzamamul Qureshi
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codefetch-cli"
7
- version = "1.2.3"
7
+ version = "1.2.4"
8
8
  description = "Quickly fetch code files from GitHub repositories with simple commands"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -7,7 +7,7 @@ long_description = readme_path.read_text(encoding="utf-8") if readme_path.exists
7
7
 
8
8
  setup(
9
9
  name="codefetch-cli",
10
- version="1.2.3",
10
+ version="1.2.4",
11
11
  description="Quickly fetch code files from GitHub repositories with simple commands",
12
12
  long_description=long_description,
13
13
  long_description_content_type="text/markdown",
File without changes
File without changes
File without changes