resulgit 1.0.13 → 1.0.15

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/resulgit.js +96 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resulgit",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "A powerful CLI tool for version control system operations - clone, commit, push, pull, merge, branch management, and more",
5
5
  "main": "resulgit.js",
6
6
  "bin": {
package/resulgit.js CHANGED
@@ -100,6 +100,43 @@ async function request(method, url, body, token) {
100
100
  return res.text()
101
101
  }
102
102
 
103
+ /**
104
+ * Upload files as blobs via multipart form data
105
+ * @param {string} server - Server URL
106
+ * @param {string} repoId - Repository ID
107
+ * @param {Record<string, string>} files - Map of file paths to content
108
+ * @param {string} token - Auth token
109
+ * @returns {Promise<Record<string, string>>} Map of file paths to blob IDs
110
+ */
111
+ async function uploadBlobs(server, repoId, files, token) {
112
+ const FormData = (await import('node:buffer')).File ? globalThis.FormData : (await import('undici')).FormData
113
+ const formData = new FormData()
114
+
115
+ for (const [filePath, content] of Object.entries(files)) {
116
+ // Create a Blob/File from the content
117
+ const blob = new Blob([content], { type: 'text/plain' })
118
+ formData.append('files', blob, filePath)
119
+ }
120
+
121
+ const url = new URL(`/api/repositories/${repoId}/blobs`, server).toString()
122
+ const headers = {}
123
+ if (token) headers['Authorization'] = `Bearer ${token}`
124
+
125
+ const res = await fetch(url, {
126
+ method: 'POST',
127
+ headers,
128
+ body: formData
129
+ })
130
+
131
+ if (!res.ok) {
132
+ const text = await res.text()
133
+ throw new Error(`Blob upload failed: ${res.status} ${res.statusText} ${text}`)
134
+ }
135
+
136
+ const data = await res.json()
137
+ return data.blobs || {}
138
+ }
139
+
103
140
  function print(obj, json) {
104
141
  if (json) {
105
142
  process.stdout.write(JSON.stringify(obj, null, 2) + '\n')
@@ -1393,14 +1430,44 @@ async function cmdPush(opts) {
1393
1430
  }
1394
1431
  if (conflicts.length > 0) {
1395
1432
  spinnerFail(spinner, 'Push blocked by conflicts')
1433
+
1434
+ // Write conflict markers to local files
1435
+ for (const c of conflicts) {
1436
+ const localContent = local[c.path]?.content || ''
1437
+ const remoteContent = remote.files[c.path] || ''
1438
+
1439
+ // Create conflict-marked content
1440
+ const conflictContent = [
1441
+ '<<<<<<< LOCAL (your changes)',
1442
+ localContent,
1443
+ '=======',
1444
+ remoteContent,
1445
+ '>>>>>>> REMOTE (server changes)'
1446
+ ].join('\n')
1447
+
1448
+ // Write to local file
1449
+ const conflictPath = path.join(dir, c.path)
1450
+ await fs.promises.mkdir(path.dirname(conflictPath), { recursive: true })
1451
+ await fs.promises.writeFile(conflictPath, conflictContent, 'utf8')
1452
+ }
1453
+
1396
1454
  if (opts.json === 'true') {
1397
- print({ conflicts }, true)
1455
+ print({ conflicts, message: 'Conflict markers written to files. Resolve them and try again.' }, true)
1398
1456
  } else {
1399
- process.stderr.write(color('Error: Cannot push with conflicts\\n', 'red'))
1400
- process.stderr.write(color('Conflicts detected:\\n', 'yellow'))
1457
+ process.stderr.write(color('\nConflicts detected! The following files have been updated with conflict markers:\n', 'red'))
1401
1458
  for (const c of conflicts) {
1402
- process.stderr.write(color(` ${c.path}:${c.line}\\n`, 'red'))
1459
+ process.stderr.write(color(` ${c.path}\n`, 'yellow'))
1403
1460
  }
1461
+ process.stderr.write(color('\nTo resolve:\n', 'cyan'))
1462
+ process.stderr.write(color(' 1. Open the files above and look for conflict markers:\n', 'dim'))
1463
+ process.stderr.write(color(' <<<<<<< LOCAL (your changes)\n', 'dim'))
1464
+ process.stderr.write(color(' ... your version ...\n', 'dim'))
1465
+ process.stderr.write(color(' =======\n', 'dim'))
1466
+ process.stderr.write(color(' ... server version ...\n', 'dim'))
1467
+ process.stderr.write(color(' >>>>>>> REMOTE (server changes)\n', 'dim'))
1468
+ process.stderr.write(color(' 2. Edit the files to keep the changes you want\n', 'dim'))
1469
+ process.stderr.write(color(' 3. Remove the conflict markers\n', 'dim'))
1470
+ process.stderr.write(color(' 4. Run: resulgit add . && resulgit commit -m "Resolved conflicts" && resulgit push\n\n', 'dim'))
1404
1471
  }
1405
1472
  return
1406
1473
  }
@@ -1429,8 +1496,31 @@ async function cmdPush(opts) {
1429
1496
  // Hook doesn't exist or other error, continue
1430
1497
  }
1431
1498
 
1432
- const body = { message: localMeta.pendingCommit?.message || (opts.message || 'Push'), files: merged, branchName: remoteMeta.branch }
1433
- spinnerUpdate(spinner, `Pushing ${Object.keys(merged).length} file(s) to '${remoteMeta.branch}'...`)
1499
+ // Step 1: Upload files as blobs
1500
+ spinnerUpdate(spinner, `Uploading ${Object.keys(merged).length} file(s)...`)
1501
+ let blobMap = {}
1502
+ try {
1503
+ blobMap = await uploadBlobs(server, remoteMeta.repoId, merged, token)
1504
+ } catch (err) {
1505
+ // If blob upload fails, fall back to sending content directly
1506
+ // (This handles servers that accept content in commits endpoint)
1507
+ spinnerUpdate(spinner, 'Blob upload not available, sending files directly...')
1508
+ blobMap = merged // Use content directly
1509
+ }
1510
+
1511
+ // Step 2: Create commit with blob IDs (or content if blob upload failed)
1512
+ const commitFiles = {}
1513
+ for (const [filePath, content] of Object.entries(merged)) {
1514
+ // Use blob ID if available, otherwise use content
1515
+ commitFiles[filePath] = blobMap[filePath] || content
1516
+ }
1517
+
1518
+ const body = {
1519
+ message: localMeta.pendingCommit?.message || (opts.message || 'Push'),
1520
+ files: commitFiles,
1521
+ branchName: remoteMeta.branch
1522
+ }
1523
+ spinnerUpdate(spinner, `Creating commit on '${remoteMeta.branch}'...`)
1434
1524
  const url = new URL(`/api/repositories/${remoteMeta.repoId}/commits`, server).toString()
1435
1525
  const data = await request('POST', url, body, token)
1436
1526
  localMeta.baseCommitId = data.id || remote.commitId || ''