diff options
23 files changed, 541 insertions, 74 deletions
@@ -0,0 +1,9 @@ +NEWS for Morph +============== + +This file contains high-level summaries of what's changed in each release. + +Version 11, released 2013-10-01 +------------------------------- + +* NEWS file added. diff --git a/morphlib/__init__.py b/morphlib/__init__.py index b1e3c7c3..7eb3f975 100644 --- a/morphlib/__init__.py +++ b/morphlib/__init__.py @@ -78,6 +78,7 @@ import sourcepool import stagingarea import stopwatch import sysbranchdir +import systemmetadatadir import tempdir import util import workspace diff --git a/morphlib/app.py b/morphlib/app.py index 08b020ff..a0833d45 100644 --- a/morphlib/app.py +++ b/morphlib/app.py @@ -318,11 +318,15 @@ class Morph(cliapp.Application): visit(reponame, ref, filename, absref, tree, morphology) if morphology['kind'] == 'system': - queue.extend((s['repo'], s['ref'], '%s.morph' % s['morph']) + queue.extend((s['repo'] or reponame, + s['ref'] or ref, + '%s.morph' % s['morph']) for s in morphology['strata']) elif morphology['kind'] == 'stratum': if morphology['build-depends']: - queue.extend((s['repo'], s['ref'], '%s.morph' % s['morph']) + queue.extend((s['repo'] or reponame, + s['ref'] or ref, + '%s.morph' % s['morph']) for s in morphology['build-depends']) queue.extend((c['repo'], c['ref'], '%s.morph' % c['morph']) for c in morphology['chunks']) diff --git a/morphlib/artifactresolver.py b/morphlib/artifactresolver.py index 186d5357..17f038a2 100644 --- a/morphlib/artifactresolver.py +++ b/morphlib/artifactresolver.py @@ -155,8 +155,8 @@ class ArtifactResolver(object): for info in source.morphology['strata']: stratum_source = self._source_pool.lookup( - info['repo'], - info['ref'], + info['repo'] or source.repo_name, + info['ref'] or source.original_ref, '%s.morph' % info['morph']) stratum_name = stratum_source.morphology.builds_artifacts[0] @@ -178,8 +178,8 @@ class ArtifactResolver(object): if stratum.source.morphology['build-depends']: for stratum_info in stratum.source.morphology['build-depends']: other_source = self._source_pool.lookup( - stratum_info['repo'], - stratum_info['ref'], + stratum_info['repo'] or stratum.source.repo_name, + stratum_info['ref'] or stratum.source.original_ref, '%s.morph' % stratum_info['morph']) other_stratum = self._get_artifact( diff --git a/morphlib/buildcommand.py b/morphlib/buildcommand.py index d7007233..e76b7a14 100644 --- a/morphlib/buildcommand.py +++ b/morphlib/buildcommand.py @@ -163,8 +163,8 @@ class BuildCommand(object): def _validate_cross_refs_for_xxx(self, src, srcpool, specs, wanted): for spec in specs: - repo_name = spec['repo'] - ref = spec['ref'] + repo_name = spec['repo'] or src.repo_name + ref = spec['ref'] or src.original_ref filename = '%s.morph' % spec['morph'] logging.debug( 'Validating cross ref to %s:%s:%s' % diff --git a/morphlib/morph2.py b/morphlib/morph2.py index 862f34ab..a10dda8d 100644 --- a/morphlib/morph2.py +++ b/morphlib/morph2.py @@ -236,7 +236,8 @@ class Morphology(object): continue value = self._apply_changes_for_key(key, live_dict, original_dict) - if value is not None: + # VILE HACK to preserve nulls in repo/ref fields + if value is not None or key in ('repo', 'ref'): output_dict[key] = value return output_dict diff --git a/morphlib/morphloader.py b/morphlib/morphloader.py index 9601784c..8c20d9ed 100644 --- a/morphlib/morphloader.py +++ b/morphlib/morphloader.py @@ -54,11 +54,12 @@ class InvalidFieldError(morphlib.Error): self.msg = ( 'Field %s not allowed in morphology %s' % (field, morphology)) -class ObsoleteFieldError(morphlib.Error): +class ObsoleteFieldsError(morphlib.Error): - def __init__(self, field, morphology): - self.msg = ( - 'Field %s is now obsolete but present in %s' % (field, morphology)) + def __init__(self, fields, morphology): + self.msg = ( + 'Morphology %s uses obsolete fields: %s' % + (morphology, ' '.join(fields))) class UnknownArchitectureError(morphlib.Error): @@ -307,9 +308,9 @@ class MorphologyLoader(object): self._require_field(field, morphology) def _deny_obsolete_fields(self, fields, morphology): - for field in morphology: - if field in fields: - raise ObsoleteFieldError(field, morphology.filename) + obsolete_ones = [x for x in morphology if x in fields] + if obsolete_ones: + raise ObsoleteFieldsError(obsolete_ones, morphology.filename) def _deny_unknown_fields(self, allowed, morphology): for field in morphology: diff --git a/morphlib/morphloader_tests.py b/morphlib/morphloader_tests.py index a06ba1e7..f38d58e8 100644 --- a/morphlib/morphloader_tests.py +++ b/morphlib/morphloader_tests.py @@ -102,7 +102,7 @@ build-system: dummy 'system-kind': 'foo', }) self.assertRaises( - morphlib.morphloader.ObsoleteFieldError, self.loader.validate, m) + morphlib.morphloader.ObsoleteFieldsError, self.loader.validate, m) def test_fails_to_validate_system_with_obsolete_disk_size_field(self): m = morphlib.morph3.Morphology({ @@ -112,7 +112,7 @@ build-system: dummy 'disk-size': 'over 9000', }) self.assertRaises( - morphlib.morphloader.ObsoleteFieldError, self.loader.validate, m) + morphlib.morphloader.ObsoleteFieldsError, self.loader.validate, m) def test_fails_to_validate_system_with_no_fields(self): m = morphlib.morph3.Morphology({ diff --git a/morphlib/morphset.py b/morphlib/morphset.py index 3c07d58e..9ef1e804 100644 --- a/morphlib/morphset.py +++ b/morphlib/morphset.py @@ -95,9 +95,11 @@ class MorphologySet(object): repo_url, ref, morph = self._find_spec( system_morph['strata'], stratum_name) - if repo_url is None: + if (repo_url, ref, morph) == (None, None, None): raise StratumNotInSystemError(system_morph['name'], stratum_name) - m = self._get_morphology(repo_url, ref, '%s.morph' % morph) + m = self._get_morphology(repo_url or system_morph.repo_url, + ref or system_morph.ref, + '%s.morph' % morph) if m is None: raise StratumNotInSetError(stratum_name) return m @@ -118,7 +120,7 @@ class MorphologySet(object): repo_url, ref, morph = self._find_spec( stratum_morph['chunks'], chunk_name) - if repo_url is None: + if (repo_url, ref, morph) == (None, None, None): raise ChunkNotInStratumError(stratum_morph['name'], chunk_name) return repo_url, ref, morph diff --git a/morphlib/plugins/branch_and_merge_new_plugin.py b/morphlib/plugins/branch_and_merge_new_plugin.py index 621bf907..0a43b918 100644 --- a/morphlib/plugins/branch_and_merge_new_plugin.py +++ b/morphlib/plugins/branch_and_merge_new_plugin.py @@ -15,6 +15,7 @@ import cliapp +import contextlib import glob import logging import os @@ -55,6 +56,15 @@ class SimpleBranchAndMergePlugin(cliapp.Plugin): arg_synopsis='-- COMMAND [ARGS...]') self.app.add_subcommand('status', self.status, arg_synopsis='') + self.app.add_subcommand('branch-from-image', self.branch_from_image, + arg_synopsis='BRANCH') + group_branch = 'Branching Options' + self.app.settings.string(['metadata-dir'], + 'Set metadata location for branch-from-image' + ' (default: /baserock)', + metavar='DIR', + default='/baserock', + group=group_branch) def disable(self): pass @@ -97,6 +107,40 @@ class SimpleBranchAndMergePlugin(cliapp.Plugin): ws = morphlib.workspace.open('.') self.app.output.write('%s\n' % ws.root) + # TODO: Move this somewhere nicer + @contextlib.contextmanager + def _initializing_system_branch(self, ws, root_url, system_branch, + cached_repo, base_ref): + '''A context manager for system branches under construction. + + The purpose of this context manager is to factor out the branch + cleanup code for if an exception occurs while a branch is being + constructed. + + This could be handled by a higher order function which takes + a function to initialize the branch as a parameter, but with + statements look nicer and are more obviously about resource + cleanup. + + ''' + root_dir = ws.get_default_system_branch_directory_name(system_branch) + try: + sb = morphlib.sysbranchdir.create( + root_dir, root_url, system_branch) + gd = sb.clone_cached_repo(cached_repo, base_ref) + + yield (sb, gd) + + gd.update_submodules(self.app) + gd.update_remotes() + + except BaseException as e: + # Oops. Clean up. + logging.error('Caught exception: %s' % str(e)) + logging.info('Removing half-finished branch %s' % system_branch) + self._remove_branch_dir_safe(ws.root, root_dir) + raise + def checkout(self, args): '''Check out an existing system branch. @@ -124,6 +168,7 @@ class SimpleBranchAndMergePlugin(cliapp.Plugin): root_url = args[0] system_branch = args[1] + base_ref = system_branch self._require_git_user_config() @@ -139,27 +184,12 @@ class SimpleBranchAndMergePlugin(cliapp.Plugin): # Check the git branch exists. cached_repo.resolve_ref(system_branch) - root_dir = ws.get_default_system_branch_directory_name(system_branch) - - try: - # Create the system branch directory. This doesn't yet clone - # the root repository there. - sb = morphlib.sysbranchdir.create( - root_dir, root_url, system_branch) - - gd = sb.clone_cached_repo(cached_repo, system_branch) + with self._initializing_system_branch( + ws, root_url, system_branch, cached_repo, base_ref) as (sb, gd): if not self._checkout_has_systems(gd): - raise BranchRootHasNoSystemsError(root_url, system_branch) + raise BranchRootHasNoSystemsError(base_ref) - gd.update_submodules(self.app) - gd.update_remotes() - except BaseException as e: - # Oops. Clean up. - logging.error('Caught exception: %s' % str(e)) - logging.info('Removing half-finished branch %s' % system_branch) - self._remove_branch_dir_safe(ws.root, root_dir) - raise def branch(self, args): '''Create a new system branch. @@ -212,29 +242,14 @@ class SimpleBranchAndMergePlugin(cliapp.Plugin): # Make sure the base_ref exists. cached_repo.resolve_ref(base_ref) - root_dir = ws.get_default_system_branch_directory_name(system_branch) + with self._initializing_system_branch( + ws, root_url, system_branch, cached_repo, base_ref) as (sb, gd): - try: - # Create the system branch directory. This doesn't yet clone - # the root repository there. - sb = morphlib.sysbranchdir.create( - root_dir, root_url, system_branch) - - gd = sb.clone_cached_repo(cached_repo, base_ref) gd.branch(system_branch, base_ref) gd.checkout(system_branch) if not self._checkout_has_systems(gd): - raise BranchRootHasNoSystemsError(root_url, base_ref) - - gd.update_submodules(self.app) - gd.update_remotes() - except BaseException as e: - # Oops. Clean up. - logging.error('Caught exception: %s' % str(e)) - logging.info('Removing half-finished branch %s' % system_branch) - self._remove_branch_dir_safe(ws.root, root_dir) - raise + raise BranchRootHasNoSystemsError(base_ref) def _save_dirty_morphologies(self, loader, sb, morphs): logging.debug('Saving dirty morphologies: start') @@ -266,7 +281,9 @@ class SimpleBranchAndMergePlugin(cliapp.Plugin): # of triplets (repo url, ref, filename). return [ - (spec['repo'], spec['ref'], '%s.morph' % spec['morph']) + (spec['repo'] or morph.repo_url, + spec['ref'] or morph.ref, + '%s.morph' % spec['morph']) for spec in specs ] @@ -669,6 +686,9 @@ class SimpleBranchAndMergePlugin(cliapp.Plugin): #TODO: Stop using app.resolve_ref def resolve_refs(morphs): for repo, ref in morphs.list_refs(): + # You can't resolve null refs, so don't attempt to. + if repo is None or ref is None: + continue # TODO: Handle refs that are only in workspace in general if (repo == sb.root_repository_url and ref == sb.system_branch_name): @@ -774,3 +794,98 @@ class SimpleBranchAndMergePlugin(cliapp.Plugin): if not has_uncommitted_changes: self.app.output.write("\nNo repos have outstanding changes.\n") + + def branch_from_image(self, args): + '''Produce a branch of an existing system image. + + Given the metadata specified by --metadata-dir, create a new + branch then petrify it to the state of the commits at the time + the system was built. + + If --metadata-dir is not specified, it defaults to your currently + running system. + + ''' + if len(args) != 1: + raise cliapp.AppException( + "branch-from-image needs exactly 1 argument " + "of the new system branch's name") + system_branch = args[0] + metadata_path = self.app.settings['metadata-dir'] + alias_resolver = morphlib.repoaliasresolver.RepoAliasResolver( + self.app.settings['repo-alias']) + + self._require_git_user_config() + + ws = morphlib.workspace.open('.') + + system, metadata = self._load_system_metadata(metadata_path) + resolved_refs = dict(self._resolve_refs_from_metadata(alias_resolver, + metadata)) + logging.debug('Resolved refs: %r' % resolved_refs) + base_ref = system['sha1'] + # The previous version would fall back to deducing this from the repo + # url and the repo alias resolver, but this does not always work, and + # new systems always have repo-alias in the metadata + root_url = system['repo-alias'] + + lrc, rrc = morphlib.util.new_repo_caches(self.app) + cached_repo = lrc.get_updated_repo(root_url) + + + with self._initializing_system_branch( + ws, root_url, system_branch, cached_repo, base_ref) as (sb, gd): + + # TODO: It's nasty to clone to a sha1 then create a branch + # of that sha1 then check it out, a nicer API may be the + # initial clone not checking out a branch at all, then + # the user creates and checks out their own branches + gd.branch(system_branch, base_ref) + gd.checkout(system_branch) + + loader = morphlib.morphloader.MorphologyLoader() + morphs = self._load_all_sysbranch_morphologies(sb, loader) + + morphs.repoint_refs(sb.root_repository_url, + sb.system_branch_name) + + morphs.petrify_chunks(resolved_refs) + + self._save_dirty_morphologies(loader, sb, morphs.morphologies) + + @staticmethod + def _load_system_metadata(path): + '''Load all metadata in `path` corresponding to a single System. + ''' + + smd = morphlib.systemmetadatadir.SystemMetadataDir(path) + metadata = smd.values() + systems = [md for md in metadata + if 'kind' in md and md['kind'] == 'system'] + + if not systems: + raise cliapp.AppException( + 'Metadata directory does not contain any systems.') + if len(systems) > 1: + raise cliapp.AppException( + 'Metadata directory contains multiple systems.') + system_metadatum = systems[0] + + metadata_cache_id_lookup = dict((md['cache-key'], md) + for md in metadata) + + return system_metadatum, metadata_cache_id_lookup + + @staticmethod + def _resolve_refs_from_metadata(alias_resolver, metadata): + '''Pre-resolve a set of refs from existing metadata. + + Given the metadata, generate a mapping of all the (repo, ref) + pairs defined in the metadata and the commit id they resolved to. + + ''' + for md in metadata.itervalues(): + repourls = set((md['repo-alias'], md['repo'])) + repourls.update(alias_resolver.aliases_from_url(md['repo'])) + for repourl in repourls: + yield ((repourl, md['original_ref']), md['sha1']) diff --git a/morphlib/plugins/branch_and_merge_plugin.py b/morphlib/plugins/branch_and_merge_plugin.py index ea18dd47..fec16415 100644 --- a/morphlib/plugins/branch_and_merge_plugin.py +++ b/morphlib/plugins/branch_and_merge_plugin.py @@ -68,15 +68,9 @@ class BranchAndMergePlugin(cliapp.Plugin): self.app.add_subcommand('build', self.build, arg_synopsis='SYSTEM') self.app.add_subcommand('old-status', self.status) - self.app.add_subcommand('branch-from-image', self.branch_from_image, - arg_synopsis='REPO BRANCH') - group_branch = 'Branching Options' - self.app.settings.string(['metadata-dir'], - 'Set metadata location for branch-from-image' - ' (default: /baserock)', - metavar='DIR', - default='/baserock', - group=group_branch) + self.app.add_subcommand('old-branch-from-image', + self.branch_from_image, + arg_synopsis='REPO BRANCH') # Advanced commands self.app.add_subcommand('old-foreach', self.foreach, diff --git a/morphlib/systemmetadatadir.py b/morphlib/systemmetadatadir.py new file mode 100644 index 00000000..eac5b446 --- /dev/null +++ b/morphlib/systemmetadatadir.py @@ -0,0 +1,87 @@ +# Copyright (C) 2013 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 +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# =*= License: GPL-2 =*= + + +import collections +import glob +import json +import os + + +class SystemMetadataDir(collections.MutableMapping): + + '''An abstraction over the /baserock metadata directory. + + This allows methods of iterating over it, and accessing it like + a dict. + + The /baserock metadata directory contains information about all of + the chunks in a built system. It exists to provide traceability from + the input sources to the output. + + If you create the object with smd = SystemMetadataDir('/baserock') + data = smd['key'] will read /baserock/key.meta and return its JSON + encoded contents as native python objects. + + smd['key'] = data will write data to /baserock/key.meta as JSON + + The key may not have '\0' characters in it since the underlying + system calls don't support embedded NUL bytes. + + The key may not have '/' characters in it since we do not support + morphologies with slashes in their names. + + ''' + + def __init__(self, metadata_path): + collections.MutableMapping.__init__(self) + self._metadata_path = metadata_path + + def _join_path(self, *args): + return os.path.join(self._metadata_path, *args) + + def _raw_path_iter(self): + return glob.iglob(self._join_path('*.meta')) + + @staticmethod + def _check_key(key): + if any(c in key for c in "\0/"): + raise KeyError(key) + + def __getitem__(self, key): + self._check_key(key) + try: + with open(self._join_path('%s.meta' % key), 'r') as f: + return json.load(f) + except IOError: + raise KeyError(key) + + def __setitem__(self, key, value): + self._check_key(key) + with open(self._join_path('%s.meta' % key), 'w') as f: + json.dump(value, f, indent=4, sort_keys=True) + + def __delitem__(self, key): + self._check_key(key) + os.unlink(self._join_path('%s.meta' % key)) + + def __iter__(self): + return (os.path.basename(fn)[:-len('.meta')] + for fn in self._raw_path_iter()) + + def __len__(self): + return len(list(self._raw_path_iter())) diff --git a/morphlib/systemmetadatadir_tests.py b/morphlib/systemmetadatadir_tests.py new file mode 100644 index 00000000..0126f862 --- /dev/null +++ b/morphlib/systemmetadatadir_tests.py @@ -0,0 +1,75 @@ +# Copyright (C) 2013 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 +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# =*= License: GPL-2 =*= + + +import operator +import os +import shutil +import tempfile +import unittest + +import morphlib + + +class SystemMetadataDirTests(unittest.TestCase): + + def setUp(self): + self.tempdir = tempfile.mkdtemp() + self.metadatadir = os.path.join(self.tempdir, 'baserock') + os.mkdir(self.metadatadir) + self.smd = morphlib.systemmetadatadir.SystemMetadataDir( + self.metadatadir) + + def tearDown(self): + shutil.rmtree(self.tempdir) + + def test_add_new(self): + self.smd['key'] = {'foo': 'bar'} + self.assertEqual(self.smd['key']['foo'], 'bar') + + def test_replace(self): + self.smd['key'] = {'foo': 'bar'} + self.smd['key'] = {'foo': 'baz'} + self.assertEqual(self.smd['key']['foo'], 'baz') + + def test_remove(self): + self.smd['key'] = {'foo': 'bar'} + del self.smd['key'] + self.assertTrue('key' not in self.smd) + + def test_iterate(self): + self.smd['build-essential'] = "Some data" + self.smd['core'] = "More data" + self.smd['foundation'] = "Yet more data" + self.assertEqual(sorted(self.smd.keys()), + ['build-essential', 'core', 'foundation']) + self.assertEqual(dict(self.smd.iteritems()), + { + 'build-essential': "Some data", + 'core': "More data", + 'foundation': "Yet more data", + }) + + def test_raises_KeyError(self): + self.assertRaises(KeyError, operator.getitem, self.smd, 'key') + + def test_validates_keys(self): + for key in ('foo/bar', 'baz\0quux'): + self.assertRaises(KeyError, operator.getitem, self.smd, key) + self.assertRaises(KeyError, operator.setitem, + self.smd, key, 'value') + self.assertRaises(KeyError, operator.delitem, self.smd, key) diff --git a/scripts/nullify-local-refs b/scripts/nullify-local-refs new file mode 100755 index 00000000..5db5c587 --- /dev/null +++ b/scripts/nullify-local-refs @@ -0,0 +1,18 @@ +#!/usr/bin/python + +import yaml, sys +repo = sys.argv[1] +ref = sys.argv[2] +for filename in sys.argv[3:]: + with open(filename, "r") as f: + d = yaml.load(f) + if "strata" in d: + for spec in d["strata"]: + if spec["repo"] == repo and spec["ref"] == ref: + spec["repo"] = spec["ref"] = None + if "build-depends" in d: + for spec in d["build-depends"]: + if spec["repo"] == repo and spec["ref"] == ref: + spec["repo"] = spec["ref"] = None + with open(filename, "w") as f: + yaml.dump(d, f) diff --git a/tests.as-root/branch-from-image-works.script b/tests.as-root/branch-from-image-works.script index 9f82f629..942301e8 100755 --- a/tests.as-root/branch-from-image-works.script +++ b/tests.as-root/branch-from-image-works.script @@ -48,8 +48,7 @@ git commit --quiet -m 'Make hello say goodbye' workspace="$DATADIR/workspace" "$SRCDIR/scripts/test-morph" init "$workspace" cd "$workspace" -"$SRCDIR/scripts/test-morph" branch-from-image \ - test:morphs mybranch \ +"$SRCDIR/scripts/test-morph" branch-from-image mybranch \ --metadata-dir="$extracted/baserock" cd mybranch/test:morphs grep -qFe "$hello_chunk_commit" hello-stratum.morph diff --git a/tests.branching/foreach-handles-full-urls.stdout b/tests.branching/foreach-handles-full-urls.stdout index 3abae62c..cee2f70a 100644 --- a/tests.branching/foreach-handles-full-urls.stdout +++ b/tests.branching/foreach-handles-full-urls.stdout @@ -1,4 +1,4 @@ file://TMP/morphs # On branch master -nothing to commit (working directory clean) +nothing to commit, working directory clean diff --git a/tests.branching/tag-works-with-multiple-morphs-repos.stdout b/tests.branching/tag-works-with-multiple-morphs-repos.stdout index ac2018e9..81fbf20d 100644 --- a/tests.branching/tag-works-with-multiple-morphs-repos.stdout +++ b/tests.branching/tag-works-with-multiple-morphs-repos.stdout @@ -93,10 +93,10 @@ CommitDate: Tue Jul 31 16:51:54 2012 +0000 create tag --- - stratum1.morph | 9 ++++++--- - stratum2.morph | 14 ++++++++++++++ - stratum3.morph | 9 +++++++++ - test-system.morph | 9 ++++++--- + stratum1.morph | 9 ++++++--- + stratum2.morph | 14 ++++++++++++++ + stratum3.morph | 9 +++++++++ + test-system.morph | 9 ++++++--- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/stratum1.morph b/stratum1.morph diff --git a/tests.build/build-system-with-null-refs.script b/tests.build/build-system-with-null-refs.script new file mode 100755 index 00000000..e23dcafa --- /dev/null +++ b/tests.build/build-system-with-null-refs.script @@ -0,0 +1,24 @@ +#!/bin/sh +# +# Copyright (C) 2013 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 +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +## Test building a system with null refs + +set -eu + +"$SRCDIR/scripts/test-morph" build-morphology \ + test:morphs-repo master hello-system diff --git a/tests.build/build-system-with-null-refs.setup b/tests.build/build-system-with-null-refs.setup new file mode 100755 index 00000000..cbf53076 --- /dev/null +++ b/tests.build/build-system-with-null-refs.setup @@ -0,0 +1,23 @@ +#!/bin/sh +# +# Copyright (C) 2013 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 +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +set -eu + +cd "$DATADIR/morphs-repo" +"$SRCDIR/scripts/nullify-local-refs" test:morphs master *.morph +git add *.morph +git commit --quiet -m "Nullify all refs" diff --git a/tests.deploy/deploy-with-null-refs.script b/tests.deploy/deploy-with-null-refs.script new file mode 100755 index 00000000..c283debf --- /dev/null +++ b/tests.deploy/deploy-with-null-refs.script @@ -0,0 +1,35 @@ +#!/bin/bash +# +# Copyright (C) 2013 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 +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + + +set -eu + + +. "$SRCDIR/tests.deploy/setup-build" + +cd "$DATADIR/workspace/branch1" +"$SRCDIR/scripts/nullify-local-refs" test:morphs master test:morphs/*.morph + +"$SRCDIR/scripts/test-morph" build hello-system + +"$SRCDIR/scripts/test-morph" build linux-system + +"$SRCDIR/scripts/test-morph" --log "$DATADIR/deploy.log" \ + deploy test_cluster \ + linux-system-2.HOSTNAME="baserock-rocks-even-more" \ + > /dev/null diff --git a/yarns/branches-workspaces.yarn b/yarns/branches-workspaces.yarn index 5273f396..f523ebcd 100644 --- a/yarns/branches-workspaces.yarn +++ b/yarns/branches-workspaces.yarn @@ -290,3 +290,65 @@ Creating a tag twice should fail. WHEN attempting to tag system branch foo as test123 THEN morph failed +Working with null repositories and refs +--------------------------------------- + +It is convenient to not explicitly name the repository and branch of +a stratum morphology, instead assuming it is the same as the current +morphology. + +These can be checked out like normal system branches. + + SCENARIO check out an existing system branch with null refs + GIVEN a workspace + AND a git server + AND null refs for local strata + WHEN checking out the master system branch + THEN the system branch master is checked out + +Likewise we can also create new system branches from these, and we +wouldn't need to worry about changing the system branch ref. + + + SCENARIO branch off a system branch with null refs + GIVEN a workspace + AND a git server + AND null refs for local strata + WHEN creating system branch foo + THEN the system branch foo is checked out + +When we edit a morphology with null refs, they stay null. + + SCENARIO editing with null refs + GIVEN a workspace + AND a git server + AND null refs for local strata + +When creating the branch, the refs remain null. + + WHEN creating system branch foo + THEN in branch foo, system test-system refs test-stratum in None + +After editing the stratum they remain null. + + WHEN editing stratum test-stratum in system test-system in branch foo + THEN in branch foo, system test-system refs test-stratum in None + +Refs to chunks are still altered as usual + + WHEN editing chunk test-chunk in test-stratum in test-system in branch foo + THEN in branch foo, system test-system refs test-stratum in None + AND in branch foo, stratum test-stratum refs test-chunk in foo + AND edited chunk test:test-chunk has git branch foo + +Petrifying also leaves null refs unmolested + + SCENARIO morph petrifies null refs + GIVEN a workspace + AND a git server + AND null refs for local strata + WHEN creating system branch foo + AND pushing system branch foo to git server + AND remembering all refs in foo + AND petrifying foo + THEN in branch foo, system test-system refs test-stratum in None diff --git a/yarns/implementations.yarn b/yarns/implementations.yarn index cfb744f7..e35e4219 100644 --- a/yarns/implementations.yarn +++ b/yarns/implementations.yarn @@ -114,6 +114,18 @@ another to hold a chunk. repo-alias = test=file://$DATADIR/gits/%s#file://$DATADIR/gits/%s EOF +Morphologies need to support having a null ref, which means look for the +stratum in the same repository and ref. Testing this requires different +morphologies. + + IMPLEMENTS GIVEN null refs for local strata + nullify_local_refs test:morphs master \ + "$DATADIR/gits/morphs/test-system.morph" \ + "$DATADIR/gits/morphs/test-stratum.morph" + run_in "$DATADIR/gits/morphs" git add . + run_in "$DATADIR/gits/morphs" git commit -m "Use null refs." + + Implementation sections for system branch operations ---------------------------------------------------- diff --git a/yarns/morph.shell-lib b/yarns/morph.shell-lib index 4fb1eb10..448c60ce 100644 --- a/yarns/morph.shell-lib +++ b/yarns/morph.shell-lib @@ -144,6 +144,11 @@ assert_morphologies_are_petrified() } +nullify_local_refs() +{ + "$SRCDIR/scripts/nullify-local-refs" "$@" +} + # Currently, yarn isn't setting $SRCDIR to point at the project source # directory. We simulate this here. |