git-ripper 1.4.13 → 1.5.1
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.
- package/README.md +53 -2
- package/package.json +1 -1
- package/src/archiver.js +4 -2
- package/src/downloader.js +57 -15
- package/src/index.js +18 -4
- package/src/parser.js +12 -5
package/README.md
CHANGED
|
@@ -96,6 +96,7 @@ git-ripper https://github.com/username/repository/tree/branch/folder --zip="my-a
|
|
|
96
96
|
| Option | Description | Default |
|
|
97
97
|
| -------------------------- | ---------------------------------------- | ----------------- |
|
|
98
98
|
| `-o, --output <directory>` | Specify output directory | Current directory |
|
|
99
|
+
| `--gh-token <token>` | GitHub Personal Access Token | - |
|
|
99
100
|
| `--zip [filename]` | Create ZIP archive of downloaded content | - |
|
|
100
101
|
| `--no-resume` | Disable resume functionality | - |
|
|
101
102
|
| `--force-restart` | Ignore existing checkpoints and restart | - |
|
|
@@ -103,6 +104,46 @@ git-ripper https://github.com/username/repository/tree/branch/folder --zip="my-a
|
|
|
103
104
|
| `-V, --version` | Show version number | - |
|
|
104
105
|
| `-h, --help` | Show help | - |
|
|
105
106
|
|
|
107
|
+
## Authentication (Private Repositories & Rate Limits)
|
|
108
|
+
|
|
109
|
+
To download from private repositories or to increase your API rate limit, you need to provide a GitHub Personal Access Token (PAT).
|
|
110
|
+
|
|
111
|
+
### How to Generate a Token
|
|
112
|
+
|
|
113
|
+
You can use either a **Fine-grained token** (Recommended) or a **Classic token**.
|
|
114
|
+
|
|
115
|
+
#### Option A: Fine-grained Token (Recommended)
|
|
116
|
+
|
|
117
|
+
1. Go to **Settings** > **Developer settings** > **Personal access tokens** > **Fine-grained tokens**.
|
|
118
|
+
2. Click **Generate new token**.
|
|
119
|
+
3. Name it (e.g., "Git-ripper").
|
|
120
|
+
4. **Resource owner**: Select your user.
|
|
121
|
+
5. **Repository access**: Select **Only select repositories** and choose the private repository you want to download from.
|
|
122
|
+
6. **Permissions**:
|
|
123
|
+
- Click on **Repository permissions**.
|
|
124
|
+
- Find **Contents** and change Access to **Read-only**.
|
|
125
|
+
- _Note: Metadata permission is selected automatically._
|
|
126
|
+
7. Click **Generate token**.
|
|
127
|
+
|
|
128
|
+
#### Option B: Classic Token
|
|
129
|
+
|
|
130
|
+
1. Go to **Settings** > **Developer settings** > **Personal access tokens** > **Tokens (classic)**.
|
|
131
|
+
2. Click **Generate new token** > **Generate new token (classic)**.
|
|
132
|
+
3. Give your token a descriptive name.
|
|
133
|
+
4. **Select Scopes:**
|
|
134
|
+
- **For Private Repositories:** Select the **`repo`** scope (Full control of private repositories).
|
|
135
|
+
5. Click **Generate token**.
|
|
136
|
+
|
|
137
|
+
### Using the Token
|
|
138
|
+
|
|
139
|
+
Pass the token using the `--gh-token` flag:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
git-ripper https://github.com/username/private-repo/tree/main/src --gh-token ghp_YourTokenHere
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
> **Security Note:** Be careful not to share your token or commit it to public repositories.
|
|
146
|
+
|
|
106
147
|
## Examples
|
|
107
148
|
|
|
108
149
|
### Extract a Component Library
|
|
@@ -133,6 +174,13 @@ git-ripper https://github.com/nodejs/node/tree/main/doc -o ./node-docs
|
|
|
133
174
|
git-ripper https://github.com/tailwindlabs/tailwindcss/tree/master/src/components -o ./tailwind-components
|
|
134
175
|
```
|
|
135
176
|
|
|
177
|
+
### Download from Private Repository
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# Download from a private repository using a token
|
|
181
|
+
git-ripper https://github.com/my-org/private-project/tree/main/src --gh-token ghp_abc123...
|
|
182
|
+
```
|
|
183
|
+
|
|
136
184
|
### Download and Create Archive
|
|
137
185
|
|
|
138
186
|
```bash
|
|
@@ -207,7 +255,7 @@ The resume functionality uses checkpoint files stored in `.git_ripper_checkpoint
|
|
|
207
255
|
|
|
208
256
|
## Configuration
|
|
209
257
|
|
|
210
|
-
Git-ripper works out of the box without configuration. For rate-limited GitHub API usage,
|
|
258
|
+
Git-ripper works out of the box without configuration. For rate-limited GitHub API usage or private repositories, use the `--gh-token` option as described in the [Authentication](#authentication-private-repositories--rate-limits) section.
|
|
211
259
|
|
|
212
260
|
## Troubleshooting
|
|
213
261
|
|
|
@@ -219,7 +267,10 @@ Git-ripper works out of the box without configuration. For rate-limited GitHub A
|
|
|
219
267
|
Error: Request failed with status code 403
|
|
220
268
|
```
|
|
221
269
|
|
|
222
|
-
**Solution**: GitHub limits unauthenticated API requests.
|
|
270
|
+
**Solution**: GitHub limits unauthenticated API requests. You can either:
|
|
271
|
+
|
|
272
|
+
1. Wait a few minutes and try again
|
|
273
|
+
2. Use the `--gh-token` option with a Personal Access Token to significantly increase your rate limit
|
|
223
274
|
|
|
224
275
|
#### Invalid URL Format
|
|
225
276
|
|
package/package.json
CHANGED
package/src/archiver.js
CHANGED
|
@@ -128,12 +128,14 @@ export const createArchive = (sourceDir, outputPath) => {
|
|
|
128
128
|
* @param {object} repoInfo - Repository information object
|
|
129
129
|
* @param {string} outputDir - Directory where files should be downloaded
|
|
130
130
|
* @param {string} archiveName - Custom name for the archive file (optional)
|
|
131
|
+
* @param {object} [options] - Download options including token
|
|
131
132
|
* @returns {Promise<string>} - Path to the created archive
|
|
132
133
|
*/
|
|
133
134
|
export const downloadAndArchive = async (
|
|
134
135
|
repoInfo,
|
|
135
136
|
outputDir,
|
|
136
|
-
archiveName = null
|
|
137
|
+
archiveName = null,
|
|
138
|
+
options = {}
|
|
137
139
|
) => {
|
|
138
140
|
const { downloadFolder } = await import("./downloader.js");
|
|
139
141
|
|
|
@@ -146,7 +148,7 @@ export const downloadAndArchive = async (
|
|
|
146
148
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
147
149
|
try {
|
|
148
150
|
// Download the folder contents
|
|
149
|
-
const downloadResult = await downloadFolder(repoInfo, tempDir);
|
|
151
|
+
const downloadResult = await downloadFolder(repoInfo, tempDir, options);
|
|
150
152
|
|
|
151
153
|
// Check if download failed
|
|
152
154
|
if (downloadResult && !downloadResult.success) {
|
package/src/downloader.js
CHANGED
|
@@ -45,10 +45,16 @@ const getSpinnerFrame = () => {
|
|
|
45
45
|
* @param {string} repo - Repository name
|
|
46
46
|
* @param {string} branch - Branch name
|
|
47
47
|
* @param {string} folderPath - Path to the folder
|
|
48
|
+
* @param {string} [token] - GitHub Personal Access Token
|
|
48
49
|
* @returns {Promise<Array>} - Promise resolving to an array of file objects
|
|
49
50
|
* @throws {Error} - Throws error on API failures instead of returning empty array
|
|
50
51
|
*/
|
|
51
|
-
const fetchFolderContents = async (owner, repo, branch, folderPath) => {
|
|
52
|
+
const fetchFolderContents = async (owner, repo, branch, folderPath, token) => {
|
|
53
|
+
const headers = {
|
|
54
|
+
Accept: "application/vnd.github+json",
|
|
55
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
56
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
57
|
+
};
|
|
52
58
|
let effectiveBranch = branch;
|
|
53
59
|
if (!effectiveBranch) {
|
|
54
60
|
// If no branch is specified, fetch the default branch for the repository
|
|
@@ -56,7 +62,7 @@ const fetchFolderContents = async (owner, repo, branch, folderPath) => {
|
|
|
56
62
|
const repoInfoUrl = `https://api.github.com/repos/${encodeURIComponent(
|
|
57
63
|
owner
|
|
58
64
|
)}/${encodeURIComponent(repo)}`;
|
|
59
|
-
const repoInfoResponse = await axios.get(repoInfoUrl);
|
|
65
|
+
const repoInfoResponse = await axios.get(repoInfoUrl, { headers });
|
|
60
66
|
effectiveBranch = repoInfoResponse.data.default_branch;
|
|
61
67
|
if (!effectiveBranch) {
|
|
62
68
|
throw new Error(
|
|
@@ -85,7 +91,7 @@ const fetchFolderContents = async (owner, repo, branch, folderPath) => {
|
|
|
85
91
|
)}?recursive=1`;
|
|
86
92
|
|
|
87
93
|
try {
|
|
88
|
-
const response = await axios.get(apiUrl);
|
|
94
|
+
const response = await axios.get(apiUrl, { headers });
|
|
89
95
|
|
|
90
96
|
// Check if GitHub API returned truncated results
|
|
91
97
|
if (response.data.truncated) {
|
|
@@ -125,7 +131,7 @@ const fetchFolderContents = async (owner, repo, branch, folderPath) => {
|
|
|
125
131
|
isRateLimit = true;
|
|
126
132
|
errorMessage = `GitHub API rate limit exceeded. Please wait until ${new Date(
|
|
127
133
|
parseInt(error.response.headers["x-ratelimit-reset"]) * 1000
|
|
128
|
-
).toLocaleTimeString()} or
|
|
134
|
+
).toLocaleTimeString()} or use the --gh-token option to increase your rate limit.`;
|
|
129
135
|
} else {
|
|
130
136
|
errorMessage = `Access forbidden: ${
|
|
131
137
|
error.response.data.message ||
|
|
@@ -162,9 +168,20 @@ const fetchFolderContents = async (owner, repo, branch, folderPath) => {
|
|
|
162
168
|
* @param {string} branch - Branch name
|
|
163
169
|
* @param {string} filePath - Path to the file
|
|
164
170
|
* @param {string} outputPath - Path where the file should be saved
|
|
171
|
+
* @param {string} [token] - GitHub Personal Access Token
|
|
165
172
|
* @returns {Promise<Object>} - Object containing download status
|
|
166
173
|
*/
|
|
167
|
-
const downloadFile = async (
|
|
174
|
+
const downloadFile = async (
|
|
175
|
+
owner,
|
|
176
|
+
repo,
|
|
177
|
+
branch,
|
|
178
|
+
filePath,
|
|
179
|
+
outputPath,
|
|
180
|
+
token
|
|
181
|
+
) => {
|
|
182
|
+
const headers = {
|
|
183
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
184
|
+
};
|
|
168
185
|
let effectiveBranch = branch;
|
|
169
186
|
if (!effectiveBranch) {
|
|
170
187
|
// If no branch is specified, fetch the default branch for the repository
|
|
@@ -174,7 +191,7 @@ const downloadFile = async (owner, repo, branch, filePath, outputPath) => {
|
|
|
174
191
|
const repoInfoUrl = `https://api.github.com/repos/${encodeURIComponent(
|
|
175
192
|
owner
|
|
176
193
|
)}/${encodeURIComponent(repo)}`;
|
|
177
|
-
const repoInfoResponse = await axios.get(repoInfoUrl);
|
|
194
|
+
const repoInfoResponse = await axios.get(repoInfoUrl, { headers });
|
|
178
195
|
effectiveBranch = repoInfoResponse.data.default_branch;
|
|
179
196
|
if (!effectiveBranch) {
|
|
180
197
|
// console.error(chalk.red(`Could not determine default branch for ${owner}/${repo} for file ${filePath}.`));
|
|
@@ -213,7 +230,10 @@ const downloadFile = async (owner, repo, branch, filePath, outputPath) => {
|
|
|
213
230
|
const url = `${baseUrl}${fileUrlPath}`;
|
|
214
231
|
|
|
215
232
|
try {
|
|
216
|
-
const response = await axios.get(url, {
|
|
233
|
+
const response = await axios.get(url, {
|
|
234
|
+
responseType: "arraybuffer",
|
|
235
|
+
headers,
|
|
236
|
+
});
|
|
217
237
|
|
|
218
238
|
// Ensure the directory exists
|
|
219
239
|
try {
|
|
@@ -338,18 +358,28 @@ const createProgressRenderer = (owner, repo, folderPath) => {
|
|
|
338
358
|
* @param {string} repoInfo.branch - Branch name
|
|
339
359
|
* @param {string} repoInfo.folderPath - Path to the folder
|
|
340
360
|
* @param {string} outputDir - Directory where files should be saved
|
|
361
|
+
* @param {Object} [options] - Download options
|
|
362
|
+
* @param {string} [options.token] - GitHub Personal Access Token
|
|
341
363
|
* @returns {Promise<void>} - Promise that resolves when all files are downloaded
|
|
342
364
|
*/
|
|
343
365
|
const downloadFolder = async (
|
|
344
366
|
{ owner, repo, branch, folderPath },
|
|
345
|
-
outputDir
|
|
367
|
+
outputDir,
|
|
368
|
+
options = {}
|
|
346
369
|
) => {
|
|
370
|
+
const { token } = options;
|
|
347
371
|
console.log(
|
|
348
372
|
chalk.cyan(`Analyzing repository structure for ${owner}/${repo}...`)
|
|
349
373
|
);
|
|
350
374
|
|
|
351
375
|
try {
|
|
352
|
-
const contents = await fetchFolderContents(
|
|
376
|
+
const contents = await fetchFolderContents(
|
|
377
|
+
owner,
|
|
378
|
+
repo,
|
|
379
|
+
branch,
|
|
380
|
+
folderPath,
|
|
381
|
+
token
|
|
382
|
+
);
|
|
353
383
|
|
|
354
384
|
if (!contents || contents.length === 0) {
|
|
355
385
|
const message = `No files found in ${folderPath || "repository root"}`;
|
|
@@ -428,7 +458,8 @@ const downloadFolder = async (
|
|
|
428
458
|
repo,
|
|
429
459
|
branch,
|
|
430
460
|
item.path,
|
|
431
|
-
outputFilePath
|
|
461
|
+
outputFilePath,
|
|
462
|
+
token
|
|
432
463
|
);
|
|
433
464
|
|
|
434
465
|
// Update progress metrics
|
|
@@ -537,7 +568,7 @@ const downloadFolder = async (
|
|
|
537
568
|
};
|
|
538
569
|
|
|
539
570
|
// Export functions in ESM format
|
|
540
|
-
export { downloadFolder, downloadFolderWithResume };
|
|
571
|
+
export { downloadFolder, downloadFolderWithResume, downloadFile };
|
|
541
572
|
|
|
542
573
|
/**
|
|
543
574
|
* Downloads all files from a folder in a GitHub repository with resume capability
|
|
@@ -547,10 +578,14 @@ const downloadFolderWithResume = async (
|
|
|
547
578
|
outputDir,
|
|
548
579
|
options = { resume: true, forceRestart: false }
|
|
549
580
|
) => {
|
|
550
|
-
const { resume = true, forceRestart = false } = options;
|
|
581
|
+
const { resume = true, forceRestart = false, token } = options;
|
|
551
582
|
|
|
552
583
|
if (!resume) {
|
|
553
|
-
return downloadFolder(
|
|
584
|
+
return downloadFolder(
|
|
585
|
+
{ owner, repo, branch, folderPath },
|
|
586
|
+
outputDir,
|
|
587
|
+
options
|
|
588
|
+
);
|
|
554
589
|
}
|
|
555
590
|
|
|
556
591
|
const resumeManager = new ResumeManager();
|
|
@@ -622,7 +657,13 @@ const downloadFolderWithResume = async (
|
|
|
622
657
|
);
|
|
623
658
|
|
|
624
659
|
try {
|
|
625
|
-
const contents = await fetchFolderContents(
|
|
660
|
+
const contents = await fetchFolderContents(
|
|
661
|
+
owner,
|
|
662
|
+
repo,
|
|
663
|
+
branch,
|
|
664
|
+
folderPath,
|
|
665
|
+
token
|
|
666
|
+
);
|
|
626
667
|
if (!contents || contents.length === 0) {
|
|
627
668
|
const message = `No files found in ${folderPath || "repository root"}`;
|
|
628
669
|
console.log(chalk.yellow(message));
|
|
@@ -741,7 +782,8 @@ const downloadFolderWithResume = async (
|
|
|
741
782
|
repo,
|
|
742
783
|
branch,
|
|
743
784
|
item.path,
|
|
744
|
-
outputFilePath
|
|
785
|
+
outputFilePath,
|
|
786
|
+
token
|
|
745
787
|
);
|
|
746
788
|
|
|
747
789
|
if (result.success) {
|
package/src/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { program } from "commander";
|
|
2
2
|
import { parseGitHubUrl } from "./parser.js";
|
|
3
|
-
import { downloadFolder, downloadFolderWithResume } from "./downloader.js";
|
|
3
|
+
import { downloadFolder, downloadFolderWithResume, downloadFile } from "./downloader.js";
|
|
4
4
|
import { downloadAndArchive } from "./archiver.js";
|
|
5
5
|
import { ResumeManager } from "./resumeManager.js";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { dirname, join, resolve, basename } from "node:path";
|
|
8
8
|
import fs from "node:fs";
|
|
9
9
|
import process from "node:process";
|
|
10
10
|
import chalk from "chalk";
|
|
@@ -61,6 +61,7 @@ const initializeCLI = () => {
|
|
|
61
61
|
.description("Clone specific folders from GitHub repositories")
|
|
62
62
|
.argument("[url]", "GitHub URL of the folder to clone")
|
|
63
63
|
.option("-o, --output <directory>", "Output directory", process.cwd())
|
|
64
|
+
.option("--gh-token <token>", "GitHub Personal Access Token for private repositories")
|
|
64
65
|
.option("--zip [filename]", "Create ZIP archive of downloaded files")
|
|
65
66
|
.option("--no-resume", "Disable resume functionality")
|
|
66
67
|
.option("--force-restart", "Ignore existing checkpoints and start fresh")
|
|
@@ -122,6 +123,7 @@ const initializeCLI = () => {
|
|
|
122
123
|
const downloadOptions = {
|
|
123
124
|
resume: options.resume !== false, // Default to true unless --no-resume
|
|
124
125
|
forceRestart: options.forceRestart || false,
|
|
126
|
+
token: options.ghToken,
|
|
125
127
|
};
|
|
126
128
|
|
|
127
129
|
let operationType = createArchive ? "archive" : "download";
|
|
@@ -131,7 +133,19 @@ const initializeCLI = () => {
|
|
|
131
133
|
try {
|
|
132
134
|
if (createArchive) {
|
|
133
135
|
console.log(`Creating ZIP archive...`);
|
|
134
|
-
await downloadAndArchive(parsedUrl, options.output, archiveName);
|
|
136
|
+
await downloadAndArchive(parsedUrl, options.output, archiveName, downloadOptions);
|
|
137
|
+
} else if (parsedUrl.type === "blob") {
|
|
138
|
+
console.log(`Downloading file to: ${options.output}`);
|
|
139
|
+
const fileName = basename(parsedUrl.folderPath);
|
|
140
|
+
const outputPath = join(options.output, fileName);
|
|
141
|
+
result = await downloadFile(
|
|
142
|
+
parsedUrl.owner,
|
|
143
|
+
parsedUrl.repo,
|
|
144
|
+
parsedUrl.branch,
|
|
145
|
+
parsedUrl.folderPath,
|
|
146
|
+
outputPath,
|
|
147
|
+
options.ghToken
|
|
148
|
+
);
|
|
135
149
|
} else {
|
|
136
150
|
console.log(`Downloading folder to: ${options.output}`);
|
|
137
151
|
if (downloadOptions.resume) {
|
|
@@ -141,7 +155,7 @@ const initializeCLI = () => {
|
|
|
141
155
|
downloadOptions
|
|
142
156
|
);
|
|
143
157
|
} else {
|
|
144
|
-
result = await downloadFolder(parsedUrl, options.output);
|
|
158
|
+
result = await downloadFolder(parsedUrl, options.output, downloadOptions);
|
|
145
159
|
}
|
|
146
160
|
}
|
|
147
161
|
} catch (opError) {
|
package/src/parser.js
CHANGED
|
@@ -6,7 +6,7 @@ export function parseGitHubUrl(url) {
|
|
|
6
6
|
|
|
7
7
|
// Validate if it's a GitHub URL
|
|
8
8
|
const githubUrlPattern =
|
|
9
|
-
/^https?:\/\/(?:www\.)?github\.com\/([^\/]+)\/([^\/]+)(?:\/(
|
|
9
|
+
/^https?:\/\/(?:www\.)?github\.com\/([^\/]+)\/([^\/]+)(?:\/(tree|blob)\/([^\/]+)(?:\/(.+))?)?$/;
|
|
10
10
|
const match = url.match(githubUrlPattern);
|
|
11
11
|
|
|
12
12
|
if (!match) {
|
|
@@ -17,14 +17,21 @@ export function parseGitHubUrl(url) {
|
|
|
17
17
|
|
|
18
18
|
// Extract components from the matched pattern
|
|
19
19
|
const owner = decodeURIComponent(match[1]);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
let repo = decodeURIComponent(match[2]);
|
|
21
|
+
|
|
22
|
+
// Remove .git suffix if present
|
|
23
|
+
if (repo.endsWith(".git")) {
|
|
24
|
+
repo = repo.slice(0, -4);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const type = match[3] || "tree"; // Default to tree if not present (root of repo)
|
|
28
|
+
const branch = match[4] ? decodeURIComponent(match[4]) : ""; // Branch is an empty string if not present
|
|
29
|
+
const folderPath = match[5] ? decodeURIComponent(match[5]) : ""; // Empty string if no folder path
|
|
23
30
|
|
|
24
31
|
// Additional validation
|
|
25
32
|
if (!owner || !repo) {
|
|
26
33
|
throw new Error("Invalid GitHub URL: Missing repository owner or name");
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
return { owner, repo, branch, folderPath };
|
|
36
|
+
return { owner, repo, branch, folderPath, type };
|
|
30
37
|
}
|