expo-tiddlywiki-filesystem-android-external-storage 2.5.0 → 2.7.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,25 +4,20 @@ 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
|
|
8
7
|
import org.eclipse.jgit.lib.Constants
|
|
9
|
-
import org.eclipse.jgit.lib.NullProgressMonitor
|
|
10
8
|
import org.eclipse.jgit.lib.ObjectId
|
|
11
9
|
import org.eclipse.jgit.lib.Repository
|
|
12
10
|
import org.eclipse.jgit.revwalk.RevWalk
|
|
13
11
|
import org.eclipse.jgit.storage.file.FileRepositoryBuilder
|
|
14
|
-
import org.eclipse.jgit.transport.
|
|
12
|
+
import org.eclipse.jgit.transport.RefSpec
|
|
15
13
|
import org.eclipse.jgit.transport.TransportHttp
|
|
16
14
|
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider
|
|
17
15
|
import org.eclipse.jgit.treewalk.TreeWalk
|
|
16
|
+
import org.eclipse.jgit.treewalk.filter.PathFilterGroup
|
|
18
17
|
import org.json.JSONArray
|
|
19
18
|
import org.json.JSONObject
|
|
20
|
-
import java.io.ByteArrayInputStream
|
|
21
19
|
import java.io.ByteArrayOutputStream
|
|
22
20
|
import java.io.File
|
|
23
|
-
import java.io.InputStream
|
|
24
|
-
import java.net.HttpURLConnection
|
|
25
|
-
import java.net.URL
|
|
26
21
|
import android.util.Base64
|
|
27
22
|
|
|
28
23
|
/**
|
|
@@ -49,567 +44,90 @@ internal object GitHelper {
|
|
|
49
44
|
.build()
|
|
50
45
|
}
|
|
51
46
|
|
|
52
|
-
|
|
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,
|
|
47
|
+
/** Apply custom HTTP headers and credentials to a JGit transport command. */
|
|
48
|
+
private fun applyHeaders(
|
|
49
|
+
command: org.eclipse.jgit.api.TransportCommand<*, *>,
|
|
157
50
|
headers: String?
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
51
|
+
) {
|
|
52
|
+
// Parse custom headers JSON
|
|
53
|
+
val headerMap = mutableMapOf<String, String>()
|
|
54
|
+
if (headers != null) {
|
|
162
55
|
try {
|
|
163
|
-
val
|
|
164
|
-
|
|
165
|
-
|
|
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()
|
|
56
|
+
val headerObj = JSONObject(headers)
|
|
57
|
+
for (key in headerObj.keys()) {
|
|
58
|
+
headerMap[key] = headerObj.getString(key)
|
|
183
59
|
}
|
|
184
|
-
|
|
185
|
-
|
|
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()
|
|
60
|
+
} catch (e: Exception) {
|
|
61
|
+
android.util.Log.w("GitHelper", "Failed to parse headers: ${e.message}")
|
|
269
62
|
}
|
|
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
63
|
}
|
|
275
|
-
return result.toString()
|
|
276
|
-
}
|
|
277
64
|
|
|
278
|
-
|
|
279
|
-
val
|
|
280
|
-
|
|
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")
|
|
299
|
-
}
|
|
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)
|
|
65
|
+
// Extract Basic Auth from Authorization header and set as CredentialsProvider.
|
|
66
|
+
val authHeader = headerMap["Authorization"] ?: headerMap["authorization"]
|
|
67
|
+
if (authHeader != null && authHeader.startsWith("Basic ", ignoreCase = true)) {
|
|
328
68
|
try {
|
|
329
|
-
val
|
|
330
|
-
val
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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()
|
|
69
|
+
val decoded = String(Base64.decode(authHeader.substring(6), Base64.DEFAULT))
|
|
70
|
+
val colonIndex = decoded.indexOf(':')
|
|
71
|
+
val username = if (colonIndex >= 0) decoded.substring(0, colonIndex) else ""
|
|
72
|
+
val password = if (colonIndex >= 0) decoded.substring(colonIndex + 1) else decoded
|
|
73
|
+
command.setCredentialsProvider(UsernamePasswordCredentialsProvider(username, password))
|
|
74
|
+
} catch (e: Exception) {
|
|
75
|
+
android.util.Log.w("GitHelper", "Failed to decode Basic auth: ${e.message}")
|
|
465
76
|
}
|
|
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
77
|
}
|
|
471
|
-
return result.toString()
|
|
472
|
-
}
|
|
473
78
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
|
490
|
-
}
|
|
79
|
+
command.setTransportConfigCallback { transport ->
|
|
80
|
+
if (transport is TransportHttp) {
|
|
81
|
+
transport.setAdditionalHeaders(headerMap)
|
|
491
82
|
}
|
|
492
|
-
var count = 0
|
|
493
|
-
for (commit in walk) {
|
|
494
|
-
oids.add(commit.id)
|
|
495
|
-
if (++count >= maxCount) break
|
|
496
|
-
}
|
|
497
|
-
walk.close()
|
|
498
|
-
} catch (e: Exception) {
|
|
499
|
-
android.util.Log.w("GitFetch", "collectHaveOids error: ${e.message}")
|
|
500
83
|
}
|
|
501
|
-
return oids
|
|
502
84
|
}
|
|
503
85
|
|
|
504
86
|
/**
|
|
505
|
-
*
|
|
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.
|
|
87
|
+
* Ensure repository git config forces protocol.version=0 and
|
|
88
|
+
* constrains pack memory for Android's limited heap.
|
|
517
89
|
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
90
|
+
* TidGi Desktop's git server uses `git --stateless-rpc` which only speaks V0/V1.
|
|
91
|
+
* JGit defaults to V2 negotiation, which causes the error:
|
|
92
|
+
* "Starting read stage without written request data pending is not supported"
|
|
93
|
+
* because the server doesn't handle V2 capability advertisements.
|
|
521
94
|
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
* - 3 = error messages
|
|
526
|
-
*
|
|
527
|
-
* We need to handle both side-band and non-side-band responses.
|
|
95
|
+
* Pack memory limits prevent OOM on large repos (Android heap is ~268MB).
|
|
96
|
+
* Default JGit settings: deltaCacheSize=50MB, windowMemory=unlimited,
|
|
97
|
+
* bigFileThreshold=50MB — far too much for a mobile device.
|
|
528
98
|
*/
|
|
529
|
-
private fun
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
val
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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}")
|
|
99
|
+
private fun ensureProtocolV0(repo: Repository) {
|
|
100
|
+
val config = repo.config
|
|
101
|
+
var dirty = false
|
|
102
|
+
val current = config.getString("protocol", null, "version")
|
|
103
|
+
if (current != "0") {
|
|
104
|
+
config.setString("protocol", null, "version", "0")
|
|
105
|
+
dirty = true
|
|
106
|
+
}
|
|
107
|
+
// Limit pack memory to avoid OOM on push
|
|
108
|
+
// pack.windowMemory: max bytes for delta search window (per thread), default unlimited
|
|
109
|
+
if (config.getLong("pack", "windowmemory", 0) == 0L) {
|
|
110
|
+
config.setLong("pack", null, "windowmemory", 10L * 1024 * 1024) // 10MB
|
|
111
|
+
dirty = true
|
|
112
|
+
}
|
|
113
|
+
// pack.deltaCacheSize: total delta cache, default 50MB
|
|
114
|
+
if (config.getLong("pack", "deltacachesize", 50L * 1024 * 1024) >= 50L * 1024 * 1024) {
|
|
115
|
+
config.setLong("pack", null, "deltacachesize", 5L * 1024 * 1024) // 5MB
|
|
116
|
+
dirty = true
|
|
117
|
+
}
|
|
118
|
+
// pack.threads: limit to 1 to reduce memory pressure
|
|
119
|
+
if (config.getInt("pack", "threads", 0) == 0) {
|
|
120
|
+
config.setInt("pack", null, "threads", 1)
|
|
121
|
+
dirty = true
|
|
122
|
+
}
|
|
123
|
+
// pack.window: reduce from 10 to 5
|
|
124
|
+
if (config.getInt("pack", "window", 10) > 5) {
|
|
125
|
+
config.setInt("pack", null, "window", 5)
|
|
126
|
+
dirty = true
|
|
127
|
+
}
|
|
128
|
+
if (dirty) {
|
|
129
|
+
config.save()
|
|
609
130
|
}
|
|
610
|
-
|
|
611
|
-
val result = packBytes.toByteArray()
|
|
612
|
-
return if (result.isEmpty()) null else result
|
|
613
131
|
}
|
|
614
132
|
|
|
615
133
|
// ─── Git status (JGit) ──────────────────────────────────────────
|
|
@@ -796,11 +314,108 @@ internal object GitHelper {
|
|
|
796
314
|
return result.toString()
|
|
797
315
|
}
|
|
798
316
|
|
|
799
|
-
// ─── Git push (
|
|
800
|
-
// gitPush() is defined earlier in this file using raw HTTP transport.
|
|
317
|
+
// ─── Git push (JGit) ────────────────────────────────────────────
|
|
801
318
|
|
|
802
|
-
|
|
803
|
-
|
|
319
|
+
/**
|
|
320
|
+
* Push local branch to remote using JGit (efficient native pack building).
|
|
321
|
+
* JGit handles pack construction in Java with bounded memory usage,
|
|
322
|
+
* avoiding the OOM that isomorphic-git causes on large repos.
|
|
323
|
+
*/
|
|
324
|
+
fun gitPush(
|
|
325
|
+
gitRootDir: String,
|
|
326
|
+
remoteName: String,
|
|
327
|
+
localBranch: String,
|
|
328
|
+
remoteBranch: String,
|
|
329
|
+
force: Boolean,
|
|
330
|
+
headers: String?
|
|
331
|
+
): String {
|
|
332
|
+
val result = JSONObject()
|
|
333
|
+
try {
|
|
334
|
+
val repo = openRepo(gitRootDir)
|
|
335
|
+
try {
|
|
336
|
+
ensureProtocolV0(repo)
|
|
337
|
+
val git = Git(repo)
|
|
338
|
+
val pushCommand = git.push()
|
|
339
|
+
.setRemote(remoteName)
|
|
340
|
+
.setRefSpecs(RefSpec("refs/heads/$localBranch:$remoteBranch"))
|
|
341
|
+
.setForce(force)
|
|
342
|
+
|
|
343
|
+
applyHeaders(pushCommand, headers)
|
|
344
|
+
|
|
345
|
+
val pushResults = pushCommand.call()
|
|
346
|
+
|
|
347
|
+
val resultsArray = JSONArray()
|
|
348
|
+
for (pushResult in pushResults) {
|
|
349
|
+
for (update in pushResult.remoteUpdates) {
|
|
350
|
+
val updateObj = JSONObject()
|
|
351
|
+
updateObj.put("remoteName", update.remoteName)
|
|
352
|
+
updateObj.put("status", update.status.name)
|
|
353
|
+
updateObj.put("message", update.message ?: "")
|
|
354
|
+
resultsArray.put(updateObj)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
result.put("ok", true)
|
|
359
|
+
result.put("updates", resultsArray)
|
|
360
|
+
android.util.Log.i("GitPush", "Push completed: ${resultsArray.length()} updates")
|
|
361
|
+
} finally {
|
|
362
|
+
repo.close()
|
|
363
|
+
}
|
|
364
|
+
} catch (e: Exception) {
|
|
365
|
+
result.put("ok", false)
|
|
366
|
+
result.put("error", e.message ?: "Unknown push error")
|
|
367
|
+
android.util.Log.e("GitPush", "Push failed: ${e.message}", e)
|
|
368
|
+
}
|
|
369
|
+
return result.toString()
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ─── Git fetch (JGit) ───────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Fetch from remote using JGit (efficient native pack handling).
|
|
376
|
+
*/
|
|
377
|
+
fun gitFetch(
|
|
378
|
+
gitRootDir: String,
|
|
379
|
+
remoteName: String,
|
|
380
|
+
branch: String,
|
|
381
|
+
headers: String?
|
|
382
|
+
): String {
|
|
383
|
+
val result = JSONObject()
|
|
384
|
+
try {
|
|
385
|
+
val repo = openRepo(gitRootDir)
|
|
386
|
+
try {
|
|
387
|
+
ensureProtocolV0(repo)
|
|
388
|
+
val git = Git(repo)
|
|
389
|
+
val fetchCommand = git.fetch()
|
|
390
|
+
.setRemote(remoteName)
|
|
391
|
+
.setRefSpecs(RefSpec("+refs/heads/$branch:refs/remotes/$remoteName/$branch"))
|
|
392
|
+
|
|
393
|
+
applyHeaders(fetchCommand, headers)
|
|
394
|
+
|
|
395
|
+
val fetchResult = fetchCommand.call()
|
|
396
|
+
|
|
397
|
+
val updatesArray = JSONArray()
|
|
398
|
+
for (update in fetchResult.trackingRefUpdates) {
|
|
399
|
+
val updateObj = JSONObject()
|
|
400
|
+
updateObj.put("ref", update.localName)
|
|
401
|
+
updateObj.put("oldObjectId", update.oldObjectId?.name ?: "")
|
|
402
|
+
updateObj.put("newObjectId", update.newObjectId?.name ?: "")
|
|
403
|
+
updatesArray.put(updateObj)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
result.put("ok", true)
|
|
407
|
+
result.put("updates", updatesArray)
|
|
408
|
+
android.util.Log.i("GitFetch", "Fetch completed: ${updatesArray.length()} updates")
|
|
409
|
+
} finally {
|
|
410
|
+
repo.close()
|
|
411
|
+
}
|
|
412
|
+
} catch (e: Exception) {
|
|
413
|
+
result.put("ok", false)
|
|
414
|
+
result.put("error", e.message ?: "Unknown fetch error")
|
|
415
|
+
android.util.Log.e("GitFetch", "Fetch failed: ${e.message}", e)
|
|
416
|
+
}
|
|
417
|
+
return result.toString()
|
|
418
|
+
}
|
|
804
419
|
|
|
805
420
|
// ─── Git checkout changed files (JGit) ──────────────────────────
|
|
806
421
|
|
|
@@ -1038,36 +653,7 @@ internal object GitHelper {
|
|
|
1038
653
|
cloneCommand.setNoTags()
|
|
1039
654
|
}
|
|
1040
655
|
|
|
1041
|
-
|
|
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
|
-
}
|
|
656
|
+
applyHeaders(cloneCommand, headers)
|
|
1071
657
|
|
|
1072
658
|
val git = cloneCommand.call()
|
|
1073
659
|
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.
|
|
3
|
+
"version": "2.7.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",
|