codefetch-cli 1.2.2__tar.gz → 1.2.3__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.2/codefetch_cli.egg-info → codefetch_cli-1.2.3}/PKG-INFO +25 -2
- {codefetch_cli-1.2.2 → codefetch_cli-1.2.3}/README.md +24 -1
- codefetch_cli-1.2.3/codefetch/__init__.py +146 -0
- {codefetch_cli-1.2.2 → codefetch_cli-1.2.3}/codefetch/cli.py +217 -200
- {codefetch_cli-1.2.2 → codefetch_cli-1.2.3/codefetch_cli.egg-info}/PKG-INFO +25 -2
- {codefetch_cli-1.2.2 → codefetch_cli-1.2.3}/pyproject.toml +1 -1
- {codefetch_cli-1.2.2 → codefetch_cli-1.2.3}/setup.py +1 -1
- codefetch_cli-1.2.2/codefetch/__init__.py +0 -5
- {codefetch_cli-1.2.2 → codefetch_cli-1.2.3}/LICENSE +0 -0
- {codefetch_cli-1.2.2 → codefetch_cli-1.2.3}/codefetch_cli.egg-info/SOURCES.txt +0 -0
- {codefetch_cli-1.2.2 → codefetch_cli-1.2.3}/codefetch_cli.egg-info/dependency_links.txt +0 -0
- {codefetch_cli-1.2.2 → codefetch_cli-1.2.3}/codefetch_cli.egg-info/entry_points.txt +0 -0
- {codefetch_cli-1.2.2 → codefetch_cli-1.2.3}/codefetch_cli.egg-info/top_level.txt +0 -0
- {codefetch_cli-1.2.2 → codefetch_cli-1.2.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codefetch-cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
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 |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Quickly fetch code files from GitHub repositories with simple commands.
|
|
4
4
|
|
|
5
|
-
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.
|
|
5
|
+
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.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -13,6 +13,7 @@ CodeFetch is a lightweight command-line utility designed to quickly inspect, pre
|
|
|
13
13
|
- **Automatic Path & Case Resolution**: Automatically resolves case mismatches and finds files nested inside subdirectories.
|
|
14
14
|
- **Repository Discovery**: List all public repositories for any GitHub user with `--repos`.
|
|
15
15
|
- **Tree Exploration**: List all supported code files within a repository with `--list`.
|
|
16
|
+
- **Python Library Support**: Import and use programmatically with `import codefetch as cf`.
|
|
16
17
|
- **Optional Persistent Defaults**: Save a default repository or user with `--set-default` if you frequently work with the same repository.
|
|
17
18
|
- **Zero Runtime Dependencies**: Built using only the Python standard library with zero third-party dependencies.
|
|
18
19
|
|
|
@@ -108,6 +109,28 @@ codefetch --reset-config
|
|
|
108
109
|
|
|
109
110
|
---
|
|
110
111
|
|
|
112
|
+
## Python API Usage
|
|
113
|
+
|
|
114
|
+
You can also use CodeFetch programmatically within your Python scripts:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
import codefetch as cf
|
|
118
|
+
|
|
119
|
+
# 1. Fetch file content as a string directly in memory
|
|
120
|
+
makefile_text = cf.get("torvalds/linux", "Makefile")
|
|
121
|
+
# Or shorthand single argument syntax:
|
|
122
|
+
readme_text = cf.get("psf/requests/README.md")
|
|
123
|
+
|
|
124
|
+
# 2. Download a file to disk
|
|
125
|
+
cf.download("torvalds/linux", "Makefile", output="linux_Makefile")
|
|
126
|
+
|
|
127
|
+
# 3. List all code files in a repository
|
|
128
|
+
files = cf.list("torvalds/linux")
|
|
129
|
+
print(f"Found {len(files)} files")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
111
134
|
## Command Reference
|
|
112
135
|
|
|
113
136
|
| Flag | Description | Example |
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""
|
|
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")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__version__ = "1.2.3"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get(target, filename=None, branch="main"):
|
|
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: 'main', automatically falls back to 'master').
|
|
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
|
+
fetch_raw_direct,
|
|
41
|
+
get_repo_tree_with_branch,
|
|
42
|
+
resolve_file_in_tree,
|
|
43
|
+
parse_repo_identifier,
|
|
44
|
+
resolve_invocation,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if filename:
|
|
48
|
+
owner, repo = parse_repo_identifier(target)
|
|
49
|
+
file_path = filename.strip("/\\")
|
|
50
|
+
else:
|
|
51
|
+
owner, repo, resolved_branch, file_path = resolve_invocation(
|
|
52
|
+
[target], None, None, branch, None, None, "main"
|
|
53
|
+
)
|
|
54
|
+
if resolved_branch:
|
|
55
|
+
branch = resolved_branch
|
|
56
|
+
|
|
57
|
+
if not owner or not repo or not file_path:
|
|
58
|
+
raise ValueError(f"Could not resolve owner, repo, and filename from target='{target}', filename='{filename}'")
|
|
59
|
+
|
|
60
|
+
# 1. Try fast direct raw fetch
|
|
61
|
+
found_branch, content = fetch_raw_direct(owner, repo, branch, file_path)
|
|
62
|
+
if content is not None:
|
|
63
|
+
return content
|
|
64
|
+
|
|
65
|
+
# 2. Try repository tree search
|
|
66
|
+
actual_branch, tree_data = get_repo_tree_with_branch(owner, repo, branch)
|
|
67
|
+
matched = resolve_file_in_tree(tree_data, file_path)
|
|
68
|
+
if matched:
|
|
69
|
+
url = f"https://raw.githubusercontent.com/{owner}/{repo}/{actual_branch}/{matched}"
|
|
70
|
+
req = urllib.request.Request(url, headers={"User-Agent": "codefetch"})
|
|
71
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
72
|
+
return resp.read().decode("utf-8")
|
|
73
|
+
|
|
74
|
+
raise FileNotFoundError(f"File '{file_path}' not found in {owner}/{repo} ({actual_branch})")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def download(target, filename=None, output=None, branch="main"):
|
|
78
|
+
"""
|
|
79
|
+
Download a file from GitHub and save it locally.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
target: 'owner/repo' or full path 'owner/repo/path/to/file'.
|
|
83
|
+
filename: Optional path to file in repo if target is 'owner/repo'.
|
|
84
|
+
output: Destination filename/path on local disk (default: original filename).
|
|
85
|
+
branch: Branch name (default: 'main').
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
str: Path to the downloaded file.
|
|
89
|
+
"""
|
|
90
|
+
content = get(target, filename=filename, branch=branch)
|
|
91
|
+
dest_name = output
|
|
92
|
+
if not dest_name:
|
|
93
|
+
resolved_file = filename if filename else target.rsplit("/", 1)[-1]
|
|
94
|
+
dest_name = resolved_file.rsplit("/", 1)[-1]
|
|
95
|
+
|
|
96
|
+
with open(dest_name, "w", encoding="utf-8") as f:
|
|
97
|
+
f.write(content)
|
|
98
|
+
|
|
99
|
+
return dest_name
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def list(target, branch="main"):
|
|
103
|
+
"""
|
|
104
|
+
List supported code files in a repository.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
target: 'owner/repo' or repository URL.
|
|
108
|
+
branch: Branch name (default: 'main').
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
list[str]: Relative paths of code files in the repository.
|
|
112
|
+
"""
|
|
113
|
+
from .cli import (
|
|
114
|
+
parse_repo_identifier,
|
|
115
|
+
get_repo_tree_with_branch,
|
|
116
|
+
CODE_EXTENSIONS,
|
|
117
|
+
NAMED_FILES,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
owner, repo = parse_repo_identifier(target)
|
|
121
|
+
if not owner or not repo:
|
|
122
|
+
raise ValueError(f"Invalid repository identifier: '{target}'")
|
|
123
|
+
|
|
124
|
+
actual_branch, tree_data = get_repo_tree_with_branch(owner, repo, branch)
|
|
125
|
+
files = []
|
|
126
|
+
for item in tree_data.get("tree", []):
|
|
127
|
+
if item.get("type") != "blob":
|
|
128
|
+
continue
|
|
129
|
+
path = item.get("path", "")
|
|
130
|
+
base_name = path.rsplit("/", 1)[-1].lower()
|
|
131
|
+
suffix = ""
|
|
132
|
+
if "." in base_name:
|
|
133
|
+
suffix = "." + base_name.rsplit(".", 1)[-1]
|
|
134
|
+
if suffix in CODE_EXTENSIONS or base_name in NAMED_FILES:
|
|
135
|
+
files.append(path)
|
|
136
|
+
|
|
137
|
+
files.sort(key=str.lower)
|
|
138
|
+
return files
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
__all__ = [
|
|
142
|
+
"__version__",
|
|
143
|
+
"get",
|
|
144
|
+
"download",
|
|
145
|
+
"list",
|
|
146
|
+
]
|
|
@@ -18,6 +18,8 @@ DEFAULT_BRANCH = "main"
|
|
|
18
18
|
|
|
19
19
|
CONFIG_FILE = Path.home() / ".codefetch.json"
|
|
20
20
|
|
|
21
|
+
__version__ = "1.2.3"
|
|
22
|
+
|
|
21
23
|
# Supported code, data, and config extensions
|
|
22
24
|
CODE_EXTENSIONS = {
|
|
23
25
|
# Data & Tables
|
|
@@ -56,85 +58,81 @@ 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: main)
|
|
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
|
|
|
94
105
|
|
|
95
106
|
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
|
-
}
|
|
107
|
+
"""Load user configuration from ~/.codefetch.json."""
|
|
102
108
|
if CONFIG_FILE.exists():
|
|
103
109
|
try:
|
|
104
110
|
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
105
|
-
|
|
106
|
-
if isinstance(saved, dict):
|
|
107
|
-
if saved.get("owner"):
|
|
108
|
-
config["owner"] = saved["owner"]
|
|
109
|
-
if saved.get("repo"):
|
|
110
|
-
config["repo"] = saved["repo"]
|
|
111
|
-
if saved.get("branch"):
|
|
112
|
-
config["branch"] = saved["branch"]
|
|
111
|
+
return json.load(f)
|
|
113
112
|
except Exception:
|
|
114
|
-
|
|
115
|
-
return
|
|
113
|
+
return {}
|
|
114
|
+
return {}
|
|
116
115
|
|
|
117
116
|
|
|
118
117
|
def save_config(owner=None, repo=None, branch=None):
|
|
119
|
-
"""Save configuration to ~/.codefetch.json."""
|
|
120
|
-
|
|
121
|
-
if owner:
|
|
122
|
-
|
|
123
|
-
if repo:
|
|
124
|
-
|
|
125
|
-
if branch:
|
|
126
|
-
|
|
118
|
+
"""Save user configuration to ~/.codefetch.json."""
|
|
119
|
+
cfg = load_config()
|
|
120
|
+
if owner is not None:
|
|
121
|
+
cfg["owner"] = owner
|
|
122
|
+
if repo is not None:
|
|
123
|
+
cfg["repo"] = repo
|
|
124
|
+
if branch is not None:
|
|
125
|
+
cfg["branch"] = branch
|
|
126
|
+
|
|
127
127
|
try:
|
|
128
128
|
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
129
|
-
json.dump(
|
|
130
|
-
return True
|
|
129
|
+
json.dump(cfg, f, indent=2)
|
|
131
130
|
except Exception as e:
|
|
132
|
-
print(f"
|
|
133
|
-
return False
|
|
131
|
+
print(f"Warning: Could not save configuration: {e}")
|
|
134
132
|
|
|
135
133
|
|
|
136
134
|
def get_headers():
|
|
137
|
-
"""
|
|
135
|
+
"""Build request headers, attaching GITHUB_TOKEN if present in environment."""
|
|
138
136
|
headers = {
|
|
139
137
|
"User-Agent": "codefetch",
|
|
140
138
|
"Accept": "application/vnd.github+json",
|
|
@@ -146,10 +144,10 @@ def get_headers():
|
|
|
146
144
|
|
|
147
145
|
|
|
148
146
|
def get_json(url):
|
|
149
|
-
"""Fetch
|
|
150
|
-
|
|
151
|
-
with urllib.request.urlopen(
|
|
152
|
-
return json.loads(
|
|
147
|
+
"""Fetch JSON from GitHub API with rate limit handling."""
|
|
148
|
+
req = urllib.request.Request(url, headers=get_headers())
|
|
149
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
150
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
153
151
|
|
|
154
152
|
|
|
155
153
|
def parse_repo_identifier(repo_str, default_owner=None):
|
|
@@ -170,37 +168,89 @@ def parse_repo_identifier(repo_str, default_owner=None):
|
|
|
170
168
|
return default_owner, repo_str.removesuffix(".git")
|
|
171
169
|
|
|
172
170
|
|
|
173
|
-
def
|
|
174
|
-
"""
|
|
175
|
-
|
|
171
|
+
def fetch_raw_direct(owner, repo, branch, clean_filename):
|
|
172
|
+
"""
|
|
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).
|
|
177
|
+
"""
|
|
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
|
|
195
|
+
|
|
196
|
+
|
|
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"
|
|
210
|
+
try:
|
|
211
|
+
return b, get_json(url)
|
|
212
|
+
except urllib.error.HTTPError as e:
|
|
213
|
+
if e.code == 404:
|
|
214
|
+
continue
|
|
215
|
+
raise
|
|
216
|
+
|
|
217
|
+
# Query repository info to discover actual default branch (e.g. develop, trunk, etc.)
|
|
176
218
|
try:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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)
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
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
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
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
|
|
186
241
|
|
|
187
242
|
|
|
188
243
|
def get_user_repos(owner):
|
|
189
|
-
"""Fetch public repositories for a
|
|
244
|
+
"""Fetch public repositories list for a user."""
|
|
190
245
|
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 []
|
|
246
|
+
return get_json(url)
|
|
195
247
|
|
|
196
248
|
|
|
197
249
|
def list_repos(owner=None, default_repo=None):
|
|
198
|
-
"""List all public repositories for
|
|
250
|
+
"""List all public repositories for a GitHub user."""
|
|
199
251
|
if not owner:
|
|
200
|
-
print("Error: No GitHub
|
|
201
|
-
print("Usage:")
|
|
202
|
-
print(" codefetch -R <username>")
|
|
203
|
-
print(" codefetch -R -u <username>")
|
|
252
|
+
print("Error: No GitHub username specified.")
|
|
253
|
+
print("Usage: codefetch --repos <username>")
|
|
204
254
|
return
|
|
205
255
|
|
|
206
256
|
try:
|
|
@@ -209,15 +259,17 @@ def list_repos(owner=None, default_repo=None):
|
|
|
209
259
|
print(f"No public repositories found for user '{owner}'.")
|
|
210
260
|
return
|
|
211
261
|
|
|
212
|
-
print(f"
|
|
213
|
-
for r in repos:
|
|
262
|
+
print(f"Public repositories for '{owner}':\n")
|
|
263
|
+
for i, r in enumerate(repos, 1):
|
|
214
264
|
name = r.get("name", "")
|
|
215
|
-
desc = r.get("description") or ""
|
|
265
|
+
desc = r.get("description") or "No description"
|
|
216
266
|
is_def = " (default)" if default_repo and name.lower() == default_repo.lower() else ""
|
|
217
267
|
marker = "*" if is_def else "-"
|
|
218
|
-
|
|
219
|
-
|
|
268
|
+
print(f" {marker} {name:<22}{is_def}")
|
|
269
|
+
if desc and desc != "No description":
|
|
270
|
+
print(f" {desc}")
|
|
220
271
|
|
|
272
|
+
print(f"\nTotal: {len(repos)} repositories")
|
|
221
273
|
print("\nCommands:")
|
|
222
274
|
print(f" codefetch -l {owner}/<repo> List files in a repository")
|
|
223
275
|
print(f" codefetch {owner}/<repo> <filename> Download a file from a repository")
|
|
@@ -241,7 +293,7 @@ def list_files(owner, repo, branch="main"):
|
|
|
241
293
|
return
|
|
242
294
|
|
|
243
295
|
try:
|
|
244
|
-
data =
|
|
296
|
+
actual_branch, data = get_repo_tree_with_branch(owner, repo, branch)
|
|
245
297
|
if data.get("truncated"):
|
|
246
298
|
print("Warning: GitHub returned a truncated file list.")
|
|
247
299
|
|
|
@@ -262,10 +314,10 @@ def list_files(owner, repo, branch="main"):
|
|
|
262
314
|
files.sort(key=str.lower)
|
|
263
315
|
|
|
264
316
|
if not files:
|
|
265
|
-
print(f"No supported code files found in {owner}/{repo}.")
|
|
317
|
+
print(f"No supported code files found in {owner}/{repo} ({actual_branch}).")
|
|
266
318
|
return
|
|
267
319
|
|
|
268
|
-
print(f"Available files in {owner}/{repo} ({
|
|
320
|
+
print(f"Available files in {owner}/{repo} ({actual_branch}):\n")
|
|
269
321
|
for i, path in enumerate(files, 1):
|
|
270
322
|
print(f"{i:>3}. {path}")
|
|
271
323
|
|
|
@@ -368,50 +420,64 @@ def fetch_file(filename, owner=None, repo=None, branch=DEFAULT_BRANCH,
|
|
|
368
420
|
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
369
421
|
return resp.read().decode("utf-8")
|
|
370
422
|
|
|
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
423
|
content = None
|
|
376
424
|
resolved_path = clean_filename
|
|
425
|
+
actual_branch = branch
|
|
377
426
|
|
|
427
|
+
# 1. Direct raw fetch first (fastest, preserves GitHub API rate limit)
|
|
378
428
|
try:
|
|
379
|
-
|
|
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
|
|
380
436
|
except urllib.error.HTTPError as e:
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
try:
|
|
384
|
-
tree_data = get_repo_tree(owner, repo, branch)
|
|
385
|
-
matched = resolve_file_in_tree(tree_data, clean_filename)
|
|
386
|
-
if matched:
|
|
387
|
-
resolved_path = matched
|
|
388
|
-
content = download_url(f"{raw_base}/{resolved_path}")
|
|
389
|
-
if resolved_path != clean_filename:
|
|
390
|
-
print(f"(Resolved '{clean_filename}' -> '{resolved_path}')")
|
|
391
|
-
else:
|
|
392
|
-
# Check other public repositories for the same user
|
|
393
|
-
other_repo, other_match = find_file_in_other_repos(owner, repo, clean_filename)
|
|
394
|
-
if other_repo:
|
|
395
|
-
print(f"File not found: '{clean_filename}' in {owner}/{repo}.")
|
|
396
|
-
print(f"\n[Tip] Found '{other_match}' in repository '{other_repo}'!")
|
|
397
|
-
print(f"To fetch it, run:\n codefetch {owner}/{other_repo} {other_match}")
|
|
398
|
-
return
|
|
399
|
-
else:
|
|
400
|
-
print(f"File not found: '{clean_filename}' in {owner}/{repo}.")
|
|
401
|
-
print(f"Run 'codefetch -l {owner}/{repo}' to see available files.")
|
|
402
|
-
return
|
|
403
|
-
except Exception:
|
|
404
|
-
print(f"File not found: '{clean_filename}' in {owner}/{repo}.")
|
|
405
|
-
return
|
|
406
|
-
else:
|
|
407
|
-
print(f"Download failed: HTTP {e.code}")
|
|
408
|
-
return
|
|
437
|
+
print(f"Download failed: HTTP {e.code}")
|
|
438
|
+
return
|
|
409
439
|
except urllib.error.URLError:
|
|
410
440
|
print("Could not connect to GitHub.")
|
|
411
441
|
return
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
442
|
+
|
|
443
|
+
# 2. If direct fetch didn't find the file (404), fall back to smart repository tree search
|
|
444
|
+
if content is None:
|
|
445
|
+
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
|
|
475
|
+
except UnicodeDecodeError:
|
|
476
|
+
print("The file is not UTF-8 text, so it cannot be displayed or saved as text.")
|
|
477
|
+
return
|
|
478
|
+
except Exception:
|
|
479
|
+
print(f"File not found: '{clean_filename}' in {owner}/{repo}.")
|
|
480
|
+
return
|
|
415
481
|
|
|
416
482
|
if content is None:
|
|
417
483
|
return
|
|
@@ -423,7 +489,7 @@ def fetch_file(filename, owner=None, repo=None, branch=DEFAULT_BRANCH,
|
|
|
423
489
|
try:
|
|
424
490
|
with open(out_name, "w", encoding="utf-8") as f:
|
|
425
491
|
f.write(content)
|
|
426
|
-
print(f"Downloaded: {out_name} (from {owner}/{repo})")
|
|
492
|
+
print(f"Downloaded: {out_name} (from {owner}/{repo}@{actual_branch})")
|
|
427
493
|
except Exception as e:
|
|
428
494
|
print(f"Error saving file '{out_name}': {e}")
|
|
429
495
|
|
|
@@ -440,76 +506,27 @@ class CodefetchParser(argparse.ArgumentParser):
|
|
|
440
506
|
|
|
441
507
|
|
|
442
508
|
def build_parser():
|
|
443
|
-
"""Build argument parser with full CLI options."""
|
|
444
509
|
parser = CodefetchParser(
|
|
445
510
|
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",
|
|
511
|
+
description="Fetch code files from GitHub repositories.",
|
|
512
|
+
add_help=False,
|
|
512
513
|
)
|
|
514
|
+
|
|
515
|
+
parser.add_argument("args", nargs="*", help="Repository and filename targets")
|
|
516
|
+
parser.add_argument("-s", "--show", action="store_true", help="Print file content to terminal")
|
|
517
|
+
parser.add_argument("-l", "--list", action="store_true", help="List code files in repository")
|
|
518
|
+
parser.add_argument("-R", "--repos", action="store_true", help="List public repositories for a user")
|
|
519
|
+
parser.add_argument("-r", "--repo", type=str, help="Repository name or URL")
|
|
520
|
+
parser.add_argument("-u", "--user", type=str, help="GitHub username")
|
|
521
|
+
parser.add_argument("-b", "--branch", type=str, help="Branch name (default: main)")
|
|
522
|
+
parser.add_argument("-o", "--output", type=str, help="Custom output filename or path")
|
|
523
|
+
parser.add_argument("--set-default", type=str, help="Save default repository (e.g. owner/repo)")
|
|
524
|
+
parser.add_argument("--set-default-user", type=str, help="Save default GitHub user")
|
|
525
|
+
parser.add_argument("--config", action="store_true", help="Display active configuration")
|
|
526
|
+
parser.add_argument("--reset-config", action="store_true", help="Reset configuration back to defaults")
|
|
527
|
+
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
|
|
528
|
+
parser.add_argument("-h", "--help", action="help", help="Show help message")
|
|
529
|
+
|
|
513
530
|
return parser
|
|
514
531
|
|
|
515
532
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codefetch-cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
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 |
|
|
@@ -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.3",
|
|
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
|