acker-dacker 0.1.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 +79 -0
- package/index.ts +10 -0
- package/package.json +35 -0
- package/src/cli-options.ts +93 -0
- package/src/cli.ts +115 -0
- package/src/file-policy.ts +121 -0
- package/src/git-handoff-selection.ts +255 -0
- package/src/git-handoff.ts +294 -0
- package/src/git.ts +84 -0
- package/src/gitignore.ts +139 -0
- package/src/path-patterns.ts +79 -0
- package/src/repository-pack.ts +276 -0
- package/src/zip.ts +327 -0
package/src/gitignore.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { globPatternToRegexSource, isPathWithin, normalizeArchivePath } from './path-patterns';
|
|
3
|
+
|
|
4
|
+
type GitIgnoreRule = {
|
|
5
|
+
readonly directoryOnly: boolean;
|
|
6
|
+
readonly matcher: RegExp;
|
|
7
|
+
readonly negated: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const parseGitIgnoreRule = (line: string): GitIgnoreRule | null => {
|
|
11
|
+
let pattern = line.replace(/[ \t]+$/u, '');
|
|
12
|
+
if (pattern.length === 0 || pattern.startsWith('#')) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const escapedLeadingMarker = pattern.startsWith('\\#') || pattern.startsWith('\\!');
|
|
17
|
+
if (escapedLeadingMarker) {
|
|
18
|
+
pattern = pattern.slice(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let negated = false;
|
|
22
|
+
if (!escapedLeadingMarker && pattern.startsWith('!')) {
|
|
23
|
+
negated = true;
|
|
24
|
+
pattern = pattern.slice(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const directoryOnly = pattern.endsWith('/') && !pattern.endsWith('\\/');
|
|
28
|
+
if (directoryOnly) {
|
|
29
|
+
pattern = pattern.slice(0, -1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const anchored = pattern.startsWith('/');
|
|
33
|
+
if (anchored) {
|
|
34
|
+
pattern = pattern.slice(1);
|
|
35
|
+
}
|
|
36
|
+
if (pattern.length === 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const hasSlash = pattern.includes('/');
|
|
41
|
+
const source = globPatternToRegexSource(pattern);
|
|
42
|
+
const matcher = new RegExp(anchored || hasSlash ? `^${source}$` : `(?:^|/)${source}(?:$|/)`);
|
|
43
|
+
|
|
44
|
+
return { directoryOnly, matcher, negated };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const parseGitIgnoreRules = (contents: string): readonly GitIgnoreRule[] => {
|
|
48
|
+
const rules: GitIgnoreRule[] = [];
|
|
49
|
+
for (const line of contents.split(/\r?\n/u)) {
|
|
50
|
+
const rule = parseGitIgnoreRule(line);
|
|
51
|
+
if (rule) {
|
|
52
|
+
rules.push(rule);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return rules;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const matchesGitIgnoreRule = (rule: GitIgnoreRule, relativePath: string, isDirectory: boolean): boolean => {
|
|
59
|
+
if (!rule.directoryOnly || isDirectory) {
|
|
60
|
+
return rule.matcher.test(relativePath);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const segments = relativePath.split('/');
|
|
64
|
+
for (let end = 1; end < segments.length; end += 1) {
|
|
65
|
+
if (rule.matcher.test(segments.slice(0, end).join('/'))) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export class GitIgnoreMatcher {
|
|
73
|
+
private readonly rulesByDirectory = new Map<string, readonly GitIgnoreRule[]>();
|
|
74
|
+
|
|
75
|
+
public constructor(private readonly rootDir: string) {}
|
|
76
|
+
|
|
77
|
+
private async rulesForDirectory(directory: string): Promise<readonly GitIgnoreRule[]> {
|
|
78
|
+
const cachedRules = this.rulesByDirectory.get(directory);
|
|
79
|
+
if (cachedRules) {
|
|
80
|
+
return cachedRules;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const ignoreFile = Bun.file(path.join(directory, '.gitignore'));
|
|
84
|
+
if (!(await ignoreFile.exists())) {
|
|
85
|
+
this.rulesByDirectory.set(directory, []);
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const rules = parseGitIgnoreRules(await ignoreFile.text());
|
|
91
|
+
this.rulesByDirectory.set(directory, rules);
|
|
92
|
+
return rules;
|
|
93
|
+
} catch {
|
|
94
|
+
this.rulesByDirectory.set(directory, []);
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public async isIgnored(absolutePath: string, isDirectory: boolean): Promise<boolean> {
|
|
100
|
+
const parentDirectory = path.dirname(absolutePath);
|
|
101
|
+
if (!isPathWithin(parentDirectory, this.rootDir)) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const directories: string[] = [];
|
|
106
|
+
let directory = parentDirectory;
|
|
107
|
+
while (true) {
|
|
108
|
+
directories.push(directory);
|
|
109
|
+
if (directory === this.rootDir) {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const parent = path.dirname(directory);
|
|
114
|
+
if (parent === directory || !isPathWithin(parent, this.rootDir)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
directory = parent;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
directories.reverse();
|
|
121
|
+
|
|
122
|
+
let ignored = false;
|
|
123
|
+
for (const ruleDirectory of directories) {
|
|
124
|
+
const relativePath = normalizeArchivePath(path.relative(ruleDirectory, absolutePath));
|
|
125
|
+
if (relativePath.length === 0 || relativePath === '..' || relativePath.startsWith('../')) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const rules = await this.rulesForDirectory(ruleDirectory);
|
|
130
|
+
for (const rule of rules) {
|
|
131
|
+
if (matchesGitIgnoreRule(rule, relativePath, isDirectory)) {
|
|
132
|
+
ignored = !rule.negated;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return ignored;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
export const compareStableStrings = (left: string, right: string): number =>
|
|
4
|
+
left < right ? -1 : left > right ? 1 : 0;
|
|
5
|
+
|
|
6
|
+
export const normalizeArchivePath = (filePath: string): string => filePath.split(path.sep).join('/');
|
|
7
|
+
|
|
8
|
+
const escapeRegexCharacter = (character: string): string => character.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
|
|
9
|
+
|
|
10
|
+
export const globPatternToRegexSource = (pattern: string): string => {
|
|
11
|
+
let source = '';
|
|
12
|
+
|
|
13
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
14
|
+
const character = pattern[index]!;
|
|
15
|
+
|
|
16
|
+
if (character === '\\') {
|
|
17
|
+
if (index + 1 < pattern.length) {
|
|
18
|
+
index += 1;
|
|
19
|
+
source += escapeRegexCharacter(pattern[index]!);
|
|
20
|
+
} else {
|
|
21
|
+
source += '\\\\';
|
|
22
|
+
}
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (character === '*') {
|
|
27
|
+
if (pattern[index + 1] === '*') {
|
|
28
|
+
index += 1;
|
|
29
|
+
if (pattern[index + 1] === '/') {
|
|
30
|
+
index += 1;
|
|
31
|
+
source += '(?:.*/)?';
|
|
32
|
+
} else {
|
|
33
|
+
source += '.*';
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
source += '[^/]*';
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (character === '?') {
|
|
42
|
+
source += '[^/]';
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (character === '[') {
|
|
47
|
+
const closingBracket = pattern.indexOf(']', index + 1);
|
|
48
|
+
if (closingBracket !== -1) {
|
|
49
|
+
const characterClass = pattern.slice(index + 1, closingBracket);
|
|
50
|
+
const classPrefix = characterClass.startsWith('!') ? '^' : '';
|
|
51
|
+
const classBody = characterClass.startsWith('!') ? characterClass.slice(1) : characterClass;
|
|
52
|
+
source += `[${classPrefix}${classBody}]`;
|
|
53
|
+
index = closingBracket;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
source += escapeRegexCharacter(character);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return source;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const globToRegex = (pattern: string): RegExp => {
|
|
65
|
+
const normalized = pattern.trim().replace(/\\/g, '/');
|
|
66
|
+
if (normalized.startsWith('*.')) {
|
|
67
|
+
const extension = normalized.slice(2).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
68
|
+
return new RegExp(`\\.${extension}$`, 'i');
|
|
69
|
+
}
|
|
70
|
+
const regexSource = normalized
|
|
71
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
72
|
+
.replace(/\*\*/g, '.*')
|
|
73
|
+
.replace(/(?<!\.)\*/g, '[^/]*');
|
|
74
|
+
return new RegExp(`(?:^|/)${regexSource}$`, 'i');
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const isPathWithin = (candidate: string, root: string): boolean => {
|
|
78
|
+
return candidate === root || candidate.startsWith(`${root}${path.sep}`);
|
|
79
|
+
};
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type { Dirent } from 'node:fs';
|
|
2
|
+
import { mkdir, readdir, stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { detectXcodeProject, isExcludedDirectory, isExcludedFile } from './file-policy';
|
|
5
|
+
import { GitIgnoreMatcher } from './gitignore';
|
|
6
|
+
import { compareStableStrings, normalizeArchivePath } from './path-patterns';
|
|
7
|
+
|
|
8
|
+
export type RepositoryInfo = {
|
|
9
|
+
readonly archivePrefix: string;
|
|
10
|
+
readonly isXcodeProject: boolean;
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly path: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type WalkItem = {
|
|
16
|
+
readonly relativePath: string;
|
|
17
|
+
readonly sourcePath: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type RepositoryPackStats = {
|
|
21
|
+
readonly fileCount: number;
|
|
22
|
+
readonly totalBytes: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const extractLocalPath = (spec: string): string | null => {
|
|
26
|
+
let cleaned = spec.trim();
|
|
27
|
+
if (cleaned.startsWith('file:')) {
|
|
28
|
+
cleaned = cleaned.slice(5);
|
|
29
|
+
} else if (cleaned.startsWith('link:')) {
|
|
30
|
+
cleaned = cleaned.slice(5);
|
|
31
|
+
} else if (!cleaned.startsWith('./') && !cleaned.startsWith('../') && !cleaned.startsWith('/')) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const hashIndex = cleaned.indexOf('#');
|
|
35
|
+
if (hashIndex !== -1) {
|
|
36
|
+
cleaned = cleaned.slice(0, hashIndex);
|
|
37
|
+
}
|
|
38
|
+
return cleaned.length > 0 ? cleaned : null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const findPackageLocalDependencies = (parsed: Record<string, unknown>): readonly string[] => {
|
|
42
|
+
const localPaths: string[] = [];
|
|
43
|
+
const dependencySections = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
44
|
+
|
|
45
|
+
for (const section of dependencySections) {
|
|
46
|
+
const dependencies = parsed[section];
|
|
47
|
+
if (dependencies && typeof dependencies === 'object' && !Array.isArray(dependencies)) {
|
|
48
|
+
for (const value of Object.values(dependencies)) {
|
|
49
|
+
if (typeof value === 'string') {
|
|
50
|
+
const localPath = extractLocalPath(value);
|
|
51
|
+
if (localPath) {
|
|
52
|
+
localPaths.push(localPath);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const repository = parsed.repository;
|
|
60
|
+
if (typeof repository === 'string') {
|
|
61
|
+
const repositoryPath = extractLocalPath(repository);
|
|
62
|
+
if (repositoryPath) {
|
|
63
|
+
localPaths.push(repositoryPath);
|
|
64
|
+
}
|
|
65
|
+
} else if (repository && typeof repository === 'object' && !Array.isArray(repository)) {
|
|
66
|
+
const url = (repository as { url?: unknown }).url;
|
|
67
|
+
if (typeof url === 'string') {
|
|
68
|
+
const repositoryPath = extractLocalPath(url);
|
|
69
|
+
if (repositoryPath) {
|
|
70
|
+
localPaths.push(repositoryPath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return localPaths;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const resolveIncludePath = async (includeSpec: string, targetDir: string): Promise<string | null> => {
|
|
79
|
+
const rawPath = includeSpec.replace(/^~(?=$|\/)/, process.env.HOME ?? '');
|
|
80
|
+
const parentDirectory = path.dirname(targetDir);
|
|
81
|
+
const candidates = [
|
|
82
|
+
path.resolve(parentDirectory, rawPath),
|
|
83
|
+
path.resolve(targetDir, rawPath),
|
|
84
|
+
path.resolve(process.cwd(), rawPath),
|
|
85
|
+
path.resolve(rawPath),
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
for (const candidate of candidates) {
|
|
89
|
+
const candidateStats = await stat(candidate).catch(() => null);
|
|
90
|
+
if (candidateStats?.isDirectory()) {
|
|
91
|
+
return candidate;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const discoverRecursiveRepositories = async (
|
|
98
|
+
targetDir: string,
|
|
99
|
+
includes: readonly string[],
|
|
100
|
+
): Promise<readonly RepositoryInfo[]> => {
|
|
101
|
+
const canonicalTarget = path.resolve(targetDir);
|
|
102
|
+
const targetStats = await stat(canonicalTarget).catch(() => null);
|
|
103
|
+
if (!targetStats?.isDirectory()) {
|
|
104
|
+
throw new Error(`Target path is not a directory: ${canonicalTarget}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const discoveredRepositories = new Map<string, { isXcodeProject: boolean; name: string }>();
|
|
108
|
+
const queue: string[] = [canonicalTarget];
|
|
109
|
+
|
|
110
|
+
for (const include of includes) {
|
|
111
|
+
const resolvedInclude = await resolveIncludePath(include, canonicalTarget);
|
|
112
|
+
if (resolvedInclude) {
|
|
113
|
+
queue.push(resolvedInclude);
|
|
114
|
+
} else {
|
|
115
|
+
console.warn(`Warning: Included path not found or not a directory: ${include}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const visited = new Set<string>();
|
|
120
|
+
|
|
121
|
+
while (queue.length > 0) {
|
|
122
|
+
const current = queue.shift()!;
|
|
123
|
+
const canonicalPath = path.resolve(current);
|
|
124
|
+
if (visited.has(canonicalPath)) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
visited.add(canonicalPath);
|
|
128
|
+
|
|
129
|
+
const folderStats = await stat(canonicalPath).catch(() => null);
|
|
130
|
+
if (!folderStats?.isDirectory()) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const packageJsonPath = path.join(canonicalPath, 'package.json');
|
|
135
|
+
let repositoryName = path.basename(canonicalPath);
|
|
136
|
+
|
|
137
|
+
if (await Bun.file(packageJsonPath).exists()) {
|
|
138
|
+
try {
|
|
139
|
+
const packageJson = (await Bun.file(packageJsonPath).json()) as Record<string, unknown>;
|
|
140
|
+
if (typeof packageJson.name === 'string' && packageJson.name.trim().length > 0) {
|
|
141
|
+
repositoryName = packageJson.name.trim();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const localDependency of findPackageLocalDependencies(packageJson)) {
|
|
145
|
+
const resolvedDependency = path.resolve(canonicalPath, localDependency);
|
|
146
|
+
if (!visited.has(resolvedDependency)) {
|
|
147
|
+
queue.push(resolvedDependency);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
// Invalid package metadata does not prevent the repository itself from being packed.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
discoveredRepositories.set(canonicalPath, {
|
|
156
|
+
isXcodeProject: await detectXcodeProject(canonicalPath),
|
|
157
|
+
name: repositoryName,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const allPaths = [...discoveredRepositories.keys()];
|
|
162
|
+
let commonParent = path.dirname(canonicalTarget);
|
|
163
|
+
if (allPaths.length > 1) {
|
|
164
|
+
let candidate = commonParent;
|
|
165
|
+
while (candidate !== path.dirname(candidate)) {
|
|
166
|
+
if (allPaths.every((repositoryPath) => repositoryPath === candidate || repositoryPath.startsWith(`${candidate}${path.sep}`))) {
|
|
167
|
+
commonParent = candidate;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
candidate = path.dirname(candidate);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const repositories: RepositoryInfo[] = [];
|
|
175
|
+
for (const [canonicalPath, repository] of discoveredRepositories.entries()) {
|
|
176
|
+
repositories.push({
|
|
177
|
+
archivePrefix: normalizeArchivePath(path.relative(commonParent, canonicalPath)),
|
|
178
|
+
isXcodeProject: repository.isXcodeProject,
|
|
179
|
+
name: repository.name,
|
|
180
|
+
path: canonicalPath,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return repositories.sort((left, right) => compareStableStrings(left.name, right.name));
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const processWalkEntry = async ({
|
|
188
|
+
destinationDirectory,
|
|
189
|
+
entry,
|
|
190
|
+
filterMatchers,
|
|
191
|
+
ignoreMatcher,
|
|
192
|
+
isXcodeProject,
|
|
193
|
+
item,
|
|
194
|
+
queue,
|
|
195
|
+
}: {
|
|
196
|
+
readonly destinationDirectory: string;
|
|
197
|
+
readonly entry: Dirent;
|
|
198
|
+
readonly filterMatchers: readonly RegExp[];
|
|
199
|
+
readonly ignoreMatcher: GitIgnoreMatcher;
|
|
200
|
+
readonly isXcodeProject: boolean;
|
|
201
|
+
readonly item: WalkItem;
|
|
202
|
+
readonly queue: WalkItem[];
|
|
203
|
+
}): Promise<{ bytes: number; isFile: boolean }> => {
|
|
204
|
+
if (entry.isSymbolicLink()) {
|
|
205
|
+
return { bytes: 0, isFile: false };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const relativePath = item.relativePath ? path.join(item.relativePath, entry.name) : entry.name;
|
|
209
|
+
const sourcePath = path.join(item.sourcePath, entry.name);
|
|
210
|
+
|
|
211
|
+
if (entry.isDirectory()) {
|
|
212
|
+
if (!isExcludedDirectory(entry.name, isXcodeProject) && !(await ignoreMatcher.isIgnored(sourcePath, true))) {
|
|
213
|
+
queue.push({ relativePath, sourcePath });
|
|
214
|
+
}
|
|
215
|
+
return { bytes: 0, isFile: false };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!entry.isFile() || isExcludedFile(entry.name, isXcodeProject)) {
|
|
219
|
+
return { bytes: 0, isFile: false };
|
|
220
|
+
}
|
|
221
|
+
if (await ignoreMatcher.isIgnored(sourcePath, false)) {
|
|
222
|
+
return { bytes: 0, isFile: false };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (filterMatchers.length > 0) {
|
|
226
|
+
const normalizedRelativePath = normalizeArchivePath(relativePath);
|
|
227
|
+
const matches = filterMatchers.some(
|
|
228
|
+
(matcher) => matcher.test(normalizedRelativePath) || matcher.test(entry.name),
|
|
229
|
+
);
|
|
230
|
+
if (!matches) {
|
|
231
|
+
return { bytes: 0, isFile: false };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const destinationPath = path.join(destinationDirectory, relativePath);
|
|
236
|
+
await mkdir(path.dirname(destinationPath), { recursive: true });
|
|
237
|
+
const sourceFile = Bun.file(sourcePath);
|
|
238
|
+
await Bun.write(destinationPath, sourceFile);
|
|
239
|
+
|
|
240
|
+
return { bytes: sourceFile.size, isFile: true };
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export const copyRepositoryFiles = async (
|
|
244
|
+
repository: RepositoryInfo,
|
|
245
|
+
stagingRoot: string,
|
|
246
|
+
filterMatchers: readonly RegExp[],
|
|
247
|
+
): Promise<RepositoryPackStats> => {
|
|
248
|
+
let fileCount = 0;
|
|
249
|
+
let totalBytes = 0;
|
|
250
|
+
const destinationDirectory = path.join(stagingRoot, repository.archivePrefix);
|
|
251
|
+
const ignoreMatcher = new GitIgnoreMatcher(repository.path);
|
|
252
|
+
const queue: WalkItem[] = [{ relativePath: '', sourcePath: repository.path }];
|
|
253
|
+
|
|
254
|
+
while (queue.length > 0) {
|
|
255
|
+
const item = queue.shift()!;
|
|
256
|
+
const entries = await readdir(item.sourcePath, { withFileTypes: true });
|
|
257
|
+
|
|
258
|
+
for (const entry of entries.sort((left, right) => compareStableStrings(left.name, right.name))) {
|
|
259
|
+
const result = await processWalkEntry({
|
|
260
|
+
destinationDirectory,
|
|
261
|
+
entry,
|
|
262
|
+
filterMatchers,
|
|
263
|
+
ignoreMatcher,
|
|
264
|
+
isXcodeProject: repository.isXcodeProject,
|
|
265
|
+
item,
|
|
266
|
+
queue,
|
|
267
|
+
});
|
|
268
|
+
if (result.isFile) {
|
|
269
|
+
fileCount += 1;
|
|
270
|
+
totalBytes += result.bytes;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { fileCount, totalBytes };
|
|
276
|
+
};
|