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.
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
158
|
+
): String {
|
|
159
|
+
val result = JSONObject()
|
|
52
160
|
try {
|
|
53
|
-
val
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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("
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|