blob: 9bd87ad25cc38cc1ef7f6db06eca368694c440db [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake 'Fetch' git implementation
5
6git fetcher support the SRC_URI with format of:
7SRC_URI = "git://some.host/somepath;OptionA=xxx;OptionB=xxx;..."
8
9Supported SRC_URI options are:
10
11- branch
12 The git branch to retrieve from. The default is "master"
13
14 This option also supports multiple branch fetching, with branches
15 separated by commas. In multiple branches case, the name option
16 must have the same number of names to match the branches, which is
17 used to specify the SRC_REV for the branch
18 e.g:
19 SRC_URI="git://some.host/somepath;branch=branchX,branchY;name=nameX,nameY"
20 SRCREV_nameX = "xxxxxxxxxxxxxxxxxxxx"
21 SRCREV_nameY = "YYYYYYYYYYYYYYYYYYYY"
22
23- tag
24 The git tag to retrieve. The default is "master"
25
26- protocol
27 The method to use to access the repository. Common options are "git",
28 "http", "https", "file", "ssh" and "rsync". The default is "git".
29
30- rebaseable
31 rebaseable indicates that the upstream git repo may rebase in the future,
32 and current revision may disappear from upstream repo. This option will
33 remind fetcher to preserve local cache carefully for future use.
34 The default value is "0", set rebaseable=1 for rebaseable git repo.
35
36- nocheckout
37 Don't checkout source code when unpacking. set this option for the recipe
38 who has its own routine to checkout code.
39 The default is "0", set nocheckout=1 if needed.
40
41- bareclone
42 Create a bare clone of the source code and don't checkout the source code
43 when unpacking. Set this option for the recipe who has its own routine to
44 checkout code and tracking branch requirements.
45 The default is "0", set bareclone=1 if needed.
46
47- nobranch
48 Don't check the SHA validation for branch. set this option for the recipe
49 referring to commit which is valid in tag instead of branch.
50 The default is "0", set nobranch=1 if needed.
51
52"""
53
54#Copyright (C) 2005 Richard Purdie
55#
56# This program is free software; you can redistribute it and/or modify
57# it under the terms of the GNU General Public License version 2 as
58# published by the Free Software Foundation.
59#
60# This program is distributed in the hope that it will be useful,
61# but WITHOUT ANY WARRANTY; without even the implied warranty of
62# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
63# GNU General Public License for more details.
64#
65# You should have received a copy of the GNU General Public License along
66# with this program; if not, write to the Free Software Foundation, Inc.,
67# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
68
Patrick Williamsd7e96312015-09-22 08:09:05 -050069import errno
Patrick Williamsc124f4f2015-09-15 14:41:29 -050070import os
71import re
72import bb
73from bb import data
74from bb.fetch2 import FetchMethod
75from bb.fetch2 import runfetchcmd
76from bb.fetch2 import logger
77
78class Git(FetchMethod):
79 """Class to fetch a module or modules from git repositories"""
80 def init(self, d):
81 pass
82
83 def supports(self, ud, d):
84 """
85 Check to see if a given url can be fetched with git.
86 """
87 return ud.type in ['git']
88
89 def supports_checksum(self, urldata):
90 return False
91
92 def urldata_init(self, ud, d):
93 """
94 init git specific variable within url data
95 so that the git method like latest_revision() can work
96 """
97 if 'protocol' in ud.parm:
98 ud.proto = ud.parm['protocol']
99 elif not ud.host:
100 ud.proto = 'file'
101 else:
102 ud.proto = "git"
103
104 if not ud.proto in ('git', 'file', 'ssh', 'http', 'https', 'rsync'):
105 raise bb.fetch2.ParameterError("Invalid protocol type", ud.url)
106
107 ud.nocheckout = ud.parm.get("nocheckout","0") == "1"
108
109 ud.rebaseable = ud.parm.get("rebaseable","0") == "1"
110
111 ud.nobranch = ud.parm.get("nobranch","0") == "1"
112
113 # bareclone implies nocheckout
114 ud.bareclone = ud.parm.get("bareclone","0") == "1"
115 if ud.bareclone:
116 ud.nocheckout = 1
117
118 ud.unresolvedrev = {}
119 branches = ud.parm.get("branch", "master").split(',')
120 if len(branches) != len(ud.names):
121 raise bb.fetch2.ParameterError("The number of name and branch parameters is not balanced", ud.url)
122 ud.branches = {}
123 for name in ud.names:
124 branch = branches[ud.names.index(name)]
125 ud.branches[name] = branch
126 ud.unresolvedrev[name] = branch
127
128 ud.basecmd = data.getVar("FETCHCMD_git", d, True) or "git -c core.fsyncobjectfiles=0"
129
130 ud.write_tarballs = ((data.getVar("BB_GENERATE_MIRROR_TARBALLS", d, True) or "0") != "0") or ud.rebaseable
131
132 ud.setup_revisons(d)
133
134 for name in ud.names:
135 # Ensure anything that doesn't look like a sha256 checksum/revision is translated into one
136 if not ud.revisions[name] or len(ud.revisions[name]) != 40 or (False in [c in "abcdef0123456789" for c in ud.revisions[name]]):
137 if ud.revisions[name]:
138 ud.unresolvedrev[name] = ud.revisions[name]
139 ud.revisions[name] = self.latest_revision(ud, d, name)
140
141 gitsrcname = '%s%s' % (ud.host.replace(':', '.'), ud.path.replace('/', '.').replace('*', '.'))
142 if gitsrcname.startswith('.'):
143 gitsrcname = gitsrcname[1:]
144
145 # for rebaseable git repo, it is necessary to keep mirror tar ball
146 # per revision, so that even the revision disappears from the
147 # upstream repo in the future, the mirror will remain intact and still
148 # contains the revision
149 if ud.rebaseable:
150 for name in ud.names:
151 gitsrcname = gitsrcname + '_' + ud.revisions[name]
152 ud.mirrortarball = 'git2_%s.tar.gz' % (gitsrcname)
153 ud.fullmirror = os.path.join(d.getVar("DL_DIR", True), ud.mirrortarball)
154 gitdir = d.getVar("GITDIR", True) or (d.getVar("DL_DIR", True) + "/git2/")
155 ud.clonedir = os.path.join(gitdir, gitsrcname)
156
157 ud.localfile = ud.clonedir
158
159 def localpath(self, ud, d):
160 return ud.clonedir
161
162 def need_update(self, ud, d):
163 if not os.path.exists(ud.clonedir):
164 return True
165 os.chdir(ud.clonedir)
166 for name in ud.names:
167 if not self._contains_ref(ud, d, name):
168 return True
169 if ud.write_tarballs and not os.path.exists(ud.fullmirror):
170 return True
171 return False
172
173 def try_premirror(self, ud, d):
174 # If we don't do this, updating an existing checkout with only premirrors
175 # is not possible
176 if d.getVar("BB_FETCH_PREMIRRORONLY", True) is not None:
177 return True
178 if os.path.exists(ud.clonedir):
179 return False
180 return True
181
182 def download(self, ud, d):
183 """Fetch url"""
184
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500185 # If the checkout doesn't exist and the mirror tarball does, extract it
186 if not os.path.exists(ud.clonedir) and os.path.exists(ud.fullmirror):
187 bb.utils.mkdirhier(ud.clonedir)
188 os.chdir(ud.clonedir)
189 runfetchcmd("tar -xzf %s" % (ud.fullmirror), d)
190
191 repourl = self._get_repo_url(ud)
192
193 # If the repo still doesn't exist, fallback to cloning it
194 if not os.path.exists(ud.clonedir):
195 # We do this since git will use a "-l" option automatically for local urls where possible
196 if repourl.startswith("file://"):
197 repourl = repourl[7:]
198 clone_cmd = "%s clone --bare --mirror %s %s" % (ud.basecmd, repourl, ud.clonedir)
199 if ud.proto.lower() != 'file':
200 bb.fetch2.check_network_access(d, clone_cmd)
201 runfetchcmd(clone_cmd, d)
202
203 os.chdir(ud.clonedir)
204 # Update the checkout if needed
205 needupdate = False
206 for name in ud.names:
207 if not self._contains_ref(ud, d, name):
208 needupdate = True
209 if needupdate:
210 try:
211 runfetchcmd("%s remote rm origin" % ud.basecmd, d)
212 except bb.fetch2.FetchError:
213 logger.debug(1, "No Origin")
214
215 runfetchcmd("%s remote add --mirror=fetch origin %s" % (ud.basecmd, repourl), d)
216 fetch_cmd = "%s fetch -f --prune %s refs/*:refs/*" % (ud.basecmd, repourl)
217 if ud.proto.lower() != 'file':
218 bb.fetch2.check_network_access(d, fetch_cmd, ud.url)
219 runfetchcmd(fetch_cmd, d)
220 runfetchcmd("%s prune-packed" % ud.basecmd, d)
221 runfetchcmd("%s pack-redundant --all | xargs -r rm" % ud.basecmd, d)
Patrick Williamsd7e96312015-09-22 08:09:05 -0500222 try:
223 os.unlink(ud.fullmirror)
224 except OSError as exc:
225 if exc.errno != errno.ENOENT:
226 raise
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500227 os.chdir(ud.clonedir)
228 for name in ud.names:
229 if not self._contains_ref(ud, d, name):
230 raise bb.fetch2.FetchError("Unable to find revision %s in branch %s even from upstream" % (ud.revisions[name], ud.branches[name]))
231
232 def build_mirror_data(self, ud, d):
233 # Generate a mirror tarball if needed
Patrick Williamsd7e96312015-09-22 08:09:05 -0500234 if ud.write_tarballs and not os.path.exists(ud.fullmirror):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500235 # it's possible that this symlink points to read-only filesystem with PREMIRROR
236 if os.path.islink(ud.fullmirror):
237 os.unlink(ud.fullmirror)
238
239 os.chdir(ud.clonedir)
240 logger.info("Creating tarball of git repository")
241 runfetchcmd("tar -czf %s %s" % (ud.fullmirror, os.path.join(".") ), d)
242 runfetchcmd("touch %s.done" % (ud.fullmirror), d)
243
244 def unpack(self, ud, destdir, d):
245 """ unpack the downloaded src to destdir"""
246
247 subdir = ud.parm.get("subpath", "")
248 if subdir != "":
249 readpathspec = ":%s" % (subdir)
250 def_destsuffix = "%s/" % os.path.basename(subdir.rstrip('/'))
251 else:
252 readpathspec = ""
253 def_destsuffix = "git/"
254
255 destsuffix = ud.parm.get("destsuffix", def_destsuffix)
256 destdir = ud.destdir = os.path.join(destdir, destsuffix)
257 if os.path.exists(destdir):
258 bb.utils.prunedir(destdir)
259
260 cloneflags = "-s -n"
261 if ud.bareclone:
262 cloneflags += " --mirror"
263
264 # Versions of git prior to 1.7.9.2 have issues where foo.git and foo get confused
265 # and you end up with some horrible union of the two when you attempt to clone it
266 # The least invasive workaround seems to be a symlink to the real directory to
267 # fool git into ignoring any .git version that may also be present.
268 #
269 # The issue is fixed in more recent versions of git so we can drop this hack in future
270 # when that version becomes common enough.
271 clonedir = ud.clonedir
272 if not ud.path.endswith(".git"):
273 indirectiondir = destdir[:-1] + ".indirectionsymlink"
274 if os.path.exists(indirectiondir):
275 os.remove(indirectiondir)
276 bb.utils.mkdirhier(os.path.dirname(indirectiondir))
277 os.symlink(ud.clonedir, indirectiondir)
278 clonedir = indirectiondir
279
280 runfetchcmd("%s clone %s %s/ %s" % (ud.basecmd, cloneflags, clonedir, destdir), d)
281 os.chdir(destdir)
282 repourl = self._get_repo_url(ud)
283 runfetchcmd("%s remote set-url origin %s" % (ud.basecmd, repourl), d)
284 if not ud.nocheckout:
285 if subdir != "":
286 runfetchcmd("%s read-tree %s%s" % (ud.basecmd, ud.revisions[ud.names[0]], readpathspec), d)
287 runfetchcmd("%s checkout-index -q -f -a" % ud.basecmd, d)
288 elif not ud.nobranch:
289 branchname = ud.branches[ud.names[0]]
290 runfetchcmd("%s checkout -B %s %s" % (ud.basecmd, branchname, \
291 ud.revisions[ud.names[0]]), d)
292 runfetchcmd("%s branch --set-upstream %s origin/%s" % (ud.basecmd, branchname, \
293 branchname), d)
294 else:
295 runfetchcmd("%s checkout %s" % (ud.basecmd, ud.revisions[ud.names[0]]), d)
296
297 return True
298
299 def clean(self, ud, d):
300 """ clean the git directory """
301
302 bb.utils.remove(ud.localpath, True)
303 bb.utils.remove(ud.fullmirror)
304 bb.utils.remove(ud.fullmirror + ".done")
305
306 def supports_srcrev(self):
307 return True
308
309 def _contains_ref(self, ud, d, name):
310 cmd = ""
311 if ud.nobranch:
312 cmd = "%s log --pretty=oneline -n 1 %s -- 2> /dev/null | wc -l" % (
313 ud.basecmd, ud.revisions[name])
314 else:
315 cmd = "%s branch --contains %s --list %s 2> /dev/null | wc -l" % (
316 ud.basecmd, ud.revisions[name], ud.branches[name])
317 try:
318 output = runfetchcmd(cmd, d, quiet=True)
319 except bb.fetch2.FetchError:
320 return False
321 if len(output.split()) > 1:
322 raise bb.fetch2.FetchError("The command '%s' gave output with more then 1 line unexpectedly, output: '%s'" % (cmd, output))
323 return output.split()[0] != "0"
324
325 def _get_repo_url(self, ud):
326 """
327 Return the repository URL
328 """
329 if ud.user:
330 username = ud.user + '@'
331 else:
332 username = ""
333 return "%s://%s%s%s" % (ud.proto, username, ud.host, ud.path)
334
335 def _revision_key(self, ud, d, name):
336 """
337 Return a unique key for the url
338 """
339 return "git:" + ud.host + ud.path.replace('/', '.') + ud.unresolvedrev[name]
340
341 def _lsremote(self, ud, d, search):
342 """
343 Run git ls-remote with the specified search string
344 """
345 repourl = self._get_repo_url(ud)
346 cmd = "%s ls-remote %s %s" % \
347 (ud.basecmd, repourl, search)
348 if ud.proto.lower() != 'file':
349 bb.fetch2.check_network_access(d, cmd)
350 output = runfetchcmd(cmd, d, True)
351 if not output:
352 raise bb.fetch2.FetchError("The command %s gave empty output unexpectedly" % cmd, ud.url)
353 return output
354
355 def _latest_revision(self, ud, d, name):
356 """
357 Compute the HEAD revision for the url
358 """
359 output = self._lsremote(ud, d, "")
360 # Tags of the form ^{} may not work, need to fallback to other form
361 if ud.unresolvedrev[name][:5] == "refs/":
362 head = ud.unresolvedrev[name]
363 tag = ud.unresolvedrev[name]
364 else:
365 head = "refs/heads/%s" % ud.unresolvedrev[name]
366 tag = "refs/tags/%s" % ud.unresolvedrev[name]
367 for s in [head, tag + "^{}", tag]:
368 for l in output.split('\n'):
369 if s in l:
370 return l.split()[0]
371 raise bb.fetch2.FetchError("Unable to resolve '%s' in upstream git repository in git ls-remote output for %s" % \
372 (ud.unresolvedrev[name], ud.host+ud.path))
373
374 def latest_versionstring(self, ud, d):
375 """
376 Compute the latest release name like "x.y.x" in "x.y.x+gitHASH"
377 by searching through the tags output of ls-remote, comparing
378 versions and returning the highest match.
379 """
380 pupver = ('', '')
381
382 tagregex = re.compile(d.getVar('GITTAGREGEX', True) or "(?P<pver>([0-9][\.|_]?)+)")
383 try:
384 output = self._lsremote(ud, d, "refs/tags/*")
385 except bb.fetch2.FetchError or bb.fetch2.NetworkAccess:
386 return pupver
387
388 verstring = ""
389 revision = ""
390 for line in output.split("\n"):
391 if not line:
392 break
393
394 tag_head = line.split("/")[-1]
395 # Ignore non-released branches
396 m = re.search("(alpha|beta|rc|final)+", tag_head)
397 if m:
398 continue
399
400 # search for version in the line
401 tag = tagregex.search(tag_head)
402 if tag == None:
403 continue
404
405 tag = tag.group('pver')
406 tag = tag.replace("_", ".")
407
408 if verstring and bb.utils.vercmp(("0", tag, ""), ("0", verstring, "")) < 0:
409 continue
410
411 verstring = tag
412 revision = line.split()[0]
413 pupver = (verstring, revision)
414
415 return pupver
416
417 def _build_revision(self, ud, d, name):
418 return ud.revisions[name]
419
420 def gitpkgv_revision(self, ud, d, name):
421 """
422 Return a sortable revision number by counting commits in the history
423 Based on gitpkgv.bblass in meta-openembedded
424 """
425 rev = self._build_revision(ud, d, name)
426 localpath = ud.localpath
427 rev_file = os.path.join(localpath, "oe-gitpkgv_" + rev)
428 if not os.path.exists(localpath):
429 commits = None
430 else:
431 if not os.path.exists(rev_file) or not os.path.getsize(rev_file):
432 from pipes import quote
433 commits = bb.fetch2.runfetchcmd(
434 "git rev-list %s -- | wc -l" % (quote(rev)),
435 d, quiet=True).strip().lstrip('0')
436 if commits:
437 open(rev_file, "w").write("%d\n" % int(commits))
438 else:
439 commits = open(rev_file, "r").readline(128).strip()
440 if commits:
441 return False, "%s+%s" % (commits, rev[:7])
442 else:
443 return True, str(rev)
444
445 def checkstatus(self, fetch, ud, d):
446 try:
447 self._lsremote(ud, d, "")
448 return True
449 except FetchError:
450 return False