diff --git a/doc/configuration.rst b/doc/configuration.rst index cdaa6891..153cd83f 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -480,7 +480,8 @@ Image Build Settings .. note:: Config can contain anything what is accepted by - koji image-build --config configfile.ini + ``koji image-build --config configfile.ini`` + Repo is currently the only option which is being automatically transformed into a string. @@ -488,6 +489,10 @@ Image Build Settings The 'format' attr is [('image_type', 'image_suffix'), ...]. productmd should ideally contain all of image types and suffixes. + If ``ksurl`` ends with ``#HEAD``, Pungi will figure out the SHA1 hash of + current HEAD and use that instead. + + Example ------- :: @@ -501,6 +506,7 @@ Example 'target': 'koji-target-name', 'ksversion': 'F23', # value from pykickstart 'version': '23', + # correct SHA1 hash will be put into the URL below automatically 'ksurl': 'https://git.fedorahosted.org/git/spin-kickstarts.git?somedirectoryifany#HEAD', 'kickstart': "fedora-docker-base.ks", 'repo': ["http://someextrarepos.org/repo", "ftp://rekcod.oi/repo]. diff --git a/pungi/phases/image_build.py b/pungi/phases/image_build.py index 0234bb7e..aad3f985 100644 --- a/pungi/phases/image_build.py +++ b/pungi/phases/image_build.py @@ -4,7 +4,7 @@ import os import time -from pungi.util import get_arch_variant_data +from pungi.util import get_arch_variant_data, resolve_git_url from pungi.phases.base import PhaseBase from pungi.linker import Linker from pungi.paths import translate_path @@ -34,6 +34,9 @@ class ImageBuildPhase(PhaseBase): for variant in self.compose.get_variants(arch=arch): image_build_data = get_arch_variant_data(self.compose.conf, self.name, arch, variant) for image_conf in image_build_data: + # Replace possible ambiguous ref name with explicit hash. + if 'ksurl' in image_conf: + image_conf['ksurl'] = resolve_git_url(image_conf['ksurl']) image_conf["arches"] = arch # passed to get_image_build_cmd as dict image_conf["variant"] = variant # ^ image_conf["install_tree"] = translate_path(self.compose, self.compose.paths.compose.os_tree(arch, variant)) # ^ diff --git a/pungi/util.py b/pungi/util.py index 54f43e16..18ef2352 100644 --- a/pungi/util.py +++ b/pungi/util.py @@ -23,6 +23,7 @@ import hashlib import errno import pipes import re +import urlparse from kobo.shortcuts import run from productmd.common import get_major_version @@ -214,6 +215,32 @@ def get_arch_variant_data(conf, var_name, arch, variant): return result +def resolve_git_url(url): + """Given a url to a Git repo specifying HEAD as a ref, replace that + specifier with actual SHA1 of the commit. + + Otherwise, the original URL will be returned. + + Raises RuntimeError if there was an error. Most likely cause is failure to + run git command. + """ + r = urlparse.urlsplit(url) + if r.fragment != 'HEAD': + return url + + baseurl = urlparse.urlunsplit((r.scheme, r.netloc, r.path, '', '')) + _, output = run(['git', 'ls-remote', baseurl, r.fragment]) + + lines = [line for line in output.split('\n') if line] + if len(lines) != 1: + # This should never happen. HEAD can not match multiple commits in a + # single repo, and there can not be a repo without a HEAD. + raise RuntimeError('Failed to resolve %s', url) + + fragment = lines[0].split()[0] + return urlparse.urlunsplit((r.scheme, r.netloc, r.path, r.query, fragment)) + + # fomat: {arch|*: [data]} def get_arch_data(conf, var_name, arch): result = [] diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100755 index 00000000..fe13a47f --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +import mock +import os +import sys +import unittest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from pungi import util + + +class TestGitRefResolver(unittest.TestCase): + + @mock.patch('pungi.util.run') + def test_successful_resolve(self, run): + run.return_value = (0, 'CAFEBABE\tHEAD\n') + + url = util.resolve_git_url('https://git.example.com/repo.git?somedir#HEAD') + + self.assertEqual(url, 'https://git.example.com/repo.git?somedir#CAFEBABE') + run.assert_called_once_with(['git', 'ls-remote', 'https://git.example.com/repo.git', 'HEAD']) + + @mock.patch('pungi.util.run') + def test_resolve_missing_spec(self, run): + url = util.resolve_git_url('https://git.example.com/repo.git') + + self.assertEqual(url, 'https://git.example.com/repo.git') + self.assertEqual(run.mock_calls, []) + + @mock.patch('pungi.util.run') + def test_resolve_non_head_spec(self, run): + url = util.resolve_git_url('https://git.example.com/repo.git#some-tag') + + self.assertEqual(url, 'https://git.example.com/repo.git#some-tag') + self.assertEqual(run.mock_calls, []) + + @mock.patch('pungi.util.run') + def test_resolve_ambiguous(self, run): + run.return_value = (0, 'CAFEBABE\tF11\nDEADBEEF\tF10\n') + + with self.assertRaises(RuntimeError): + util.resolve_git_url('https://git.example.com/repo.git?somedir#HEAD') + + run.assert_called_once_with(['git', 'ls-remote', 'https://git.example.com/repo.git', 'HEAD']) + + +if __name__ == "__main__": + unittest.main()