expo-tiddlywiki-filesystem-android-external-storage 2.4.0 → 2.5.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.
@@ -4,19 +4,25 @@ import org.eclipse.jgit.api.Git
4
4
  import org.eclipse.jgit.api.ResetCommand
5
5
  import org.eclipse.jgit.diff.DiffEntry
6
6
  import org.eclipse.jgit.diff.DiffFormatter
7
+ import org.eclipse.jgit.internal.storage.pack.PackWriter
7
8
  import org.eclipse.jgit.lib.Constants
9
+ import org.eclipse.jgit.lib.NullProgressMonitor
8
10
  import org.eclipse.jgit.lib.ObjectId
9
11
  import org.eclipse.jgit.lib.Repository
10
12
  import org.eclipse.jgit.revwalk.RevWalk
11
13
  import org.eclipse.jgit.storage.file.FileRepositoryBuilder
12
- import org.eclipse.jgit.transport.RefSpec
14
+ import org.eclipse.jgit.transport.PacketLineIn
13
15
  import org.eclipse.jgit.transport.TransportHttp
16
+ import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider
14
17
  import org.eclipse.jgit.treewalk.TreeWalk
15
- import org.eclipse.jgit.treewalk.filter.PathFilterGroup
16
18
  import org.json.JSONArray
17
19
  import org.json.JSONObject
20
+ import java.io.ByteArrayInputStream
18
21
  import java.io.ByteArrayOutputStream
19
22
  import java.io.File
23
+ import java.io.InputStream
24
+ import java.net.HttpURLConnection
25
+ import java.net.URL
20
26
  import android.util.Base64
21
27
 
22
28
  /**
@@ -43,26 +49,567 @@ internal object GitHelper {
43
49
  .build()
44
50
  }
45
51
 
46
- /** Apply custom HTTP headers to a JGit transport command. */
47
- private fun applyHeaders(
48
- command: org.eclipse.jgit.api.TransportCommand<*, *>,
52
+ // ─── Raw HTTP helpers for Git Smart HTTP protocol ─────────────────
53
+
54
+ /**
55
+ * Parse headers JSON into a map.
56
+ */
57
+ private fun parseHeaders(headers: String?): Map<String, String> {
58
+ if (headers == null) return emptyMap()
59
+ return try {
60
+ val obj = JSONObject(headers)
61
+ val map = mutableMapOf<String, String>()
62
+ for (key in obj.keys()) map[key] = obj.getString(key)
63
+ map
64
+ } catch (e: Exception) {
65
+ emptyMap()
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Open an HTTP connection with custom headers applied.
71
+ */
72
+ private fun openHttpConnection(url: String, method: String, headers: Map<String, String>, contentType: String? = null): HttpURLConnection {
73
+ val conn = URL(url).openConnection() as HttpURLConnection
74
+ conn.requestMethod = method
75
+ conn.connectTimeout = 30_000
76
+ conn.readTimeout = 120_000
77
+ conn.instanceFollowRedirects = true
78
+ for ((k, v) in headers) conn.setRequestProperty(k, v)
79
+ if (contentType != null) conn.setRequestProperty("Content-Type", contentType)
80
+ return conn
81
+ }
82
+
83
+ /**
84
+ * Parse pkt-line formatted /info/refs response into a map of ref→oid.
85
+ * Also returns the set of capabilities advertised by the server.
86
+ */
87
+ private data class ServerRefs(
88
+ val refs: Map<String, String>,
89
+ val capabilities: Set<String>
90
+ )
91
+
92
+ private fun parseInfoRefs(input: InputStream, service: String): ServerRefs {
93
+ val pktIn = PacketLineIn(input)
94
+ val refs = mutableMapOf<String, String>()
95
+ val capabilities = mutableSetOf<String>()
96
+
97
+ // First: skip service announcement line(s) and flush
98
+ // Format: "# service=git-upload-pack\n" + "0000"
99
+ // The server may also send the line as a pkt-line.
100
+ var firstRef = true
101
+ while (true) {
102
+ val line = pktIn.readString()
103
+ if (PacketLineIn.isEnd(line)) break // flush packet after service announcement
104
+ // Some servers include "# service=..." in pkt-line format
105
+ if (line.startsWith("# ")) continue
106
+ // If we got here without a flush, it's the first ref line
107
+ // (shouldn't happen with standard servers)
108
+ }
109
+
110
+ // Now read refs
111
+ while (true) {
112
+ val line = pktIn.readString()
113
+ if (PacketLineIn.isEnd(line)) break
114
+ if (line.isEmpty()) continue
115
+
116
+ val parts = line.split(" ", limit = 2)
117
+ if (parts.size < 2) continue
118
+ val oid = parts[0]
119
+ val refWithCaps = parts[1]
120
+
121
+ if (firstRef && refWithCaps.contains('\u0000')) {
122
+ // First ref line contains capabilities after NUL
123
+ val refParts = refWithCaps.split('\u0000', limit = 2)
124
+ refs[refParts[0]] = oid
125
+ if (refParts.size > 1) {
126
+ capabilities.addAll(refParts[1].trim().split(" "))
127
+ }
128
+ firstRef = false
129
+ } else {
130
+ refs[refWithCaps.trim()] = oid
131
+ firstRef = false
132
+ }
133
+ }
134
+
135
+ return ServerRefs(refs, capabilities)
136
+ }
137
+
138
+ // ─── Git push via raw HTTP ──────────────────────────────────────
139
+
140
+ /**
141
+ * Push local branch to remote using raw Git Smart HTTP protocol.
142
+ *
143
+ * This bypasses JGit's TransportHttp (which has issues with some servers)
144
+ * and directly implements the stateless-rpc push protocol:
145
+ *
146
+ * 1. GET /info/refs?service=git-receive-pack → parse server refs + capabilities
147
+ * 2. Build pack data containing objects the server is missing
148
+ * 3. POST /git-receive-pack with ref-update command + pack data
149
+ * 4. Parse server response for success/failure
150
+ */
151
+ fun gitPush(
152
+ gitRootDir: String,
153
+ remoteName: String,
154
+ localBranch: String,
155
+ remoteBranch: String,
156
+ force: Boolean,
49
157
  headers: String?
50
- ) {
51
- if (headers == null) return
158
+ ): String {
159
+ val result = JSONObject()
52
160
  try {
53
- val headerObj = JSONObject(headers)
54
- val headerMap = mutableMapOf<String, String>()
55
- for (key in headerObj.keys()) {
56
- headerMap[key] = headerObj.getString(key)
161
+ val repo = openRepo(gitRootDir)
162
+ try {
163
+ val headerMap = parseHeaders(headers)
164
+ val remoteUrl = repo.config.getString("remote", remoteName, "url")
165
+ ?: throw Exception("Remote '$remoteName' not configured")
166
+
167
+ // Resolve local branch to its SHA
168
+ val localRef = repo.resolve("refs/heads/$localBranch")
169
+ ?: throw Exception("Cannot resolve local branch: $localBranch")
170
+
171
+ // Step 1: GET /info/refs?service=git-receive-pack
172
+ val infoUrl = "$remoteUrl/info/refs?service=git-receive-pack"
173
+ val infoConn = openHttpConnection(infoUrl, "GET", headerMap)
174
+ infoConn.setRequestProperty("Accept", "application/x-git-receive-pack-advertisement, */*")
175
+ val serverRefs: ServerRefs
176
+ try {
177
+ if (infoConn.responseCode != 200) {
178
+ throw Exception("info/refs failed: ${infoConn.responseCode} ${infoConn.responseMessage}")
179
+ }
180
+ serverRefs = parseInfoRefs(infoConn.inputStream, "git-receive-pack")
181
+ } finally {
182
+ infoConn.disconnect()
183
+ }
184
+
185
+ // Check if push is needed
186
+ val remoteOid = serverRefs.refs[remoteBranch] ?: ObjectId.zeroId().name
187
+ if (remoteOid == localRef.name) {
188
+ result.put("ok", true)
189
+ result.put("updates", JSONArray().put(JSONObject().apply {
190
+ put("remoteName", remoteBranch)
191
+ put("status", "UP_TO_DATE")
192
+ put("message", "")
193
+ }))
194
+ return result.toString()
195
+ }
196
+
197
+ // Step 2: Build the pack data
198
+ val packBuf = ByteArrayOutputStream()
199
+
200
+ // Write the ref-update command line in pkt-line format
201
+ // Format: <old-oid> <new-oid> <ref-name>\0<capabilities>\n
202
+ val caps = mutableListOf("report-status", "side-band-64k")
203
+ if (serverRefs.capabilities.contains("ofs-delta")) caps.add("ofs-delta")
204
+ val commandLine = "$remoteOid ${localRef.name} $remoteBranch\u0000${caps.joinToString(" ")}\n"
205
+ writePktLine(packBuf, commandLine)
206
+ // Flush packet (0000)
207
+ packBuf.write("0000".toByteArray())
208
+
209
+ // Build pack with objects missing on server
210
+ val remoteObjectId = if (remoteOid == ObjectId.zeroId().name) null else ObjectId.fromString(remoteOid)
211
+ val localObjectId = localRef
212
+
213
+ val packData = ByteArrayOutputStream()
214
+ val writer = PackWriter(repo)
215
+ try {
216
+ writer.setUseBitmaps(true)
217
+ writer.setThin(true)
218
+ writer.setDeltaBaseAsOffset(serverRefs.capabilities.contains("ofs-delta"))
219
+
220
+ // Determine which objects to send
221
+ val want = setOf(localObjectId)
222
+ val have = if (remoteObjectId != null) setOf(remoteObjectId) else emptySet<ObjectId>()
223
+ writer.preparePack(NullProgressMonitor.INSTANCE, want, have)
224
+ writer.writePack(NullProgressMonitor.INSTANCE, NullProgressMonitor.INSTANCE, packData)
225
+ } finally {
226
+ writer.close()
227
+ }
228
+
229
+ packBuf.write(packData.toByteArray())
230
+
231
+ // Step 3: POST /git-receive-pack
232
+ val postUrl = "$remoteUrl/git-receive-pack"
233
+ val postConn = openHttpConnection(postUrl, "POST", headerMap, "application/x-git-receive-pack-request")
234
+ postConn.setRequestProperty("Accept", "application/x-git-receive-pack-result")
235
+ postConn.doOutput = true
236
+ val requestBody = packBuf.toByteArray()
237
+ postConn.setFixedLengthStreamingMode(requestBody.size)
238
+
239
+ try {
240
+ postConn.outputStream.use { it.write(requestBody) }
241
+
242
+ if (postConn.responseCode != 200) {
243
+ throw Exception("git-receive-pack failed: ${postConn.responseCode} ${postConn.responseMessage}")
244
+ }
245
+
246
+ // Step 4: Parse response
247
+ val responseBytes = postConn.inputStream.readBytes()
248
+ val responseStr = tryParseReceivePackResponse(responseBytes)
249
+
250
+ val updatesArray = JSONArray()
251
+ val updateObj = JSONObject()
252
+ updateObj.put("remoteName", remoteBranch)
253
+ updateObj.put("status", if (responseStr.contains("unpack ok")) "OK" else "REJECTED")
254
+ updateObj.put("message", responseStr)
255
+ updatesArray.put(updateObj)
256
+
257
+ if (!responseStr.contains("unpack ok")) {
258
+ throw Exception("Push rejected by server: $responseStr")
259
+ }
260
+
261
+ result.put("ok", true)
262
+ result.put("updates", updatesArray)
263
+ android.util.Log.i("GitPush", "Raw HTTP push completed: ${localRef.name.take(8)} → $remoteBranch")
264
+ } finally {
265
+ postConn.disconnect()
266
+ }
267
+ } finally {
268
+ repo.close()
269
+ }
270
+ } catch (e: Exception) {
271
+ result.put("ok", false)
272
+ result.put("error", e.message ?: "Unknown push error")
273
+ android.util.Log.e("GitPush", "Push failed: ${e.message}", e)
274
+ }
275
+ return result.toString()
276
+ }
277
+
278
+ private fun writePktLine(out: ByteArrayOutputStream, data: String) {
279
+ val bytes = data.toByteArray()
280
+ val length = bytes.size + 4
281
+ val hex = String.format("%04x", length)
282
+ out.write(hex.toByteArray())
283
+ out.write(bytes)
284
+ }
285
+
286
+ /**
287
+ * Try to parse receive-pack response, handling side-band encoding.
288
+ */
289
+ private fun tryParseReceivePackResponse(responseBytes: ByteArray): String {
290
+ // The response may be plain pkt-line or side-band encoded.
291
+ // Try to extract meaningful text.
292
+ val text = StringBuilder()
293
+ try {
294
+ val pktIn = PacketLineIn(ByteArrayInputStream(responseBytes))
295
+ while (true) {
296
+ val line = pktIn.readString()
297
+ if (PacketLineIn.isEnd(line)) break
298
+ text.append(line).append("\n")
57
299
  }
58
- command.setTransportConfigCallback { transport ->
59
- if (transport is TransportHttp) {
60
- transport.setAdditionalHeaders(headerMap)
300
+ } catch (e: Exception) {
301
+ // Fallback: just decode as UTF-8
302
+ text.append(String(responseBytes, Charsets.UTF_8))
303
+ }
304
+ return text.toString().trim()
305
+ }
306
+
307
+ // ─── Git fetch via raw HTTP ─────────────────────────────────────
308
+
309
+ /**
310
+ * Fetch from remote using raw Git Smart HTTP protocol.
311
+ *
312
+ * This bypasses JGit's TransportHttp and directly implements:
313
+ *
314
+ * 1. GET /info/refs?service=git-upload-pack → parse server refs
315
+ * 2. Build wants/haves negotiation message
316
+ * 3. POST /git-upload-pack with wants/haves + done → receive pack data
317
+ * 4. Parse and apply pack to local repo, update tracking refs
318
+ */
319
+ fun gitFetch(
320
+ gitRootDir: String,
321
+ remoteName: String,
322
+ branch: String,
323
+ headers: String?
324
+ ): String {
325
+ val result = JSONObject()
326
+ try {
327
+ val repo = openRepo(gitRootDir)
328
+ try {
329
+ val headerMap = parseHeaders(headers)
330
+ val remoteUrl = repo.config.getString("remote", remoteName, "url")
331
+ ?: throw Exception("Remote '$remoteName' not configured")
332
+
333
+ // Step 1: GET /info/refs?service=git-upload-pack
334
+ val infoUrl = "$remoteUrl/info/refs?service=git-upload-pack"
335
+ val infoConn = openHttpConnection(infoUrl, "GET", headerMap)
336
+ infoConn.setRequestProperty("Accept", "application/x-git-upload-pack-advertisement, */*")
337
+ val serverRefs: ServerRefs
338
+ try {
339
+ if (infoConn.responseCode != 200) {
340
+ throw Exception("info/refs failed: ${infoConn.responseCode} ${infoConn.responseMessage}")
341
+ }
342
+ serverRefs = parseInfoRefs(infoConn.inputStream, "git-upload-pack")
343
+ } finally {
344
+ infoConn.disconnect()
345
+ }
346
+
347
+ // Find the ref we want
348
+ val remoteRef = "refs/heads/$branch"
349
+ val wantOid = serverRefs.refs[remoteRef]
350
+ if (wantOid == null) {
351
+ // Branch doesn't exist on remote — nothing to fetch
352
+ result.put("ok", true)
353
+ result.put("updates", JSONArray())
354
+ return result.toString()
355
+ }
356
+
357
+ // Check if we already have this object
358
+ val wantObjectId = ObjectId.fromString(wantOid)
359
+ val trackingRef = "refs/remotes/$remoteName/$branch"
360
+ val localTrackingOid = repo.resolve(trackingRef)
361
+
362
+ if (localTrackingOid != null && localTrackingOid == wantObjectId) {
363
+ // Already up to date
364
+ result.put("ok", true)
365
+ result.put("updates", JSONArray())
366
+ return result.toString()
367
+ }
368
+
369
+ // Also check if we already have the object in our object database
370
+ if (repo.objectDatabase.has(wantObjectId)) {
371
+ // We have the object, just update the tracking ref
372
+ updateRef(repo, trackingRef, wantObjectId)
373
+ val updatesArray = JSONArray()
374
+ updatesArray.put(JSONObject().apply {
375
+ put("ref", trackingRef)
376
+ put("oldObjectId", localTrackingOid?.name ?: ObjectId.zeroId().name)
377
+ put("newObjectId", wantOid)
378
+ })
379
+ result.put("ok", true)
380
+ result.put("updates", updatesArray)
381
+ return result.toString()
382
+ }
383
+
384
+ // Step 2: Build the upload-pack request
385
+ val requestBuf = ByteArrayOutputStream()
386
+
387
+ // Write "want" lines
388
+ // First want line includes capabilities
389
+ val caps = mutableListOf("no-progress", "report-status", "side-band-64k")
390
+ if (serverRefs.capabilities.contains("ofs-delta")) caps.add("ofs-delta")
391
+ if (serverRefs.capabilities.contains("thin-pack")) caps.add("thin-pack")
392
+ writePktLine(requestBuf, "want $wantOid ${caps.joinToString(" ")}\n")
393
+
394
+ // Additional wants: also fetch any other refs we might need
395
+ // (for now, just the one branch)
396
+ // Flush after wants
397
+ requestBuf.write("0000".toByteArray())
398
+
399
+ // Write "have" lines — tell server what we already have
400
+ // Send recent commits so server can compute a thin pack
401
+ val haveOids = collectHaveOids(repo, 256)
402
+ for (have in haveOids) {
403
+ writePktLine(requestBuf, "have ${have.name}\n")
404
+ }
405
+
406
+ // "done" — single-round stateless negotiation
407
+ writePktLine(requestBuf, "done\n")
408
+ // Flush
409
+ requestBuf.write("0000".toByteArray())
410
+
411
+ // Step 3: POST /git-upload-pack
412
+ val postUrl = "$remoteUrl/git-upload-pack"
413
+ val postConn = openHttpConnection(postUrl, "POST", headerMap, "application/x-git-upload-pack-request")
414
+ postConn.setRequestProperty("Accept", "application/x-git-upload-pack-result")
415
+ postConn.doOutput = true
416
+ val requestBody = requestBuf.toByteArray()
417
+ postConn.setFixedLengthStreamingMode(requestBody.size)
418
+
419
+ try {
420
+ postConn.outputStream.use { it.write(requestBody) }
421
+
422
+ if (postConn.responseCode != 200) {
423
+ throw Exception("git-upload-pack failed: ${postConn.responseCode} ${postConn.responseMessage}")
424
+ }
425
+
426
+ // Step 4: Parse response and apply pack
427
+ val responseStream = postConn.inputStream
428
+ val responseBytes = responseStream.readBytes()
429
+
430
+ // The response is pkt-line encoded. It starts with NAK or ACK lines,
431
+ // then contains pack data (possibly side-band encoded).
432
+ val packData = extractPackData(responseBytes)
433
+
434
+ if (packData != null && packData.isNotEmpty()) {
435
+ // Parse and index the pack
436
+ val inserter = repo.newObjectInserter()
437
+ try {
438
+ val parser = inserter.newPackParser(ByteArrayInputStream(packData))
439
+ parser.setAllowThin(true)
440
+ parser.parse(NullProgressMonitor.INSTANCE)
441
+ inserter.flush()
442
+ } finally {
443
+ inserter.close()
444
+ }
445
+ }
446
+
447
+ // Update tracking ref
448
+ updateRef(repo, trackingRef, wantObjectId)
449
+
450
+ val updatesArray = JSONArray()
451
+ updatesArray.put(JSONObject().apply {
452
+ put("ref", trackingRef)
453
+ put("oldObjectId", localTrackingOid?.name ?: ObjectId.zeroId().name)
454
+ put("newObjectId", wantOid)
455
+ })
456
+
457
+ result.put("ok", true)
458
+ result.put("updates", updatesArray)
459
+ android.util.Log.i("GitFetch", "Raw HTTP fetch completed: $wantOid → $trackingRef")
460
+ } finally {
461
+ postConn.disconnect()
462
+ }
463
+ } finally {
464
+ repo.close()
465
+ }
466
+ } catch (e: Exception) {
467
+ result.put("ok", false)
468
+ result.put("error", e.message ?: "Unknown fetch error")
469
+ android.util.Log.e("GitFetch", "Fetch failed: ${e.message}", e)
470
+ }
471
+ return result.toString()
472
+ }
473
+
474
+ /**
475
+ * Collect recent commit OIDs that we can tell the server we "have".
476
+ * This helps the server send a minimal pack.
477
+ */
478
+ private fun collectHaveOids(repo: Repository, maxCount: Int): List<ObjectId> {
479
+ val oids = mutableListOf<ObjectId>()
480
+ try {
481
+ val walk = RevWalk(repo)
482
+ // Add all refs as starting points
483
+ for (ref in repo.refDatabase.refs) {
484
+ try {
485
+ val peeled = repo.refDatabase.peel(ref)
486
+ val target = peeled.peeledObjectId ?: peeled.objectId ?: continue
487
+ walk.markStart(walk.parseCommit(target))
488
+ } catch (e: Exception) {
489
+ // Skip non-commit refs
61
490
  }
62
491
  }
492
+ var count = 0
493
+ for (commit in walk) {
494
+ oids.add(commit.id)
495
+ if (++count >= maxCount) break
496
+ }
497
+ walk.close()
63
498
  } catch (e: Exception) {
64
- android.util.Log.w("GitHelper", "Failed to parse headers: ${e.message}")
499
+ android.util.Log.w("GitFetch", "collectHaveOids error: ${e.message}")
65
500
  }
501
+ return oids
502
+ }
503
+
504
+ /**
505
+ * Update a ref to point to a new object ID.
506
+ */
507
+ private fun updateRef(repo: Repository, refName: String, newId: ObjectId) {
508
+ val refUpdate = repo.updateRef(refName)
509
+ refUpdate.setNewObjectId(newId)
510
+ refUpdate.isForceUpdate = true
511
+ val updateResult = refUpdate.update()
512
+ android.util.Log.i("GitFetch", "Updated ref $refName → ${newId.name.take(8)}: $updateResult")
513
+ }
514
+
515
+ /**
516
+ * Extract pack data from a git-upload-pack response.
517
+ *
518
+ * The response format is:
519
+ * - pkt-line with "NAK\n" or "ACK <oid>\n" lines
520
+ * - Then pack data, possibly side-band encoded
521
+ *
522
+ * Side-band encoding: each pkt-line starts with a channel byte:
523
+ * - 1 = pack data
524
+ * - 2 = progress messages
525
+ * - 3 = error messages
526
+ *
527
+ * We need to handle both side-band and non-side-band responses.
528
+ */
529
+ private fun extractPackData(responseBytes: ByteArray): ByteArray? {
530
+ if (responseBytes.isEmpty()) return null
531
+
532
+ val packBytes = ByteArrayOutputStream()
533
+ val input = ByteArrayInputStream(responseBytes)
534
+
535
+ try {
536
+ // Read pkt-lines
537
+ while (input.available() > 0) {
538
+ // Read 4-byte hex length
539
+ val hexBytes = ByteArray(4)
540
+ val read = input.read(hexBytes)
541
+ if (read < 4) break
542
+
543
+ val hexStr = String(hexBytes)
544
+ if (hexStr == "0000") continue // flush packet
545
+ if (hexStr == "0001") continue // delimiter packet
546
+ if (hexStr == "0002") continue // response-end packet
547
+
548
+ val length = try { hexStr.toInt(16) } catch (e: Exception) {
549
+ // Not a valid pkt-line, might be raw pack data
550
+ // Check if the first 4 bytes are "PACK" signature
551
+ if (hexStr == "PACK") {
552
+ packBytes.write(hexBytes)
553
+ val remaining = ByteArray(input.available())
554
+ input.read(remaining)
555
+ packBytes.write(remaining)
556
+ break
557
+ }
558
+ break
559
+ }
560
+
561
+ if (length <= 4) continue // empty line
562
+
563
+ val dataLen = length - 4
564
+ val data = ByteArray(dataLen)
565
+ var totalRead = 0
566
+ while (totalRead < dataLen) {
567
+ val n = input.read(data, totalRead, dataLen - totalRead)
568
+ if (n < 0) break
569
+ totalRead += n
570
+ }
571
+
572
+ // Check the content
573
+ val text = String(data, Charsets.UTF_8).trim()
574
+ if (text == "NAK" || text.startsWith("ACK ")) continue
575
+
576
+ // Check for side-band encoding
577
+ if (data.isNotEmpty()) {
578
+ val channel = data[0].toInt()
579
+ when (channel) {
580
+ 1 -> {
581
+ // Pack data
582
+ packBytes.write(data, 1, data.size - 1)
583
+ }
584
+ 2 -> {
585
+ // Progress - log and skip
586
+ android.util.Log.d("GitFetch", "progress: ${String(data, 1, data.size - 1).trim()}")
587
+ }
588
+ 3 -> {
589
+ // Error
590
+ val errorMsg = String(data, 1, data.size - 1).trim()
591
+ android.util.Log.e("GitFetch", "server error: $errorMsg")
592
+ throw Exception("Server error during fetch: $errorMsg")
593
+ }
594
+ else -> {
595
+ // No side-band: check if this is raw pack data
596
+ if (data.size >= 4 && data[0] == 'P'.code.toByte() && data[1] == 'A'.code.toByte()
597
+ && data[2] == 'C'.code.toByte() && data[3] == 'K'.code.toByte()) {
598
+ packBytes.write(data)
599
+ } else {
600
+ // Unknown data — might be text response, skip
601
+ android.util.Log.d("GitFetch", "Unknown pkt-line: ${text.take(100)}")
602
+ }
603
+ }
604
+ }
605
+ }
606
+ }
607
+ } catch (e: Exception) {
608
+ android.util.Log.w("GitFetch", "extractPackData: ${e.message}")
609
+ }
610
+
611
+ val result = packBytes.toByteArray()
612
+ return if (result.isEmpty()) null else result
66
613
  }
67
614
 
68
615
  // ─── Git status (JGit) ──────────────────────────────────────────
@@ -249,106 +796,11 @@ internal object GitHelper {
249
796
  return result.toString()
250
797
  }
251
798
 
252
- // ─── Git push (JGit) ────────────────────────────────────────────
253
-
254
- /**
255
- * Push local branch to remote using JGit (efficient native pack building).
256
- * JGit handles pack construction in Java with bounded memory usage,
257
- * avoiding the OOM that isomorphic-git causes on large repos.
258
- */
259
- fun gitPush(
260
- gitRootDir: String,
261
- remoteName: String,
262
- localBranch: String,
263
- remoteBranch: String,
264
- force: Boolean,
265
- headers: String?
266
- ): String {
267
- val result = JSONObject()
268
- try {
269
- val repo = openRepo(gitRootDir)
270
- try {
271
- val git = Git(repo)
272
- val pushCommand = git.push()
273
- .setRemote(remoteName)
274
- .setRefSpecs(RefSpec("refs/heads/$localBranch:$remoteBranch"))
275
- .setForce(force)
276
-
277
- applyHeaders(pushCommand, headers)
278
-
279
- val pushResults = pushCommand.call()
280
-
281
- val resultsArray = JSONArray()
282
- for (pushResult in pushResults) {
283
- for (update in pushResult.remoteUpdates) {
284
- val updateObj = JSONObject()
285
- updateObj.put("remoteName", update.remoteName)
286
- updateObj.put("status", update.status.name)
287
- updateObj.put("message", update.message ?: "")
288
- resultsArray.put(updateObj)
289
- }
290
- }
291
-
292
- result.put("ok", true)
293
- result.put("updates", resultsArray)
294
- android.util.Log.i("GitPush", "Push completed: ${resultsArray.length()} updates")
295
- } finally {
296
- repo.close()
297
- }
298
- } catch (e: Exception) {
299
- result.put("ok", false)
300
- result.put("error", e.message ?: "Unknown push error")
301
- android.util.Log.e("GitPush", "Push failed: ${e.message}", e)
302
- }
303
- return result.toString()
304
- }
305
-
306
- // ─── Git fetch (JGit) ───────────────────────────────────────────
799
+ // ─── Git push (raw HTTP - see above) ──────────────────────────────
800
+ // gitPush() is defined earlier in this file using raw HTTP transport.
307
801
 
308
- /**
309
- * Fetch from remote using JGit (efficient native pack handling).
310
- */
311
- fun gitFetch(
312
- gitRootDir: String,
313
- remoteName: String,
314
- branch: String,
315
- headers: String?
316
- ): String {
317
- val result = JSONObject()
318
- try {
319
- val repo = openRepo(gitRootDir)
320
- try {
321
- val git = Git(repo)
322
- val fetchCommand = git.fetch()
323
- .setRemote(remoteName)
324
- .setRefSpecs(RefSpec("+refs/heads/$branch:refs/remotes/$remoteName/$branch"))
325
-
326
- applyHeaders(fetchCommand, headers)
327
-
328
- val fetchResult = fetchCommand.call()
329
-
330
- val updatesArray = JSONArray()
331
- for (update in fetchResult.trackingRefUpdates) {
332
- val updateObj = JSONObject()
333
- updateObj.put("ref", update.localName)
334
- updateObj.put("oldObjectId", update.oldObjectId?.name ?: "")
335
- updateObj.put("newObjectId", update.newObjectId?.name ?: "")
336
- updatesArray.put(updateObj)
337
- }
338
-
339
- result.put("ok", true)
340
- result.put("updates", updatesArray)
341
- android.util.Log.i("GitFetch", "Fetch completed: ${updatesArray.length()} updates")
342
- } finally {
343
- repo.close()
344
- }
345
- } catch (e: Exception) {
346
- result.put("ok", false)
347
- result.put("error", e.message ?: "Unknown fetch error")
348
- android.util.Log.e("GitFetch", "Fetch failed: ${e.message}", e)
349
- }
350
- return result.toString()
351
- }
802
+ // ─── Git fetch (raw HTTP - see above) ───────────────────────────
803
+ // gitFetch() is defined earlier in this file using raw HTTP transport.
352
804
 
353
805
  // ─── Git checkout changed files (JGit) ──────────────────────────
354
806
 
@@ -586,7 +1038,36 @@ internal object GitHelper {
586
1038
  cloneCommand.setNoTags()
587
1039
  }
588
1040
 
589
- applyHeaders(cloneCommand, headers)
1041
+ // Apply headers for clone (JGit transport is used here since clone
1042
+ // is typically done via archive download; this is just a fallback)
1043
+ if (headers != null) {
1044
+ try {
1045
+ val headerObj = JSONObject(headers)
1046
+ val headerMap = mutableMapOf<String, String>()
1047
+ for (key in headerObj.keys()) headerMap[key] = headerObj.getString(key)
1048
+
1049
+ val authHeader = headerMap["Authorization"] ?: headerMap["authorization"]
1050
+ if (authHeader != null && authHeader.startsWith("Basic ", ignoreCase = true)) {
1051
+ try {
1052
+ val decoded = String(Base64.decode(authHeader.substring(6), Base64.DEFAULT))
1053
+ val colonIndex = decoded.indexOf(':')
1054
+ val username = if (colonIndex >= 0) decoded.substring(0, colonIndex) else ""
1055
+ val password = if (colonIndex >= 0) decoded.substring(colonIndex + 1) else decoded
1056
+ cloneCommand.setCredentialsProvider(UsernamePasswordCredentialsProvider(username, password))
1057
+ } catch (e: Exception) {
1058
+ android.util.Log.w("GitHelper", "Failed to decode Basic auth: ${e.message}")
1059
+ }
1060
+ }
1061
+
1062
+ cloneCommand.setTransportConfigCallback { transport ->
1063
+ if (transport is TransportHttp) {
1064
+ transport.setAdditionalHeaders(headerMap)
1065
+ }
1066
+ }
1067
+ } catch (e: Exception) {
1068
+ android.util.Log.w("GitHelper", "Failed to parse clone headers: ${e.message}")
1069
+ }
1070
+ }
590
1071
 
591
1072
  val git = cloneCommand.call()
592
1073
  val headId = git.repository.resolve(Constants.HEAD)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-tiddlywiki-filesystem-android-external-storage",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "Expo native module for TidGi-Mobile: filesystem I/O + TiddlyWiki .tid/.meta/.json batch parsing + git status in Kotlin (Android) and Swift (iOS)",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",