git-ripper 1.5.1 → 1.5.2
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/LICENSE +20 -20
- package/README.md +340 -340
- package/bin/git-ripper.js +3 -3
- package/package.json +62 -62
- package/src/archiver.js +210 -210
- package/src/downloader.js +904 -904
- package/src/index.js +195 -195
- package/src/parser.js +37 -37
- package/src/resumeManager.js +213 -213
package/src/index.js
CHANGED
|
@@ -1,195 +1,195 @@
|
|
|
1
|
-
import { program } from "commander";
|
|
2
|
-
import { parseGitHubUrl } from "./parser.js";
|
|
3
|
-
import { downloadFolder, downloadFolderWithResume, downloadFile } from "./downloader.js";
|
|
4
|
-
import { downloadAndArchive } from "./archiver.js";
|
|
5
|
-
import { ResumeManager } from "./resumeManager.js";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { dirname, join, resolve, basename } from "node:path";
|
|
8
|
-
import fs from "node:fs";
|
|
9
|
-
import process from "node:process";
|
|
10
|
-
import chalk from "chalk";
|
|
11
|
-
|
|
12
|
-
// Get package.json for version
|
|
13
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
-
const __dirname = dirname(__filename);
|
|
15
|
-
const packagePath = join(__dirname, "..", "package.json");
|
|
16
|
-
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Validates and ensures the output directory exists
|
|
20
|
-
* @param {string} outputDir - The directory path to validate
|
|
21
|
-
* @returns {string} - The resolved directory path
|
|
22
|
-
* @throws {Error} - If the directory is invalid or cannot be created
|
|
23
|
-
*/
|
|
24
|
-
const validateOutputDirectory = (outputDir) => {
|
|
25
|
-
if (!outputDir) {
|
|
26
|
-
throw new Error("Output directory is required");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Resolve to absolute path
|
|
30
|
-
const resolvedDir = resolve(outputDir);
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
// Check if directory exists, if not try to create it
|
|
34
|
-
if (!fs.existsSync(resolvedDir)) {
|
|
35
|
-
fs.mkdirSync(resolvedDir, { recursive: true });
|
|
36
|
-
} else {
|
|
37
|
-
// Check if it's actually a directory
|
|
38
|
-
const stats = fs.statSync(resolvedDir);
|
|
39
|
-
if (!stats.isDirectory()) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
`Output path exists but is not a directory: ${outputDir}`
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Check if the directory is writable
|
|
47
|
-
fs.accessSync(resolvedDir, fs.constants.W_OK);
|
|
48
|
-
|
|
49
|
-
return resolvedDir;
|
|
50
|
-
} catch (error) {
|
|
51
|
-
if (error.code === "EACCES") {
|
|
52
|
-
throw new Error(`Permission denied: Cannot write to ${outputDir}`);
|
|
53
|
-
}
|
|
54
|
-
throw new Error(`Invalid output directory: ${error.message}`);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const initializeCLI = () => {
|
|
59
|
-
program
|
|
60
|
-
.version(packageJson.version)
|
|
61
|
-
.description("Clone specific folders from GitHub repositories")
|
|
62
|
-
.argument("[url]", "GitHub URL of the folder to clone")
|
|
63
|
-
.option("-o, --output <directory>", "Output directory", process.cwd())
|
|
64
|
-
.option("--gh-token <token>", "GitHub Personal Access Token for private repositories")
|
|
65
|
-
.option("--zip [filename]", "Create ZIP archive of downloaded files")
|
|
66
|
-
.option("--no-resume", "Disable resume functionality")
|
|
67
|
-
.option("--force-restart", "Ignore existing checkpoints and start fresh")
|
|
68
|
-
.option("--list-checkpoints", "List all existing download checkpoints")
|
|
69
|
-
.action(async (url, options) => {
|
|
70
|
-
try {
|
|
71
|
-
// Handle list checkpoints option
|
|
72
|
-
if (options.listCheckpoints) {
|
|
73
|
-
const resumeManager = new ResumeManager();
|
|
74
|
-
const checkpoints = resumeManager.listCheckpoints();
|
|
75
|
-
|
|
76
|
-
if (checkpoints.length === 0) {
|
|
77
|
-
console.log(chalk.yellow("No download checkpoints found."));
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
console.log(chalk.cyan("\nDownload Checkpoints:"));
|
|
82
|
-
checkpoints.forEach((cp, index) => {
|
|
83
|
-
console.log(chalk.blue(`\n${index + 1}. ID: ${cp.id}`));
|
|
84
|
-
console.log(` URL: ${cp.url}`);
|
|
85
|
-
console.log(` Output: ${cp.outputDir}`);
|
|
86
|
-
console.log(` Progress: ${cp.progress}`);
|
|
87
|
-
console.log(
|
|
88
|
-
` Last Updated: ${new Date(cp.timestamp).toLocaleString()}`
|
|
89
|
-
);
|
|
90
|
-
if (cp.failedFiles > 0) {
|
|
91
|
-
console.log(chalk.yellow(` Failed Files: ${cp.failedFiles}`));
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
console.log();
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// URL is required for download operations
|
|
99
|
-
if (!url) {
|
|
100
|
-
console.error(
|
|
101
|
-
chalk.red("Error: URL is required for download operations")
|
|
102
|
-
);
|
|
103
|
-
console.log("Use --list-checkpoints to see existing downloads");
|
|
104
|
-
process.exit(1);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
console.log(`Parsing URL: ${url}`);
|
|
108
|
-
const parsedUrl = parseGitHubUrl(url);
|
|
109
|
-
|
|
110
|
-
// Validate output directory
|
|
111
|
-
try {
|
|
112
|
-
options.output = validateOutputDirectory(options.output);
|
|
113
|
-
} catch (dirError) {
|
|
114
|
-
throw new Error(`Output directory error: ${dirError.message}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Handle archive option
|
|
118
|
-
const createArchive = options.zip !== undefined;
|
|
119
|
-
const archiveName =
|
|
120
|
-
typeof options.zip === "string" ? options.zip : null;
|
|
121
|
-
|
|
122
|
-
// Prepare download options
|
|
123
|
-
const downloadOptions = {
|
|
124
|
-
resume: options.resume !== false, // Default to true unless --no-resume
|
|
125
|
-
forceRestart: options.forceRestart || false,
|
|
126
|
-
token: options.ghToken,
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
let operationType = createArchive ? "archive" : "download";
|
|
130
|
-
let result = null;
|
|
131
|
-
let error = null;
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
if (createArchive) {
|
|
135
|
-
console.log(`Creating ZIP archive...`);
|
|
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
|
-
);
|
|
149
|
-
} else {
|
|
150
|
-
console.log(`Downloading folder to: ${options.output}`);
|
|
151
|
-
if (downloadOptions.resume) {
|
|
152
|
-
result = await downloadFolderWithResume(
|
|
153
|
-
parsedUrl,
|
|
154
|
-
options.output,
|
|
155
|
-
downloadOptions
|
|
156
|
-
);
|
|
157
|
-
} else {
|
|
158
|
-
result = await downloadFolder(parsedUrl, options.output, downloadOptions);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
} catch (opError) {
|
|
162
|
-
error = opError;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Consolidated result and error handling
|
|
166
|
-
if (error) {
|
|
167
|
-
const failMsg =
|
|
168
|
-
operationType === "archive"
|
|
169
|
-
? `Archive creation failed: ${error.message}`
|
|
170
|
-
: `Download failed: ${error.message}`;
|
|
171
|
-
console.error(chalk.red(failMsg));
|
|
172
|
-
process.exit(1);
|
|
173
|
-
} else if (!createArchive && result && !result.success) {
|
|
174
|
-
console.error(chalk.red(`Download failed`));
|
|
175
|
-
process.exit(1);
|
|
176
|
-
} else if (!createArchive && result && result.isEmpty) {
|
|
177
|
-
console.log("Operation completed - no files to download!");
|
|
178
|
-
} else {
|
|
179
|
-
console.log("Operation completed successfully!");
|
|
180
|
-
}
|
|
181
|
-
} catch (error) {
|
|
182
|
-
console.error("Error:", error.message);
|
|
183
|
-
process.exit(1);
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
program.parse(process.argv);
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
// Ensure function is executed when run directly
|
|
191
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
192
|
-
initializeCLI();
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export { initializeCLI, downloadFolder };
|
|
1
|
+
import { program } from "commander";
|
|
2
|
+
import { parseGitHubUrl } from "./parser.js";
|
|
3
|
+
import { downloadFolder, downloadFolderWithResume, downloadFile } from "./downloader.js";
|
|
4
|
+
import { downloadAndArchive } from "./archiver.js";
|
|
5
|
+
import { ResumeManager } from "./resumeManager.js";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { dirname, join, resolve, basename } from "node:path";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import process from "node:process";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
|
|
12
|
+
// Get package.json for version
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
const packagePath = join(__dirname, "..", "package.json");
|
|
16
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validates and ensures the output directory exists
|
|
20
|
+
* @param {string} outputDir - The directory path to validate
|
|
21
|
+
* @returns {string} - The resolved directory path
|
|
22
|
+
* @throws {Error} - If the directory is invalid or cannot be created
|
|
23
|
+
*/
|
|
24
|
+
const validateOutputDirectory = (outputDir) => {
|
|
25
|
+
if (!outputDir) {
|
|
26
|
+
throw new Error("Output directory is required");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Resolve to absolute path
|
|
30
|
+
const resolvedDir = resolve(outputDir);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Check if directory exists, if not try to create it
|
|
34
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
35
|
+
fs.mkdirSync(resolvedDir, { recursive: true });
|
|
36
|
+
} else {
|
|
37
|
+
// Check if it's actually a directory
|
|
38
|
+
const stats = fs.statSync(resolvedDir);
|
|
39
|
+
if (!stats.isDirectory()) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Output path exists but is not a directory: ${outputDir}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check if the directory is writable
|
|
47
|
+
fs.accessSync(resolvedDir, fs.constants.W_OK);
|
|
48
|
+
|
|
49
|
+
return resolvedDir;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error.code === "EACCES") {
|
|
52
|
+
throw new Error(`Permission denied: Cannot write to ${outputDir}`);
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`Invalid output directory: ${error.message}`);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const initializeCLI = () => {
|
|
59
|
+
program
|
|
60
|
+
.version(packageJson.version)
|
|
61
|
+
.description("Clone specific folders from GitHub repositories")
|
|
62
|
+
.argument("[url]", "GitHub URL of the folder to clone")
|
|
63
|
+
.option("-o, --output <directory>", "Output directory", process.cwd())
|
|
64
|
+
.option("--gh-token <token>", "GitHub Personal Access Token for private repositories")
|
|
65
|
+
.option("--zip [filename]", "Create ZIP archive of downloaded files")
|
|
66
|
+
.option("--no-resume", "Disable resume functionality")
|
|
67
|
+
.option("--force-restart", "Ignore existing checkpoints and start fresh")
|
|
68
|
+
.option("--list-checkpoints", "List all existing download checkpoints")
|
|
69
|
+
.action(async (url, options) => {
|
|
70
|
+
try {
|
|
71
|
+
// Handle list checkpoints option
|
|
72
|
+
if (options.listCheckpoints) {
|
|
73
|
+
const resumeManager = new ResumeManager();
|
|
74
|
+
const checkpoints = resumeManager.listCheckpoints();
|
|
75
|
+
|
|
76
|
+
if (checkpoints.length === 0) {
|
|
77
|
+
console.log(chalk.yellow("No download checkpoints found."));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(chalk.cyan("\nDownload Checkpoints:"));
|
|
82
|
+
checkpoints.forEach((cp, index) => {
|
|
83
|
+
console.log(chalk.blue(`\n${index + 1}. ID: ${cp.id}`));
|
|
84
|
+
console.log(` URL: ${cp.url}`);
|
|
85
|
+
console.log(` Output: ${cp.outputDir}`);
|
|
86
|
+
console.log(` Progress: ${cp.progress}`);
|
|
87
|
+
console.log(
|
|
88
|
+
` Last Updated: ${new Date(cp.timestamp).toLocaleString()}`
|
|
89
|
+
);
|
|
90
|
+
if (cp.failedFiles > 0) {
|
|
91
|
+
console.log(chalk.yellow(` Failed Files: ${cp.failedFiles}`));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
console.log();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// URL is required for download operations
|
|
99
|
+
if (!url) {
|
|
100
|
+
console.error(
|
|
101
|
+
chalk.red("Error: URL is required for download operations")
|
|
102
|
+
);
|
|
103
|
+
console.log("Use --list-checkpoints to see existing downloads");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log(`Parsing URL: ${url}`);
|
|
108
|
+
const parsedUrl = parseGitHubUrl(url);
|
|
109
|
+
|
|
110
|
+
// Validate output directory
|
|
111
|
+
try {
|
|
112
|
+
options.output = validateOutputDirectory(options.output);
|
|
113
|
+
} catch (dirError) {
|
|
114
|
+
throw new Error(`Output directory error: ${dirError.message}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Handle archive option
|
|
118
|
+
const createArchive = options.zip !== undefined;
|
|
119
|
+
const archiveName =
|
|
120
|
+
typeof options.zip === "string" ? options.zip : null;
|
|
121
|
+
|
|
122
|
+
// Prepare download options
|
|
123
|
+
const downloadOptions = {
|
|
124
|
+
resume: options.resume !== false, // Default to true unless --no-resume
|
|
125
|
+
forceRestart: options.forceRestart || false,
|
|
126
|
+
token: options.ghToken,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
let operationType = createArchive ? "archive" : "download";
|
|
130
|
+
let result = null;
|
|
131
|
+
let error = null;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
if (createArchive) {
|
|
135
|
+
console.log(`Creating ZIP archive...`);
|
|
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
|
+
);
|
|
149
|
+
} else {
|
|
150
|
+
console.log(`Downloading folder to: ${options.output}`);
|
|
151
|
+
if (downloadOptions.resume) {
|
|
152
|
+
result = await downloadFolderWithResume(
|
|
153
|
+
parsedUrl,
|
|
154
|
+
options.output,
|
|
155
|
+
downloadOptions
|
|
156
|
+
);
|
|
157
|
+
} else {
|
|
158
|
+
result = await downloadFolder(parsedUrl, options.output, downloadOptions);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch (opError) {
|
|
162
|
+
error = opError;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Consolidated result and error handling
|
|
166
|
+
if (error) {
|
|
167
|
+
const failMsg =
|
|
168
|
+
operationType === "archive"
|
|
169
|
+
? `Archive creation failed: ${error.message}`
|
|
170
|
+
: `Download failed: ${error.message}`;
|
|
171
|
+
console.error(chalk.red(failMsg));
|
|
172
|
+
process.exit(1);
|
|
173
|
+
} else if (!createArchive && result && !result.success) {
|
|
174
|
+
console.error(chalk.red(`Download failed`));
|
|
175
|
+
process.exit(1);
|
|
176
|
+
} else if (!createArchive && result && result.isEmpty) {
|
|
177
|
+
console.log("Operation completed - no files to download!");
|
|
178
|
+
} else {
|
|
179
|
+
console.log("Operation completed successfully!");
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error("Error:", error.message);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
program.parse(process.argv);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Ensure function is executed when run directly
|
|
191
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
192
|
+
initializeCLI();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export { initializeCLI, downloadFolder };
|
package/src/parser.js
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
export function parseGitHubUrl(url) {
|
|
2
|
-
// Validate the URL format
|
|
3
|
-
if (!url || typeof url !== "string") {
|
|
4
|
-
throw new Error("Invalid URL: URL must be a non-empty string");
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
// Validate if it's a GitHub URL
|
|
8
|
-
const githubUrlPattern =
|
|
9
|
-
/^https?:\/\/(?:www\.)?github\.com\/([^\/]+)\/([^\/]+)(?:\/(tree|blob)\/([^\/]+)(?:\/(.+))?)?$/;
|
|
10
|
-
const match = url.match(githubUrlPattern);
|
|
11
|
-
|
|
12
|
-
if (!match) {
|
|
13
|
-
throw new Error(
|
|
14
|
-
"Invalid GitHub URL format. Expected: https://github.com/owner/repo[/tree|/blob]/branch/folder_or_file"
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Extract components from the matched pattern
|
|
19
|
-
const owner = decodeURIComponent(match[1]);
|
|
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
|
|
30
|
-
|
|
31
|
-
// Additional validation
|
|
32
|
-
if (!owner || !repo) {
|
|
33
|
-
throw new Error("Invalid GitHub URL: Missing repository owner or name");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return { owner, repo, branch, folderPath, type };
|
|
37
|
-
}
|
|
1
|
+
export function parseGitHubUrl(url) {
|
|
2
|
+
// Validate the URL format
|
|
3
|
+
if (!url || typeof url !== "string") {
|
|
4
|
+
throw new Error("Invalid URL: URL must be a non-empty string");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Validate if it's a GitHub URL
|
|
8
|
+
const githubUrlPattern =
|
|
9
|
+
/^https?:\/\/(?:www\.)?github\.com\/([^\/]+)\/([^\/]+)(?:\/(tree|blob)\/([^\/]+)(?:\/(.+))?)?$/;
|
|
10
|
+
const match = url.match(githubUrlPattern);
|
|
11
|
+
|
|
12
|
+
if (!match) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"Invalid GitHub URL format. Expected: https://github.com/owner/repo[/tree|/blob]/branch/folder_or_file"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Extract components from the matched pattern
|
|
19
|
+
const owner = decodeURIComponent(match[1]);
|
|
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
|
|
30
|
+
|
|
31
|
+
// Additional validation
|
|
32
|
+
if (!owner || !repo) {
|
|
33
|
+
throw new Error("Invalid GitHub URL: Missing repository owner or name");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { owner, repo, branch, folderPath, type };
|
|
37
|
+
}
|