Skip to content

Commit f439bdb

Browse files
committed
feat: use github/gitlab tarball download as main method of cloning git
repos
1 parent fc3db45 commit f439bdb

7 files changed

Lines changed: 191 additions & 97 deletions

File tree

packages/lde-core/src/global/init.lua

Lines changed: 161 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,81 @@ local function sanitize(s)
3131
return (string.gsub(s, "[^%w_%-]", "_"))
3232
end
3333

34+
--- Returns "github", "gitlab", or nil if the URL is not a recognized git host.
35+
---@param url string
36+
---@return string?
37+
local function isRecognizedGitHost(url)
38+
if url:match("^https?://github%.com/") then return "github" end
39+
if url:match("^https?://gitlab%.com/") then return "gitlab" end
40+
return nil
41+
end
42+
43+
--- Builds a tarball URL for a recognized git host at a given ref.
44+
---@param url string # git clone URL (may have .git suffix or /tree/... paths)
45+
---@param ref string # commit SHA, branch name, or tag
46+
---@param hostType string # "github" or "gitlab"
47+
---@return string
48+
local function buildTarballUrl(url, ref, hostType)
49+
local base = url:gsub("%.git$", "")
50+
base = base:gsub("/tree/.*$", "")
51+
base = base:gsub("/$", "")
52+
53+
if hostType == "github" then
54+
return base .. "/archive/" .. ref .. ".tar.gz"
55+
elseif hostType == "gitlab" then
56+
local repoName = base:match("/([^/]+)$")
57+
return base .. "/-/archive/" .. ref .. "/" .. repoName .. "-" .. ref .. ".tar.gz"
58+
end
59+
60+
error("Unknown host type: " .. hostType)
61+
end
62+
63+
--- Downloads and extracts a git tarball for a recognized host into repoDir.
64+
---@param url string
65+
---@param commit string
66+
---@param hostType string
67+
---@param repoDir string
68+
---@param label string
69+
local function downloadTarball(url, commit, hostType, repoDir, label)
70+
local tarballUrl = buildTarballUrl(url, commit, hostType)
71+
local bar = lde.verbose and ansi.ProgressBar("Downloading " .. label) or nil
72+
fs.mkdir(repoDir)
73+
74+
local archiveFile = repoDir .. ".archive"
75+
76+
local dlOpts
77+
if bar then
78+
dlOpts = {
79+
progress = function(dltotal, dlnow)
80+
local ratio = dltotal > 0 and (dlnow / dltotal) or nil
81+
local info = dltotal > 0
82+
and (ansi.formatBytes(dlnow) .. " / " .. ansi.formatBytes(dltotal))
83+
or ansi.formatBytes(dlnow)
84+
bar:update(ratio, info)
85+
end
86+
}
87+
end
88+
89+
local ok, dlErr = curl.download(tarballUrl, archiveFile, dlOpts)
90+
if not ok then
91+
fs.rmdir(repoDir)
92+
fs.delete(archiveFile)
93+
if bar then bar:fail("Downloading " .. label) end
94+
error("Failed to download " .. tarballUrl .. ": " .. (dlErr or ""))
95+
end
96+
97+
local ok2, err2 = Archive.new(archiveFile):extract(repoDir, { stripComponents = true })
98+
fs.delete(archiveFile)
99+
100+
if not ok2 then
101+
fs.rmdir(repoDir)
102+
if bar then bar:fail("Downloading " .. label) end
103+
error("Failed to extract " .. label .. ": " .. (err2 or ""))
104+
end
105+
106+
if bar then bar:done("Downloaded " .. label) end
107+
end
108+
34109
---@type string?
35110
local dirOverride = nil
36111

@@ -157,75 +232,78 @@ function global.resolveRegistryVersion(portfile, version)
157232
return latest, versions[latest]
158233
end
159234

160-
--- Builds the cache directory name for a git repo.
161-
--- Format: name, name-branch, or name-branch-commit
235+
--- Builds the cache directory name for a git repo: <name>-<commit>.
162236
---@param repoName string
163-
---@param branch string?
164-
---@param commit string?
237+
---@param commit string
165238
---@return string
166-
function global.getGitRepoDir(repoName, branch, commit)
167-
local parts = { sanitize(repoName) }
168-
169-
if branch then
170-
parts[#parts + 1] = sanitize(branch)
171-
end
172-
173-
if commit then
174-
parts[#parts + 1] = sanitize(commit)
175-
end
176-
177-
local fullName = table.concat(parts, "-")
178-
return path.join(global.getGitCacheDir(), fullName)
239+
function global.getGitRepoDir(repoName, commit)
240+
return path.join(global.getGitCacheDir(), sanitize(repoName) .. "-" .. sanitize(commit))
179241
end
180242

243+
--- Git clone fallback for unrecognized hosts. Always checks out the specific commit.
181244
---@param repoName string
182245
---@param repoUrl string
183-
---@param branch string?
184-
---@param commit string?
246+
---@param commit string
185247
---@param progress fun(stats: table)?
186-
function global.cloneDir(repoName, repoUrl, branch, commit, progress)
187-
local repoDir = global.getGitRepoDir(repoName, branch, commit)
188-
local repo, err = git2.clone(repoUrl, repoDir, branch, nil, progress)
248+
function global.cloneDir(repoName, repoUrl, commit, progress)
249+
local repoDir = global.getGitRepoDir(repoName, commit)
250+
local repo, err = git2.clone(repoUrl, repoDir, nil, nil, progress)
189251
if not repo then return nil, err end
190252
repo:updateSubmodules(nil, progress)
191-
if commit then
192-
local ok, cerr = repo:checkout(commit)
193-
if not ok then return nil, cerr end
194-
end
253+
local ok, cerr = repo:checkout(commit)
254+
if not ok then return nil, cerr end
195255
return true
196256
end
197257

258+
--- Ensures a git repo is cached locally (via tarball for GitHub/GitLab, git clone otherwise).
259+
--- Always resolves to a specific commit. Returns the cache directory and the pinned commit.
198260
---@param repoName string
199261
---@param repoUrl string
200262
---@param branch string?
201263
---@param commit string?
264+
---@return string repoDir
265+
---@return string commit
202266
function global.getOrInitGitRepo(repoName, repoUrl, branch, commit)
203-
local repoDir = global.getGitRepoDir(repoName, branch, commit)
267+
if not commit then
268+
local ref = branch and ("refs/heads/" .. branch) or "HEAD"
269+
local sha, err = git2.lsRemote(repoUrl, ref)
270+
if not sha then
271+
error("Failed to resolve '" .. ref .. "' for " .. repoUrl .. ": " .. (err or ""))
272+
end
273+
commit = sha
274+
end
275+
276+
local repoDir = global.getGitRepoDir(repoName, commit)
204277
if not fs.exists(repoDir) then
205-
local progress
206-
local bar = lde.verbose and ansi.ProgressBar("Cloning " .. repoName) or nil
207-
if bar then
208-
local totalObjs = 0
209-
progress = function(stats)
210-
if stats.total_objects > 0 then
211-
totalObjs = stats.total_objects
278+
local hostType = isRecognizedGitHost(repoUrl)
279+
if hostType then
280+
downloadTarball(repoUrl, commit, hostType, repoDir, repoName)
281+
else
282+
local progress
283+
local bar = lde.verbose and ansi.ProgressBar("Cloning " .. repoName) or nil
284+
if bar then
285+
local totalObjs = 0
286+
progress = function(stats)
287+
if stats.total_objects > 0 then
288+
totalObjs = stats.total_objects
289+
end
290+
local ratio = totalObjs > 0 and (stats.indexed_objects / totalObjs) or nil
291+
local info = totalObjs > 0
292+
and string.format("%d/%d objects", stats.indexed_objects, totalObjs)
293+
or string.format("%d objects, %s", stats.received_objects, ansi.formatBytes(stats.received_bytes))
294+
bar:update(ratio, info)
212295
end
213-
local ratio = totalObjs > 0 and (stats.indexed_objects / totalObjs) or nil
214-
local info = totalObjs > 0
215-
and string.format("%d/%d objects", stats.indexed_objects, totalObjs)
216-
or string.format("%d objects, %s", stats.received_objects, ansi.formatBytes(stats.received_bytes))
217-
bar:update(ratio, info)
218296
end
297+
local ok, err = global.cloneDir(repoName, repoUrl, commit, progress)
298+
if not ok then
299+
if bar then bar:fail("Cloning " .. repoName) end
300+
error("Failed to clone git repository: " .. err)
301+
end
302+
if bar then bar:done("Cloned " .. repoName) end
219303
end
220-
local ok, err = global.cloneDir(repoName, repoUrl, branch, commit, progress)
221-
if not ok then
222-
if bar then bar:fail("Cloning " .. repoName) end
223-
error("Failed to clone git repository: " .. err)
224-
end
225-
if bar then bar:done("Cloned " .. repoName) end
226304
end
227305

228-
return repoDir
306+
return repoDir, commit
229307
end
230308

231309
--- Downloads and extracts an archive URL (.zip, .tar.gz, .tar.bz2, etc.) into the cache.
@@ -305,22 +383,51 @@ function global.repoNameFromUrl(url)
305383
return url:match("([^/]+)%.git$") or url:match("([^/]+)$")
306384
end
307385

308-
--- Clones or retrieves a cached git repo directory (simple name+branch key, no commit).
386+
--- Clones or retrieves a cached git repo directory. Always resolves to a specific commit.
387+
--- Checks existing cache entries first to avoid network where possible.
309388
---@param repoName string
310389
---@param cloneUrl string
311390
---@param branch string?
312391
---@return string repoDir
392+
---@return string commit
313393
function global.getOrCloneRepo(repoName, cloneUrl, branch)
314-
local safeName = branch and (repoName .. "-" .. branch) or repoName
315-
local repoDir = global.getGitRepoDir(safeName)
394+
-- Check for any existing cache entry for this repo name
395+
local prefix = sanitize(repoName) .. "-"
396+
local cacheDir = global.getGitCacheDir()
397+
if fs.isdir(cacheDir) then
398+
for entry in fs.readdir(cacheDir) do
399+
if entry.type == "dir" and entry.name:sub(1, #prefix) == prefix then
400+
local commit = entry.name:sub(#prefix + 1)
401+
return path.join(cacheDir, entry.name), commit
402+
end
403+
end
404+
end
405+
406+
-- No cache hit, resolve and download
407+
local ref = branch and ("refs/heads/" .. branch) or "HEAD"
408+
local commit, err = git2.lsRemote(cloneUrl, ref)
409+
if not commit then
410+
error("Failed to resolve ref for " .. cloneUrl .. ": " .. (err or ""))
411+
end
412+
413+
local repoDir = global.getGitRepoDir(repoName, commit)
316414
if not fs.exists(repoDir) then
317-
local repo, err = git2.clone(cloneUrl, repoDir, branch)
318-
if not repo then
319-
error("Failed to clone git repository: " .. (err or "unknown error"))
415+
local hostType = isRecognizedGitHost(cloneUrl)
416+
if hostType then
417+
downloadTarball(cloneUrl, commit, hostType, repoDir, repoName)
418+
else
419+
local repo, cerr = git2.clone(cloneUrl, repoDir, branch)
420+
if not repo then
421+
error("Failed to clone git repository: " .. (cerr or "unknown error"))
422+
end
423+
repo:updateSubmodules()
424+
local ok, cerr2 = repo:checkout(commit)
425+
if not ok then
426+
error("Failed to checkout commit: " .. (cerr2 or "unknown error"))
427+
end
320428
end
321-
repo:updateSubmodules()
322429
end
323-
return repoDir
430+
return repoDir, commit
324431
end
325432

326433
--- Finds a named package inside a directory by scanning for lde.json files.

packages/lde-core/src/package/init.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ function Package:getDependencyPath(dir, info, relativeTo)
189189
relativeTo = relativeTo or self.dir
190190

191191
if info.git then
192-
return global.getGitRepoDir(dir, info.branch, info.commit)
192+
return global.getGitRepoDir(dir, info.commit)
193193
elseif info.path then
194194
return path.normalize(path.join(relativeTo, info.path))
195195
elseif info.archive then

packages/lde-core/src/package/install/git.lua

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,14 @@
11
local fs = require("fs")
22
local path = require("path")
3-
local git2 = require("git2-sys")
43
local lde = require("lde-core")
54

65
---@param packageName string
76
---@param depInfo lde.Package.Config.GitDependency
87
---@return lde.Package, lde.Lockfile.GitDependency
98
local function resolve(packageName, depInfo)
10-
local repoDir = lde.global.getOrInitGitRepo(packageName, depInfo.git, depInfo.branch, depInfo.commit)
11-
12-
local resolvedCommit = depInfo.commit
13-
if not resolvedCommit then
14-
local repo, openErr = git2.open(repoDir)
15-
if not repo then
16-
error("Failed to resolve HEAD commit for git dependency: " .. (openErr or ""))
17-
end
18-
local sha, revErr = repo:revparse("HEAD")
19-
if not sha then
20-
error("Failed to resolve HEAD commit for git dependency: " .. (revErr or ""))
21-
end
22-
resolvedCommit = sha
23-
end
9+
local repoDir, resolvedCommit = lde.global.getOrInitGitRepo(
10+
packageName, depInfo.git, depInfo.branch, depInfo.commit
11+
)
2412

2513
---@type lde.Lockfile.GitDependency
2614
local lockEntry = {

packages/lde-core/src/package/update.lua

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,27 @@ local luarocks = require("luarocks")
77
local global = require("lde-core.global")
88
local util = require("lde-core.util")
99

10-
--- Updates a single git dependency by pulling latest changes.
11-
--- Only applies to git dependencies without a pinned commit.
10+
--- Checks a git dependency for newer commits via ls-remote.
1211
---@param name string
1312
---@param depInfo lde.Package.Config.GitDependency
1413
---@return boolean updated
1514
---@return string message
1615
local function updateGitDependency(name, depInfo)
17-
if depInfo.commit then
18-
return false, "skipped (pinned to commit)"
16+
local ref = depInfo.branch and ("refs/heads/" .. depInfo.branch) or "HEAD"
17+
local latestCommit, err = git2.lsRemote(depInfo.git, ref)
18+
if not latestCommit then
19+
return false, "failed: " .. (err or "unknown error")
1920
end
2021

21-
local repoDir = global.getGitRepoDir(name, depInfo.branch, depInfo.commit)
22-
if not fs.exists(repoDir) then
23-
return false, "skipped (not installed)"
22+
if depInfo.commit and latestCommit == depInfo.commit then
23+
return false, "already up to date (" .. latestCommit:sub(1, 7) .. ")"
2424
end
2525

26-
local repo, openErr = git2.open(repoDir)
27-
if not repo then
28-
return false, "failed: " .. (openErr or "unknown error")
29-
end
30-
31-
local ok, pullErr = repo:pull()
32-
if not ok then
33-
return false, "failed: " .. (pullErr or "unknown error")
34-
end
26+
local msg = depInfo.commit
27+
and (depInfo.commit:sub(1, 7) .. " -> " .. latestCommit:sub(1, 7))
28+
or ("at " .. latestCommit:sub(1, 7))
3529

36-
return true, "updated"
30+
return true, msg
3731
end
3832

3933
--- Updates a registry dependency to the latest compatible version (same major).

packages/lde-core/src/util/init.lua

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ local util = {}
22

33
local fs = require("fs")
44
local path = require("path")
5-
local git2 = require("git2-sys")
65
local json = require("json")
76
local rocked = require("rocked")
87
local ansi = require("ansi")
@@ -126,10 +125,8 @@ function util.openRockspecUrl(name, url, branch, commit)
126125
local dir, lockEntry
127126
if sourceUrl:match("^git") then
128127
sourceUrl = util.normalizeGitUrl(sourceUrl)
129-
dir = lde.global.getOrInitGitRepo(name, sourceUrl, branch or sourceTag, commit)
130-
local repo = git2.open(dir)
131-
local sha = repo and repo:revparse("HEAD")
132-
lockEntry = { git = sourceUrl, commit = sha or commit, rockspec = url }
128+
dir, commit = lde.global.getOrInitGitRepo(name, sourceUrl, branch or sourceTag, commit)
129+
lockEntry = { git = sourceUrl, commit = commit, rockspec = url }
133130
elseif sourceUrl:match("^https?://") then
134131
dir = lde.global.getOrInitArchive(sourceUrl)
135132
lockEntry = { archive = sourceUrl, rockspec = url }

packages/lde-core/tests/main.test.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ test.it("git dep: installs root package, not a sub-package, when repo has lde.js
369369
-- Pinning a fake commit in the dep skips the getCommitHash call entirely,
370370
-- keeping the test self-contained (no real git repo needed).
371371
local fakeCommit = "abc1234567890abcdef1234567890abcdef123456"
372-
local repoDir = lde.global.getGitRepoDir("my-root-pkg", nil, fakeCommit)
372+
local repoDir = lde.global.getGitRepoDir("my-root-pkg", fakeCommit)
373373
fs.rmdir(repoDir)
374374
fs.mkdir(repoDir)
375375

0 commit comments

Comments
 (0)