gtx-cli 2.6.7 → 2.6.8

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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # gtx-cli
2
2
 
3
+ ## 2.6.8
4
+
5
+ ### Patch Changes
6
+
7
+ - [#981](https://github.com/generaltranslation/gt/pull/981) [`fca3a25`](https://github.com/generaltranslation/gt/commit/fca3a2583eb7f21bc3ef13516351d479f7bef882) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Handling source file movement to persist existing translations instead of retranslating
8
+
9
+ - Updated dependencies [[`fca3a25`](https://github.com/generaltranslation/gt/commit/fca3a2583eb7f21bc3ef13516351d479f7bef882)]:
10
+ - generaltranslation@8.1.8
11
+
3
12
  ## 2.6.7
4
13
 
5
14
  ### Patch Changes
@@ -136,12 +136,14 @@ export async function aggregateFiles(settings) {
136
136
  addedMintlifyTitle = result.addedTitle;
137
137
  }
138
138
  const sanitizedContent = sanitizeFileContent(processedContent);
139
+ // Always hash original content for versionId
140
+ const computedVersionId = hashStringSync(content);
139
141
  return {
140
142
  content: sanitizedContent,
141
143
  fileName: relativePath,
142
144
  fileFormat: fileType.toUpperCase(),
143
145
  fileId: hashStringSync(relativePath),
144
- versionId: hashStringSync(addedMintlifyTitle ? processedContent : content),
146
+ versionId: computedVersionId,
145
147
  locale: settings.defaultLocale,
146
148
  };
147
149
  })
@@ -1 +1 @@
1
- export declare const PACKAGE_VERSION = "2.6.7";
1
+ export declare const PACKAGE_VERSION = "2.6.8";
@@ -1,2 +1,2 @@
1
1
  // This file is auto-generated. Do not edit manually.
2
- export const PACKAGE_VERSION = '2.6.7';
2
+ export const PACKAGE_VERSION = '2.6.8';
@@ -13,6 +13,12 @@ export declare class UploadSourcesStep extends WorkflowStep<{
13
13
  private spinner;
14
14
  private result;
15
15
  constructor(gt: GT, settings: Settings);
16
+ /**
17
+ * Detects file moves by comparing local files against orphaned files.
18
+ * A move is detected when a local file has the same versionId (content hash)
19
+ * as an orphaned file but a different fileId (path hash).
20
+ */
21
+ private detectMoves;
16
22
  run({ files, branchData, }: {
17
23
  files: FileToUpload[];
18
24
  branchData: BranchData;
@@ -11,21 +11,70 @@ export class UploadSourcesStep extends WorkflowStep {
11
11
  this.gt = gt;
12
12
  this.settings = settings;
13
13
  }
14
+ /**
15
+ * Detects file moves by comparing local files against orphaned files.
16
+ * A move is detected when a local file has the same versionId (content hash)
17
+ * as an orphaned file but a different fileId (path hash).
18
+ */
19
+ detectMoves(localFiles, orphanedFiles) {
20
+ const moves = [];
21
+ // Build a map of versionId -> orphaned file
22
+ const orphansByVersionId = new Map();
23
+ for (const orphan of orphanedFiles) {
24
+ orphansByVersionId.set(orphan.versionId, orphan);
25
+ }
26
+ // Check each local file against orphaned files
27
+ for (const local of localFiles) {
28
+ const orphan = orphansByVersionId.get(local.versionId);
29
+ if (orphan && orphan.fileId !== local.fileId) {
30
+ // Same content, different path = move detected
31
+ moves.push({
32
+ oldFileId: orphan.fileId,
33
+ newFileId: local.fileId,
34
+ newFileName: local.fileName,
35
+ });
36
+ // Remove from map to avoid matching same orphan twice
37
+ orphansByVersionId.delete(local.versionId);
38
+ }
39
+ }
40
+ return moves;
41
+ }
14
42
  async run({ files, branchData, }) {
15
43
  if (files.length === 0) {
16
44
  logger.info('No files to upload found... skipping upload step');
17
45
  return [];
18
46
  }
47
+ const currentBranchId = branchData.currentBranch.id;
19
48
  this.spinner.start(`Syncing ${files.length} file${files.length !== 1 ? 's' : ''} with General Translation API...`);
20
- // First, figure out which files need to be uploaded
21
- const fileData = await this.gt.queryFileData({
22
- sourceFiles: files.map((f) => ({
23
- fileId: f.fileId,
24
- versionId: f.versionId,
25
- branchId: f.branchId ?? branchData.currentBranch.id,
26
- })),
27
- });
28
- // build a map of branch:fileId:versionId to fileData
49
+ // Query file data and orphaned files in parallel
50
+ const [fileData, orphanedFilesResult] = await Promise.all([
51
+ this.gt.queryFileData({
52
+ sourceFiles: files.map((f) => ({
53
+ fileId: f.fileId,
54
+ versionId: f.versionId,
55
+ branchId: f.branchId ?? currentBranchId,
56
+ })),
57
+ }),
58
+ this.gt.getOrphanedFiles(currentBranchId, files.map((f) => f.fileId)),
59
+ ]);
60
+ // Detect file moves
61
+ const moves = this.detectMoves(files, orphanedFilesResult.orphanedFiles);
62
+ // Track successfully moved files
63
+ let successfullyMovedFileIds = new Set();
64
+ // Process moves if any were detected
65
+ if (moves.length > 0) {
66
+ this.spinner.message(`Detected ${moves.length} moved file${moves.length !== 1 ? 's' : ''}, preserving translations...`);
67
+ const moveResult = await this.gt.processFileMoves(moves, {
68
+ branchId: currentBranchId,
69
+ });
70
+ // Only track files where the move actually succeeded
71
+ successfullyMovedFileIds = new Set(moveResult.results.filter((r) => r.success).map((r) => r.newFileId));
72
+ const failed = moveResult.summary.failed;
73
+ if (failed > 0) {
74
+ logger.warn(`Failed to migrate ${failed} moved file${failed !== 1 ? 's' : ''}`);
75
+ }
76
+ }
77
+ // Build a map of branch:fileId:versionId to fileData
29
78
  const fileDataMap = new Map();
30
79
  fileData.sourceFiles?.forEach((f) => {
31
80
  fileDataMap.set(`${f.branchId}:${f.fileId}:${f.versionId}`, f);
@@ -34,7 +83,8 @@ export class UploadSourcesStep extends WorkflowStep {
34
83
  const filesToUpload = [];
35
84
  const filesToSkipUpload = [];
36
85
  files.forEach((f) => {
37
- if (fileDataMap.has(`${f.branchId ?? branchData.currentBranch.id}:${f.fileId}:${f.versionId}`)) {
86
+ const key = `${f.branchId ?? currentBranchId}:${f.fileId}:${f.versionId}`;
87
+ if (fileDataMap.has(key) || successfullyMovedFileIds.has(f.fileId)) {
38
88
  filesToSkipUpload.push(f);
39
89
  }
40
90
  else {
@@ -44,7 +94,7 @@ export class UploadSourcesStep extends WorkflowStep {
44
94
  const response = await this.gt.uploadSourceFiles(filesToUpload.map((f) => ({
45
95
  source: {
46
96
  ...f,
47
- branchId: f.branchId ?? branchData.currentBranch.id,
97
+ branchId: f.branchId ?? currentBranchId,
48
98
  locale: this.settings.defaultLocale,
49
99
  incomingBranchId: branchData.incomingBranch?.id,
50
100
  checkedOutBranchId: branchData.checkedOutBranch?.id,
@@ -58,13 +108,14 @@ export class UploadSourcesStep extends WorkflowStep {
58
108
  this.result.push(...filesToSkipUpload.map((f) => ({
59
109
  fileId: f.fileId,
60
110
  versionId: f.versionId,
61
- branchId: f.branchId ?? branchData.currentBranch.id,
111
+ branchId: f.branchId ?? currentBranchId,
62
112
  fileName: f.fileName,
63
113
  fileFormat: f.fileFormat,
64
114
  dataFormat: f.dataFormat,
65
115
  locale: f.locale,
66
116
  })));
67
- this.spinner.stop(chalk.green('Files uploaded successfully'));
117
+ const moveMsg = moves.length > 0 ? ` (${moves.length} moved)` : '';
118
+ this.spinner.stop(chalk.green(`Files uploaded successfully${moveMsg}`));
68
119
  return this.result;
69
120
  }
70
121
  async wait() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtx-cli",
3
- "version": "2.6.7",
3
+ "version": "2.6.8",
4
4
  "main": "dist/index.js",
5
5
  "bin": "dist/main.js",
6
6
  "files": [
@@ -106,7 +106,7 @@
106
106
  "unified": "^11.0.5",
107
107
  "unist-util-visit": "^5.0.0",
108
108
  "yaml": "^2.8.0",
109
- "generaltranslation": "8.1.7"
109
+ "generaltranslation": "8.1.8"
110
110
  },
111
111
  "devDependencies": {
112
112
  "@babel/types": "^7.28.4",