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.
- {codefetch_cli-1.2.3/codefetch_cli.egg-info → codefetch_cli-1.2.4}/PKG-INFO +1 -1
- {codefetch_cli-1.2.3 → codefetch_cli-1.2.4}/codefetch/__init__.py +30 -16
- {codefetch_cli-1.2.3 → codefetch_cli-1.2.4}/codefetch/cli.py +285 -142
- {codefetch_cli-1.2.3 → codefetch_cli-1.2.4/codefetch_cli.egg-info}/PKG-INFO +1 -1
- {codefetch_cli-1.2.3 → codefetch_cli-1.2.4}/pyproject.toml +1 -1
- {codefetch_cli-1.2.3 → codefetch_cli-1.2.4}/setup.py +1 -1
- {codefetch_cli-1.2.3 → codefetch_cli-1.2.4}/LICENSE +0 -0
- {codefetch_cli-1.2.3 → codefetch_cli-1.2.4}/README.md +0 -0
- {codefetch_cli-1.2.3 → codefetch_cli-1.2.4}/codefetch_cli.egg-info/SOURCES.txt +0 -0
- {codefetch_cli-1.2.3 → codefetch_cli-1.2.4}/codefetch_cli.egg-info/dependency_links.txt +0 -0
- {codefetch_cli-1.2.3 → codefetch_cli-1.2.4}/codefetch_cli.egg-info/entry_points.txt +0 -0
- {codefetch_cli-1.2.3 → codefetch_cli-1.2.4}/codefetch_cli.egg-info/top_level.txt +0 -0
- {codefetch_cli-1.2.3 → codefetch_cli-1.2.4}/setup.cfg +0 -0
|
@@ -16,17 +16,17 @@ Usage in Python:
|
|
|
16
16
|
files = cf.list("torvalds/linux")
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
__version__ = "1.2.
|
|
19
|
+
__version__ = "1.2.4"
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def get(target, filename=None, branch=
|
|
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:
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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}/{
|
|
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} ({
|
|
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=
|
|
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:
|
|
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=
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
17
|
+
DEFAULT_BRANCH = None
|
|
18
18
|
|
|
19
19
|
CONFIG_FILE = Path.home() / ".codefetch.json"
|
|
20
20
|
|
|
21
|
-
__version__ = "1.2.
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
176
|
+
def get_default_branch(owner, repo):
|
|
172
177
|
"""
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
179
|
-
if
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if
|
|
223
|
-
|
|
224
|
-
return
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
238
|
-
"""
|
|
239
|
-
|
|
240
|
-
|
|
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 "
|
|
293
|
+
desc = (r.get("description") or "").strip()
|
|
266
294
|
is_def = " (default)" if default_repo and name.lower() == default_repo.lower() else ""
|
|
267
|
-
|
|
268
|
-
|
|
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(
|
|
273
|
-
print("
|
|
274
|
-
print(f" codefetch -
|
|
275
|
-
print(f" codefetch {owner}/<repo>
|
|
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=
|
|
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
|
-
|
|
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} ({
|
|
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} ({
|
|
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
|
-
|
|
435
|
+
default_b = r.get("default_branch") or "main"
|
|
391
436
|
try:
|
|
392
|
-
tree_data = get_repo_tree(owner, r_name,
|
|
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=
|
|
448
|
+
def fetch_file(filename, owner=None, repo=None, branch=None,
|
|
404
449
|
show=False, output=None):
|
|
405
|
-
"""
|
|
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 =
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
#
|
|
444
|
-
|
|
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
|
-
|
|
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
|
|
479
|
-
|
|
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:
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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:
|
|
@@ -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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|