create-confluence-sync 1.0.3 → 1.0.5
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/package.json +1 -1
- package/src/api.js +27 -0
- package/src/git.js +19 -0
- package/src/hook.js +9 -4
- package/src/sync.js +55 -4
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -184,6 +184,32 @@ export function createApiClient(config) {
|
|
|
184
184
|
};
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
async function movePage(pageId, newParentId, title, body, currentVersion) {
|
|
188
|
+
const payload = {
|
|
189
|
+
type: 'page',
|
|
190
|
+
title,
|
|
191
|
+
version: { number: currentVersion + 1 },
|
|
192
|
+
body: {
|
|
193
|
+
storage: {
|
|
194
|
+
value: body,
|
|
195
|
+
representation: 'storage',
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
ancestors: [{ id: String(newParentId) }],
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const data = await jsonRequest(
|
|
202
|
+
baseUrl, token, 'PUT',
|
|
203
|
+
`/rest/api/content/${pageId}`,
|
|
204
|
+
payload
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
id: String(data.id),
|
|
209
|
+
version: data.version.number,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
187
213
|
async function downloadAttachment(downloadPath) {
|
|
188
214
|
const { buffer } = await request(baseUrl, token, 'GET', downloadPath);
|
|
189
215
|
return buffer;
|
|
@@ -201,6 +227,7 @@ export function createApiClient(config) {
|
|
|
201
227
|
getPageAttachments,
|
|
202
228
|
createPage,
|
|
203
229
|
updatePage,
|
|
230
|
+
movePage,
|
|
204
231
|
deletePage,
|
|
205
232
|
downloadAttachment,
|
|
206
233
|
};
|
package/src/git.js
CHANGED
|
@@ -23,6 +23,24 @@ function getDeletedFiles(projectRoot) {
|
|
|
23
23
|
return output.split('\n').filter(f => f.endsWith('.xhtml'));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function getRenamedFiles(projectRoot) {
|
|
27
|
+
let output;
|
|
28
|
+
try {
|
|
29
|
+
output = exec(projectRoot, 'git -c core.quotePath=false diff --name-status -M HEAD~1 HEAD');
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
if (!output) return [];
|
|
34
|
+
const renames = [];
|
|
35
|
+
for (const line of output.split('\n')) {
|
|
36
|
+
const match = line.match(/^R\d+\t(.+)\t(.+)$/);
|
|
37
|
+
if (match && match[2].endsWith('.xhtml')) {
|
|
38
|
+
renames.push({ from: match[1], to: match[2] });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return renames;
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
function getChangedFilesUncommitted(projectRoot) {
|
|
27
45
|
const output = exec(projectRoot, 'git diff --name-only HEAD');
|
|
28
46
|
if (!output) return [];
|
|
@@ -123,6 +141,7 @@ function initRepo(projectRoot) {
|
|
|
123
141
|
export {
|
|
124
142
|
getChangedFiles,
|
|
125
143
|
getDeletedFiles,
|
|
144
|
+
getRenamedFiles,
|
|
126
145
|
getChangedFilesUncommitted,
|
|
127
146
|
createBranch,
|
|
128
147
|
switchBranch,
|
package/src/hook.js
CHANGED
|
@@ -6,6 +6,7 @@ import { pull, push, detectHidden } from './sync.js';
|
|
|
6
6
|
import {
|
|
7
7
|
getChangedFiles,
|
|
8
8
|
getDeletedFiles,
|
|
9
|
+
getRenamedFiles,
|
|
9
10
|
getCurrentBranch,
|
|
10
11
|
createBranch,
|
|
11
12
|
switchBranch,
|
|
@@ -60,9 +61,13 @@ async function main() {
|
|
|
60
61
|
// Create API client
|
|
61
62
|
const apiClient = createApiClient(config);
|
|
62
63
|
|
|
63
|
-
// Detect
|
|
64
|
+
// Detect renames (must happen before detectHidden to exclude renamed files)
|
|
65
|
+
const renames = getRenamedFiles(projectRoot);
|
|
66
|
+
const renamedFromPaths = new Set(renames.map(r => r.from.replace(/\\/g, '/')));
|
|
67
|
+
|
|
68
|
+
// Detect hidden (only check files deleted in this commit, excluding renames)
|
|
64
69
|
const deletedFiles = getDeletedFiles(projectRoot);
|
|
65
|
-
const hiddenCount = detectHidden(tree, projectRoot, deletedFiles);
|
|
70
|
+
const hiddenCount = detectHidden(tree, projectRoot, deletedFiles, renamedFromPaths);
|
|
66
71
|
if (hiddenCount > 0) {
|
|
67
72
|
log(`Hidden: ${hiddenCount} pages marked as hidden`);
|
|
68
73
|
}
|
|
@@ -102,11 +107,11 @@ async function main() {
|
|
|
102
107
|
deleteBranch(projectRoot, 'confluence/pull');
|
|
103
108
|
|
|
104
109
|
// --- Push (Local -> Confluence) ---
|
|
105
|
-
const pushResult = await push(apiClient, tree, projectRoot, config.space, changedFiles);
|
|
110
|
+
const pushResult = await push(apiClient, tree, projectRoot, config.space, changedFiles, renames);
|
|
106
111
|
saveTree(projectRoot, tree);
|
|
107
112
|
commitAll(projectRoot, `${PREFIX} push to Confluence + update tree`);
|
|
108
113
|
|
|
109
|
-
log(`Push: ${pushResult.created} created, ${pushResult.updated} updated`);
|
|
114
|
+
log(`Push: ${pushResult.created} created, ${pushResult.updated} updated, ${pushResult.moved} moved/renamed`);
|
|
110
115
|
log('Sync complete!');
|
|
111
116
|
}
|
|
112
117
|
|
package/src/sync.js
CHANGED
|
@@ -161,11 +161,61 @@ export async function pull(apiClient, tree, projectRoot, spaceKey) {
|
|
|
161
161
|
return { created: created.length, updated: updated.length, deleted: deleted.length, renamed };
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
export async function push(apiClient, tree, projectRoot, spaceKey, changedFiles) {
|
|
164
|
+
export async function push(apiClient, tree, projectRoot, spaceKey, changedFiles, renames = []) {
|
|
165
165
|
let created = 0;
|
|
166
166
|
let updated = 0;
|
|
167
|
+
let moved = 0;
|
|
167
168
|
|
|
168
|
-
|
|
169
|
+
// --- Handle renames/moves before the main loop ---
|
|
170
|
+
const renamedFromPaths = new Set(renames.map(r => r.from.replace(/\\/g, '/')));
|
|
171
|
+
const renamedToPaths = new Set(renames.map(r => r.to.replace(/\\/g, '/')));
|
|
172
|
+
|
|
173
|
+
for (const { from, to } of renames) {
|
|
174
|
+
const fromNorm = from.replace(/\\/g, '/');
|
|
175
|
+
const toNorm = to.replace(/\\/g, '/');
|
|
176
|
+
const existing = getPageByPath(tree, fromNorm);
|
|
177
|
+
|
|
178
|
+
if (!existing) continue;
|
|
179
|
+
|
|
180
|
+
const pageId = existing.pageId;
|
|
181
|
+
const newTitle = path.basename(toNorm).replace(/\.xhtml$/, '');
|
|
182
|
+
const newParentId = pathToParentId(tree, toNorm);
|
|
183
|
+
|
|
184
|
+
const absolutePath = path.resolve(projectRoot, toNorm);
|
|
185
|
+
const body = await fs.readFile(absolutePath, 'utf-8');
|
|
186
|
+
|
|
187
|
+
let result;
|
|
188
|
+
const parentChanged = newParentId && newParentId !== existing.parentId;
|
|
189
|
+
|
|
190
|
+
if (parentChanged) {
|
|
191
|
+
result = await apiClient.movePage(pageId, newParentId, newTitle, body, existing.version);
|
|
192
|
+
console.log(`Moved: ${fromNorm} → ${toNorm}`);
|
|
193
|
+
} else {
|
|
194
|
+
result = await apiClient.updatePage(pageId, newTitle, body, existing.version);
|
|
195
|
+
console.log(`Renamed: ${fromNorm} → ${toNorm}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
addPage(tree, pageId, {
|
|
199
|
+
title: newTitle,
|
|
200
|
+
fsName: sanitizeName(newTitle),
|
|
201
|
+
path: toNorm,
|
|
202
|
+
parentId: parentChanged ? newParentId : existing.parentId,
|
|
203
|
+
version: result.version,
|
|
204
|
+
});
|
|
205
|
+
moved++;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Sort by path depth — create parents before children
|
|
209
|
+
const sorted = [...changedFiles].sort((a, b) => {
|
|
210
|
+
const depthA = a.replace(/\\/g, '/').split('/').length;
|
|
211
|
+
const depthB = b.replace(/\\/g, '/').split('/').length;
|
|
212
|
+
return depthA - depthB;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
for (let filePath of sorted) {
|
|
216
|
+
// Skip files that are part of a rename operation
|
|
217
|
+
const normalizedCheck = filePath.replace(/\\/g, '/');
|
|
218
|
+
if (renamedToPaths.has(normalizedCheck)) continue;
|
|
169
219
|
let normalized = filePath.replace(/\\/g, '/');
|
|
170
220
|
let absolutePath = path.resolve(projectRoot, normalized);
|
|
171
221
|
|
|
@@ -220,16 +270,17 @@ export async function push(apiClient, tree, projectRoot, spaceKey, changedFiles)
|
|
|
220
270
|
|
|
221
271
|
tree.lastSync = new Date().toISOString();
|
|
222
272
|
|
|
223
|
-
return { created, updated };
|
|
273
|
+
return { created, updated, moved };
|
|
224
274
|
}
|
|
225
275
|
|
|
226
|
-
export function detectHidden(tree, projectRoot, deletedFiles) {
|
|
276
|
+
export function detectHidden(tree, projectRoot, deletedFiles, excludePaths = new Set()) {
|
|
227
277
|
let count = 0;
|
|
228
278
|
|
|
229
279
|
if (deletedFiles && deletedFiles.length > 0) {
|
|
230
280
|
// Fast path: only check files we know were deleted in the commit
|
|
231
281
|
for (const filePath of deletedFiles) {
|
|
232
282
|
const normalized = filePath.replace(/\\/g, '/');
|
|
283
|
+
if (excludePaths.has(normalized)) continue;
|
|
233
284
|
const page = getPageByPath(tree, normalized);
|
|
234
285
|
if (page && !page.hidden) {
|
|
235
286
|
console.log(`Hidden: ${normalized}`);
|