summaryrefslogtreecommitdiff
path: root/src/luarocks/fetch/git.lua
blob: 29892e92714809d0d3cb25ac50ebb032e3f16875 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165

--- Fetch back-end for retrieving sources from GIT.
local git = {}

local unpack = unpack or table.unpack

local fs = require("luarocks.fs")
local dir = require("luarocks.dir")
local vers = require("luarocks.core.vers")
local util = require("luarocks.util")

local cached_git_version

--- Get git version.
-- @param git_cmd string: name of git command.
-- @return table: git version as returned by luarocks.core.vers.parse_version.
local function git_version(git_cmd)
   if not cached_git_version then
      local version_line = io.popen(fs.Q(git_cmd)..' --version'):read()
      local version_string = version_line:match('%d-%.%d+%.?%d*')
      cached_git_version = vers.parse_version(version_string)
   end

   return cached_git_version
end

--- Check if git satisfies version requirement.
-- @param git_cmd string: name of git command.
-- @param version string: required version.
-- @return boolean: true if git matches version or is newer, false otherwise.
local function git_is_at_least(git_cmd, version)
   return git_version(git_cmd) >= vers.parse_version(version)
end

--- Git >= 1.7.10 can clone a branch **or tag**, < 1.7.10 by branch only. We
-- need to know this in order to build the appropriate command; if we can't
-- clone by tag then we'll have to issue a subsequent command to check out the
-- given tag.
-- @param git_cmd string: name of git command.
-- @return boolean: Whether Git can clone by tag.
local function git_can_clone_by_tag(git_cmd)
   return git_is_at_least(git_cmd, "1.7.10")
end

--- Git >= 1.8.4 can fetch submodules shallowly, saving bandwidth and time for
-- submodules with large history.
-- @param git_cmd string: name of git command.
-- @return boolean: Whether Git can fetch submodules shallowly.
local function git_supports_shallow_submodules(git_cmd)
   return git_is_at_least(git_cmd, "1.8.4")
end

--- Git >= 2.10 can fetch submodules shallowly according to .gitmodules configuration, allowing more granularity.
-- @param git_cmd string: name of git command.
-- @return boolean: Whether Git can fetch submodules shallowly according to .gitmodules.
local function git_supports_shallow_recommendations(git_cmd)
   return git_is_at_least(git_cmd, "2.10.0")
end

local function git_identifier(git_cmd, ver)
   if not (ver:match("^dev%-%d+$") or ver:match("^scm%-%d+$")) then
      return nil
   end
   local pd = io.popen(fs.command_at(fs.current_dir(), fs.Q(git_cmd).." log --pretty=format:%ai_%h -n 1"))
   if not pd then
      return nil
   end
   local date_hash = pd:read("*l")
   pd:close()
   if not date_hash then
      return nil
   end
   local date, time, tz, hash = date_hash:match("([^%s]+) ([^%s]+) ([^%s]+)_([^%s]+)")
   date = date:gsub("%-", "")
   time = time:gsub(":", "")
   return date .. "." .. time .. "." .. hash
end

--- Download sources for building a rock, using git.
-- @param rockspec table: The rockspec table
-- @param extract boolean: Unused in this module (required for API purposes.)
-- @param dest_dir string or nil: If set, will extract to the given directory.
-- @return (string, string) or (nil, string): The absolute pathname of
-- the fetched source tarball and the temporary directory created to
-- store it; or nil and an error message.
function git.get_sources(rockspec, extract, dest_dir, depth)
   assert(rockspec:type() == "rockspec")
   assert(type(dest_dir) == "string" or not dest_dir)

   local git_cmd = rockspec.variables.GIT
   local name_version = rockspec.name .. "-" .. rockspec.version
   local module = dir.base_name(rockspec.source.url)
   -- Strip off .git from base name if present
   module = module:gsub("%.git$", "")

   local ok, err_msg = fs.is_tool_available(git_cmd, "Git")
   if not ok then
      return nil, err_msg
   end

   local store_dir
   if not dest_dir then
      store_dir = fs.make_temp_dir(name_version)
      if not store_dir then
         return nil, "Failed creating temporary directory."
      end
      util.schedule_function(fs.delete, store_dir)
   else
      store_dir = dest_dir
   end
   store_dir = fs.absolute_name(store_dir)
   local ok, err = fs.change_dir(store_dir)
   if not ok then return nil, err end

   local command = {fs.Q(git_cmd), "clone", depth or "--depth=1", rockspec.source.url, module}
   local tag_or_branch = rockspec.source.tag or rockspec.source.branch
   -- If the tag or branch is explicitly set to "master" in the rockspec, then
   -- we can avoid passing it to Git since it's the default.
   if tag_or_branch == "master" then tag_or_branch = nil end
   if tag_or_branch then
      if git_can_clone_by_tag(git_cmd) then
         -- The argument to `--branch` can actually be a branch or a tag as of
         -- Git 1.7.10.
         table.insert(command, 3, "--branch=" .. tag_or_branch)
      end
   end
   if not fs.execute(unpack(command)) then
      return nil, "Failed cloning git repository."
   end
   ok, err = fs.change_dir(module)
   if not ok then return nil, err end
   if tag_or_branch and not git_can_clone_by_tag() then
      if not fs.execute(fs.Q(git_cmd), "checkout", tag_or_branch) then
         return nil, 'Failed to check out the "' .. tag_or_branch ..'" tag or branch.'
      end
   end

   -- Fetching git submodules is supported only when rockspec format is >= 3.0.
   if rockspec:format_is_at_least("3.0") then
      command = {fs.Q(git_cmd), "submodule", "update", "--init", "--recursive"}

      if git_supports_shallow_recommendations(git_cmd) then
         table.insert(command, 5, "--recommend-shallow")
      elseif git_supports_shallow_submodules(git_cmd) then
         -- Fetch only the last commit of each submodule.
         table.insert(command, 5, "--depth=1")
      end

      if not fs.execute(unpack(command)) then
         return nil, 'Failed to fetch submodules.'
      end
   end

   if not rockspec.source.tag then
      rockspec.source.identifier = git_identifier(git_cmd, rockspec.version)
   end

   fs.delete(dir.path(store_dir, module, ".git"))
   fs.delete(dir.path(store_dir, module, ".gitignore"))
   fs.pop_dir()
   fs.pop_dir()
   return module, store_dir
end

return git