blob: 40658ff94c7d3e257fdf02b1db4034bed457be49 [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
69import os
70import re
71import bb
72from bb import data
73from bb.fetch2 import FetchMethod
74from bb.fetch2 import runfetchcmd
75from bb.fetch2 import logger
76
77class Git(FetchMethod):
78 """Class to fetch a module or modules from git repositories"""
79 def init(self, d):
80 pass
81
82 def supports(self, ud, d):
83 """
84 Check to see if a given url can be fetched with git.
85 """
86 return ud.type in ['git']
87
88 def supports_checksum(self, urldata):
89 return False
90
91 def urldata_init(self, ud, d):
92 """
93 init git specific variable within url data
94 so that the git method like latest_revision() can work
95 """
96 if 'protocol' in ud.parm:
97 ud.proto = ud.parm['protocol']
98 elif not ud.host:
99 ud.proto = 'file'
100 else:
101 ud.proto = "git"
102
103 if not ud.proto in ('git', 'file', 'ssh', 'http', 'https', 'rsync'):
104 raise bb.fetch2.ParameterError("Invalid protocol type", ud.url)
105
106 ud.nocheckout = ud.parm.get("nocheckout","0") == "1"
107
108 ud.rebaseable = ud.parm.get("rebaseable","0") == "1"
109
110 ud.nobranch = ud.parm.get("nobranch","0") == "1"
111
112 # bareclone implies nocheckout
113 ud.bareclone = ud.parm.get("bareclone","0") == "1"
114 if ud.bareclone:
115 ud.nocheckout = 1
116
117 ud.unresolvedrev = {}
118 branches = ud.parm.get("branch", "master").split(',')
119 if len(branches) != len(ud.names):
120 raise bb.fetch2.ParameterError("The number of name and branch parameters is not balanced", ud.url)
121 ud.branches = {}
122 for name in ud.names:
123 branch = branches[ud.names.index(name)]
124 ud.branches[name] = branch
125 ud.unresolvedrev[name] = branch
126
127 ud.basecmd = data.getVar("FETCHCMD_git", d, True) or "git -c core.fsyncobjectfiles=0"
128
129 ud.write_tarballs = ((data.getVar("BB_GENERATE_MIRROR_TARBALLS", d, True) or "0") != "0") or ud.rebaseable
130
131 ud.setup_revisons(d)
132
133 for name in ud.names:
134 # Ensure anything that doesn't look like a sha256 checksum/revision is translated into one
135 if not ud.revisions[name] or len(ud.revisions[name]) != 40 or (False in [c in "abcdef0123456789" for c in ud.revisions[name]]):
136 if ud.revisions[name]:
137 ud.unresolvedrev[name] = ud.revisions[name]
138 ud.revisions[name] = self.latest_revision(ud, d, name)
139
140 gitsrcname = '%s%s' % (ud.host.replace(':', '.'), ud.path.replace('/', '.').replace('*', '.'))
141 if gitsrcname.startswith('.'):
142 gitsrcname = gitsrcname[1:]
143
144 # for rebaseable git repo, it is necessary to keep mirror tar ball
145 # per revision, so that even the revision disappears from the
146 # upstream repo in the future, the mirror will remain intact and still
147 # contains the revision
148 if ud.rebaseable:
149 for name in ud.names:
150 gitsrcname = gitsrcname + '_' + ud.revisions[name]
151 ud.mirrortarball = 'git2_%s.tar.gz' % (gitsrcname)
152 ud.fullmirror = os.path.join(d.getVar("DL_DIR", True), ud.mirrortarball)
153 gitdir = d.getVar("GITDIR", True) or (d.getVar("DL_DIR", True) + "/git2/")
154 ud.clonedir = os.path.join(gitdir, gitsrcname)
155
156 ud.localfile = ud.clonedir
157
158 def localpath(self, ud, d):
159 return ud.clonedir
160
161 def need_update(self, ud, d):
162 if not os.path.exists(ud.clonedir):
163 return True
164 os.chdir(ud.clonedir)
165 for name in ud.names:
166 if not self._contains_ref(ud, d, name):
167 return True
168 if ud.write_tarballs and not os.path.exists(ud.fullmirror):
169 return True
170 return False
171
172 def try_premirror(self, ud, d):
173 # If we don't do this, updating an existing checkout with only premirrors
174 # is not possible
175 if d.getVar("BB_FETCH_PREMIRRORONLY", True) is not None:
176 return True
177 if os.path.exists(ud.clonedir):
178 return False
179 return True
180
181 def download(self, ud, d):
182 """Fetch url"""
183
184 ud.repochanged = not os.path.exists(ud.fullmirror)
185
186 # If the checkout doesn't exist and the mirror tarball does, extract it
187 if not os.path.exists(ud.clonedir) and os.path.exists(ud.fullmirror):
188 bb.utils.mkdirhier(ud.clonedir)
189 os.chdir(ud.clonedir)
190 runfetchcmd("tar -xzf %s" % (ud.fullmirror), d)
191
192 repourl = self._get_repo_url(ud)
193
194 # If the repo still doesn't exist, fallback to cloning it
195 if not os.path.exists(ud.clonedir):
196 # We do this since git will use a "-l" option automatically for local urls where possible
197 if repourl.startswith("file://"):
198 repourl = repourl[7:]
199 clone_cmd = "%s clone --bare --mirror %s %s" % (ud.basecmd, repourl, ud.clonedir)
200 if ud.proto.lower() != 'file':
201 bb.fetch2.check_network_access(d, clone_cmd)
202 runfetchcmd(clone_cmd, d)
203
204 os.chdir(ud.clonedir)
205 # Update the checkout if needed
206 needupdate = False
207 for name in ud.names:
208 if not self._contains_ref(ud, d, name):
209 needupdate = True
210 if needupdate:
211 try:
212 runfetchcmd("%s remote rm origin" % ud.basecmd, d)
213 except bb.fetch2.FetchError:
214 logger.debug(1, "No Origin")
215
216 runfetchcmd("%s remote add --mirror=fetch origin %s" % (ud.basecmd, repourl), d)
217 fetch_cmd = "%s fetch -f --prune %s refs/*:refs/*" % (ud.basecmd, repourl)
218 if ud.proto.lower() != 'file':
219 bb.fetch2.check_network_access(d, fetch_cmd, ud.url)
220 runfetchcmd(fetch_cmd, d)
221 runfetchcmd("%s prune-packed" % ud.basecmd, d)
222 runfetchcmd("%s pack-redundant --all | xargs -r rm" % ud.basecmd, d)
223 ud.repochanged = True
224 os.chdir(ud.clonedir)
225 for name in ud.names:
226 if not self._contains_ref(ud, d, name):
227 raise bb.fetch2.FetchError("Unable to find revision %s in branch %s even from upstream" % (ud.revisions[name], ud.branches[name]))
228
229 def build_mirror_data(self, ud, d):
230 # Generate a mirror tarball if needed
231 if ud.write_tarballs and (ud.repochanged or not os.path.exists(ud.fullmirror)):
232 # it's possible that this symlink points to read-only filesystem with PREMIRROR
233 if os.path.islink(ud.fullmirror):
234 os.unlink(ud.fullmirror)
235
236 os.chdir(ud.clonedir)
237 logger.info("Creating tarball of git repository")
238 runfetchcmd("tar -czf %s %s" % (ud.fullmirror, os.path.join(".") ), d)
239 runfetchcmd("touch %s.done" % (ud.fullmirror), d)
240
241 def unpack(self, ud, destdir, d):
242 """ unpack the downloaded src to destdir"""
243
244 subdir = ud.parm.get("subpath", "")
245 if subdir != "":
246 readpathspec = ":%s" % (subdir)
247 def_destsuffix = "%s/" % os.path.basename(subdir.rstrip('/'))
248 else:
249 readpathspec = ""
250 def_destsuffix = "git/"
251
252 destsuffix = ud.parm.get("destsuffix", def_destsuffix)
253 destdir = ud.destdir = os.path.join(destdir, destsuffix)
254 if os.path.exists(destdir):
255 bb.utils.prunedir(destdir)
256
257 cloneflags = "-s -n"
258 if ud.bareclone:
259 cloneflags += " --mirror"
260
261 # Versions of git prior to 1.7.9.2 have issues where foo.git and foo get confused
262 # and you end up with some horrible union of the two when you attempt to clone it
263 # The least invasive workaround seems to be a symlink to the real directory to
264 # fool git into ignoring any .git version that may also be present.
265 #
266 # The issue is fixed in more recent versions of git so we can drop this hack in future
267 # when that version becomes common enough.
268 clonedir = ud.clonedir
269 if not ud.path.endswith(".git"):
270 indirectiondir = destdir[:-1] + ".indirectionsymlink"
271 if os.path.exists(indirectiondir):
272 os.remove(indirectiondir)
273 bb.utils.mkdirhier(os.path.dirname(indirectiondir))
274 os.symlink(ud.clonedir, indirectiondir)
275 clonedir = indirectiondir
276
277 runfetchcmd("%s clone %s %s/ %s" % (ud.basecmd, cloneflags, clonedir, destdir), d)
278 os.chdir(destdir)
279 repourl = self._get_repo_url(ud)
280 runfetchcmd("%s remote set-url origin %s" % (ud.basecmd, repourl), d)
281 if not ud.nocheckout:
282 if subdir != "":
283 runfetchcmd("%s read-tree %s%s" % (ud.basecmd, ud.revisions[ud.names[0]], readpathspec), d)
284 runfetchcmd("%s checkout-index -q -f -a" % ud.basecmd, d)
285 elif not ud.nobranch:
286 branchname = ud.branches[ud.names[0]]
287 runfetchcmd("%s checkout -B %s %s" % (ud.basecmd, branchname, \
288 ud.revisions[ud.names[0]]), d)
289 runfetchcmd("%s branch --set-upstream %s origin/%s" % (ud.basecmd, branchname, \
290 branchname), d)
291 else:
292 runfetchcmd("%s checkout %s" % (ud.basecmd, ud.revisions[ud.names[0]]), d)
293
294 return True
295
296 def clean(self, ud, d):
297 """ clean the git directory """
298
299 bb.utils.remove(ud.localpath, True)
300 bb.utils.remove(ud.fullmirror)
301 bb.utils.remove(ud.fullmirror + ".done")
302
303 def supports_srcrev(self):
304 return True
305
306 def _contains_ref(self, ud, d, name):
307 cmd = ""
308 if ud.nobranch:
309 cmd = "%s log --pretty=oneline -n 1 %s -- 2> /dev/null | wc -l" % (
310 ud.basecmd, ud.revisions[name])
311 else:
312 cmd = "%s branch --contains %s --list %s 2> /dev/null | wc -l" % (
313 ud.basecmd, ud.revisions[name], ud.branches[name])
314 try:
315 output = runfetchcmd(cmd, d, quiet=True)
316 except bb.fetch2.FetchError:
317 return False
318 if len(output.split()) > 1:
319 raise bb.fetch2.FetchError("The command '%s' gave output with more then 1 line unexpectedly, output: '%s'" % (cmd, output))
320 return output.split()[0] != "0"
321
322 def _get_repo_url(self, ud):
323 """
324 Return the repository URL
325 """
326 if ud.user:
327 username = ud.user + '@'
328 else:
329 username = ""
330 return "%s://%s%s%s" % (ud.proto, username, ud.host, ud.path)
331
332 def _revision_key(self, ud, d, name):
333 """
334 Return a unique key for the url
335 """
336 return "git:" + ud.host + ud.path.replace('/', '.') + ud.unresolvedrev[name]
337
338 def _lsremote(self, ud, d, search):
339 """
340 Run git ls-remote with the specified search string
341 """
342 repourl = self._get_repo_url(ud)
343 cmd = "%s ls-remote %s %s" % \
344 (ud.basecmd, repourl, search)
345 if ud.proto.lower() != 'file':
346 bb.fetch2.check_network_access(d, cmd)
347 output = runfetchcmd(cmd, d, True)
348 if not output:
349 raise bb.fetch2.FetchError("The command %s gave empty output unexpectedly" % cmd, ud.url)
350 return output
351
352 def _latest_revision(self, ud, d, name):
353 """
354 Compute the HEAD revision for the url
355 """
356 output = self._lsremote(ud, d, "")
357 # Tags of the form ^{} may not work, need to fallback to other form
358 if ud.unresolvedrev[name][:5] == "refs/":
359 head = ud.unresolvedrev[name]
360 tag = ud.unresolvedrev[name]
361 else:
362 head = "refs/heads/%s" % ud.unresolvedrev[name]
363 tag = "refs/tags/%s" % ud.unresolvedrev[name]
364 for s in [head, tag + "^{}", tag]:
365 for l in output.split('\n'):
366 if s in l:
367 return l.split()[0]
368 raise bb.fetch2.FetchError("Unable to resolve '%s' in upstream git repository in git ls-remote output for %s" % \
369 (ud.unresolvedrev[name], ud.host+ud.path))
370
371 def latest_versionstring(self, ud, d):
372 """
373 Compute the latest release name like "x.y.x" in "x.y.x+gitHASH"
374 by searching through the tags output of ls-remote, comparing
375 versions and returning the highest match.
376 """
377 pupver = ('', '')
378
379 tagregex = re.compile(d.getVar('GITTAGREGEX', True) or "(?P<pver>([0-9][\.|_]?)+)")
380 try:
381 output = self._lsremote(ud, d, "refs/tags/*")
382 except bb.fetch2.FetchError or bb.fetch2.NetworkAccess:
383 return pupver
384
385 verstring = ""
386 revision = ""
387 for line in output.split("\n"):
388 if not line:
389 break
390
391 tag_head = line.split("/")[-1]
392 # Ignore non-released branches
393 m = re.search("(alpha|beta|rc|final)+", tag_head)
394 if m:
395 continue
396
397 # search for version in the line
398 tag = tagregex.search(tag_head)
399 if tag == None:
400 continue
401
402 tag = tag.group('pver')
403 tag = tag.replace("_", ".")
404
405 if verstring and bb.utils.vercmp(("0", tag, ""), ("0", verstring, "")) < 0:
406 continue
407
408 verstring = tag
409 revision = line.split()[0]
410 pupver = (verstring, revision)
411
412 return pupver
413
414 def _build_revision(self, ud, d, name):
415 return ud.revisions[name]
416
417 def gitpkgv_revision(self, ud, d, name):
418 """
419 Return a sortable revision number by counting commits in the history
420 Based on gitpkgv.bblass in meta-openembedded
421 """
422 rev = self._build_revision(ud, d, name)
423 localpath = ud.localpath
424 rev_file = os.path.join(localpath, "oe-gitpkgv_" + rev)
425 if not os.path.exists(localpath):
426 commits = None
427 else:
428 if not os.path.exists(rev_file) or not os.path.getsize(rev_file):
429 from pipes import quote
430 commits = bb.fetch2.runfetchcmd(
431 "git rev-list %s -- | wc -l" % (quote(rev)),
432 d, quiet=True).strip().lstrip('0')
433 if commits:
434 open(rev_file, "w").write("%d\n" % int(commits))
435 else:
436 commits = open(rev_file, "r").readline(128).strip()
437 if commits:
438 return False, "%s+%s" % (commits, rev[:7])
439 else:
440 return True, str(rev)
441
442 def checkstatus(self, fetch, ud, d):
443 try:
444 self._lsremote(ud, d, "")
445 return True
446 except FetchError:
447 return False