codefetch-cli 1.2.2__py3-none-any.whl → 1.2.4__py3-none-any.whl
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/__init__.py +157 -2
- codefetch/cli.py +364 -204
- {codefetch_cli-1.2.2.dist-info → codefetch_cli-1.2.4.dist-info}/METADATA +25 -2
- codefetch_cli-1.2.4.dist-info/RECORD +8 -0
- codefetch_cli-1.2.2.dist-info/RECORD +0 -8
- {codefetch_cli-1.2.2.dist-info → codefetch_cli-1.2.4.dist-info}/WHEEL +0 -0
- {codefetch_cli-1.2.2.dist-info → codefetch_cli-1.2.4.dist-info}/entry_points.txt +0 -0
- {codefetch_cli-1.2.2.dist-info → codefetch_cli-1.2.4.dist-info}/licenses/LICENSE +0 -0
- {codefetch_cli-1.2.2.dist-info → codefetch_cli-1.2.4.dist-info}/top_level.txt +0 -0
codefetch/__init__.py
CHANGED
|
@@ -1,5 +1,160 @@
|
|
|
1
1
|
"""
|
|
2
|
-
CodeFetch -
|
|
2
|
+
CodeFetch - Easily preview and download code files from GitHub repositories.
|
|
3
|
+
|
|
4
|
+
Usage in Python:
|
|
5
|
+
import codefetch as cf
|
|
6
|
+
|
|
7
|
+
# 1. Fetch file content as string:
|
|
8
|
+
content = cf.get("torvalds/linux", "Makefile")
|
|
9
|
+
# or shorthand target string:
|
|
10
|
+
content = cf.get("torvalds/linux/Makefile")
|
|
11
|
+
|
|
12
|
+
# 2. Download file to local disk:
|
|
13
|
+
cf.download("torvalds/linux", "Makefile", output="linux_makefile.txt")
|
|
14
|
+
|
|
15
|
+
# 3. List available code files:
|
|
16
|
+
files = cf.list("torvalds/linux")
|
|
3
17
|
"""
|
|
4
18
|
|
|
5
|
-
__version__ = "1.2.
|
|
19
|
+
__version__ = "1.2.4"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get(target, filename=None, branch=None):
|
|
23
|
+
"""
|
|
24
|
+
Fetch and return the text content of a file from GitHub.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
target: 'owner/repo' or full path 'owner/repo/path/to/file' or full GitHub URL.
|
|
28
|
+
filename: Optional path to file in repo if target is 'owner/repo'.
|
|
29
|
+
branch: Branch name (default: None, automatically detects repo default branch).
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
str: File content.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
FileNotFoundError: If the file does not exist in the repository.
|
|
36
|
+
ValueError: If arguments cannot be resolved into a repository and filename.
|
|
37
|
+
"""
|
|
38
|
+
import urllib.request
|
|
39
|
+
from .cli import (
|
|
40
|
+
get_default_branch,
|
|
41
|
+
check_branch_exists,
|
|
42
|
+
fetch_raw_single,
|
|
43
|
+
get_repo_tree,
|
|
44
|
+
resolve_file_in_tree,
|
|
45
|
+
parse_repo_identifier,
|
|
46
|
+
resolve_invocation,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if filename:
|
|
50
|
+
owner, repo = parse_repo_identifier(target)
|
|
51
|
+
file_path = filename.strip("/\\")
|
|
52
|
+
else:
|
|
53
|
+
owner, repo, resolved_branch, file_path = resolve_invocation(
|
|
54
|
+
[target], None, None, branch, None, None
|
|
55
|
+
)
|
|
56
|
+
if resolved_branch:
|
|
57
|
+
branch = resolved_branch
|
|
58
|
+
|
|
59
|
+
if not owner or not repo or not file_path:
|
|
60
|
+
raise ValueError(f"Could not resolve owner, repo, and filename from target='{target}', filename='{filename}'")
|
|
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
|
+
|
|
68
|
+
# 1. Try fast direct raw fetch
|
|
69
|
+
content = fetch_raw_single(owner, repo, target_branch, file_path)
|
|
70
|
+
if content is not None:
|
|
71
|
+
return content
|
|
72
|
+
|
|
73
|
+
# 2. Try repository tree search
|
|
74
|
+
tree_data = get_repo_tree(owner, repo, target_branch)
|
|
75
|
+
matched = resolve_file_in_tree(tree_data, file_path)
|
|
76
|
+
if matched:
|
|
77
|
+
url = f"https://raw.githubusercontent.com/{owner}/{repo}/{target_branch}/{matched}"
|
|
78
|
+
req = urllib.request.Request(url, headers={"User-Agent": "codefetch"})
|
|
79
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
80
|
+
return resp.read().decode("utf-8")
|
|
81
|
+
|
|
82
|
+
raise FileNotFoundError(f"File '{file_path}' not found in {owner}/{repo} ({target_branch})")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def download(target, filename=None, output=None, branch=None):
|
|
86
|
+
"""
|
|
87
|
+
Download a file from GitHub and save it locally.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
target: 'owner/repo' or full path 'owner/repo/path/to/file'.
|
|
91
|
+
filename: Optional path to file in repo if target is 'owner/repo'.
|
|
92
|
+
output: Destination filename/path on local disk (default: original filename).
|
|
93
|
+
branch: Branch name (default: None, automatically detects repo default branch).
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
str: Path to the downloaded file.
|
|
97
|
+
"""
|
|
98
|
+
content = get(target, filename=filename, branch=branch)
|
|
99
|
+
dest_name = output
|
|
100
|
+
if not dest_name:
|
|
101
|
+
resolved_file = filename if filename else target.rsplit("/", 1)[-1]
|
|
102
|
+
dest_name = resolved_file.rsplit("/", 1)[-1]
|
|
103
|
+
|
|
104
|
+
with open(dest_name, "w", encoding="utf-8") as f:
|
|
105
|
+
f.write(content)
|
|
106
|
+
|
|
107
|
+
return dest_name
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def list(target, branch=None):
|
|
111
|
+
"""
|
|
112
|
+
List supported code files in a repository.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
target: 'owner/repo' or repository URL.
|
|
116
|
+
branch: Branch name (default: None, automatically detects repo default branch).
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
list[str]: Relative paths of code files in the repository.
|
|
120
|
+
"""
|
|
121
|
+
from .cli import (
|
|
122
|
+
parse_repo_identifier,
|
|
123
|
+
get_default_branch,
|
|
124
|
+
check_branch_exists,
|
|
125
|
+
get_repo_tree,
|
|
126
|
+
CODE_EXTENSIONS,
|
|
127
|
+
NAMED_FILES,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
owner, repo = parse_repo_identifier(target)
|
|
131
|
+
if not owner or not repo:
|
|
132
|
+
raise ValueError(f"Invalid repository identifier: '{target}'")
|
|
133
|
+
|
|
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)
|
|
139
|
+
files = []
|
|
140
|
+
for item in tree_data.get("tree", []):
|
|
141
|
+
if item.get("type") != "blob":
|
|
142
|
+
continue
|
|
143
|
+
path = item.get("path", "")
|
|
144
|
+
base_name = path.rsplit("/", 1)[-1].lower()
|
|
145
|
+
suffix = ""
|
|
146
|
+
if "." in base_name:
|
|
147
|
+
suffix = "." + base_name.rsplit(".", 1)[-1]
|
|
148
|
+
if suffix in CODE_EXTENSIONS or base_name in NAMED_FILES:
|
|
149
|
+
files.append(path)
|
|
150
|
+
|
|
151
|
+
files.sort(key=str.lower)
|
|
152
|
+
return files
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
__all__ = [
|
|
156
|
+
"__version__",
|
|
157
|
+
"get",
|
|
158
|
+
"download",
|
|
159
|
+
"list",
|
|
160
|
+
]
|
codefetch/cli.py
CHANGED
|
@@ -14,10 +14,12 @@ 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.4"
|
|
22
|
+
|
|
21
23
|
# Supported code, data, and config extensions
|
|
22
24
|
CODE_EXTENSIONS = {
|
|
23
25
|
# Data & Tables
|
|
@@ -56,85 +58,86 @@ CodeFetch - Download single code files from GitHub without cloning entire repos.
|
|
|
56
58
|
|
|
57
59
|
USAGE:
|
|
58
60
|
codefetch <owner>/<repo> <filename> Download a file from any GitHub repository
|
|
59
|
-
codefetch <filename> Download a file (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
codefetch
|
|
78
|
-
codefetch
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
61
|
+
codefetch <filename> Download a file (uses saved default repo)
|
|
62
|
+
codefetch -s <owner>/<repo> <filename> Preview file in terminal without saving
|
|
63
|
+
codefetch -l <owner>/<repo> List all code files in a repository
|
|
64
|
+
codefetch -R <username> List public repositories for a user
|
|
65
|
+
|
|
66
|
+
POPULAR EXAMPLES:
|
|
67
|
+
# Download from any repository:
|
|
68
|
+
codefetch owner/repo main.py
|
|
69
|
+
codefetch torvalds/linux Makefile
|
|
70
|
+
|
|
71
|
+
# Preview code directly in terminal:
|
|
72
|
+
codefetch -s owner/repo app.js
|
|
73
|
+
codefetch -s torvalds/linux Makefile
|
|
74
|
+
|
|
75
|
+
# Save with a custom name or path:
|
|
76
|
+
codefetch owner/repo data.csv -o my_data.csv
|
|
77
|
+
|
|
78
|
+
# Explore repositories and files:
|
|
79
|
+
codefetch -l torvalds/linux # list files in a repo
|
|
80
|
+
codefetch -R torvalds # list repos belonging to user
|
|
81
|
+
|
|
82
|
+
SETTING A DEFAULT REPOSITORY (OPTIONAL):
|
|
83
|
+
If you frequently work with the same repo, you can set it as default:
|
|
84
|
+
codefetch --set-default owner/repo # sets default repo
|
|
85
|
+
codefetch script.py # now you don't need to type owner/repo!
|
|
86
|
+
codefetch --config # view active configuration
|
|
87
|
+
codefetch --reset-config # reset all defaults
|
|
88
|
+
|
|
89
|
+
FLAGS & OPTIONS:
|
|
90
|
+
-s, --show Print file content to terminal instead of downloading
|
|
91
|
+
-o, --output <file> Specify output filename or path
|
|
92
|
+
-l, --list List available code files in the repository
|
|
93
|
+
-R, --repos List public repositories for a GitHub user
|
|
94
|
+
-r, --repo <repo> Specify repository name or URL
|
|
95
|
+
-u, --user <user> Specify GitHub username / organization
|
|
96
|
+
-b, --branch <branch> Specify branch name (default: repository default branch)
|
|
97
|
+
--set-default <repo> Save default repository (e.g. owner/repo)
|
|
98
|
+
--set-default-user <user> Save default user/organization
|
|
99
|
+
--config Show current saved configuration
|
|
100
|
+
--reset-config Reset configuration back to defaults
|
|
101
|
+
-v, --version Show version number
|
|
102
|
+
-h, --help Show this help message
|
|
92
103
|
"""
|
|
93
104
|
|
|
105
|
+
_REPO_DEFAULT_BRANCH_CACHE = {}
|
|
106
|
+
_REPO_BRANCHES_CACHE = {}
|
|
107
|
+
|
|
94
108
|
|
|
95
109
|
def load_config():
|
|
96
|
-
"""Load configuration from
|
|
97
|
-
config = {
|
|
98
|
-
"owner": os.environ.get("CODEFETCH_USER", DEFAULT_OWNER),
|
|
99
|
-
"repo": os.environ.get("CODEFETCH_REPO", DEFAULT_REPO),
|
|
100
|
-
"branch": os.environ.get("CODEFETCH_BRANCH", DEFAULT_BRANCH),
|
|
101
|
-
}
|
|
110
|
+
"""Load user configuration from ~/.codefetch.json."""
|
|
102
111
|
if CONFIG_FILE.exists():
|
|
103
112
|
try:
|
|
104
113
|
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
config["repo"] = saved["repo"]
|
|
111
|
-
if saved.get("branch"):
|
|
112
|
-
config["branch"] = saved["branch"]
|
|
114
|
+
data = json.load(f)
|
|
115
|
+
return {
|
|
116
|
+
"owner": data.get("owner"),
|
|
117
|
+
"repo": data.get("repo"),
|
|
118
|
+
}
|
|
113
119
|
except Exception:
|
|
114
|
-
|
|
115
|
-
return
|
|
120
|
+
return {}
|
|
121
|
+
return {}
|
|
116
122
|
|
|
117
123
|
|
|
118
|
-
def save_config(owner=None, repo=None
|
|
119
|
-
"""Save configuration to ~/.codefetch.json."""
|
|
120
|
-
|
|
121
|
-
if owner:
|
|
122
|
-
|
|
123
|
-
if repo:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
config["branch"] = branch
|
|
124
|
+
def save_config(owner=None, repo=None):
|
|
125
|
+
"""Save user configuration to ~/.codefetch.json (no branch saved in config)."""
|
|
126
|
+
cfg = load_config()
|
|
127
|
+
if owner is not None:
|
|
128
|
+
cfg["owner"] = owner
|
|
129
|
+
if repo is not None:
|
|
130
|
+
cfg["repo"] = repo
|
|
131
|
+
|
|
127
132
|
try:
|
|
128
133
|
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
129
|
-
json.dump(
|
|
130
|
-
return True
|
|
134
|
+
json.dump(cfg, f, indent=2)
|
|
131
135
|
except Exception as e:
|
|
132
|
-
print(f"
|
|
133
|
-
return False
|
|
136
|
+
print(f"Warning: Could not save configuration: {e}")
|
|
134
137
|
|
|
135
138
|
|
|
136
139
|
def get_headers():
|
|
137
|
-
"""
|
|
140
|
+
"""Build request headers, attaching GITHUB_TOKEN if present in environment."""
|
|
138
141
|
headers = {
|
|
139
142
|
"User-Agent": "codefetch",
|
|
140
143
|
"Accept": "application/vnd.github+json",
|
|
@@ -146,10 +149,10 @@ def get_headers():
|
|
|
146
149
|
|
|
147
150
|
|
|
148
151
|
def get_json(url):
|
|
149
|
-
"""Fetch
|
|
150
|
-
|
|
151
|
-
with urllib.request.urlopen(
|
|
152
|
-
return json.loads(
|
|
152
|
+
"""Fetch JSON from GitHub API with rate limit handling."""
|
|
153
|
+
req = urllib.request.Request(url, headers=get_headers())
|
|
154
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
155
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
153
156
|
|
|
154
157
|
|
|
155
158
|
def parse_repo_identifier(repo_str, default_owner=None):
|
|
@@ -170,37 +173,112 @@ def parse_repo_identifier(repo_str, default_owner=None):
|
|
|
170
173
|
return default_owner, repo_str.removesuffix(".git")
|
|
171
174
|
|
|
172
175
|
|
|
173
|
-
def
|
|
174
|
-
"""
|
|
175
|
-
|
|
176
|
+
def get_default_branch(owner, repo):
|
|
177
|
+
"""
|
|
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.
|
|
180
|
+
"""
|
|
181
|
+
key = f"{owner.lower()}/{repo.lower()}"
|
|
182
|
+
if key in _REPO_DEFAULT_BRANCH_CACHE:
|
|
183
|
+
return _REPO_DEFAULT_BRANCH_CACHE[key]
|
|
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
|
|
194
|
+
|
|
195
|
+
# Fast fallback probe via CDN if API is unavailable or rate-limited
|
|
196
|
+
for b in ("main", "master"):
|
|
197
|
+
try:
|
|
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
|
+
|
|
217
|
+
try:
|
|
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
|
|
224
|
+
except Exception:
|
|
225
|
+
pass
|
|
226
|
+
return []
|
|
227
|
+
|
|
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
|
|
176
236
|
try:
|
|
177
|
-
|
|
237
|
+
get_json(f"https://api.github.com/repos/{owner}/{repo}/branches/{branch}")
|
|
238
|
+
return True
|
|
178
239
|
except urllib.error.HTTPError as e:
|
|
179
|
-
if e.code == 404
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
240
|
+
if e.code == 404:
|
|
241
|
+
return False
|
|
242
|
+
return True
|
|
243
|
+
except Exception:
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
|
|
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:
|
|
185
262
|
raise
|
|
186
263
|
|
|
187
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)
|
|
269
|
+
|
|
270
|
+
|
|
188
271
|
def get_user_repos(owner):
|
|
189
|
-
"""Fetch public repositories for a
|
|
272
|
+
"""Fetch public repositories list for a user."""
|
|
190
273
|
url = f"https://api.github.com/users/{owner}/repos?per_page=100&sort=updated"
|
|
191
|
-
|
|
192
|
-
return get_json(url)
|
|
193
|
-
except Exception:
|
|
194
|
-
return []
|
|
274
|
+
return get_json(url)
|
|
195
275
|
|
|
196
276
|
|
|
197
277
|
def list_repos(owner=None, default_repo=None):
|
|
198
|
-
"""List all public repositories for
|
|
278
|
+
"""List all public repositories for a GitHub user with clean spacing."""
|
|
199
279
|
if not owner:
|
|
200
|
-
print("Error: No GitHub
|
|
201
|
-
print("Usage:")
|
|
202
|
-
print(" codefetch -R <username>")
|
|
203
|
-
print(" codefetch -R -u <username>")
|
|
280
|
+
print("Error: No GitHub username specified.")
|
|
281
|
+
print("Usage: codefetch --repos <username>")
|
|
204
282
|
return
|
|
205
283
|
|
|
206
284
|
try:
|
|
@@ -209,19 +287,21 @@ def list_repos(owner=None, default_repo=None):
|
|
|
209
287
|
print(f"No public repositories found for user '{owner}'.")
|
|
210
288
|
return
|
|
211
289
|
|
|
212
|
-
print(f"
|
|
213
|
-
for r in repos:
|
|
290
|
+
print(f"Public repositories for '{owner}' ({len(repos)} total):\n")
|
|
291
|
+
for i, r in enumerate(repos, 1):
|
|
214
292
|
name = r.get("name", "")
|
|
215
|
-
desc = r.get("description") or ""
|
|
293
|
+
desc = (r.get("description") or "").strip()
|
|
216
294
|
is_def = " (default)" if default_repo and name.lower() == default_repo.lower() else ""
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
295
|
+
print(f" {i:>2}. {name}{is_def}")
|
|
296
|
+
if desc:
|
|
297
|
+
print(f" {desc}")
|
|
298
|
+
print()
|
|
220
299
|
|
|
221
|
-
print("
|
|
222
|
-
print(f" codefetch
|
|
223
|
-
print(f" codefetch {owner}/<repo> <filename>
|
|
224
|
-
print(f" codefetch
|
|
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")
|
|
225
305
|
|
|
226
306
|
except urllib.error.HTTPError as e:
|
|
227
307
|
print(f"Could not list repositories: HTTP {e.code}")
|
|
@@ -231,8 +311,8 @@ def list_repos(owner=None, default_repo=None):
|
|
|
231
311
|
print(f"Could not list repositories: {e}")
|
|
232
312
|
|
|
233
313
|
|
|
234
|
-
def list_files(owner, repo, branch=
|
|
235
|
-
"""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."""
|
|
236
316
|
if not owner or not repo:
|
|
237
317
|
print("Error: No repository specified to list.\n")
|
|
238
318
|
print("Usage:")
|
|
@@ -240,8 +320,25 @@ def list_files(owner, repo, branch="main"):
|
|
|
240
320
|
print(" codefetch -u <owner> -r <repo> -l")
|
|
241
321
|
return
|
|
242
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
|
+
|
|
243
340
|
try:
|
|
244
|
-
data = get_repo_tree(owner, repo,
|
|
341
|
+
data = get_repo_tree(owner, repo, target_branch)
|
|
245
342
|
if data.get("truncated"):
|
|
246
343
|
print("Warning: GitHub returned a truncated file list.")
|
|
247
344
|
|
|
@@ -262,10 +359,10 @@ def list_files(owner, repo, branch="main"):
|
|
|
262
359
|
files.sort(key=str.lower)
|
|
263
360
|
|
|
264
361
|
if not files:
|
|
265
|
-
print(f"No supported code files found in {owner}/{repo}.")
|
|
362
|
+
print(f"No supported code files found in {owner}/{repo} ({target_branch}).")
|
|
266
363
|
return
|
|
267
364
|
|
|
268
|
-
print(f"Available files in {owner}/{repo} ({
|
|
365
|
+
print(f"Available files in {owner}/{repo} ({target_branch}):\n")
|
|
269
366
|
for i, path in enumerate(files, 1):
|
|
270
367
|
print(f"{i:>3}. {path}")
|
|
271
368
|
|
|
@@ -335,9 +432,9 @@ def find_file_in_other_repos(owner, current_repo, target_filename):
|
|
|
335
432
|
r_name = r.get("name")
|
|
336
433
|
if not r_name or (current_repo and r_name.lower() == current_repo.lower()):
|
|
337
434
|
continue
|
|
338
|
-
|
|
435
|
+
default_b = r.get("default_branch") or "main"
|
|
339
436
|
try:
|
|
340
|
-
tree_data = get_repo_tree(owner, r_name,
|
|
437
|
+
tree_data = get_repo_tree(owner, r_name, default_b)
|
|
341
438
|
match = resolve_file_in_tree(tree_data, target_filename)
|
|
342
439
|
if match:
|
|
343
440
|
return r_name, match
|
|
@@ -348,9 +445,14 @@ def find_file_in_other_repos(owner, current_repo, target_filename):
|
|
|
348
445
|
return None, None
|
|
349
446
|
|
|
350
447
|
|
|
351
|
-
def fetch_file(filename, owner=None, repo=None, branch=
|
|
448
|
+
def fetch_file(filename, owner=None, repo=None, branch=None,
|
|
352
449
|
show=False, output=None):
|
|
353
|
-
"""
|
|
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
|
+
"""
|
|
354
456
|
if not owner or not repo:
|
|
355
457
|
print("Error: No GitHub repository specified.\n")
|
|
356
458
|
print("Usage:")
|
|
@@ -368,50 +470,159 @@ def fetch_file(filename, owner=None, repo=None, branch=DEFAULT_BRANCH,
|
|
|
368
470
|
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
369
471
|
return resp.read().decode("utf-8")
|
|
370
472
|
|
|
371
|
-
# 1. Direct raw fetch first (fastest, preserves GitHub API rate limit)
|
|
372
|
-
raw_base = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}"
|
|
373
|
-
url = f"{raw_base}/{clean_filename}"
|
|
374
|
-
|
|
375
473
|
content = None
|
|
376
474
|
resolved_path = clean_filename
|
|
475
|
+
actual_branch = None
|
|
377
476
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
|
520
|
+
|
|
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
|
|
578
|
+
|
|
579
|
+
# Case B: No branch was specified (branch is None) -> fetch repo default branch
|
|
580
|
+
else:
|
|
581
|
+
actual_branch = default_branch
|
|
582
|
+
try:
|
|
583
|
+
content = fetch_raw_single(owner, repo, default_branch, clean_filename)
|
|
584
|
+
except UnicodeDecodeError:
|
|
585
|
+
print("The file is not UTF-8 text, so it cannot be displayed or saved as text.")
|
|
586
|
+
return
|
|
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.")
|
|
593
|
+
return
|
|
594
|
+
|
|
595
|
+
if content is None:
|
|
596
|
+
# Tree search on default branch
|
|
383
597
|
try:
|
|
384
|
-
tree_data = get_repo_tree(owner, repo,
|
|
598
|
+
tree_data = get_repo_tree(owner, repo, default_branch)
|
|
385
599
|
matched = resolve_file_in_tree(tree_data, clean_filename)
|
|
386
600
|
if matched:
|
|
387
601
|
resolved_path = matched
|
|
388
|
-
content = download_url(f"{
|
|
602
|
+
content = download_url(f"https://raw.githubusercontent.com/{owner}/{repo}/{default_branch}/{resolved_path}")
|
|
389
603
|
if resolved_path != clean_filename:
|
|
390
604
|
print(f"(Resolved '{clean_filename}' -> '{resolved_path}')")
|
|
391
605
|
else:
|
|
392
606
|
# Check other public repositories for the same user
|
|
393
607
|
other_repo, other_match = find_file_in_other_repos(owner, repo, clean_filename)
|
|
394
608
|
if other_repo:
|
|
395
|
-
print(f"File not found: '{clean_filename}' in {owner}/{repo}.")
|
|
609
|
+
print(f"File not found: '{clean_filename}' in {owner}/{repo} ({default_branch}).")
|
|
396
610
|
print(f"\n[Tip] Found '{other_match}' in repository '{other_repo}'!")
|
|
397
611
|
print(f"To fetch it, run:\n codefetch {owner}/{other_repo} {other_match}")
|
|
398
612
|
return
|
|
399
613
|
else:
|
|
400
|
-
print(f"File not found: '{clean_filename}' in {owner}/{repo}.")
|
|
614
|
+
print(f"File not found: '{clean_filename}' in {owner}/{repo} ({default_branch}).")
|
|
401
615
|
print(f"Run 'codefetch -l {owner}/{repo}' to see available files.")
|
|
402
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
|
|
403
623
|
except Exception:
|
|
404
|
-
print(f"File not found: '{clean_filename}' in {owner}/{repo}.")
|
|
624
|
+
print(f"File not found: '{clean_filename}' in {owner}/{repo} ({default_branch}).")
|
|
405
625
|
return
|
|
406
|
-
else:
|
|
407
|
-
print(f"Download failed: HTTP {e.code}")
|
|
408
|
-
return
|
|
409
|
-
except urllib.error.URLError:
|
|
410
|
-
print("Could not connect to GitHub.")
|
|
411
|
-
return
|
|
412
|
-
except UnicodeDecodeError:
|
|
413
|
-
print("The file is not UTF-8 text, so it cannot be displayed or saved as text.")
|
|
414
|
-
return
|
|
415
626
|
|
|
416
627
|
if content is None:
|
|
417
628
|
return
|
|
@@ -423,7 +634,7 @@ def fetch_file(filename, owner=None, repo=None, branch=DEFAULT_BRANCH,
|
|
|
423
634
|
try:
|
|
424
635
|
with open(out_name, "w", encoding="utf-8") as f:
|
|
425
636
|
f.write(content)
|
|
426
|
-
print(f"Downloaded: {out_name} (from {owner}/{repo})")
|
|
637
|
+
print(f"Downloaded: {out_name} (from {owner}/{repo}@{actual_branch})")
|
|
427
638
|
except Exception as e:
|
|
428
639
|
print(f"Error saving file '{out_name}': {e}")
|
|
429
640
|
|
|
@@ -440,87 +651,38 @@ class CodefetchParser(argparse.ArgumentParser):
|
|
|
440
651
|
|
|
441
652
|
|
|
442
653
|
def build_parser():
|
|
443
|
-
"""Build argument parser with full CLI options."""
|
|
444
654
|
parser = CodefetchParser(
|
|
445
655
|
prog="codefetch",
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
parser.add_argument(
|
|
449
|
-
"args",
|
|
450
|
-
nargs="*",
|
|
451
|
-
help="Target specification: <owner>/<repo> <filename> or <filename>",
|
|
452
|
-
)
|
|
453
|
-
parser.add_argument(
|
|
454
|
-
"-r", "--repo",
|
|
455
|
-
default=None,
|
|
456
|
-
help="Repository name or URL (e.g. repo or owner/repo)",
|
|
457
|
-
)
|
|
458
|
-
parser.add_argument(
|
|
459
|
-
"-u", "--user",
|
|
460
|
-
default=None,
|
|
461
|
-
help="GitHub user/org name",
|
|
462
|
-
)
|
|
463
|
-
parser.add_argument(
|
|
464
|
-
"-b", "--branch",
|
|
465
|
-
default=None,
|
|
466
|
-
help=f"Branch name (default: {DEFAULT_BRANCH})",
|
|
467
|
-
)
|
|
468
|
-
parser.add_argument(
|
|
469
|
-
"-l", "--list",
|
|
470
|
-
action="store_true",
|
|
471
|
-
help="List available code files in the repository",
|
|
472
|
-
)
|
|
473
|
-
parser.add_argument(
|
|
474
|
-
"-R", "--repos",
|
|
475
|
-
action="store_true",
|
|
476
|
-
help="List all public repositories for a user",
|
|
477
|
-
)
|
|
478
|
-
parser.add_argument(
|
|
479
|
-
"-s", "--show",
|
|
480
|
-
action="store_true",
|
|
481
|
-
help="Display file content in terminal without saving to disk",
|
|
482
|
-
)
|
|
483
|
-
parser.add_argument(
|
|
484
|
-
"-o", "--output",
|
|
485
|
-
default=None,
|
|
486
|
-
help="Custom destination filename or output path",
|
|
487
|
-
)
|
|
488
|
-
parser.add_argument(
|
|
489
|
-
"--set-default",
|
|
490
|
-
metavar="REPO",
|
|
491
|
-
help="Set persistent default repository in local config",
|
|
492
|
-
)
|
|
493
|
-
parser.add_argument(
|
|
494
|
-
"--set-default-user",
|
|
495
|
-
metavar="USER",
|
|
496
|
-
help="Set persistent default GitHub user in local config",
|
|
497
|
-
)
|
|
498
|
-
parser.add_argument(
|
|
499
|
-
"--config",
|
|
500
|
-
action="store_true",
|
|
501
|
-
help="Display current CodeFetch configuration",
|
|
502
|
-
)
|
|
503
|
-
parser.add_argument(
|
|
504
|
-
"--reset-config",
|
|
505
|
-
action="store_true",
|
|
506
|
-
help="Reset configuration back to defaults",
|
|
507
|
-
)
|
|
508
|
-
parser.add_argument(
|
|
509
|
-
"-v", "--version",
|
|
510
|
-
action="version",
|
|
511
|
-
version="codefetch 1.2.2",
|
|
656
|
+
description="Fetch code files from GitHub repositories.",
|
|
657
|
+
add_help=False,
|
|
512
658
|
)
|
|
659
|
+
|
|
660
|
+
parser.add_argument("args", nargs="*", help="Repository and filename targets")
|
|
661
|
+
parser.add_argument("-s", "--show", action="store_true", help="Print file content to terminal")
|
|
662
|
+
parser.add_argument("-l", "--list", action="store_true", help="List code files in repository")
|
|
663
|
+
parser.add_argument("-R", "--repos", action="store_true", help="List public repositories for a user")
|
|
664
|
+
parser.add_argument("-r", "--repo", type=str, help="Repository name or URL")
|
|
665
|
+
parser.add_argument("-u", "--user", type=str, help="GitHub username")
|
|
666
|
+
parser.add_argument("-b", "--branch", type=str, default=None, help="Branch name (default: repository default branch)")
|
|
667
|
+
parser.add_argument("-o", "--output", type=str, help="Custom output filename or path")
|
|
668
|
+
parser.add_argument("--set-default", type=str, help="Save default repository (e.g. owner/repo)")
|
|
669
|
+
parser.add_argument("--set-default-user", type=str, help="Save default GitHub user")
|
|
670
|
+
parser.add_argument("--config", action="store_true", help="Display active configuration")
|
|
671
|
+
parser.add_argument("--reset-config", action="store_true", help="Reset configuration back to defaults")
|
|
672
|
+
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
|
|
673
|
+
parser.add_argument("-h", "--help", action="help", help="Show help message")
|
|
674
|
+
|
|
513
675
|
return parser
|
|
514
676
|
|
|
515
677
|
|
|
516
678
|
def resolve_invocation(positional_args, repo_flag, user_flag, branch_flag,
|
|
517
|
-
default_owner, default_repo
|
|
679
|
+
default_owner, default_repo):
|
|
518
680
|
"""
|
|
519
681
|
Resolve arguments into (owner, repo, branch, filename).
|
|
520
682
|
"""
|
|
521
683
|
owner = user_flag or default_owner
|
|
522
684
|
repo = default_repo
|
|
523
|
-
branch = branch_flag
|
|
685
|
+
branch = branch_flag # None if not provided
|
|
524
686
|
filename = None
|
|
525
687
|
|
|
526
688
|
if repo_flag:
|
|
@@ -579,7 +741,6 @@ def main():
|
|
|
579
741
|
config = load_config()
|
|
580
742
|
default_owner = config.get("owner", DEFAULT_OWNER)
|
|
581
743
|
default_repo = config.get("repo", DEFAULT_REPO)
|
|
582
|
-
default_branch = config.get("branch", DEFAULT_BRANCH)
|
|
583
744
|
|
|
584
745
|
# Handle --reset-config
|
|
585
746
|
if args.reset_config:
|
|
@@ -615,12 +776,12 @@ def main():
|
|
|
615
776
|
print("CodeFetch Configuration:")
|
|
616
777
|
print(f" Default User: {default_owner or 'None (not set)'}")
|
|
617
778
|
print(f" Default Repository: {default_repo or 'None (not set)'}")
|
|
618
|
-
print(
|
|
779
|
+
print(" Default Branch: None (auto-detect repository default)")
|
|
619
780
|
print(f" Config File: {CONFIG_FILE}")
|
|
620
781
|
return
|
|
621
782
|
|
|
622
783
|
owner = args.user or default_owner
|
|
623
|
-
branch = args.branch
|
|
784
|
+
branch = args.branch # None if not provided
|
|
624
785
|
|
|
625
786
|
# Handle --repos (list repositories)
|
|
626
787
|
if args.repos:
|
|
@@ -665,7 +826,6 @@ def main():
|
|
|
665
826
|
args.branch,
|
|
666
827
|
default_owner,
|
|
667
828
|
default_repo,
|
|
668
|
-
default_branch,
|
|
669
829
|
)
|
|
670
830
|
|
|
671
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
|
+
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
|
|
@@ -24,7 +24,7 @@ Dynamic: requires-python
|
|
|
24
24
|
|
|
25
25
|
Quickly fetch code files from GitHub repositories with simple commands.
|
|
26
26
|
|
|
27
|
-
CodeFetch is a lightweight command-line utility designed to quickly inspect, preview, and download individual source code files directly from any GitHub repository without needing to clone full repositories.
|
|
27
|
+
CodeFetch is a lightweight command-line utility and Python library designed to quickly inspect, preview, and download individual source code files directly from any GitHub repository without needing to clone full repositories.
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
@@ -35,6 +35,7 @@ CodeFetch is a lightweight command-line utility designed to quickly inspect, pre
|
|
|
35
35
|
- **Automatic Path & Case Resolution**: Automatically resolves case mismatches and finds files nested inside subdirectories.
|
|
36
36
|
- **Repository Discovery**: List all public repositories for any GitHub user with `--repos`.
|
|
37
37
|
- **Tree Exploration**: List all supported code files within a repository with `--list`.
|
|
38
|
+
- **Python Library Support**: Import and use programmatically with `import codefetch as cf`.
|
|
38
39
|
- **Optional Persistent Defaults**: Save a default repository or user with `--set-default` if you frequently work with the same repository.
|
|
39
40
|
- **Zero Runtime Dependencies**: Built using only the Python standard library with zero third-party dependencies.
|
|
40
41
|
|
|
@@ -130,6 +131,28 @@ codefetch --reset-config
|
|
|
130
131
|
|
|
131
132
|
---
|
|
132
133
|
|
|
134
|
+
## Python API Usage
|
|
135
|
+
|
|
136
|
+
You can also use CodeFetch programmatically within your Python scripts:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
import codefetch as cf
|
|
140
|
+
|
|
141
|
+
# 1. Fetch file content as a string directly in memory
|
|
142
|
+
makefile_text = cf.get("torvalds/linux", "Makefile")
|
|
143
|
+
# Or shorthand single argument syntax:
|
|
144
|
+
readme_text = cf.get("psf/requests/README.md")
|
|
145
|
+
|
|
146
|
+
# 2. Download a file to disk
|
|
147
|
+
cf.download("torvalds/linux", "Makefile", output="linux_Makefile")
|
|
148
|
+
|
|
149
|
+
# 3. List all code files in a repository
|
|
150
|
+
files = cf.list("torvalds/linux")
|
|
151
|
+
print(f"Found {len(files)} files")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
133
156
|
## Command Reference
|
|
134
157
|
|
|
135
158
|
| Flag | Description | Example |
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
codefetch/__init__.py,sha256=3VbBrolY7csLzdP8QSlksD1iBNJqaEa2CvyZhg_sdYk,5049
|
|
2
|
+
codefetch/cli.py,sha256=uun_VFjFI_B4PPFzKJhPU8IcTrsQY0OuU6nzAYRXIvQ,32190
|
|
3
|
+
codefetch_cli-1.2.4.dist-info/licenses/LICENSE,sha256=hqEPPm8jxPava5BfKIj3wTIXNyWNwsIvPv2daPCOijY,1094
|
|
4
|
+
codefetch_cli-1.2.4.dist-info/METADATA,sha256=a6mkv8w6CLgKRlQTXFb-UAEfdLCJCjhrPpjAtu041Dk,6675
|
|
5
|
+
codefetch_cli-1.2.4.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
6
|
+
codefetch_cli-1.2.4.dist-info/entry_points.txt,sha256=jFLRsBB_USeSdqjewcXZfnOzY56s0PZat7mnYkYdrXg,49
|
|
7
|
+
codefetch_cli-1.2.4.dist-info/top_level.txt,sha256=kHrftHbyqugb8LQXjW1dR9uI-JFjZQJ0sUReIa45yXQ,10
|
|
8
|
+
codefetch_cli-1.2.4.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
codefetch/__init__.py,sha256=OgTOFFfQIh-gT-X8j2hheukYFmfAe0SvzIuA_LosY6Y,98
|
|
2
|
-
codefetch/cli.py,sha256=muL99Mr9tCPZ0UJp3YfVcVGMxTLvCPGfEbOmH-GpNCU,23588
|
|
3
|
-
codefetch_cli-1.2.2.dist-info/licenses/LICENSE,sha256=hqEPPm8jxPava5BfKIj3wTIXNyWNwsIvPv2daPCOijY,1094
|
|
4
|
-
codefetch_cli-1.2.2.dist-info/METADATA,sha256=pOL04v-tCvJap5vLYlCE9bDXBv768lm-LR5gghvyzlc,6000
|
|
5
|
-
codefetch_cli-1.2.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
6
|
-
codefetch_cli-1.2.2.dist-info/entry_points.txt,sha256=jFLRsBB_USeSdqjewcXZfnOzY56s0PZat7mnYkYdrXg,49
|
|
7
|
-
codefetch_cli-1.2.2.dist-info/top_level.txt,sha256=kHrftHbyqugb8LQXjW1dR9uI-JFjZQJ0sUReIa45yXQ,10
|
|
8
|
-
codefetch_cli-1.2.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|