git-ripper 1.4.13 → 1.5.0
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 +42 -1
- package/package.json +1 -1
- package/src/archiver.js +4 -2
- package/src/downloader.js +30 -14
- 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
|
|
@@ -207,7 +248,7 @@ The resume functionality uses checkpoint files stored in `.git_ripper_checkpoint
|
|
|
207
248
|
|
|
208
249
|
## Configuration
|
|
209
250
|
|
|
210
|
-
Git-ripper works out of the box without configuration. For rate-limited GitHub API usage,
|
|
251
|
+
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
252
|
|
|
212
253
|
## Troubleshooting
|
|
213
254
|
|
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) {
|
|
@@ -162,9 +168,13 @@ 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 (owner, repo, branch, filePath, outputPath) => {
|
|
174
|
+
const downloadFile = async (owner, repo, branch, filePath, outputPath, token) => {
|
|
175
|
+
const headers = {
|
|
176
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
177
|
+
};
|
|
168
178
|
let effectiveBranch = branch;
|
|
169
179
|
if (!effectiveBranch) {
|
|
170
180
|
// If no branch is specified, fetch the default branch for the repository
|
|
@@ -174,7 +184,7 @@ const downloadFile = async (owner, repo, branch, filePath, outputPath) => {
|
|
|
174
184
|
const repoInfoUrl = `https://api.github.com/repos/${encodeURIComponent(
|
|
175
185
|
owner
|
|
176
186
|
)}/${encodeURIComponent(repo)}`;
|
|
177
|
-
const repoInfoResponse = await axios.get(repoInfoUrl);
|
|
187
|
+
const repoInfoResponse = await axios.get(repoInfoUrl, { headers });
|
|
178
188
|
effectiveBranch = repoInfoResponse.data.default_branch;
|
|
179
189
|
if (!effectiveBranch) {
|
|
180
190
|
// console.error(chalk.red(`Could not determine default branch for ${owner}/${repo} for file ${filePath}.`));
|
|
@@ -213,7 +223,7 @@ const downloadFile = async (owner, repo, branch, filePath, outputPath) => {
|
|
|
213
223
|
const url = `${baseUrl}${fileUrlPath}`;
|
|
214
224
|
|
|
215
225
|
try {
|
|
216
|
-
const response = await axios.get(url, { responseType: "arraybuffer" });
|
|
226
|
+
const response = await axios.get(url, { responseType: "arraybuffer", headers });
|
|
217
227
|
|
|
218
228
|
// Ensure the directory exists
|
|
219
229
|
try {
|
|
@@ -338,18 +348,22 @@ const createProgressRenderer = (owner, repo, folderPath) => {
|
|
|
338
348
|
* @param {string} repoInfo.branch - Branch name
|
|
339
349
|
* @param {string} repoInfo.folderPath - Path to the folder
|
|
340
350
|
* @param {string} outputDir - Directory where files should be saved
|
|
351
|
+
* @param {Object} [options] - Download options
|
|
352
|
+
* @param {string} [options.token] - GitHub Personal Access Token
|
|
341
353
|
* @returns {Promise<void>} - Promise that resolves when all files are downloaded
|
|
342
354
|
*/
|
|
343
355
|
const downloadFolder = async (
|
|
344
356
|
{ owner, repo, branch, folderPath },
|
|
345
|
-
outputDir
|
|
357
|
+
outputDir,
|
|
358
|
+
options = {}
|
|
346
359
|
) => {
|
|
360
|
+
const { token } = options;
|
|
347
361
|
console.log(
|
|
348
362
|
chalk.cyan(`Analyzing repository structure for ${owner}/${repo}...`)
|
|
349
363
|
);
|
|
350
364
|
|
|
351
365
|
try {
|
|
352
|
-
const contents = await fetchFolderContents(owner, repo, branch, folderPath);
|
|
366
|
+
const contents = await fetchFolderContents(owner, repo, branch, folderPath, token);
|
|
353
367
|
|
|
354
368
|
if (!contents || contents.length === 0) {
|
|
355
369
|
const message = `No files found in ${folderPath || "repository root"}`;
|
|
@@ -428,7 +442,8 @@ const downloadFolder = async (
|
|
|
428
442
|
repo,
|
|
429
443
|
branch,
|
|
430
444
|
item.path,
|
|
431
|
-
outputFilePath
|
|
445
|
+
outputFilePath,
|
|
446
|
+
token
|
|
432
447
|
);
|
|
433
448
|
|
|
434
449
|
// Update progress metrics
|
|
@@ -537,7 +552,7 @@ const downloadFolder = async (
|
|
|
537
552
|
};
|
|
538
553
|
|
|
539
554
|
// Export functions in ESM format
|
|
540
|
-
export { downloadFolder, downloadFolderWithResume };
|
|
555
|
+
export { downloadFolder, downloadFolderWithResume, downloadFile };
|
|
541
556
|
|
|
542
557
|
/**
|
|
543
558
|
* Downloads all files from a folder in a GitHub repository with resume capability
|
|
@@ -547,10 +562,10 @@ const downloadFolderWithResume = async (
|
|
|
547
562
|
outputDir,
|
|
548
563
|
options = { resume: true, forceRestart: false }
|
|
549
564
|
) => {
|
|
550
|
-
const { resume = true, forceRestart = false } = options;
|
|
565
|
+
const { resume = true, forceRestart = false, token } = options;
|
|
551
566
|
|
|
552
567
|
if (!resume) {
|
|
553
|
-
return downloadFolder({ owner, repo, branch, folderPath }, outputDir);
|
|
568
|
+
return downloadFolder({ owner, repo, branch, folderPath }, outputDir, options);
|
|
554
569
|
}
|
|
555
570
|
|
|
556
571
|
const resumeManager = new ResumeManager();
|
|
@@ -622,7 +637,7 @@ const downloadFolderWithResume = async (
|
|
|
622
637
|
);
|
|
623
638
|
|
|
624
639
|
try {
|
|
625
|
-
const contents = await fetchFolderContents(owner, repo, branch, folderPath);
|
|
640
|
+
const contents = await fetchFolderContents(owner, repo, branch, folderPath, token);
|
|
626
641
|
if (!contents || contents.length === 0) {
|
|
627
642
|
const message = `No files found in ${folderPath || "repository root"}`;
|
|
628
643
|
console.log(chalk.yellow(message));
|
|
@@ -741,7 +756,8 @@ const downloadFolderWithResume = async (
|
|
|
741
756
|
repo,
|
|
742
757
|
branch,
|
|
743
758
|
item.path,
|
|
744
|
-
outputFilePath
|
|
759
|
+
outputFilePath,
|
|
760
|
+
token
|
|
745
761
|
);
|
|
746
762
|
|
|
747
763
|
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
|
}
|