diff options
-rwxr-xr-x | morph | 61 | ||||
-rw-r--r-- | morphlib/blobs.py | 15 | ||||
-rw-r--r-- | morphlib/blobs_tests.py | 62 | ||||
-rw-r--r-- | morphlib/builddependencygraph.py | 66 | ||||
-rw-r--r-- | morphlib/builder.py | 73 | ||||
-rw-r--r-- | morphlib/git.py | 67 | ||||
-rw-r--r-- | morphlib/morphology.py | 37 | ||||
-rw-r--r-- | morphlib/morphology_tests.py | 82 | ||||
-rw-r--r-- | morphlib/morphologyloader.py | 35 | ||||
-rw-r--r-- | morphlib/morphologyloader_tests.py | 11 | ||||
-rw-r--r-- | morphlib/sourcemanager.py | 23 | ||||
-rw-r--r-- | morphlib/sourcemanager_tests.py | 46 | ||||
-rw-r--r-- | tests/missing-ref.stderr | 2 | ||||
-rw-r--r-- | tests/show-dependencies.stdout | 244 |
14 files changed, 438 insertions, 386 deletions
@@ -3,7 +3,7 @@ # WARNING: THIS IS HIGHLY EXPERIMENTAL CODE RIGHT NOW. JUST PROOF OF CONCEPT. # DO NOT RUN UNTIL YOU KNOW WHAT YOU ARE DOING. # -# Copyright (C) 2011 Codethink Limited +# Copyright (C) 2011-2012 Codethink Limited # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -25,6 +25,7 @@ import os import urlparse import morphlib +from morphlib.morphologyloader import MorphologyLoader from morphlib.builddependencygraph import BuildDependencyGraph @@ -79,8 +80,10 @@ class Morph(cliapp.Application): ''' tempdir = morphlib.tempdir.Tempdir() - loader = morphlib.morphologyloader.MorphologyLoader(self.settings) - builder = morphlib.builder.Builder(tempdir, self, loader) + morph_loader = MorphologyLoader(self.settings) + source_manager = morphlib.sourcemanager.SourceManager(self) + builder = morphlib.builder.Builder(tempdir, self, morph_loader, + source_manager) if not os.path.exists(self.settings['cachedir']): os.mkdir(self.settings['cachedir']) @@ -90,21 +93,13 @@ class Morph(cliapp.Application): repo, ref, filename = args[:3] args = args[3:] - # resolve the URL to the repository - base_url = self.settings['git-base-url'] - if not base_url.endswith('/'): - base_url += '/' - repo = urlparse.urljoin(base_url, repo) - if not repo.endswith('/'): - repo += '/' - # derive a build order from the dependency graph - morphology = loader.load(repo, ref, filename) - graph = BuildDependencyGraph(loader, morphology) + graph = BuildDependencyGraph(source_manager, morph_loader, + repo, ref, filename) graph.resolve() blobs, order = graph.build_order() - self.msg('Building %s' % morphology) + self.msg('Building %s|%s|%s' % (repo, ref, filename)) # build things in this order ret.append(builder.build(blobs, order)) @@ -172,25 +167,17 @@ class Morph(cliapp.Application): def cmd_show_dependencies(self, args): '''Dumps the dependency tree of all input morphologies.''' + morph_loader = MorphologyLoader(self.settings) + source_manager = morphlib.sourcemanager.SourceManager(self) + while len(args) >= 3: # read the build tuple from the command line repo, ref, filename = args[:3] args = args[3:] - # resolve the URL to the repository - base_url = self.settings['git-base-url'] - if not base_url.endswith('/'): - base_url += '/' - repo = urlparse.urljoin(base_url, repo) - if not repo.endswith('/'): - repo += '/' - - # load the morphology corresponding to the build tuple - loader = morphlib.morphologyloader.MorphologyLoader(self.settings) - morphology = loader.load(repo, ref, filename) - # create a dependency graph for the morphology - graph = BuildDependencyGraph(loader, morphology) + graph = BuildDependencyGraph(source_manager, morph_loader, + repo, ref, filename) graph.resolve() # print the graph @@ -208,6 +195,26 @@ class Morph(cliapp.Application): for blob in sorted(group, key=str): self.output.write(' %s\n' % blob) + def cmd_update_gits(self, args): + tempdir = morphlib.tempdir.Tempdir() + morph_loader = MorphologyLoader(self.settings) + source_manager = morphlib.sourcemanager.SourceManager(self) + while len(args) >= 3: + # read the build tuple from the command line + repo, ref, filename = args[:3] + args = args[3:] + + # first step: clone the corresponding repo + treeish = source_manager.get_treeish(repo, ref) + morph = morph_loader.load(treeish, filename) + blob = morphlib.blobs.Blob.create_blob(morph) + + # second step: compute the cache ID, which will implicitly + # clone all repositories needed to build the blob + builder = morphlib.builder.Builder(tempdir, self, morph_loader, + source_manager) + builder.get_cache_id(blob) + def msg(self, msg): '''Show a message to the user about what is going on.''' logging.debug(msg) diff --git a/morphlib/blobs.py b/morphlib/blobs.py index 1732f000..d02643a6 100644 --- a/morphlib/blobs.py +++ b/morphlib/blobs.py @@ -14,8 +14,23 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +import os + + class Blob(object): + @staticmethod + def create_blob(morph): + if morph.kind == 'stratum': + return Stratum(morph) + elif morph.kind == 'chunk': + return Chunk(morph) + elif morph.kind == 'system': + return System(morph) + else: + raise TypeError('Morphology %s has an unknown type: %s' % + (morph.filename, morph.kind)) + def __init__(self, morph): self.parents = [] self.morph = morph diff --git a/morphlib/blobs_tests.py b/morphlib/blobs_tests.py index c012206a..f92658ee 100644 --- a/morphlib/blobs_tests.py +++ b/morphlib/blobs_tests.py @@ -21,6 +21,52 @@ import morphlib class BlobsTests(unittest.TestCase): + def test_create_a_chunk_blob(self): + class FakeChunkMorph(object): + @property + def kind(self): + return 'chunk' + + morph = FakeChunkMorph() + chunk = morphlib.blobs.Blob.create_blob(morph) + self.assertTrue(isinstance(chunk, morphlib.blobs.Chunk)) + self.assertEqual(morph, chunk.morph) + + def test_create_a_stratum_blob(self): + class FakeStratumMorph(object): + @property + def kind(self): + return 'stratum' + + morph = FakeStratumMorph() + stratum = morphlib.blobs.Blob.create_blob(morph) + self.assertTrue(isinstance(stratum, morphlib.blobs.Stratum)) + self.assertEqual(morph, stratum.morph) + + def test_create_a_system_blob(self): + class FakeSystemMorph(object): + @property + def kind(self): + return 'system' + + morph = FakeSystemMorph() + system = morphlib.blobs.Blob.create_blob(morph) + self.assertTrue(isinstance(system, morphlib.blobs.System)) + self.assertEqual(morph, system.morph) + + def test_create_an_invalid_blob(self): + class FakeInvalidMorph(object): + @property + def kind(self): + return 'invalid' + + @property + def filename(self): + return '/foo/bar/baz.morph' + + morph = FakeInvalidMorph() + self.assertRaises(TypeError, morphlib.blobs.Blob.create_blob, morph) + def test_blob_with_parents(self): blob1 = morphlib.blobs.Blob(None) blob2 = morphlib.blobs.Blob(None) @@ -93,17 +139,23 @@ class BlobsTests(unittest.TestCase): self.assertFalse(blob2.depends_on(blob1)) def test_chunks(self): - settings = { 'git-base-url': '' } + settings = { 'git-base-url': [] } loader = morphlib.morphologyloader.MorphologyLoader(settings) loader._get_morph_text = self.get_morph_text - - stratum_morph = loader.load('repo', 'ref', 'foo.morph') + + class FakeTreeish(object): + def __init__(self): + self.original_repo = 'test-repo' + faketreeish = FakeTreeish() + + faketreeish.original_repo = 'hello' + stratum_morph = loader.load(faketreeish, 'foo.morph') stratum = morphlib.blobs.Stratum(stratum_morph) self.assertEquals(len(stratum.chunks), 1) self.assertTrue('foo' in stratum.chunks) self.assertEqual(['.'], stratum.chunks['foo']) - chunk_morph = loader.load('repo', 'ref', 'bar.morph') + chunk_morph = loader.load(faketreeish, 'bar.morph') chunk = morphlib.blobs.Chunk(chunk_morph) self.assertEqual(len(chunk.chunks), 2) self.assertTrue('include' in chunk.chunks) @@ -111,7 +163,7 @@ class BlobsTests(unittest.TestCase): self.assertTrue('src' in chunk.chunks) self.assertEqual(chunk.chunks['src'], ['src/']) - def get_morph_text(self, repo, ref, filename): + def get_morph_text(self, treeish, filename): if filename == 'foo.morph': return (''' { diff --git a/morphlib/builddependencygraph.py b/morphlib/builddependencygraph.py index a280abbd..c0f2b27c 100644 --- a/morphlib/builddependencygraph.py +++ b/morphlib/builddependencygraph.py @@ -31,14 +31,19 @@ class BuildDependencyGraph(object): # pragma: no cover ''' - def __init__(self, loader, morph): - self.loader = loader - self.morph = morph + def __init__(self, source_manager, morph_loader, repo, ref, filename): + self.source_manager = source_manager + self.morph_loader = morph_loader + self.root_repo = repo + self.root_ref = ref + self.root_filename = filename self.blobs = set() - def create_blob(self, morph): + def create_blob(self, treeish, filename): '''Creates a blob from a morphology.''' + morph = self.morph_loader.load(treeish, filename) + if morph.kind == 'stratum': return morphlib.blobs.Stratum(morph) elif morph.kind == 'chunk': @@ -46,19 +51,19 @@ class BuildDependencyGraph(object): # pragma: no cover else: return morphlib.blobs.System(morph) - def get_blob(self, info): - '''Takes a (repo, ref, filename) tuple and looks up the blob for it. + def get_blob(self, treeish, filename): + '''Takes a repo, ref, filename and looks up the blob for them. Loads the corresponding morphology and chunk/stratum/system object on-demand if it is not cached yet. ''' - blob = self.cached_blobs.get(info, None) + key = (treeish, filename) + blob = self.cached_blobs.get(key, None) if not blob: - morphology = self.loader.load(info[0], info[1], info[2]) - blob = self.create_blob(morphology) - self.cached_blobs[info] = blob + blob = self.create_blob(treeish, filename) + self.cached_blobs[key] = blob return blob def resolve(self): @@ -72,17 +77,17 @@ class BuildDependencyGraph(object): # pragma: no cover self.resolve_chunks() def resolve_root(self): - # convert the morphology to a chunk/stratum/system object - root = self.create_blob(self.morph) + # prepare the repo, load the morphology and blob information + treeish = self.source_manager.get_treeish(self.root_repo, + self.root_ref) + root = self.get_blob(treeish, self.root_filename) self.blobs.add(root) # load all strata the morph depends on (only if it's a system image) if root.morph.kind == 'system': for stratum_name in root.morph.strata: - info = (root.morph.repo, - root.morph.ref, - '%s.morph' % stratum_name) - stratum = self.get_blob(info) + filename = '%s.morph' % stratum_name + stratum = self.get_blob(treeish, filename) root.add_dependency(stratum) stratum.add_parent(root) self.blobs.add(stratum) @@ -112,14 +117,9 @@ class BuildDependencyGraph(object): # pragma: no cover # verify that the build-depends format is valid if isinstance(stratum.morph.build_depends, list): for depname in stratum.morph.build_depends: - # prepare a tuple for the dependency stratum - repo = stratum.morph.repo - ref = stratum.morph.ref - filename = '%s.morph' % depname - info = (repo, ref, filename) - # load the dependency stratum on demand - depstratum = self.get_blob(info) + depstratum = self.get_blob(stratum.morph.treeish, + '%s.morph' % depname) self.blobs.add(depstratum) # add the dependency stratum to the graph @@ -138,10 +138,6 @@ class BuildDependencyGraph(object): # pragma: no cover ''' - if self.morph.kind == 'chunk': - blob = self.create_blob(self.morph) - self.blobs.add(blob) - blobs = list(self.blobs) for blob in blobs: if isinstance(blob, morphlib.blobs.Stratum): @@ -164,10 +160,10 @@ class BuildDependencyGraph(object): # pragma: no cover filename = '%s.morph' % (source['morph'] if 'morph' in source else source['name']) - info = (repo, ref, filename) # load the chunk on demand - chunk = self.get_blob(info) + treeish = self.source_manager.get_treeish(repo, ref) + chunk = self.get_blob(treeish, filename) chunk.add_parent(stratum) # store (name -> chunk) association to avoid loading the chunk twice @@ -190,15 +186,14 @@ class BuildDependencyGraph(object): # pragma: no cover dependency = name_to_chunk[depname] chunk.add_dependency(dependency) else: - filename = os.path.basename(stratum.morph.filename) raise Exception('%s: source %s references %s before it ' - 'is defined' % (filename, + 'is defined' % (stratum.morph.filename, source['name'], depname)) else: - filename = os.path.basename(stratum.morph.filename) raise Exception('%s: source %s uses an invalid build-depends ' - 'format' % (filename, source['name'])) + 'format' % + (stratum.morph.filename, source['name'])) # add the chunk to stratum and graph stratum_chunks.add(chunk) @@ -304,6 +299,9 @@ class BuildDependencyGraph(object): # pragma: no cover # have found at least one cyclic dependency if len(sorting) < len(self.blobs): raise Exception('Cyclic dependencies found in the dependency ' - 'graph of "%s"' % self.morph) + 'graph of %s|%s|%s' % + (self.root_repo, + self.root_ref, + self.root_filename)) return sorting diff --git a/morphlib/builder.py b/morphlib/builder.py index d1585953..33aa5cca 100644 --- a/morphlib/builder.py +++ b/morphlib/builder.py @@ -286,10 +286,7 @@ class ChunkBuilder(BlobBuilder): self.dump_memory_profile('before creating source and tarball ' 'for chunk') tarball = self.cache_prefix + '.src.tar' - #FIXME Ugh use treeish everwhere - path = urlparse.urlparse(self.blob.morph.repo).path - t = morphlib.git.Treeish(path, self.blob.morph.ref) - morphlib.git.export_sources(t, tarball) + morphlib.git.export_sources(self.blob.morph.treeish, tarball) self.dump_memory_profile('after exporting sources') os.mkdir(self.builddir) self.ex.runv(['tar', '-C', self.builddir, '-xf', tarball]) @@ -490,14 +487,15 @@ class Builder(object): The objects may be chunks or strata.''' - def __init__(self, tempdir, app, morph_loader): + def __init__(self, tempdir, app, morph_loader, source_manager): self.tempdir = tempdir self.real_msg = app.msg self.settings = app.settings self.dump_memory_profile = app.dump_memory_profile self.cachedir = morphlib.cachedir.CacheDir(self.settings['cachedir']) - self.indent = 0 self.morph_loader = morph_loader + self.source_manager = source_manager + self.indent = 0 def msg(self, text): spaces = ' ' * self.indent @@ -569,7 +567,7 @@ class Builder(object): raise TypeError('Blob %s has unknown type %s' % (str(blob), type(blob))) - cache_id = self.get_blob_cache_id(blob) + cache_id = self.get_cache_id(blob) logging.debug('cache id: %s' % repr(cache_id)) self.dump_memory_profile('after computing cache id') @@ -584,44 +582,41 @@ class Builder(object): return builder - def get_blob_cache_id(self, blob): - # FIXME os.path.basename() only works if the .morph file is an - # immediate children of the repo location and is not located in - # a subfolder - return self.get_cache_id(blob.morph.repo, - blob.morph.ref, - os.path.basename(blob.morph.filename)) - - def get_cache_id(self, repo, ref, morph_filename): - logging.debug('get_cache_id(%s, %s, %s)' % - (repo, ref, morph_filename)) - morph = self.morph_loader.load(repo, ref, morph_filename) - if morph.kind == 'chunk': + def get_cache_id(self, blob): + logging.debug('get_cache_id(%s)' % blob) + + if blob.morph.kind == 'chunk': kids = [] - elif morph.kind == 'stratum': + elif blob.morph.kind == 'stratum': kids = [] - for source in morph.sources: - kid_repo = source['repo'] - kid_ref = source['ref'] - kid_filename = (source['morph'] - if 'morph' in source - else source['name']) - kid_filename = '%s.morph' % kid_filename - kid_cache_id = self.get_cache_id(kid_repo, kid_ref, - kid_filename) - kids.append(kid_cache_id) - elif morph.kind == 'system': + for source in blob.morph.sources: + repo = source['repo'] + ref = source['ref'] + treeish = self.source_manager.get_treeish(repo, ref) + filename = (source['morph'] + if 'morph' in source + else source['name']) + filename = '%s.morph' % filename + morph = self.morph_loader.load(treeish, filename) + chunk = morphlib.blobs.Blob.create_blob(morph) + cache_id = self.get_cache_id(chunk) + kids.append(cache_id) + elif blob.morph.kind == 'system': kids = [] - for stratum in morph.strata: - kid_filename = '%s.morph' % stratum - kid_cache_id = self.get_cache_id(repo, ref, kid_filename) - kids.append(kid_cache_id) + for stratum_name in blob.morph.strata: + filename = '%s.morph' % stratum_name + morph = self.morph_loader.load(blob.morph.treeish, filename) + stratum = morphlib.blobs.Blob.create_blob(morph) + cache_id = self.get_cache_id(stratum) + kids.append(cache_id) else: - raise NotImplementedError('unknown morph kind %s' % morph.kind) + raise NotImplementedError('unknown morph kind %s' % + blob.morph.kind) + dict_key = { - 'name': morph.name, + 'name': blob.morph.name, 'arch': morphlib.util.arch(), - 'ref': morphlib.git.get_commit_id(repo, ref), + 'ref': blob.morph.treeish.sha1, 'kids': ''.join(self.cachedir.key(k) for k in kids), } return dict_key diff --git a/morphlib/git.py b/morphlib/git.py index 2cde8c55..36cb726d 100644 --- a/morphlib/git.py +++ b/morphlib/git.py @@ -25,47 +25,60 @@ import cliapp class NoMorphs(Exception): def __init__(self, repo, ref): - Exception.__init__(self, - 'Cannot find any morpologies at %s:%s' % - (repo, ref)) + Exception.__init__(self, 'Cannot find any morpologies at %s:%s' % + (repo, ref)) class TooManyMorphs(Exception): def __init__(self, repo, ref, morphs): - Exception.__init__(self, - 'Too many morphologies at %s:%s: %s' % - (repo, ref, ', '.join(morphs))) + Exception.__init__(self, 'Too many morphologies at %s:%s: %s' % + (repo, ref, ', '.join(morphs))) + class InvalidTreeish(cliapp.AppException): def __init__(self, repo, ref): - Exception.__init__(self, - '%s is an invalid reference for repo %s' % - (ref,repo)) + Exception.__init__(self, '%s is an invalid reference for repo %s' % + (ref, repo)) + +class Treeish(object): -class Treeish: - def __init__(self, repo, ref, msg=logging.debug): + def __init__(self, repo, original_repo, ref, msg=logging.debug): self.repo = repo self.msg = msg self.sha1 = None self.ref = None + self.original_repo = original_repo self._resolve_ref(ref) - + + def __hash__(self): + return hash((self.repo, self.ref)) + + def __eq__(self, other): + return other.repo == self.repo and other.ref == self.ref + + def __str__(self): + return '%s:%s' % (self.repo, self.ref) + def _resolve_ref(self, ref): ex = morphlib.execute.Execute(self.repo, self.msg) try: - refs = ex.runv(['git', 'show-ref', ref]).split() - binascii.unhexlify(refs[0]) #Valid hex? - self.sha1 = refs[0] - self.ref = refs[1] + refs = ex.runv(['git', 'show-ref', ref]).split('\n') + + # drop the refs that are not from origin + refs = [x.split() for x in refs if 'origin' in x] + + binascii.unhexlify(refs[0][0]) #Valid hex? + self.sha1 = refs[0][0] + self.ref = refs[0][1] except morphlib.execute.CommandFailure: self._is_sha(ref) def _is_sha(self, ref): - if len(ref)!=40: - raise InvalidTreeish(self.repo,ref) + if len(ref) != 40: + raise InvalidTreeish(self.original_repo, ref) try: binascii.unhexlify(ref) @@ -73,13 +86,13 @@ class Treeish: ex.runv(['git', 'rev-list', '--no-walk', ref]) self.sha1=ref except (TypeError, morphlib.execute.CommandFailure): - raise InvalidTreeish(self.repo,ref) + raise InvalidTreeish(self.original_repo, ref) def export_sources(treeish, tar_filename): '''Export the contents of a specific commit into a compressed tarball.''' ex = morphlib.execute.Execute('.', msg=logging.debug) - ex.runv(['git', 'archive', '-o', tar_filename, '--remote', treeish.repo, - treeish.sha1]) + ex.runv(['git', 'archive', '-o', tar_filename, '--remote', + 'file://%s' % treeish.repo, treeish.sha1]) def get_morph_text(treeish, filename): '''Return a morphology from a git repository.''' @@ -108,12 +121,6 @@ def add_remote(gitdir, name, url): ex = morphlib.execute.Execute(gitdir, msg=logging.debug) return ex.runv(['git', 'remote', 'add', '-f', name, url]) -# FIXME: All usage of this must die and Treeishes should be used -def get_commit_id(repo, ref): - '''Return the full SHA-1 commit id for a repo+ref.''' - scheme, netlock, path, params, query, frag = urlparse.urlparse(repo) - assert scheme == 'file' - ex = morphlib.execute.Execute(path, msg=logging.debug) - out = ex.runv(['git', 'rev-list', '-n1', ref]) - return out.strip() - +def update_remote(gitdir, name): + ex = morphlib.execute.Execute(gitdir, msg=logging.debug) + return ex.runv(['git', 'remote', 'update', name]) diff --git a/morphlib/morphology.py b/morphlib/morphology.py index c530824c..9f9fa06b 100644 --- a/morphlib/morphology.py +++ b/morphlib/morphology.py @@ -23,30 +23,28 @@ class Morphology(object): '''Represent a morphology: description of how to build binaries.''' - def __init__(self, repo, ref, fp, baseurl=None): - self.repo = repo - self.ref = ref + def __init__(self, treeish, fp): + self.treeish = treeish + self.filename = fp.name self._fp = fp - self._baseurl = baseurl or '' self._load() def _load(self): - logging.debug('Loading morphology %s' % self._fp.name) + logging.debug('Loading morphology %s from %s' % + (self._fp.name, self.treeish)) try: self._dict = json.load(self._fp) except ValueError: - logging.error('Failed to load morphology %s' % self._fp.name) + logging.error('Failed to load morphology %s from %s' % + (self._fp.name, self.treeish)) raise if self.kind == 'stratum': for source in self.sources: if 'repo' not in source: source[u'repo'] = source['name'] - repo = self._join_with_baseurl(source['repo']) - source[u'repo'] = unicode(repo) - - self.filename = self._fp.name + source[u'repo'] = unicode(source['repo']) @property def name(self): @@ -110,20 +108,7 @@ class Morphology(object): def test_stories(self): return self._dict.get('test-stories', []) - def _join_with_baseurl(self, url): - is_relative = (':' not in url or - '/' not in url or - url.find('/') < url.find(':')) - if is_relative: - if not url.endswith('/'): - url += '/' - baseurl = self._baseurl - if baseurl and not baseurl.endswith('/'): - baseurl += '/' - return baseurl + url - else: - return url - def __str__(self): # pragma: no cover - return '%s|%s|%s' % (os.path.basename(os.path.dirname(self.repo)), - self.ref, os.path.basename(self.filename)) + return '%s|%s|%s' % (self.treeish.original_repo, + self.treeish.ref, + os.path.basename(self.filename)) diff --git a/morphlib/morphology_tests.py b/morphlib/morphology_tests.py index 2df42315..db771fbb 100644 --- a/morphlib/morphology_tests.py +++ b/morphlib/morphology_tests.py @@ -14,13 +14,17 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -import json import StringIO import unittest import morphlib +class FakeTreeish(object): + + pass + + class MockFile(StringIO.StringIO): def __init__(self, *args, **kwargs): @@ -30,19 +34,31 @@ class MockFile(StringIO.StringIO): class MorphologyTests(unittest.TestCase): + def test_constructor_with_treeish(self): + faketreeish = FakeTreeish() + morph = morphlib.morphology.Morphology( + faketreeish, + MockFile(''' + { + "name": "hello", + "kind": "chunk" + }''')) + self.assertEqual(morph.treeish, faketreeish) + def test_fails_invalid_chunk_morphology(self): def failtest(): - morph = morphlib.morphology.Morphology( - 'repo', 'ref', - MockFile(''' - { - "name": "hello", - }''')) + morphlib.morphology.Morphology( + FakeTreeish(), + MockFile(''' + { + "name": "hello", + }''')) self.assertRaises(ValueError, failtest) def test_accepts_valid_chunk_morphology(self): + faketreeish = FakeTreeish() morph = morphlib.morphology.Morphology( - 'repo', 'ref', + faketreeish, MockFile(''' { "name": "hello", @@ -69,8 +85,7 @@ class MorphologyTests(unittest.TestCase): } }''')) - self.assertEqual(morph.repo, 'repo') - self.assertEqual(morph.ref, 'ref') + self.assertEqual(morph.treeish, faketreeish) self.assertEqual(morph.filename, 'mockfile') self.assertEqual(morph.name, 'hello') self.assertEqual(morph.kind, 'chunk') @@ -92,7 +107,7 @@ class MorphologyTests(unittest.TestCase): def test_build_system_defaults_to_None(self): morph = morphlib.morphology.Morphology( - 'repo', 'ref', + FakeTreeish(), MockFile(''' { "name": "hello", @@ -102,7 +117,7 @@ class MorphologyTests(unittest.TestCase): def test_max_jobs_defaults_to_None(self): morph = morphlib.morphology.Morphology( - 'repo', 'ref', + FakeTreeish(), MockFile(''' { "name": "hello", @@ -112,7 +127,7 @@ class MorphologyTests(unittest.TestCase): def test_accepts_valid_stratum_morphology(self): morph = morphlib.morphology.Morphology( - 'repo', 'ref', + FakeTreeish(), MockFile(''' { "name": "hello", @@ -124,22 +139,21 @@ class MorphologyTests(unittest.TestCase): "ref": "ref" } ] - }'''), - baseurl='git://example.com') + }''')) self.assertEqual(morph.kind, 'stratum') self.assertEqual(morph.filename, 'mockfile') self.assertEqual(morph.sources, [ { u'name': u'foo', - u'repo': u'git://example.com/foo/', - u'ref': u'ref' + u'repo': u'foo', + u'ref': u'ref', }, ]) def test_accepts_valid_system_morphology(self): morph = morphlib.morphology.Morphology( - 'repo', 'ref', + FakeTreeish(), MockFile(''' { "name": "hello", @@ -158,35 +172,3 @@ class MorphologyTests(unittest.TestCase): self.assertEqual(morph.disk_size, '1G') self.assertEqual(morph.strata, ['foo', 'bar']) self.assertEqual(morph.test_stories, ['test-1', 'test-2']) - - -class StratumRepoTests(unittest.TestCase): - - def stratum(self, repo): - return morphlib.morphology.Morphology( - 'repo', 'ref', - MockFile(''' - { - "name": "hello", - "kind": "stratum", - "sources": - [ - { - "name": "foo", - "repo": "%s", - "ref": "HEAD" - } - ] - }''' % repo), - baseurl='git://git.baserock.org/') - - def test_leaves_absolute_repo_in_source_dict_as_is(self): - stratum = self.stratum('git://git.baserock.org/foo/') - self.assertEqual(stratum.sources[0]['repo'], - 'git://git.baserock.org/foo/') - - def test_makes_relative_repo_url_absolute_in_source_dict(self): - stratum = self.stratum('foo') - self.assertEqual(stratum.sources[0]['repo'], - 'git://git.baserock.org/foo/') - diff --git a/morphlib/morphologyloader.py b/morphlib/morphologyloader.py index 5a975a60..f59a8ddf 100644 --- a/morphlib/morphologyloader.py +++ b/morphlib/morphologyloader.py @@ -14,14 +14,12 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -import os import StringIO import urlparse import morphlib -#FIXME should use Treeishes everywhere and get stuff from the SourceManager class MorphologyLoader(object): '''Load morphologies from git and parse them into Morphology objects.''' @@ -30,31 +28,20 @@ class MorphologyLoader(object): self.settings = settings self.morphologies = {} - def load(self, repo, ref, filename): - base_url = self.settings['git-base-url'] - if not base_url.endswith('/'): - base_url += '/' - repo = urlparse.urljoin(base_url, repo) - - key = (repo, ref, filename) - + def load(self, treeish, filename): + key = (treeish, filename) if key in self.morphologies: return self.morphologies[key] else: - morph = self._get_morph_from_git(repo, ref, filename) + morph = self._get_morph_from_git(treeish, filename) self.morphologies[key] = morph return morph - def _get_morph_text(self, repo, ref, filename): # pragma: no cover - path = urlparse.urlparse(repo).path - t = morphlib.git.Treeish(path, ref) - return morphlib.git.get_morph_text(t, filename) - - def _get_morph_from_git(self, repo, ref, filename): - morph_text = self._get_morph_text(repo, ref, filename) - scheme, netlock, path, params, query, frag = urlparse.urlparse(repo) - f = StringIO.StringIO(morph_text) - f.name = os.path.join(path, filename) - morph = morphlib.morphology.Morphology(repo, ref, f, - self.settings['git-base-url']) - return morph + def _get_morph_text(self, treeish, filename): # pragma: no cover + return morphlib.git.get_morph_text(treeish, filename) + + def _get_morph_from_git(self, treeish, filename): + morph_text = self._get_morph_text(treeish, filename) + fp = StringIO.StringIO(morph_text) + fp.name = filename + return morphlib.morphology.Morphology(treeish, fp) diff --git a/morphlib/morphologyloader_tests.py b/morphlib/morphologyloader_tests.py index 475ce391..60a6b77b 100644 --- a/morphlib/morphologyloader_tests.py +++ b/morphlib/morphologyloader_tests.py @@ -26,11 +26,16 @@ class MorphologyLoaderTests(unittest.TestCase): loader = morphlib.morphologyloader.MorphologyLoader(settings) loader._get_morph_text = self.get_morph_text - morph1 = loader.load('repo', 'ref', 'hello.morph') - morph2 = loader.load('repo', 'ref', 'hello.morph') + class FakeTreeish(object): + def __init__(self): + self.original_repo = 'test-repo' + faketreeish = FakeTreeish() + + morph1 = loader.load(faketreeish, 'foo.morph') + morph2 = loader.load(faketreeish, 'foo.morph') self.assertEqual(morph1, morph2) - def get_morph_text(self, repo, ref, filename): + def get_morph_text(self, treeish, filename): return (''' { "name": "foo", diff --git a/morphlib/sourcemanager.py b/morphlib/sourcemanager.py index f78b8cd6..ba97c3ef 100644 --- a/morphlib/sourcemanager.py +++ b/morphlib/sourcemanager.py @@ -46,16 +46,21 @@ class SourceNotFound(Exception): class SourceManager(object): - def __init__(self, cachedir, app): - self.source_cache_dir = cachedir + def __init__(self, app, cachedir=None): self.msg = app.msg self.settings = app.settings + self.cached_treeishes = {} + self.cache_dir = cachedir + if not self.cache_dir: + self.cache_dir = os.path.join(app.settings['cachedir'], 'gits') def _get_git_cache(self, repo): name = quote_url(repo) - location = self.source_cache_dir + '/' + name + location = self.cache_dir + '/' + name if os.path.exists(location): + self.msg('updating cached version of %s' % location) + morphlib.git.update_remote(location, "origin") return True, location success = False @@ -63,7 +68,7 @@ class SourceManager(object): self.msg('Making sure we have a local cache of the git repo') bundle = None - if self.settings.has_key('bundle-server'): + if self.settings['bundle-server']: bundle_server = self.settings['bundle-server'] if not bundle_server.endswith('/'): bundle_server += '/' @@ -77,7 +82,7 @@ class SourceManager(object): try: urllib2.urlopen(req) self._wget(lookup_url) - bundle = self.source_cache_dir + '/' + bundle + bundle = self.cache_dir + '/' + bundle except urllib2.URLError: self.msg("Unable to find bundle %s on %s" % (bundle, bundle_server)) @@ -100,7 +105,7 @@ class SourceManager(object): return success, location def _wget(self,url): # pragma: no cover - ex = morphlib.execute.Execute(self.source_cache_dir, msg=self.msg) + ex = morphlib.execute.Execute(self.cache_dir, msg=self.msg) ex.runv(['wget', '-c', url]) def get_treeish(self, repo, ref): @@ -132,13 +137,13 @@ class SourceManager(object): full_repo = urlparse.urljoin(base_url, repo) self.msg('cache git base_url=\'%s\' full repo url=\'%s\'' % - (base_url,full_repo)) + (base_url, full_repo)) success, gitcache = self._get_git_cache(full_repo); if not success: raise SourceNotFound(repo,ref) - self.msg("creating treeish for %s ref %s" % (gitcache,ref)) - treeish = Treeish(gitcache, ref, self.msg) + self.msg("creating treeish for %s ref %s" % (gitcache, ref)) + treeish = Treeish(gitcache, repo, ref, self.msg) return treeish diff --git a/morphlib/sourcemanager_tests.py b/morphlib/sourcemanager_tests.py index c781327f..1fb0b6c0 100644 --- a/morphlib/sourcemanager_tests.py +++ b/morphlib/sourcemanager_tests.py @@ -25,8 +25,13 @@ import morphlib class DummyApp(object): - def __init__(self): - self.settings = { 'git-base-url': ['.',] } + + def __init__(self): + self.settings = { + 'git-base-url': ['.',], + 'bundle-server': None, + 'cachedir': '/foo/bar/baz', + } self.msg = lambda msg: None @@ -37,18 +42,29 @@ class SourceManagerTests(unittest.TestCase): env = os.environ env["DATADIR"]=self.temprepodir subprocess.call("./tests/show-dependencies.setup", shell=True, env=env) - self.temprepo = self.temprepodir + '/test-repo/' + self.temprepo = self.temprepodir + '/test-repo/' bundle_name = morphlib.sourcemanager.quote_url(self.temprepo) + '.bndl' subprocess.call("git bundle create %s/%s master" % (self.temprepodir, bundle_name), - shell=True, cwd=self.temprepo) + shell=True, cwd=self.temprepo) def tearDown(self): shutil.rmtree(self.temprepodir) + def test_constructor_with_and_without_cachedir(self): + app = DummyApp() + + tempdir = '/bla/bla/bla' + s = morphlib.sourcemanager.SourceManager(app, tempdir) + self.assertEqual(s.cache_dir, tempdir) + + s = morphlib.sourcemanager.SourceManager(app) + self.assertEqual(s.cache_dir, + os.path.join(app.settings['cachedir'], 'gits')) + def test_get_sha1_treeish_for_self(self): tempdir = tempfile.mkdtemp() - s = morphlib.sourcemanager.SourceManager(tempdir, DummyApp()) + s = morphlib.sourcemanager.SourceManager(DummyApp(), tempdir) t = s.get_treeish(self.temprepo, 'e28a23812eadf2fce6583b8819b9c5dbd36b9fb9') self.assertEquals(t.sha1, 'e28a23812eadf2fce6583b8819b9c5dbd36b9fb9') @@ -58,12 +74,12 @@ class SourceManagerTests(unittest.TestCase): def test_get_sha1_treeish_for_self_twice(self): tempdir = tempfile.mkdtemp() - s = morphlib.sourcemanager.SourceManager(tempdir, DummyApp()) + s = morphlib.sourcemanager.SourceManager(DummyApp(), tempdir) t = s.get_treeish(self.temprepo, 'e28a23812eadf2fce6583b8819b9c5dbd36b9fb9') self.assertEquals(t.sha1, 'e28a23812eadf2fce6583b8819b9c5dbd36b9fb9') - s = morphlib.sourcemanager.SourceManager(tempdir, DummyApp()) + s = morphlib.sourcemanager.SourceManager(DummyApp(), tempdir) t = s.get_treeish(self.temprepo, 'e28a23812eadf2fce6583b8819b9c5dbd36b9fb9') self.assertEquals(t.sha1, 'e28a23812eadf2fce6583b8819b9c5dbd36b9fb9') @@ -73,9 +89,9 @@ class SourceManagerTests(unittest.TestCase): def test_get_ref_treeish_for_self(self): tempdir = tempfile.mkdtemp() - s = morphlib.sourcemanager.SourceManager(tempdir, DummyApp()) + s = morphlib.sourcemanager.SourceManager(DummyApp(), tempdir) t = s.get_treeish(self.temprepo, 'master') - self.assertEquals(t.ref, 'refs/heads/master') + self.assertEquals(t.ref, 'refs/remotes/origin/master') shutil.rmtree(tempdir) @@ -86,11 +102,11 @@ class SourceManagerTests(unittest.TestCase): app = DummyApp() app.settings['bundle-server'] = 'file://' + bundle_server_loc - s = morphlib.sourcemanager.SourceManager(tempdir, app) + s = morphlib.sourcemanager.SourceManager(app, tempdir) def wget(url): path=urlparse(url).path - shutil.copy(path, s.source_cache_dir) + shutil.copy(path, s.cache_dir) s._wget = wget @@ -100,17 +116,16 @@ class SourceManagerTests(unittest.TestCase): shutil.rmtree(tempdir) - def test_get_sha1_treeish_for_self_bundle_fail(self): tempdir = tempfile.mkdtemp() app = DummyApp() app.settings['bundle-server'] = 'file://' + self.temprepodir - s = morphlib.sourcemanager.SourceManager(tempdir, app) + s = morphlib.sourcemanager.SourceManager(app, tempdir) def wget(url): path=urlparse(url).path - shutil.copy(path, s.source_cache_dir) + shutil.copy(path, s.cache_dir) s._wget = wget self.assertRaises(morphlib.sourcemanager.SourceNotFound, s.get_treeish, @@ -124,8 +139,7 @@ class SourceManagerTests(unittest.TestCase): app = DummyApp() app.settings['git-base-url'] = ['.', '/somewhere/else'] - - s = morphlib.sourcemanager.SourceManager(tempdir, app) + s = morphlib.sourcemanager.SourceManager(app, tempdir) t = s.get_treeish(self.temprepo, 'e28a23812eadf2fce6583b8819b9c5dbd36b9fb9') self.assertEquals(t.sha1, 'e28a23812eadf2fce6583b8819b9c5dbd36b9fb9') diff --git a/tests/missing-ref.stderr b/tests/missing-ref.stderr index a184134b..5a1ffb2a 100644 --- a/tests/missing-ref.stderr +++ b/tests/missing-ref.stderr @@ -1 +1 @@ -ERROR: non-existent-branch is an invalid reference for repo /chunk-repo/ +ERROR: non-existent-branch is an invalid reference for repo chunk-repo diff --git a/tests/show-dependencies.stdout b/tests/show-dependencies.stdout index c2db0c94..5ad63c65 100644 --- a/tests/show-dependencies.stdout +++ b/tests/show-dependencies.stdout @@ -1,133 +1,133 @@ dependency tree: - test-repo|master|cairo.morph - test-repo|master|dbus-glib.morph - -> test-repo|master|dbus.morph - -> test-repo|master|glib.morph - test-repo|master|dbus.morph - test-repo|master|exo.morph - -> test-repo|master|gtk-stack.morph - -> test-repo|master|libxfce4util.morph - test-repo|master|fontconfig.morph - test-repo|master|freetype.morph - test-repo|master|garcon.morph - -> test-repo|master|gtk-stack.morph - -> test-repo|master|libxfce4util.morph - test-repo|master|gdk-pixbuf.morph - -> test-repo|master|glib.morph - test-repo|master|glib.morph - test-repo|master|gtk-stack.morph - -> test-repo|master|cairo.morph - -> test-repo|master|dbus-glib.morph - -> test-repo|master|dbus.morph - -> test-repo|master|fontconfig.morph - -> test-repo|master|freetype.morph - -> test-repo|master|gdk-pixbuf.morph - -> test-repo|master|glib.morph - -> test-repo|master|gtk.morph - -> test-repo|master|pango.morph - test-repo|master|gtk-xfce-engine.morph - -> test-repo|master|garcon.morph - -> test-repo|master|gtk-stack.morph - -> test-repo|master|libxfce4ui.morph - -> test-repo|master|xfconf.morph - test-repo|master|gtk.morph - -> test-repo|master|cairo.morph - -> test-repo|master|gdk-pixbuf.morph - -> test-repo|master|glib.morph - -> test-repo|master|pango.morph - test-repo|master|libxfce4ui.morph - -> test-repo|master|gtk-stack.morph - -> test-repo|master|xfconf.morph - test-repo|master|libxfce4util.morph - -> test-repo|master|gtk-stack.morph - test-repo|master|pango.morph - -> test-repo|master|fontconfig.morph - -> test-repo|master|freetype.morph - test-repo|master|thunar.morph - -> test-repo|master|exo.morph - -> test-repo|master|gtk-stack.morph - -> test-repo|master|libxfce4ui.morph - test-repo|master|tumbler.morph - -> test-repo|master|gtk-stack.morph - test-repo|master|xfce-core.morph - -> test-repo|master|exo.morph - -> test-repo|master|garcon.morph - -> test-repo|master|gtk-stack.morph - -> test-repo|master|gtk-xfce-engine.morph - -> test-repo|master|libxfce4ui.morph - -> test-repo|master|libxfce4util.morph - -> test-repo|master|thunar.morph - -> test-repo|master|tumbler.morph - -> test-repo|master|xfce4-appfinder.morph - -> test-repo|master|xfce4-panel.morph - -> test-repo|master|xfce4-session.morph - -> test-repo|master|xfce4-settings.morph - -> test-repo|master|xfconf.morph - -> test-repo|master|xfdesktop.morph - -> test-repo|master|xfwm4.morph - test-repo|master|xfce4-appfinder.morph - -> test-repo|master|garcon.morph - -> test-repo|master|gtk-stack.morph - -> test-repo|master|libxfce4ui.morph - -> test-repo|master|xfconf.morph - test-repo|master|xfce4-panel.morph - -> test-repo|master|exo.morph - -> test-repo|master|garcon.morph - -> test-repo|master|gtk-stack.morph - -> test-repo|master|libxfce4ui.morph - test-repo|master|xfce4-session.morph - -> test-repo|master|exo.morph - -> test-repo|master|gtk-stack.morph - -> test-repo|master|libxfce4ui.morph - -> test-repo|master|xfconf.morph - test-repo|master|xfce4-settings.morph - -> test-repo|master|exo.morph - -> test-repo|master|gtk-stack.morph - -> test-repo|master|libxfce4ui.morph - -> test-repo|master|xfconf.morph - test-repo|master|xfconf.morph - -> test-repo|master|gtk-stack.morph - -> test-repo|master|libxfce4util.morph - test-repo|master|xfdesktop.morph - -> test-repo|master|gtk-stack.morph - -> test-repo|master|libxfce4ui.morph - -> test-repo|master|xfconf.morph - test-repo|master|xfwm4.morph - -> test-repo|master|gtk-stack.morph - -> test-repo|master|libxfce4ui.morph - -> test-repo|master|xfconf.morph + test-repo|refs/remotes/origin/master|cairo.morph + test-repo|refs/remotes/origin/master|dbus-glib.morph + -> test-repo|refs/remotes/origin/master|dbus.morph + -> test-repo|refs/remotes/origin/master|glib.morph + test-repo|refs/remotes/origin/master|dbus.morph + test-repo|refs/remotes/origin/master|exo.morph + -> test-repo|refs/remotes/origin/master|gtk-stack.morph + -> test-repo|refs/remotes/origin/master|libxfce4util.morph + test-repo|refs/remotes/origin/master|fontconfig.morph + test-repo|refs/remotes/origin/master|freetype.morph + test-repo|refs/remotes/origin/master|garcon.morph + -> test-repo|refs/remotes/origin/master|gtk-stack.morph + -> test-repo|refs/remotes/origin/master|libxfce4util.morph + test-repo|refs/remotes/origin/master|gdk-pixbuf.morph + -> test-repo|refs/remotes/origin/master|glib.morph + test-repo|refs/remotes/origin/master|glib.morph + test-repo|refs/remotes/origin/master|gtk-stack.morph + -> test-repo|refs/remotes/origin/master|cairo.morph + -> test-repo|refs/remotes/origin/master|dbus-glib.morph + -> test-repo|refs/remotes/origin/master|dbus.morph + -> test-repo|refs/remotes/origin/master|fontconfig.morph + -> test-repo|refs/remotes/origin/master|freetype.morph + -> test-repo|refs/remotes/origin/master|gdk-pixbuf.morph + -> test-repo|refs/remotes/origin/master|glib.morph + -> test-repo|refs/remotes/origin/master|gtk.morph + -> test-repo|refs/remotes/origin/master|pango.morph + test-repo|refs/remotes/origin/master|gtk-xfce-engine.morph + -> test-repo|refs/remotes/origin/master|garcon.morph + -> test-repo|refs/remotes/origin/master|gtk-stack.morph + -> test-repo|refs/remotes/origin/master|libxfce4ui.morph + -> test-repo|refs/remotes/origin/master|xfconf.morph + test-repo|refs/remotes/origin/master|gtk.morph + -> test-repo|refs/remotes/origin/master|cairo.morph + -> test-repo|refs/remotes/origin/master|gdk-pixbuf.morph + -> test-repo|refs/remotes/origin/master|glib.morph + -> test-repo|refs/remotes/origin/master|pango.morph + test-repo|refs/remotes/origin/master|libxfce4ui.morph + -> test-repo|refs/remotes/origin/master|gtk-stack.morph + -> test-repo|refs/remotes/origin/master|xfconf.morph + test-repo|refs/remotes/origin/master|libxfce4util.morph + -> test-repo|refs/remotes/origin/master|gtk-stack.morph + test-repo|refs/remotes/origin/master|pango.morph + -> test-repo|refs/remotes/origin/master|fontconfig.morph + -> test-repo|refs/remotes/origin/master|freetype.morph + test-repo|refs/remotes/origin/master|thunar.morph + -> test-repo|refs/remotes/origin/master|exo.morph + -> test-repo|refs/remotes/origin/master|gtk-stack.morph + -> test-repo|refs/remotes/origin/master|libxfce4ui.morph + test-repo|refs/remotes/origin/master|tumbler.morph + -> test-repo|refs/remotes/origin/master|gtk-stack.morph + test-repo|refs/remotes/origin/master|xfce-core.morph + -> test-repo|refs/remotes/origin/master|exo.morph + -> test-repo|refs/remotes/origin/master|garcon.morph + -> test-repo|refs/remotes/origin/master|gtk-stack.morph + -> test-repo|refs/remotes/origin/master|gtk-xfce-engine.morph + -> test-repo|refs/remotes/origin/master|libxfce4ui.morph + -> test-repo|refs/remotes/origin/master|libxfce4util.morph + -> test-repo|refs/remotes/origin/master|thunar.morph + -> test-repo|refs/remotes/origin/master|tumbler.morph + -> test-repo|refs/remotes/origin/master|xfce4-appfinder.morph + -> test-repo|refs/remotes/origin/master|xfce4-panel.morph + -> test-repo|refs/remotes/origin/master|xfce4-session.morph + -> test-repo|refs/remotes/origin/master|xfce4-settings.morph + -> test-repo|refs/remotes/origin/master|xfconf.morph + -> test-repo|refs/remotes/origin/master|xfdesktop.morph + -> test-repo|refs/remotes/origin/master|xfwm4.morph + test-repo|refs/remotes/origin/master|xfce4-appfinder.morph + -> test-repo|refs/remotes/origin/master|garcon.morph + -> test-repo|refs/remotes/origin/master|gtk-stack.morph + -> test-repo|refs/remotes/origin/master|libxfce4ui.morph + -> test-repo|refs/remotes/origin/master|xfconf.morph + test-repo|refs/remotes/origin/master|xfce4-panel.morph + -> test-repo|refs/remotes/origin/master|exo.morph + -> test-repo|refs/remotes/origin/master|garcon.morph + -> test-repo|refs/remotes/origin/master|gtk-stack.morph + -> test-repo|refs/remotes/origin/master|libxfce4ui.morph + test-repo|refs/remotes/origin/master|xfce4-session.morph + -> test-repo|refs/remotes/origin/master|exo.morph + -> test-repo|refs/remotes/origin/master|gtk-stack.morph + -> test-repo|refs/remotes/origin/master|libxfce4ui.morph + -> test-repo|refs/remotes/origin/master|xfconf.morph + test-repo|refs/remotes/origin/master|xfce4-settings.morph + -> test-repo|refs/remotes/origin/master|exo.morph + -> test-repo|refs/remotes/origin/master|gtk-stack.morph + -> test-repo|refs/remotes/origin/master|libxfce4ui.morph + -> test-repo|refs/remotes/origin/master|xfconf.morph + test-repo|refs/remotes/origin/master|xfconf.morph + -> test-repo|refs/remotes/origin/master|gtk-stack.morph + -> test-repo|refs/remotes/origin/master|libxfce4util.morph + test-repo|refs/remotes/origin/master|xfdesktop.morph + -> test-repo|refs/remotes/origin/master|gtk-stack.morph + -> test-repo|refs/remotes/origin/master|libxfce4ui.morph + -> test-repo|refs/remotes/origin/master|xfconf.morph + test-repo|refs/remotes/origin/master|xfwm4.morph + -> test-repo|refs/remotes/origin/master|gtk-stack.morph + -> test-repo|refs/remotes/origin/master|libxfce4ui.morph + -> test-repo|refs/remotes/origin/master|xfconf.morph build order: group: - test-repo|master|cairo.morph - test-repo|master|dbus.morph - test-repo|master|fontconfig.morph - test-repo|master|freetype.morph - test-repo|master|glib.morph + test-repo|refs/remotes/origin/master|cairo.morph + test-repo|refs/remotes/origin/master|dbus.morph + test-repo|refs/remotes/origin/master|fontconfig.morph + test-repo|refs/remotes/origin/master|freetype.morph + test-repo|refs/remotes/origin/master|glib.morph group: - test-repo|master|dbus-glib.morph - test-repo|master|gdk-pixbuf.morph - test-repo|master|pango.morph + test-repo|refs/remotes/origin/master|dbus-glib.morph + test-repo|refs/remotes/origin/master|gdk-pixbuf.morph + test-repo|refs/remotes/origin/master|pango.morph group: - test-repo|master|gtk.morph + test-repo|refs/remotes/origin/master|gtk.morph group: - test-repo|master|gtk-stack.morph + test-repo|refs/remotes/origin/master|gtk-stack.morph group: - test-repo|master|libxfce4util.morph - test-repo|master|tumbler.morph + test-repo|refs/remotes/origin/master|libxfce4util.morph + test-repo|refs/remotes/origin/master|tumbler.morph group: - test-repo|master|exo.morph - test-repo|master|garcon.morph - test-repo|master|xfconf.morph + test-repo|refs/remotes/origin/master|exo.morph + test-repo|refs/remotes/origin/master|garcon.morph + test-repo|refs/remotes/origin/master|xfconf.morph group: - test-repo|master|libxfce4ui.morph + test-repo|refs/remotes/origin/master|libxfce4ui.morph group: - test-repo|master|gtk-xfce-engine.morph - test-repo|master|thunar.morph - test-repo|master|xfce4-appfinder.morph - test-repo|master|xfce4-panel.morph - test-repo|master|xfce4-session.morph - test-repo|master|xfce4-settings.morph - test-repo|master|xfdesktop.morph - test-repo|master|xfwm4.morph + test-repo|refs/remotes/origin/master|gtk-xfce-engine.morph + test-repo|refs/remotes/origin/master|thunar.morph + test-repo|refs/remotes/origin/master|xfce4-appfinder.morph + test-repo|refs/remotes/origin/master|xfce4-panel.morph + test-repo|refs/remotes/origin/master|xfce4-session.morph + test-repo|refs/remotes/origin/master|xfce4-settings.morph + test-repo|refs/remotes/origin/master|xfdesktop.morph + test-repo|refs/remotes/origin/master|xfwm4.morph group: - test-repo|master|xfce-core.morph + test-repo|refs/remotes/origin/master|xfce-core.morph |