diff options
Diffstat (limited to 'morphlib/plugins/branch_and_merge_plugin.py')
-rw-r--r-- | morphlib/plugins/branch_and_merge_plugin.py | 2242 |
1 files changed, 540 insertions, 1702 deletions
diff --git a/morphlib/plugins/branch_and_merge_plugin.py b/morphlib/plugins/branch_and_merge_plugin.py index d268decf..a66098b8 100644 --- a/morphlib/plugins/branch_and_merge_plugin.py +++ b/morphlib/plugins/branch_and_merge_plugin.py @@ -15,882 +15,476 @@ import cliapp -import copy -import functools +import contextlib import glob import logging import os import shutil -import socket -import tempfile -import time -import urlparse -import uuid import morphlib -def warns_git_config(keys): - def decorator(func): - @functools.wraps(func) - def check_config(self, *args, **kwargs): - try: - morphlib.git.check_config_set(self.app.runcmd, keys) - except cliapp.AppException, e: - self.app.status(msg="WARNING: %(message)s", - message=str(e), error=True) - return func(self, *args, **kwargs) - return check_config - - return decorator - - -warns_git_identity = warns_git_config({'user.name': 'My Name', - 'user.email': 'me@example.com'}) - - class BranchAndMergePlugin(cliapp.Plugin): - def __init__(self): - # Start recording changes. - self.init_changelog() + '''Add subcommands for handling workspaces and system branches.''' def enable(self): - # User-facing commands - self.app.add_subcommand('merge', self.merge, - arg_synopsis='BRANCH') -# self.app.add_subcommand('edit', self.edit, -# arg_synopsis='SYSTEM STRATUM [CHUNK]') - self.app.add_subcommand('old-petrify', self.petrify) - self.app.add_subcommand('old-unpetrify', self.unpetrify) + self.app.add_subcommand('init', self.init, arg_synopsis='[DIR]') + self.app.add_subcommand('workspace', self.workspace, arg_synopsis='') + self.app.add_subcommand( + 'checkout', self.checkout, arg_synopsis='REPO BRANCH') + self.app.add_subcommand( + 'branch', self.branch, arg_synopsis='REPO NEW [OLD]') + self.app.add_subcommand( + 'edit', self.edit, arg_synopsis='SYSTEM STRATUM [CHUNK]') self.app.add_subcommand( - 'tag', self.tag, arg_synopsis='TAG-NAME -- [GIT-COMMIT-ARG...]') - self.app.add_subcommand('old-build', self.build, - arg_synopsis='SYSTEM') - self.app.add_subcommand('old-status', self.status) - 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, + 'petrify', self.petrify, arg_synopsis='') + self.app.add_subcommand( + 'unpetrify', self.unpetrify, arg_synopsis='') + self.app.add_subcommand( + 'show-system-branch', self.show_system_branch, arg_synopsis='') + self.app.add_subcommand( + 'show-branch-root', self.show_branch_root, arg_synopsis='') + self.app.add_subcommand('foreach', self.foreach, 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 - def init_changelog(self): - self.changelog = {} - - def log_change(self, repo, text): - if not repo in self.changelog: - self.changelog[repo] = [] - self.changelog[repo].append(text) - - def print_changelog(self, title, early_keys=[]): - if self.changelog and self.app.settings['verbose']: - msg = '\n%s:\n\n' % title - keys = [x for x in early_keys if x in self.changelog] - keys.extend([x for x in self.changelog if x not in early_keys]) - for key in keys: - messages = self.changelog[key] - msg += ' %s:\n' % key - msg += '\n'.join([' %s' % x for x in messages]) - msg += '\n\n' - self.app.output.write(msg) + def init(self, args): + '''Initialize a workspace directory. - @staticmethod - def deduce_workspace(): - dirname = os.getcwd() - while dirname != '/': - dot_morph = os.path.join(dirname, '.morph') - if os.path.isdir(dot_morph): - return dirname - dirname = os.path.dirname(dirname) - raise cliapp.AppException("Can't find the workspace directory.\n" - "Morph must be built and deployed within " - "the system branch checkout within the " - "workspace directory.") - - def deduce_system_branch(self): - # 1. Deduce the workspace. If this fails, we're not inside a workspace. - workspace = self.deduce_workspace() - - # 2. We're in a workspace. Check if we're inside a system branch. - # If we are, return its name. - dirname = os.getcwd() - while dirname != workspace and dirname != '/': - if os.path.isdir(os.path.join(dirname, '.morph-system-branch')): - branch_name = self.get_branch_config(dirname, 'branch.name') - return branch_name, dirname - dirname = os.path.dirname(dirname) - - # 3. We're in a workspace but not inside a branch. Try to find a - # branch directory in the directories below the current working - # directory. Avoid ambiguity by only recursing deeper if there - # is only one subdirectory. - for dirname in self.walk_special_directories( - os.getcwd(), special_subdir='.morph-system-branch', - max_subdirs=1): - branch_name = self.get_branch_config(dirname, 'branch.name') - return branch_name, dirname - - raise cliapp.AppException("Can't find the system branch directory.\n" - "Morph must be built and deployed within " - "the system branch checkout.") - - def find_repository(self, branch_dir, repo): - for dirname in self.walk_special_directories(branch_dir, - special_subdir='.git'): - try: - original_repo = self.get_repo_config( - dirname, 'morph.repository') - except cliapp.AppException: - # The user may have manually put a git repo in the branch - continue - if repo == original_repo: - return dirname - return None - - def find_system_branch(self, workspace, branch_name): - for dirname in self.walk_special_directories( - workspace, special_subdir='.morph-system-branch'): - branch = self.get_branch_config(dirname, 'branch.name') - if branch_name == branch: - return dirname - return None - - def set_branch_config(self, branch_dir, option, value): - filename = os.path.join(branch_dir, '.morph-system-branch', 'config') - self.app.runcmd(['git', 'config', '-f', filename, option, value], - print_command=False) - - def get_branch_config(self, branch_dir, option): - filename = os.path.join(branch_dir, '.morph-system-branch', 'config') - value = self.app.runcmd(['git', 'config', '-f', filename, option], - print_command=False) - return value.strip() - - def set_repo_config(self, repo_dir, option, value): - self.app.runcmd(['git', 'config', option, value], cwd=repo_dir, - print_command=False) - - def get_repo_config(self, repo_dir, option): - value = self.app.runcmd(['git', 'config', option], cwd=repo_dir, - print_command=False) - return value.strip() - - def get_head(self, repo_path): - '''Return the ref that the working tree is on for a repo''' - - ref = self.app.runcmd(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], - cwd=repo_path).strip() - if ref == 'HEAD': - ref = 'detached HEAD' - return ref - - def get_uncommitted_changes(self, repo_dir, env={}): - status = self.app.runcmd(['git', 'status', '--porcelain'], - cwd=repo_dir, env=env) - changes = [] - for change in status.strip().splitlines(): - xy, paths = change.strip().split(' ', 1) - if xy != '??': - changes.append(paths.split()[0]) - return changes - - def get_unmerged_changes(self, repo_dir, env={}): - '''Identifies files which have unresolved merge conflicts''' - - # The second column of the git command output is set either if the - # file has changes in the working tree or if it has conflicts. - status = self.app.runcmd(['git', 'status', '--porcelain'], - cwd=repo_dir, env=env) - changes = [] - for change in status.strip().splitlines(): - xy, paths = change[0:2], change[2:].strip() - if xy[1] != ' ' and xy != '??': - changes.append(paths.split()[0]) - return changes - - def resolve_ref(self, repodir, ref): - try: - return self.app.runcmd(['git', 'rev-parse', '--verify', ref], - cwd=repodir)[0:40] - except cliapp.AppException, e: - logging.info( - 'Ignoring error executing git rev-parse: %s' % str(e)) - return None + Command line argument: - def resolve_reponame(self, reponame): - '''Return the full pull URL of a reponame.''' + * `DIR` is the directory to use as a workspace, and defaults to + the current directory. - resolver = morphlib.repoaliasresolver.RepoAliasResolver( - self.app.settings['repo-alias']) - return resolver.pull_url(reponame) + This creates a workspace, either in the current working directory, + or if `DIR` is given, in that directory. If the directory doesn't + exist, it is created. If it does exist, it must be empty. + + You need to run `morph init` to initialise a workspace, or none + of the other system branching tools will work: they all assume + an existing workspace. Note that a workspace only exists on your + machine, not on the git server. + + Example: - def get_cached_repo(self, repo_name): - '''Return CachedRepo object from the local repository cache + morph init /src/workspace + cd /src/workspace - Repo is cached and updated if necessary. The cache itself has a - mechanism in place to avoid multiple updates per Morph invocation. ''' - self.app.status(msg='Updating git repository %s in cache' % repo_name) - if not self.app.settings['no-git-update']: - repo = self.lrc.cache_repo(repo_name) - repo.update() - else: - repo = self.lrc.get_repo(repo_name) - return repo + if not args: + args = ['.'] + elif len(args) > 1: + raise morphlib.Error('init must get at most one argument') - def clone_to_directory(self, dirname, reponame, ref): - '''Clone a repository below a directory. + ws = morphlib.workspace.create(args[0]) + self.app.status(msg='Initialized morph workspace', chatty=True) - As a side effect, clone it into the local repo cache. + def workspace(self, args): + '''Show the toplevel directory of the current workspace.''' - ''' + ws = morphlib.workspace.open('.') + self.app.output.write('%s\n' % ws.root) - # Setup. - resolver = morphlib.repoaliasresolver.RepoAliasResolver( - self.app.settings['repo-alias']) - repo = self.get_cached_repo(reponame) - - # Make sure the parent directories needed for the repo dir exist. - parent_dir = os.path.dirname(dirname) - if not os.path.exists(parent_dir): - os.makedirs(parent_dir) - - # Clone it from cache to target directory. - target_path = os.path.abspath(dirname) - repo.clone_checkout(ref, target_path) - - # Remember the repo name we cloned from in order to be able - # to identify the repo again later using the same name, even - # if the user happens to rename the directory. - self.set_repo_config(dirname, 'morph.repository', reponame) - - # Create a UUID for the clone. We will use this for naming - # temporary refs, e.g. for building. - self.set_repo_config(dirname, 'morph.uuid', uuid.uuid4().hex) - - # URL configuration - morphlib.git.set_remote(self.app.runcmd, dirname, 'origin', repo.url) - self.set_repo_config( - dirname, 'url.%s.pushInsteadOf' % resolver.push_url(reponame), - resolver.pull_url(reponame)) - morphlib.git.update_submodules(self.app, target_path) - - self.app.runcmd(['git', 'remote', 'update'], cwd=dirname) - - def load_morphology(self, repo_dir, name, ref=None): - '''Loads a morphology from a repo in a system branch - - If 'ref' is specified, the version is taken from there instead of the - working tree. Note that you shouldn't use this to fetch files on - branches other than the current system branch, because the remote in - the system branch repo may be completely out of date. Use the local - repository cache instead for this. - ''' + # 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. - if ref is None: - filename = os.path.join(repo_dir, '%s.morph' % name) - with open(filename) as f: - text = f.read() - else: - filename = '%s.morph at ref %s in %s' % (name, ref, repo_dir) - if not morphlib.git.is_valid_sha1(ref): - ref = morphlib.git.rev_parse(self.app.runcmd, repo_dir, ref) - try: - text = self.app.runcmd(['git', 'cat-file', 'blob', - '%s:%s.morph' % (ref, name)], - cwd=repo_dir) - except cliapp.AppException as e: - msg = '%s.morph was not found in %s' % (name, repo_dir) - if ref is not None: - msg += ' at ref %s' % ref - raise cliapp.AppException(msg) + 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: - morphology = morphlib.morph2.Morphology(text) - except ValueError as e: - raise morphlib.Error("Error parsing %s: %s" % - (filename, str(e))) - - self._validate_morphology(morphology, '%s.morph' % name) - - return morphology - - def _validate_morphology(self, morphology, basename): - # FIXME: This really should be in MorphologyFactory. Later. - - def require(field): - if field not in morphology: - raise morphlib.Error( - 'Required field "%s" is missing from morphology %s' % - (field, basename)) - - required = { - 'system': [ - 'name', - 'arch', - 'strata', - ], - 'stratum': [ - 'name', - 'chunks', - ], - 'chunk': [ - 'name', - ], - 'cluster': [ - 'name', - 'systems', - ], - } - - also_known = { - 'system': [ - 'kind', - 'description', - 'configuration-extensions', - ], - 'stratum': [ - 'kind', - 'description', - 'build-depends', - ], - 'chunk': [ - 'kind', - 'description', - 'build-system', - 'configure-commands', - 'build-commands', - 'test-commands', - 'install-commands', - 'max-jobs', - 'chunks', - 'devices', - ], - 'cluster': [ - 'kind' - ] - } - - require('kind') - kind = morphology['kind'] - if kind not in required: - raise morphlib.Error( - 'Unknown morphology kind "%s" in %s' % (kind, basename)) - for field in required[kind]: - require(field) - - known = required[kind] + also_known[kind] - for field in morphology.keys(): - if field not in known and not field.startswith('_orig_'): - msg = 'Unknown field "%s" in %s' % (field, basename) - logging.warning(msg) - self.app.status(msg=msg) - - def reset_work_tree_safe(self, repo_dir): - # This function avoids throwing any exceptions, so it is safe to call - # inside an 'except' block without altering the backtrace. + sb = morphlib.sysbranchdir.create( + root_dir, root_url, system_branch) + gd = sb.clone_cached_repo(cached_repo, base_ref) - command = 'git', 'reset', '--hard' - status, output, error = self.app.runcmd_unchecked(command, - cwd=repo_dir) - if status != 0: - logging.warning ("Warning: error while trying to clean up %s: %s" % - (repo_dir, error)) + yield (sb, gd) - @staticmethod - def update_morphology(repo_dir, name, morphology, output_fd=None): - if not name.endswith('.morph'): - name = '%s.morph' % name - filename = os.path.join(repo_dir, '%s' % name) - morphology.update_file(filename, output_fd=output_fd) + gd.update_submodules(self.app) + gd.update_remotes() - if name != morphology['name'] + '.morph': - logging.warning('%s: morphology "name" should match filename' % - filename) + except morphlib.sysbranchdir.SystemBranchDirectoryAlreadyExists as e: + logging.error('Caught exception: %s' % str(e)) + raise + 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 - @staticmethod - def get_edit_info(morphology_name, morphology, name, collection='strata'): - try: - return morphology.lookup_child_by_name(name) - except KeyError: - if collection is 'strata': - raise cliapp.AppException( - 'Stratum "%s" not found in system "%s"' % - (name, morphology_name)) - else: - raise cliapp.AppException( - 'Chunk "%s" not found in stratum "%s"' % - (name, morphology_name)) + def checkout(self, args): + '''Check out an existing system branch. - @staticmethod - def convert_uri_to_path(uri): - parts = urlparse.urlparse(uri) - - # If the URI path is relative, assume it is an aliased repo (e.g. - # baserock:morphs). Otherwise assume it is a full URI where we need - # to strip off the scheme and .git suffix. - if not os.path.isabs(parts.path): - return uri - else: - path = parts.netloc - if parts.path.endswith('.git'): - path = os.path.join(path, parts.path[1:-len('.git')]) - else: - path = os.path.join(path, parts.path[1:]) - return path + Command line arguments: - @staticmethod - def remove_branch_dir_safe(workspace, branch): - # This function avoids throwing any exceptions, so it is safe to call - # inside an 'except' block without altering the backtrace. + * `REPO` is the URL to the repository to the root repository of + a system branch. + * `BRANCH` is the name of the system branch. - def handle_error(function, path, excinfo): - logging.warning ("Warning: error while trying to clean up %s: %s" % - (path, excinfo)) + This will check out an existing system branch to an existing + workspace. You must create the workspace first. This only checks + out the root repository, not the repositories for individual + components. You need to use `morph edit` to check out those. - branch_dir = os.path.join(workspace, branch) - shutil.rmtree(branch_dir, onerror=handle_error) + Example: - # Remove parent directories that are empty too, avoiding exceptions - parent = os.path.dirname(branch_dir) - while parent != os.path.abspath(workspace): - if len(os.listdir(parent)) > 0 or os.path.islink(parent): - break - os.rmdir(parent) - parent = os.path.dirname(parent) + cd /src/workspace + morph checkout baserock:baserock/morphs master - @staticmethod - def iterate_branch_repos(branch_path, root_repo_path): - '''Produces a sorted list of component repos in a branch checkout''' + ''' - dirs = [d for d in BranchAndMergePlugin.walk_special_directories( - branch_path, special_subdir='.git') - if not os.path.samefile(d, root_repo_path)] - dirs.sort() + if len(args) != 2: + raise cliapp.AppException('morph checkout needs a repo and the ' + 'name of a branch as parameters') - for d in [root_repo_path] + dirs: - yield d + root_url = args[0] + system_branch = args[1] + base_ref = system_branch - @staticmethod - def walk_special_directories(root_dir, special_subdir=None, max_subdirs=0): - assert(special_subdir is not None) - assert(max_subdirs >= 0) - - visited = set() - for dirname, subdirs, files in os.walk(root_dir, followlinks=True): - # Avoid infinite recursion due to symlinks. - if dirname in visited: - subdirs[:] = [] - continue - visited.add(dirname) + self._require_git_user_config() - # Check if the current directory has the special subdirectory. - if special_subdir in subdirs: - yield dirname + # Open the workspace first thing, so user gets a quick error if + # we're not inside a workspace. + ws = morphlib.workspace.open('.') - # Do not recurse into hidden directories. - subdirs[:] = [x for x in subdirs if not x.startswith('.')] + # Make sure the root repository is in the local git repository + # cache, and is up to date. + lrc, rrc = morphlib.util.new_repo_caches(self.app) + cached_repo = lrc.get_updated_repo(root_url) - # Do not recurse if there is more than the maximum number of - # subdirectories allowed. - if max_subdirs > 0 and len(subdirs) > max_subdirs: - break + # Check the git branch exists. + cached_repo.resolve_ref(system_branch) - def read_metadata(self, metadata_path): - '''Load every metadata file in `metadata_path`. - - Given a directory containing metadata, load them into memory - and retain the id of the system. + with self._initializing_system_branch( + ws, root_url, system_branch, cached_repo, base_ref) as (sb, gd): - Returns the cache_key of the system and a mapping of cache_key - to metadata. - ''' - self.app.status(msg='Reading metadata', chatty=True) - metadata_cache_id_lookup = {} - system_key = None - for path in sorted(glob.iglob(os.path.join(metadata_path, '*.meta'))): - with open(path) as f: - metadata = morphlib.util.json.load(f) - cache_key = metadata['cache-key'] - metadata_cache_id_lookup[cache_key] = metadata - - if metadata['kind'] == 'system': - if system_key is not None: - raise morphlib.Error( - "Metadata directory contains multiple systems.") - system_key = cache_key - - if system_key is None: - raise morphlib.Error( - "Metadata directory does not contain any systems.") - - return system_key, metadata_cache_id_lookup - - def _create_branch(self, workspace, branch_name, repo, original_ref): - '''Create a branch called branch_name based off original_ref. - - NOTE: self.lrc and self.rrc need to be initialized before - calling since clone_to_directory uses them indirectly via - get_cached_repo - ''' - branch_dir = os.path.join(workspace, branch_name) - os.makedirs(branch_dir) - try: - # Create a .morph-system-branch directory to clearly identify - # this directory as a morph system branch. - os.mkdir(os.path.join(branch_dir, '.morph-system-branch')) - - # Remember the system branch name and the repository we branched - # off from initially. - self.set_branch_config(branch_dir, 'branch.name', branch_name) - self.set_branch_config(branch_dir, 'branch.root', repo) - - # Generate a UUID for the branch. We will use this for naming - # temporary refs, e.g. building. - self.set_branch_config(branch_dir, 'branch.uuid', uuid.uuid4().hex) - - # Clone into system branch directory. - repo_dir = os.path.join(branch_dir, self.convert_uri_to_path(repo)) - self.clone_to_directory(repo_dir, repo, original_ref) - - # Create a new branch in the local morphs repository. - if original_ref != branch_name: - self.app.runcmd(['git', 'checkout', '-b', branch_name, - original_ref], cwd=repo_dir) - - return branch_dir - except BaseException, e: - logging.error('Caught exception: %s' % str(e)) - logging.info('Removing half-finished branch %s' % branch_name) - self.remove_branch_dir_safe(workspace, branch_name) - raise + if gd.has_fat(): + gd.fat_init() + gd.fat_pull() - def checkout_repository(self, branch_dir, repo, ref, parent_ref=None): - '''Make a chunk or stratum repository available for a system branch - We ensure the 'system_branch' ref within 'repo' is checked out, - creating it from 'parent_ref' if required. + def branch(self, args): + '''Create a new system branch. - The function aims for permissiveness, so users can try to fix any - weirdness they have caused in the repos with another call to 'morph - edit'. + Command line arguments: + + * `REPO` is a repository URL. + * `NEW` is the name of the new system branch. + * `OLD` is the point from which to branch, and defaults to `master`. + + This creates a new system branch. It needs to be run in an + existing workspace (see `morph workspace`). It creates a new + git branch in the clone of the repository in the workspace. The + system branch will not be visible on the git server until you + push your changes to the repository. + + Example: + + cd /src/workspace + morph branch baserock:baserock/morphs jrandom/new-feature ''' - parent_ref = parent_ref or ref + if len(args) not in [2, 3]: + raise cliapp.AppException( + 'morph branch needs name of branch as parameter') - repo_dir = self.find_repository(branch_dir, repo) - if repo_dir is None: - repo_url = self.resolve_reponame(repo) - repo_dir = os.path.join(branch_dir, self.convert_uri_to_path(repo)) - self.clone_to_directory(repo_dir, repo, parent_ref) + root_url = args[0] + system_branch = args[1] + base_ref = 'master' if len(args) == 2 else args[2] + origin_base_ref = 'origin/%s' % base_ref - if self.resolve_ref(repo_dir, ref) is None: - self.log_change(repo, 'branch "%s" created from "%s"' % - (ref, parent_ref)) - command = ['git', 'checkout', '-b', ref] - else: - # git copes even if the system_branch ref is already checked out - command = ['git', 'checkout', ref] - - status, output, error = self.app.runcmd_unchecked( - command, cwd=repo_dir) - if status != 0: - raise cliapp.AppException('Command failed: %s in repo %s\n%s' % - (' '.join(command), repo, error)) - return repo_dir - - def make_available(self, spec, branch, branch_dir, root_repo, - root_repo_dir): - '''Check out the morphology that 'spec' refers to, for editing''' - - if spec.get('repo') in (None, root_repo): - # This is only possible for stratum morphologies - repo_dir = root_repo_dir - if spec.get('ref') not in (None, root_repo): - # Bring the morphology forward from its ref to the current HEAD - repo = self.lrc.get_repo(root_repo) - m = repo.load_morphology(spec['ref'], spec['morph']) - self.update_morphology(root_repo_dir, spec['morph'], m) - self.log_change(spec['repo'], - '"%s" copied from "%s" to "%s"' % - (spec['morph'], spec['ref'], branch)) - else: - repo_dir = self.checkout_repository( - branch_dir, spec['repo'], branch, parent_ref=spec['ref']) - return repo_dir + self._require_git_user_config() + + # Open the workspace first thing, so user gets a quick error if + # we're not inside a workspace. + ws = morphlib.workspace.open('.') + + # Make sure the root repository is in the local git repository + # cache, and is up to date. + lrc, rrc = morphlib.util.new_repo_caches(self.app) + cached_repo = lrc.get_updated_repo(root_url) + + # Make sure the system branch doesn't exist yet. + if cached_repo.ref_exists(system_branch): + raise cliapp.AppException( + 'branch %s already exists in repository %s' % + (system_branch, root_url)) + + # Make sure the base_ref exists. + cached_repo.resolve_ref(base_ref) + + with self._initializing_system_branch( + ws, root_url, system_branch, cached_repo, base_ref) as (sb, gd): + + gd.branch(system_branch, base_ref) + gd.checkout(system_branch) + if gd.has_fat(): + gd.fat_init() + gd.fat_pull() + + def _save_dirty_morphologies(self, loader, sb, morphs): + logging.debug('Saving dirty morphologies: start') + for morph in morphs: + if morph.dirty: + logging.debug( + 'Saving morphology: %s %s %s' % + (morph.repo_url, morph.ref, morph.filename)) + loader.unset_defaults(morph) + loader.save_to_file( + sb.get_filename(morph.repo_url, morph.filename), morph) + morph.dirty = False + logging.debug('Saving dirty morphologies: done') + + def _checkout(self, lrc, sb, repo_url, ref): + logging.debug( + 'Checking out %s (%s) into %s' % + (repo_url, ref, sb.root_directory)) + cached_repo = lrc.get_updated_repo(repo_url) + gd = sb.clone_cached_repo(cached_repo, ref) + gd.update_submodules(self.app) + gd.update_remotes() + + def _load_morphology_from_file(self, loader, dirname, filename): + full_filename = os.path.join(dirname, filename) + return loader.load_from_file(full_filename) + + def _load_morphology_from_git(self, loader, gd, ref, filename): + try: + text = gd.get_file_from_ref(ref, filename) + except cliapp.AppException: + text = gd.get_file_from_ref('origin/%s' % ref, filename) + return loader.load_from_string(text, filename) - @warns_git_identity def edit(self, args): '''Edit or checkout a component in a system branch. Command line arguments: - * `SYSTEM` is the name of a system morphology in the root repository - of the current system branch. - * `STRATUM` is the name of a stratum inside the system. - * `CHUNK` is the name of a chunk inside the stratum. + * `CHUNK` is the name of a chunk - This marks the specified stratum or chunk (if given) as being - changed within the system branch, by creating the git branches in - the affected repositories, and changing the relevant morphologies - to point at those branches. It also creates a local clone of - the git repository of the stratum or chunk. + This makes a local checkout of CHUNK in the current system branch + and edits any stratum morphology file(s) containing the chunk - For example: + ''' - morph edit devel-system-x86-64-generic devel + if len(args) != 1: + raise cliapp.AppException('morph edit needs a chunk ' + 'as parameter') + + ws = morphlib.workspace.open('.') + sb = morphlib.sysbranchdir.open_from_within('.') + loader = morphlib.morphloader.MorphologyLoader() + morphs = self._load_all_sysbranch_morphologies(sb, loader) + + def edit_chunk(morph, chunk_name): + chunk_url, chunk_ref, chunk_morph = ( + morphs.get_chunk_triplet(morph, chunk_name)) + + chunk_dirname = sb.get_git_directory_name(chunk_url) + + if not os.path.exists(chunk_dirname): + lrc, rrc = morphlib.util.new_repo_caches(self.app) + cached_repo = lrc.get_updated_repo(chunk_url) + + gd = sb.clone_cached_repo(cached_repo, chunk_ref) + if chunk_ref != sb.system_branch_name: + gd.branch(sb.system_branch_name, chunk_ref) + gd.checkout(sb.system_branch_name) + gd.update_submodules(self.app) + gd.update_remotes() + if gd.has_fat(): + gd.fat_init() + gd.fat_pull() + + # Change the refs to the chunk. + if chunk_ref != sb.system_branch_name: + morphs.change_ref( + chunk_url, chunk_ref, + chunk_morph, + sb.system_branch_name) + + return chunk_dirname + + chunk_name = args[0] + dirs = set() + found = 0 + + for morph in morphs.morphologies: + if morph['kind'] == 'stratum': + for chunk in morph['chunks']: + if chunk['name'] == chunk_name: + self.app.status( + msg='Editing %(chunk)s in %(stratum)s stratum', + chunk=chunk_name, stratum=morph['name']) + chunk_dirname = edit_chunk(morph, chunk_name) + dirs.add(chunk_dirname) + found = found + 1 + + # Save any modified strata. + + self._save_dirty_morphologies(loader, sb, morphs.morphologies) + + if found == 0: + self.app.status( + msg="No chunk %(chunk)s found. If you want to create one, add " + "an entry to a stratum morph file.", chunk=chunk_name) + + if found >= 1: + dirs_list = ', '.join(sorted(dirs)) + self.app.status( + msg="Chunk %(chunk)s source is available at %(dirs)s", + chunk=chunk_name, dirs=dirs_list) + + if found > 1: + self.app.status( + msg="Notice that this chunk appears in more than one stratum") + + def show_system_branch(self, args): + '''Show the name of the current system branch.''' + + ws = morphlib.workspace.open('.') + sb = morphlib.sysbranchdir.open_from_within('.') + self.app.output.write('%s\n' % sb.system_branch_name) + + def show_branch_root(self, args): + '''Show the name of the repository holding the system morphologies. + + This would, for example, write out something like: + + /src/ws/master/baserock:baserock/morphs + + when the master branch of the `baserock:baserock/morphs` + repository is checked out. - The above command will mark the `devel` stratum as being - modified in the current system branch. In this case, the stratum's - morphology is in the same git repository as the system morphology, - so there is no need to create a new git branch. However, the - system morphology is modified to point at the stratum morphology - in the same git branch, rather than the original branch. + ''' - In other words, where the system morphology used to say this: + ws = morphlib.workspace.open('.') + sb = morphlib.sysbranchdir.open_from_within('.') + self.app.output.write('%s\n' % sb.get_config('branch.root')) - morph: devel - repo: baserock:baserock/morphs - ref: master + def _remove_branch_dir_safe(self, workspace_root, system_branch_root): + # This function avoids throwing any exceptions, so it is safe to call + # inside an 'except' block without altering the backtrace. - The updated system morphology will now say this instead: + def handle_error(function, path, excinfo): + logging.warning ("Error while trying to clean up %s: %s" % + (path, excinfo)) - morph: devel - repo: baserock:baserock/morphs - ref: jrandom/new-feature + shutil.rmtree(system_branch_root, onerror=handle_error) - (Assuming the system branch is called `jrandom/new-feature`.) + # Remove parent directories that are empty too, avoiding exceptions + parent = os.path.dirname(system_branch_root) + while parent != os.path.abspath(workspace_root): + if len(os.listdir(parent)) > 0 or os.path.islink(parent): + break + os.rmdir(parent) + parent = os.path.dirname(parent) - Another example: + def _require_git_user_config(self): + '''Warn if the git user.name and user.email variables are not set.''' - morph edit devel-system-x86_64-generic devel gcc + keys = { + 'user.name': 'My Name', + 'user.email': 'me@example.com', + } - The above command will mark the `gcc` chunk as being edited in - the current system branch. Morph will clone the `gcc` repository - locally, into the current workspace, and create a new (local) - branch named after the system branch. It will also change the - stratum morphology to refer to the new git branch, instead of - whatever branch it was referring to originally. + try: + morphlib.git.check_config_set(self.app.runcmd, keys) + except morphlib.git.ConfigNotSetException as e: + self.app.status( + msg="WARNING: %(message)s", + message=str(e), error=True) - If the `gcc` repository already had a git branch named after - the system branch, that is reused. Similarly, if the stratum - morphology was already pointing at that branch, it doesn't - need to be changed again. In that case, the only action Morph - does is to clone the chunk repository locally, and if that was - also done already, Morph does nothing. + def foreach(self, args): + '''Run a command in each repository checked out in a system branch. - ''' + Use -- before specifying the command to separate its arguments from + Morph's own arguments. - if len(args) not in (2, 3): - raise cliapp.AppException( - 'morph edit must either get a system and a stratum ' - 'or a system, a stratum and a chunk as arguments') - - workspace = self.deduce_workspace() - branch, branch_dir = self.deduce_system_branch() - - # Find out which repository we branched off from. - root_repo = self.get_branch_config(branch_dir, 'branch.root') - root_repo_dir = self.find_repository(branch_dir, root_repo) - - system_name = args[0] - stratum_name = args[1] - chunk_name = args[2] if len(args) > 2 else None - - self.lrc, self.rrc = morphlib.util.new_repo_caches(self.app) - - # We need to touch every stratum in the system, not just the target - # the user specified, because others may have build-depends that - # point to the edited stratum. - system_morphology = self.load_morphology(root_repo_dir, system_name) - - # Test that the specified stratum exists in the system - self.get_edit_info(system_name, system_morphology, stratum_name) - - all_strata = system_morphology['strata'] - stratum_refs_to_correct = set() - - # Petrify the chunk - for stratum_spec in (s for s in all_strata - if s['morph'] == stratum_name): - stratum_repo_dir = self.make_available( - stratum_spec, branch, branch_dir, root_repo, root_repo_dir) - stratum_morphology = self.load_morphology( - stratum_repo_dir, stratum_spec['morph']) - - if chunk_name is not None: - # Change the stratum's ref to the chunk - chunk_spec = self.get_edit_info( - stratum_name, stratum_morphology, chunk_name, - collection='chunks') - - if 'unpetrify-ref' in chunk_spec: - chunk_spec['ref'] = chunk_spec['unpetrify-ref'] - del chunk_spec['unpetrify-ref'] - - self.make_available( - chunk_spec, branch, branch_dir, root_repo, - root_repo_dir) - - if chunk_spec['ref'] != branch: - chunk_spec['ref'] = branch - - self.log_change(stratum_spec['repo'], - '"%s" now includes "%s" from "%s"' % - (stratum_name, chunk_name, branch)) - stratum_refs_to_correct.add((stratum_spec['repo'], - stratum_spec['ref'], - stratum_spec['morph'])) - # Correct the System Morphology's reference - stratum_spec['ref'] = branch - self.update_morphology(stratum_repo_dir, stratum_spec['morph'], - stratum_morphology) - self.log_change(root_repo, - '"%s" now includes "%s" from "%s"' % - (system_name, stratum_name, branch)) - - # Correct all references to altered strata - while stratum_refs_to_correct: - repo, ref, morph = stratum_refs_to_correct.pop() - spec = {"repo": repo, "ref": ref, "morph": morph} - for stratum_spec in all_strata: - changed = False - if repo == root_repo: - stratum_repo_dir = root_repo_dir - else: - stratum_repo_dir = self.checkout_repository( - branch_dir, stratum_spec['repo'], - branch, stratum_spec['ref']) - stratum_morphology = self.load_morphology( - stratum_repo_dir, stratum_spec['morph']) - if ('build-depends' in stratum_morphology - and stratum_morphology['build-depends'] is not None): - for bd_spec in stratum_morphology['build-depends']: - bd_triplet = (bd_spec['repo'], - bd_spec['ref'], - bd_spec['morph']) - if (bd_triplet == (repo, ref, morph)): - bd_spec['ref'] = branch - changed = True - if changed: - stratum_refs_to_correct.add((stratum_spec['repo'], - stratum_spec['ref'], - stratum_spec['morph'])) - # Update the System morphology to use - # the modified version of the Stratum - stratum_spec['ref'] = branch - self.update_morphology(stratum_repo_dir, - stratum_spec['morph'], - stratum_morphology) - self.log_change(root_repo, - '"%s" now includes "%s" from "%s"' % - (system_name, stratum_name, branch)) - - self.update_morphology(root_repo_dir, system_name, system_morphology) - - self.print_changelog('The following changes were made but have not ' - 'been committed') - - def _get_repo_name(self, alias_resolver, metadata): - '''Attempt to find the best name for the repository. - - A defined repo-alias is preferred, but older builds may - not have it. - - A guessed repo-alias is the next best thing, but there may - be none or more alilases that would resolve to that URL, so - if there are any, use the shortest as it is likely to be - the most specific. - - If all else fails just use the URL. - ''' - if 'repo-alias' in metadata: - return metadata['repo-alias'] - - repo_url = metadata['repo'] - aliases = alias_resolver.aliases_from_url(repo_url) - - if len(aliases) >= 1: - # If there are multiple valid aliases, use the shortest - return min(aliases, key=len) - - # If there are no aliases, just return the url - return repo_url - - def _resolve_refs_from_metadata(self, alias_resolver, - metadata_cache_id_lookup): - '''Pre-resolve a set of refs from metadata. - - Resolved refs are a dict as {(repo, ref): sha1}. - - If the metadata contains the repo-alias then every - metadata item adds the mapping of its repo-alias and ref - to the commit it was built with. - - If the repo-alias does not exist, such as if the image was - built before that field was added, then mappings of every - possible repo url are added. - ''' - resolved_refs = {} - for md in metadata_cache_id_lookup.itervalues(): - if 'repo-alias' in md: - repourls = [md['repo-alias']] - else: - repourls = [md['repo']] - repourls.extend(alias_resolver.aliases_from_url(md['repo'])) - for repourl in repourls: - resolved_refs[repourl, md['original_ref']] = md['sha1'] - return resolved_refs + Command line arguments: + + * `--` indicates the end of option processing for Morph. + * `COMMAND` is a command to run. + * `ARGS` is a list of arguments or options to be passed onto + `COMMAND`. + + This runs the given `COMMAND` in each git repository belonging + to the current system branch that exists locally in the current + workspace. This can be a handy way to do the same thing in all + the local git repositories. + + For example: + + morph foreach -- git push + + The above command would push any committed changes in each + repository to the git server. - def branch_from_image(self, args): - '''Given the contents of a /baserock directory, produce a branch - of the System, petrified to when the System was made. ''' - if len(args) not in (2, 3): - raise cliapp.AppException( - 'branch-from-image needs repository, ref and path to metadata') - root_repo = args[0] - branch = args[1] - metadata_path = self.app.settings['metadata-dir'] - workspace = self.deduce_workspace() - self.lrc, self.rrc = morphlib.util.new_repo_caches(self.app) - alias_resolver = morphlib.repoaliasresolver.RepoAliasResolver( - self.app.settings['repo-alias']) + if not args: + raise cliapp.AppException('morph foreach expects a command to run') + + ws = morphlib.workspace.open('.') + sb = morphlib.sysbranchdir.open_from_within('.') - system_key, metadata_cache_id_lookup = self.read_metadata( - metadata_path) - - system_metadata = metadata_cache_id_lookup[system_key] - repo = self._get_repo_name(alias_resolver, system_metadata) - - # Which repo to use? Specified or deduced? - branch_dir = self._create_branch(workspace, branch, repo, - system_metadata['sha1']) - - # Resolve refs from metadata so petrify substitutes these refs - # into morphologies instead of the current state of the branches - resolved_refs = self._resolve_refs_from_metadata( - alias_resolver, - metadata_cache_id_lookup) - - branch_root_dir = self.find_repository(branch_dir, repo) - name = system_metadata['morphology'][:-len('.morph')] - morphology = self.load_morphology(branch_root_dir, name) - self.petrify_morphology(branch, branch_dir, - repo, branch_root_dir, - repo, branch_root_dir, # not a typo - branch, name, morphology, - petrified_morphologies=set(), - resolved_refs=resolved_refs, - update_working_tree=True) + for gd in sorted(sb.list_git_directories(), key=lambda gd: gd.dirname): + # Get the repository's original name + # Continue in the case of error, since the previous iteration + # worked in the case of the user cloning a repository in the + # system branch's directory. + try: + repo = gd.get_config('morph.repository') + except cliapp.AppException: + continue + + self.app.output.write('%s\n' % repo) + status, output, error = self.app.runcmd_unchecked( + args, cwd=gd.dirname) + self.app.output.write(output) + if status != 0: + self.app.output.write(error) + pretty_command = ' '.join(cliapp.shell_quote(arg) + for arg in args) + raise cliapp.AppException( + 'Command failed at repo %s: %s' + % (repo, pretty_command)) + self.app.output.write('\n') + self.app.output.flush() + + def _load_all_sysbranch_morphologies(self, sb, loader): + '''Read in all the morphologies in the root repository.''' + self.app.status(msg='Loading in all morphologies') + morphs = morphlib.morphset.MorphologySet() + for morph in sb.load_all_morphologies(loader): + morphs.add_morphology(morph) + return morphs def petrify(self, args): '''Convert all chunk refs in a system branch to be fixed SHA1s. @@ -931,976 +525,220 @@ class BranchAndMergePlugin(cliapp.Plugin): ''' - # Stratum refs are not petrified, because they must all be edited to - # set the new chunk refs, which requires branching them all for the - # current branch - so they will not be updated outside of the user's - # control in any case. Chunks that have already been edited on the - # current branch are also not petrified. - - if len(args) != 0: + if args: raise cliapp.AppException('morph petrify takes no arguments') - branch, branch_path = self.deduce_system_branch() - root_repo = self.get_branch_config(branch_path, 'branch.root') - root_repo_dir = self.find_repository(branch_path, root_repo) - self.lrc, self.rrc = morphlib.util.new_repo_caches(self.app) - - self.petrify_everything(branch, branch_path, root_repo, root_repo_dir, - branch, os.environ, None, True) - - def unpetrify(self, args): - '''Reverse the process of petrification. - - This undoes the changes `morph petrify` did. - - ''' - - # This function makes no attempt to 'unedit' strata that were branched - # solely so they could be petrified. - - if len(args) != 0: - raise cliapp.AppException('morph unpetrify takes no arguments') - - self.lrc, self.rrc = morphlib.util.new_repo_caches(self.app) + ws = morphlib.workspace.open('.') + sb = morphlib.sysbranchdir.open_from_within('.') + loader = morphlib.morphloader.MorphologyLoader() + lrc, rrc = morphlib.util.new_repo_caches(self.app) + update_repos = not self.app.settings['no-git-update'] - workspace = self.deduce_workspace() - branch, branch_path = self.deduce_system_branch() - root_repo = self.get_branch_config(branch_path, 'branch.root') - root_repo_dir = self.find_repository(branch_path, root_repo) + morphs = self._load_all_sysbranch_morphologies(sb, loader) - for f in sorted(glob.iglob(os.path.join(root_repo_dir, '*.morph'))): - name = os.path.basename(f)[:-len('.morph')] - morphology = self.load_morphology(root_repo_dir, name) - if morphology['kind'] != 'system': - continue - - for stratum_info in morphology['strata']: - repo_dir = self.make_available( - stratum_info, branch, branch_path, root_repo, - root_repo_dir) - stratum_info['ref'] = branch - - stratum = self.load_morphology(repo_dir, stratum_info['morph']) + #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): + continue + commit_sha1, tree_sha1 = self.app.resolve_ref( + lrc, rrc, repo, ref, update=update_repos) + yield ((repo, ref), commit_sha1) - for chunk_info in stratum['chunks']: - if 'unpetrify-ref' in chunk_info: - chunk_info['ref'] = chunk_info['unpetrify-ref'] - del chunk_info['unpetrify-ref'] - self.update_morphology(repo_dir, stratum_info['morph'], - stratum) + morphs.repoint_refs(sb.root_repository_url, + sb.system_branch_name) - self.update_morphology(root_repo_dir, name, morphology) + morphs.petrify_chunks(dict(resolve_refs(morphs))) - self.print_changelog('The following changes were made but have not ' - 'been committed') + # Write morphologies back out again. + self._save_dirty_morphologies(loader, sb, morphs.morphologies) - @warns_git_identity - def tag(self, args): - '''Create an annotated Git tag of a petrified system branch. - - Command line arguments: - - * `TAG-NAME` is the name of the Git tag to be created. - * `--` separates the Git arguments and options from the ones - Morph parses for itself. - * `GIT-COMMIT-ARG` is a `git commit` option or argument, - e.g., '-m' or '-F'. These should provide the commit message. - - This command creates an annotated Git tag that points at a commit - where all system and stratum morphologies have been petrified. - The working tree won't be petrified, only the commit. - - Example: - - morph tag release-12.765 -- -m "Release 12.765" - - ''' + def unpetrify(self, args): + '''Reverse the process of petrification. - if len(args) < 1: - raise cliapp.AppException('morph tag expects a tag name') - - tagname = args[0] - - # Deduce workspace, system branch and branch root repository. - workspace = self.deduce_workspace() - branch, branch_dir = self.deduce_system_branch() - branch_root = self.get_branch_config(branch_dir, 'branch.root') - branch_root_dir = self.find_repository(branch_dir, branch_root) - - # Prepare an environment for our internal index file. - # This index file allows us to commit changes to a tree without - # git noticing any change in the working tree or its own index. - env = dict(os.environ) - env['GIT_INDEX_FILE'] = os.path.join( - branch_root_dir, '.git', 'morph-tag-index') - - # Extract git arguments that deal with the commit message. - # This is so that we can use them for creating the tag commit. - msg = None - msg_args = [] - for i in xrange(0, len(args)): - if args[i] == '-m' or args[i] == '-F': - if i < len(args)-1: - msg_args.append(args[i]) - msg_args.append(args[i+1]) - if args[i] == '-m': - msg = args[i+1] - else: - msg = open(args[i+1]).read() - elif args[i].startswith('--message='): - msg_args.append(args[i]) - msg = args[i][len('--message='):] - - # Fail if no commit message was provided. - if not msg or not msg_args: - raise cliapp.AppException( - 'Commit message expected. Please run one of ' - 'the following commands to provide one:\n' - ' morph tag NAME -- -m "Message"\n' - ' morph tag NAME -- --message="Message"\n' - ' morph tag NAME -- -F <message file>') - - # Abort if the tag already exists. - # FIXME At the moment this only checks the local repo in the - # workspace, not the remote repo cache or the local repo cache. - if self.ref_exists_locally(branch_root_dir, 'refs/tags/%s' % tagname): - raise cliapp.AppException('%s: Tag "%s" already exists' % - (branch_root, tagname)) - - self.app.status(msg='%(repo)s: Preparing tag commit', - repo=branch_root) - - # Read current tree into the internal index. - parent_sha1 = self.resolve_ref(branch_root_dir, branch) - self.app.runcmd(['git', 'read-tree', parent_sha1], - cwd=branch_root_dir, env=env) - - self.app.status(msg='%(repo)s: Petrifying everything', - repo=branch_root) - - # Petrify everything. - self.lrc, self.rrc = morphlib.util.new_repo_caches(self.app) - self.petrify_everything(branch, branch_dir, - branch_root, branch_root_dir, - tagname, env) - - self.app.status(msg='%(repo)s: Creating tag commit', - repo=branch_root) - - # Create a dangling commit. - commit = self.create_tag_commit( - branch_root_dir, tagname, msg, env) - - self.app.status(msg='%(repo)s: Creating annotated tag "%(tag)s"', - repo=branch_root, tag=tagname) - - # Create an annotated tag for this commit. - self.create_annotated_tag(branch_root_dir, commit, env, args) - - def ref_exists_locally(self, repo_dir, ref): - try: - morphlib.git.rev_parse(self.app.runcmd, repo_dir, ref) - return True - except cliapp.AppException: - return False - - def petrify_everything(self, branch, branch_dir, - branch_root, branch_root_dir, tagref, env=os.environ, - resolved_refs=None, update_working_tree=False): - petrified_morphologies = set() - resolved_refs = resolved_refs or {} - for f in sorted(glob.iglob(os.path.join(branch_root_dir, '*.morph'))): - name = os.path.basename(f)[:-len('.morph')] - morphology = self.load_morphology(branch_root_dir, name) - self.petrify_morphology(branch, branch_dir, - branch_root, branch_root_dir, - branch_root, branch_root_dir, - tagref, name, morphology, - petrified_morphologies, resolved_refs, - env, update_working_tree) - - def petrify_morphology(self, branch, branch_dir, - branch_root, branch_root_dir, repo, repo_dir, - tagref, name, morphology, - petrified_morphologies, resolved_refs, - env=os.environ, update_working_tree=False): - self.app.status(msg='%(repo)s: Petrifying morphology \"%(morph)s\"', - repo=repo, morph=name) - - # Mark morphology as petrified so we don't petrify it twice. - petrified_morphologies.add(morphology) - - # Resolve the refs of all build dependencies (strata) and strata - # in the morphology into commit SHA1s. - strata = [] - if 'build-depends' in morphology and morphology['build-depends']: - strata += morphology['build-depends'] - if 'strata' in morphology and morphology['strata']: - strata += morphology['strata'] - for info in strata: - stratum_repo_dir = self.make_available( - info, branch, branch_dir, repo, repo_dir) - - # Load the stratum morphology and petrify it recursively if - # that hasn't happened yet. - stratum = self.load_morphology(stratum_repo_dir, info['morph']) - if not stratum in petrified_morphologies: - self.petrify_morphology(branch, branch_dir, - branch_root, branch_root_dir, - info.get('repo') or branch_root, - stratum_repo_dir, tagref, - info['morph'], stratum, - petrified_morphologies, - resolved_refs, env, - update_working_tree) - - # If this morphology is a stratum, resolve the refs of all its - # chunks into SHA1s. - if morphology['kind'] == 'stratum': - for info in morphology['chunks']: - commit = self.resolve_info(info, resolved_refs) - if info['ref'] != commit: - info['unpetrify-ref'] = info['ref'] - info['ref'] = commit - - # Write the petrified morphology to a temporary file in the - # branch root repository for inclusion in the tag commit. - with tempfile.NamedTemporaryFile(suffix='.morph') as f: - self.update_morphology( - repo_dir, name, morphology, output_fd=f.file) - - # Hash the petrified morphology and add it to the index - # for the tag commit. - sha1 = self.app.runcmd( - ['git', 'hash-object', '-t', 'blob', '-w', f.name], - cwd=branch_root_dir, env=env) - self.app.runcmd( - ['git', 'update-index', '--add', '--cacheinfo', - '100644', sha1, '%s.morph' % name], - cwd=branch_root_dir, env=env) - - # Update the working tree if requested. This can be done with - # git-checkout-index, but we still have the file, so use that - if update_working_tree: - shutil.copy(f.name, - os.path.join(branch_root_dir, '%s.morph' % name)) - - def resolve_info(self, info, resolved_refs): - '''Takes a morphology info and resolves its ref with cache support.''' - - key = (info.get('repo'), info.get('ref')) - if not key in resolved_refs: - commit_sha1, tree_sha1 = self.app.resolve_ref( - self.lrc, self.rrc, info['repo'], info['ref'], - update=not self.app.settings['no-git-update']) - resolved_refs[key] = commit_sha1 - return resolved_refs[key] - - def create_tag_commit(self, repo_dir, tagname, msg, env): - self.app.status(msg='%(repo)s: Creating commit for the tag', - repo=repo_dir) - - # Write and commit the tree. - tree = self.app.runcmd( - ['git', 'write-tree'], cwd=repo_dir, env=env).strip() - commit = self.app.runcmd( - ['git', 'commit-tree', tree, '-p', 'HEAD'], - feed_stdin=msg, cwd=repo_dir, env=env).strip() - return commit - - def create_annotated_tag(self, repo_dir, commit, env, args=[]): - self.app.status(msg='%(repo)s: Creating annotated tag for ' - 'commit %(commit)s', - repo=repo_dir, commit=commit) - - # Create an annotated tag for the commit - self.app.runcmd(['git', 'tag', '-a'] + args + [commit], - cwd=repo_dir, env=env) - - # When 'merge' is unset, git doesn't try to resolve conflicts itself in - # those files. - MERGE_ATTRIBUTE = '*.morph\t-merge\n' - - def disable_morph_merging(self, repo_dir): - attributes_file = os.path.join(repo_dir, ".git", "info", "attributes") - with open(attributes_file, 'a') as f: - f.write(self.MERGE_ATTRIBUTE) - - def enable_morph_merging(self, repo_dir): - attributes_file = os.path.join(repo_dir, ".git", "info", "attributes") - with open(attributes_file, 'r') as f: - attributes = f.read() - if attributes == self.MERGE_ATTRIBUTE: - os.unlink(attributes_file) - elif attributes.endswith(self.MERGE_ATTRIBUTE): - with morphlib.savefile.SaveFile(attributes_file, 'w') as f: - f.write(attributes[:-len(self.MERGE_ATTRIBUTE)]) - - def get_merge_files(self, repo_dir, from_sha1, to_ref, name): - '''Returns merge base, remote and local versions of a morphology - - We already ran 'git fetch', so the remote branch is available within - the target repository. + This undoes the changes `morph petrify` did. ''' - base_sha1 = self.app.runcmd(['git', 'merge-base', from_sha1, to_ref], - cwd=repo_dir).strip() - base_morph = self.load_morphology(repo_dir, name, ref=base_sha1) - from_morph = self.load_morphology(repo_dir, name, ref=from_sha1) - to_morph = self.load_morphology(repo_dir, name, ref=to_ref) - return base_morph, from_morph, to_morph - - def check_component(self, parent_kind, parent_path, from_info, to_info): - assert (parent_kind in ['system', 'stratum']) - - kind = 'chunk' if parent_kind == 'stratum' else 'stratum' - name = to_info.get('alias', to_info.get('name', to_info.get('morph'))) - path = parent_path + '.' + name - - if kind == 'chunk': - # Only chunks can be petrified - from_unpetrify_ref = from_info.get('unpetrify-ref', None) - to_unpetrify_ref = to_info.get('unpetrify-ref', None) - if from_unpetrify_ref is not None and to_unpetrify_ref is None: - self.app.output.write( - 'WARNING: chunk "%s" is now petrified\n' % path) - elif from_unpetrify_ref is None and to_unpetrify_ref is not None: - self.app.output.write( - 'WARNING: chunk "%s" is no longer petrified\n' % path) - elif from_unpetrify_ref != to_unpetrify_ref: - raise cliapp.AppException( - 'merge conflict: chunk "%s" is petrified to a different ' - 'ref in each branch' % path) - - def diff_morphologies(self, path, from_morph, to_morph): - '''Component-level diff between two versions of a morphology''' + if args: + raise cliapp.AppException('morph petrify takes no arguments') - def component_key(info): - # This function needs only to be stable and reproducible - if 'name' in info: - return (info.get('repo'), info['morph'], info['name']) - else: - return (info.get('repo'), info['morph']) + ws = morphlib.workspace.open('.') + sb = morphlib.sysbranchdir.open_from_within('.') + loader = morphlib.morphloader.MorphologyLoader() - if from_morph['name'] != to_morph['name']: - # We should enforce name == filename in load_morphology() - raise cliapp.AppException( - 'merge conflict: "name" of morphology %s (name should always ' - 'match filename)' % path) - if from_morph['kind'] != to_morph['kind']: - raise cliapp.AppException( - 'merge conflict: "kind" of morphology %s changed from %s to %s' - % (path, to_morph['kind'], from_morph['kind'])) - - kind = to_morph['kind'] - - # copy() makes a shallow copy, so editing the list elements will - # change the actual morphologies. - if kind == 'system': - from_components = copy.copy(from_morph['strata']) - to_components = copy.copy(to_morph['strata']) - elif kind == 'stratum': - from_components = copy.copy(from_morph['chunks']) - to_components = copy.copy(to_morph['chunks']) - from_components.sort(key=component_key) - to_components.sort(key=component_key) - - # These are not set() purely because a set requires a hashable type - intersection = [] # TO n FROM - from_diff = [] # FROM \ TO - to_diff = [] # TO \ FROM - while len(from_components) > 0 and len(to_components) > 0: - from_info = from_components.pop(0) - to_info = to_components.pop(0) - match = cmp(component_key(from_info), component_key(to_info)) - if match < 0: - from_diff.append(from_info) - elif match > 0: - to_diff.append(to_info) - elif match == 0: - intersection.append((from_info, to_info)) - if len(from_components) != 0: - from_diff.append(from_components.pop(0)) - if len(to_components) != 0: - to_diff.append(to_components.pop(0)) - return intersection, from_diff, to_diff - - def merge_repo(self, merged_repos, from_branch_dir, from_repo, from_ref, - to_branch_dir, to_repo, to_ref): - '''Merge changes for a system branch in a specific repository - - We disable merging for morphologies and do this manually later on. + morphs = self._load_all_sysbranch_morphologies(sb, loader) - ''' + # Restore the ref for each stratum and chunk + morphs.unpetrify_all() - if to_repo in merged_repos: - return merged_repos[to_repo] - - from_repo_dir = self.find_repository(from_branch_dir, from_repo) - to_repo_dir = self.checkout_repository(to_branch_dir, to_repo, to_ref) - - if self.get_uncommitted_changes(from_repo_dir) != []: - raise cliapp.AppException('repository %s has uncommitted ' - 'changes' % from_repo) - if self.get_uncommitted_changes(to_repo_dir) != []: - raise cliapp.AppException('repository %s has uncommitted ' - 'changes' % to_repo) - - # Fetch the local FROM branch; its sha1 will be stored in FETCH_HEAD. - # ':' in pathnames confuses git, so we have to pass it a URL. - from_repo_url = urlparse.urljoin('file://', from_repo_dir) - self.app.runcmd(['git', 'fetch', from_repo_url, from_ref], - cwd=to_repo_dir) - - # Merge everything but the morphologies; error output is ignored (it's - # not very good) and instead we report conflicts manually later on. - self.disable_morph_merging(to_repo_dir) - with open(os.path.join(to_repo_dir, '.git', 'FETCH_HEAD')) as f: - from_sha1 = f.read(40) - status, output, error = self.app.runcmd_unchecked( - ['git', 'merge', '--no-commit', '--no-ff', from_sha1], - cwd=to_repo_dir) - self.enable_morph_merging(to_repo_dir) - - merged_repos[to_repo] = (to_repo_dir, from_sha1) - return (to_repo_dir, from_sha1) - - def merge(self, args): - '''Pull and merge changes from a system branch into the current one. + # Write morphologies back out again. + self._save_dirty_morphologies(loader, sb, morphs.morphologies) - Command line arguments: - - * `BRANCH` is the name of the system branch to merge _from_. + def status(self, args): + '''Show information about the current system branch or workspace - This merges another system branch into the current one. Morph - will do a `git merge` for each component that has been edited, - and undo any changes to `ref` fields in system and stratum - morphologies that `morph edit` has made. + This shows the status of every local git repository of the + current system branch. This is similar to running `git status` + in each repository separately. - You need to be in the _target_ system branch when merging. If - you have two system branches, `TROVE_ID/release/1.2` and - `TROVE_ID/bugfixes/12765`, and want to merge the bug fix branch - into the release branch, you need to first checkout the release - system branch, and then merge the bugfix branch into that. + If run in a Morph workspace, but not in a system branch checkout, + it lists all checked out system branches in the workspace. ''' - if len(args) != 1: - raise cliapp.AppException('morph merge requires a system branch ' - 'name as its argument') - - self.lrc, self.rrc = morphlib.util.new_repo_caches(self.app) - workspace = self.deduce_workspace() - from_branch = args[0] - from_branch_dir = self.find_system_branch(workspace, from_branch) - to_branch, to_branch_dir = self.deduce_system_branch() - if from_branch_dir is None: - raise cliapp.AppException('branch %s must be checked out before ' - 'it can be merged' % from_branch) - - root_repo = self.get_branch_config(from_branch_dir, 'branch.root') - other_root_repo = self.get_branch_config(to_branch_dir, 'branch.root') - if root_repo != other_root_repo: - raise cliapp.AppException('branches do not share a root ' - 'repository : %s vs %s' % - (root_repo, other_root_repo)) - - def merge_chunk(parent_path, old_ci, ci): - self.merge_repo(merged_repos, - from_branch_dir, old_ci['repo'], from_branch, - to_branch_dir, ci['repo'], ci['ref']) - - def merge_stratum(parent_path, old_si, si): - path = parent_path + '.' + si['morph'] - - to_repo_dir, from_sha1 = self.merge_repo(merged_repos, - from_branch_dir, old_si['repo'], from_branch, - to_branch_dir, si['repo'], si['ref']) - base_morph, from_morph, to_morph = self.get_merge_files( - to_repo_dir, from_sha1, si['ref'], si['morph']) - intersection, from_diff, to_diff = self.diff_morphologies( - path, from_morph, to_morph) - for from_ci, to_ci in intersection: - self.check_component('stratum', path, from_ci, to_ci) - - changed = False - edited_chunks = [ci for ci in from_morph['chunks'] - if ci['ref'] == from_branch] - for ci in edited_chunks: - for old_ci in to_morph['chunks']: - if old_ci['repo'] == ci['repo']: - break - else: - raise cliapp.AppException( - 'chunk %s was added within this branch and ' - 'subsequently edited. This is not yet supported: ' - 'refusing to merge.' % ci['name']) - changed = True - ci['ref'] = old_ci['ref'] - merge_chunk(path, old_ci, ci) - if changed: - self.update_morphology(to_repo_dir, si['morph'], to_morph) - self.app.runcmd(['git', 'add', si['morph'] + '.morph'], - cwd=to_repo_dir) - - def merge_system(name): - base_morph, from_morph, to_morph = self.get_merge_files( - to_root_dir, from_sha1, to_branch, name) - if to_morph['kind'] != 'system': - return - - intersection, from_diff, to_diff = self.diff_morphologies( - name, from_morph, to_morph) - for from_si, to_si in intersection: - self.check_component('system', name, from_si, to_si) - - changed = False - edited_strata = [si for si in from_morph['strata'] - if si.get('ref') == from_branch] - for si in edited_strata: - for old_si in to_morph['strata']: - # We make no attempt at rename / move detection - if (old_si['morph'] == si['morph'] - and old_si.get('repo') == si.get('repo')): - break - else: - raise cliapp.AppException( - 'stratum %s was added within this branch and ' - 'subsequently edited. This is not yet supported: ' - 'refusing to merge.' % si['morph']) - changed = True - si['ref'] = old_si.get('ref') - merge_stratum(name, old_si, si) - if changed: - self.update_morphology(to_root_dir, name, to_morph) - self.app.runcmd(['git', 'add', f], cwd=to_root_dir) - - merged_repos = {} - try: - to_root_dir, from_sha1 = self.merge_repo(merged_repos, - from_branch_dir, root_repo, from_branch, - to_branch_dir, root_repo, to_branch) - - for f in sorted(glob.iglob(os.path.join(to_root_dir, '*.morph'))): - name = os.path.basename(f)[:-len('.morph')] - merge_system(name) - - success = True - for repo_name, repo_info in merged_repos.iteritems(): - repo_dir = repo_info[0] - conflicts = self.get_unmerged_changes(repo_dir) - if len(conflicts) > 0: - self.app.output.write("Merge conflicts in %s:\n\t%s\n" % - (repo_name, '\n\t'.join(conflicts))) - success = False - elif morphlib.git.index_has_changes(self.app.runcmd, repo_dir): - # Repo may not be dirty if the changes only touched refs, - # because they may now match the previous state. - msg = "Merge system branch '%s'" % from_branch - self.app.runcmd(['git', 'commit', '--all', '-m%s' % msg], - cwd=repo_dir) - if not success: - raise cliapp.AppException( - "merge errors were encountered. Please manually merge the " - "target ref into %s in the remote system branch in each " - "case, and then repeat the 'morph merge' operation." % - from_branch) - self.app.status(msg="Merge successful") - except BaseException, e: - logging.error('Caught exception: %s' % str(e)) - logging.info('Resetting half-finished merge') - for repo_dir, sha1 in merged_repos.itervalues(): - self.reset_work_tree_safe(repo_dir) - raise - - def build(self, args): - '''Build a system image in the current system branch - - Command line arguments: - - * `SYSTEM` is the name of the system to build. - - This builds a system image, and any of its components that - need building. The system name is the basename of the system - morphology, in the root repository of the current system branch, - without the `.morph` suffix in the filename. - - The location of the resulting system image artifact is printed - at the end of the build output. - - You do not need to commit your changes before building, Morph - does that for you, in a temporary branch for each build. However, - note that Morph does not untracked files to the temporary branch, - only uncommitted changes to files git already knows about. You - need to `git add` and commit each new file yourself. - - Example: - - morph build devel-system-x86_64-generic - - ''' + if args: + raise cliapp.AppException('morph status takes no arguments') - if len(args) != 1: - raise cliapp.AppException('morph build expects exactly one ' - 'parameter: the system to build') - - # Raise an exception if there is not enough space - morphlib.util.check_disk_available( - self.app.settings['tempdir'], - self.app.settings['tempdir-min-space'], - self.app.settings['cachedir'], - self.app.settings['cachedir-min-space']) - - system_name = args[0] - - # Deduce workspace and system branch and branch root repository. - workspace = self.deduce_workspace() - branch, branch_dir = self.deduce_system_branch() - branch_root = self.get_branch_config(branch_dir, 'branch.root') - branch_uuid = self.get_branch_config(branch_dir, 'branch.uuid') - - # Generate a UUID for the build. - build_uuid = uuid.uuid4().hex - - build_command = morphlib.buildcommand.BuildCommand(self.app) - build_command = self.app.hookmgr.call('new-build-command', - build_command) - push = self.app.settings['push-build-branches'] - - self.app.status(msg='Starting build %(uuid)s', uuid=build_uuid) - - self.app.status(msg='Collecting morphologies involved in ' - 'building %(system)s from %(branch)s', - system=system_name, branch=branch) - - # Get repositories of morphologies involved in building this system - # from the current system branch. - build_repos = self.get_system_build_repos( - branch, branch_dir, branch_root, system_name) - - # Generate temporary build ref names for all these repositories. - self.generate_build_ref_names(build_repos, branch_uuid) - - # Create the build refs for all these repositories and commit - # all uncommitted changes to them, updating all references - # to system branch refs to point to the build refs instead. - self.update_build_refs(build_repos, branch, build_uuid, push) - - if push: - self.push_build_refs(build_repos) - build_branch_root = branch_root + ws = morphlib.workspace.open('.') + try: + sb = morphlib.sysbranchdir.open_from_within('.') + except morphlib.sysbranchdir.NotInSystemBranch: + self._workspace_status(ws) else: - dirname = build_repos[branch_root]['dirname'] - build_branch_root = urlparse.urljoin('file://', dirname) - - # Run the build. - build_command.build([build_branch_root, - build_repos[branch_root]['build-ref'], - system_name]) + self._branch_status(ws, sb) - if push: - self.delete_remote_build_refs(build_repos) + def _workspace_status(self, ws): + '''Show information about the current workspace - self.app.status(msg='Finished build %(uuid)s', uuid=build_uuid) - - def get_system_build_repos(self, system_branch, branch_dir, - branch_root, system_name): - '''Map upstream repository URLs to their checkouts in the system branch - - Also provides the list of morphologies stored in each repository, - grouped by kind. + This lists all checked out system branches in the workspace. ''' + self.app.output.write("System branches in current workspace:\n") + branches = sorted(ws.list_system_branches(), + key=lambda x: x.root_directory) + for sb in branches: + self.app.output.write(" %s\n" % sb.get_config('branch.name')) - build_repos = {} - - def prepare_repo_info(repo, dirname): - build_repos[repo] = { - 'dirname': dirname, - 'systems': [], - 'strata': [], - 'chunks': [] - } - - def add_morphology_info(info, category): - repo = info['repo'] or branch_root - if repo in build_repos: - repo_dir = build_repos[repo]['dirname'] - else: - repo_dir = self.find_repository(branch_dir, repo) - if repo_dir: - if not repo in build_repos: - prepare_repo_info(repo, repo_dir) - build_repos[repo][category].append(info['morph']) - return repo_dir - - # Add repository and morphology of the system. - branch_root_dir = self.find_repository(branch_dir, branch_root) - prepare_repo_info(branch_root, branch_root_dir) - build_repos[branch_root]['systems'].append(system_name) - - # Traverse and add repositories and morphologies involved in - # building this system from the system branch. - system_morphology = self.load_morphology(branch_root_dir, system_name) - for info in system_morphology['strata']: - if info['ref'] == system_branch or info['ref'] is None: - repo_dir = add_morphology_info(info, 'strata') - if repo_dir: - stratum_morphology = self.load_morphology( - repo_dir, info['morph']) - for info in stratum_morphology['chunks']: - if info['ref'] == system_branch: - add_morphology_info(info, 'chunks') - - return build_repos - - def inject_build_refs(self, morphology, build_repos, will_push): - # Starting from a system or stratum morphology, update all ref - # pointers of strata or chunks involved in a system build (represented - # by build_repos) to point to temporary build refs of the repos - # involved in the system build. - def inject_build_ref(info): - if info['repo'] in build_repos and ( - info['morph'] in build_repos[info['repo']]['strata'] or - info['morph'] in build_repos[info['repo']]['chunks']): - info['ref'] = build_repos[info['repo']]['build-ref'] - if not will_push: - dirname = build_repos[info['repo']]['dirname'] - info['repo'] = urlparse.urljoin('file://', dirname) - if morphology['kind'] == 'system': - for info in morphology['strata']: - inject_build_ref(info) - elif morphology['kind'] == 'stratum': - if morphology['build-depends'] is not None: - for info in morphology['build-depends']: - inject_build_ref(info) - for info in morphology['chunks']: - inject_build_ref(info) - - def generate_build_ref_names(self, build_repos, branch_uuid): - for repo, info in build_repos.iteritems(): - repo_dir = info['dirname'] - repo_uuid = self.get_repo_config(repo_dir, 'morph.uuid') - build_ref = os.path.join(self.app.settings['build-ref-prefix'], - branch_uuid, repo_uuid) - info['build-ref'] = build_ref - - def update_build_refs(self, build_repos, system_branch, build_uuid, - will_push): - '''Update build branches for each repository with any local changes ''' - - # Define the committer. - committer_name = 'Morph (on behalf of %s)' % \ - (morphlib.git.get_user_name(self.app.runcmd)) - committer_email = morphlib.git.get_user_email(self.app.runcmd) - - for repo, info in build_repos.iteritems(): - repo_dir = info['dirname'] - build_ref = info['build-ref'] - - self.app.status(msg='%(repo)s: Creating build branch', repo=repo) - - # Obtain parent SHA1 for the temporary ref tree to be committed. - # This will either be the current commit of the temporary ref or - # HEAD in case the temporary ref does not exist yet. - system_branch_sha1 = self.resolve_ref( - repo_dir, '%s^{commit}' % system_branch) - parent_sha1 = self.resolve_ref(repo_dir, '%s^{commit}' % build_ref) - if not parent_sha1: - parent_sha1 = system_branch_sha1 - - # Prepare an environment with our internal index file. - # This index file allows us to commit changes to a tree without - # git noticing any change in working tree or its own index. - env = dict(os.environ) - env['GIT_INDEX_FILE'] = os.path.join( - repo_dir, '.git', 'morph-index') - env['GIT_COMMITTER_NAME'] = committer_name - env['GIT_COMMITTER_EMAIL'] = committer_email - - # Read tree from current HEAD into the morph index. - self.app.runcmd(['git', 'read-tree', system_branch_sha1], - cwd=repo_dir, env=env) - - self.app.status(msg='%(repo)s: Adding uncommitted changes to ' - 'build branch', repo=repo) - - # Add all local, uncommitted changes to our internal index. - changed_files = self.get_uncommitted_changes(repo_dir, env) - self.app.runcmd(['git', 'add'] + changed_files, - cwd=repo_dir, env=env) - - self.app.status(msg='%(repo)s: Updating morphologies to use ' - 'build branch instead of "%(branch)s"', - repo=repo, branch=system_branch) - - # Update all references to the system branches of strata - # and chunks to point to the temporary refs, which is needed - # for building. - filenames = info['systems'] + info['strata'] - for filename in filenames: - # Inject temporary refs in the right places in each morphology. - morphology = self.load_morphology(repo_dir, filename) - self.inject_build_refs(morphology, build_repos, will_push) - with tempfile.NamedTemporaryFile(suffix='.morph') as f: - self.update_morphology( - repo_dir, filename, morphology, output_fd=f.file) - - morphology_sha1 = self.app.runcmd( - ['git', 'hash-object', '-t', 'blob', '-w', f.name], - cwd=repo_dir, env=env) - - try: - self.app.runcmd( - ['git', 'update-index', '--cacheinfo', - '100644', morphology_sha1, '%s.morph' % filename], - cwd=repo_dir, env=env) - except cliapp.AppException, e: - raise cliapp.AppException( - "You seem to want to build %s, but '%s.morph' " - "doesn't exist in the morphologies repository. " - "Did you forget to commit it?" % - (filename, filename)) - - # Create a commit message including the build UUID. This allows us - # to collect all commits of a build across repositories and thereby - # see the changes made to the entire system between any two builds. - message = 'Morph build %s\n\nSystem branch: %s\n' % \ - (build_uuid, system_branch) - - # Write and commit the tree and update the temporary build ref. - tree = self.app.runcmd( - ['git', 'write-tree'], cwd=repo_dir, env=env).strip() - commit = self.app.runcmd( - ['git', 'commit-tree', tree, '-p', parent_sha1], - feed_stdin=message, cwd=repo_dir, env=env).strip() - self.app.runcmd( - ['git', 'update-ref', '-m', message, - 'refs/heads/%s' % build_ref, commit], - cwd=repo_dir, env=env) - - def push_build_refs(self, build_repos): - for repo, info in build_repos.iteritems(): - self.app.status(msg='%(repo)s: Pushing build branch', repo=repo) - self.app.runcmd(['git', 'push', 'origin', '%s:%s' % - (info['build-ref'], info['build-ref'])], - cwd=info['dirname']) - - def delete_remote_build_refs(self, build_repos): - for repo, info in build_repos.iteritems(): - self.app.status(msg='%(repo)s: Deleting remote build branch', - repo=repo) - self.app.runcmd(['git', 'push', 'origin', - ':%s' % info['build-ref']], cwd=info['dirname']) - - def status(self, args): - '''Show information about the current system branch or workspace + def _branch_status(self, ws, sb): + '''Show information about the current branch This shows the status of every local git repository of the current system branch. This is similar to running `git status` - in each repository separately, but the output is nicer. - - If run in a Morph workspace, but not in a system branch checkout, - it lists all checked out system branches in the workspace. + in each repository separately. ''' + branch = sb.get_config('branch.name') + root = sb.get_config('branch.root') - if len(args) != 0: - raise cliapp.AppException('morph status takes no arguments') - - workspace = self.deduce_workspace() - try: - branch, branch_path = self.deduce_system_branch() - except cliapp.AppException: - branch = None - - if branch is None: - self.app.output.write("System branches in current workspace:\n") - branch_dirs = sorted(self.walk_special_directories( - workspace, special_subdir='.morph-system-branch')) - for dirname in branch_dirs: - branch = self.get_branch_config(dirname, 'branch.name') - self.app.output.write(" %s\n" % branch) - return - - root_repo = self.get_branch_config(branch_path, 'branch.root') - root_repo_path = self.find_repository(branch_path, root_repo) - - self.app.output.write("On branch %s, root %s\n" % (branch, root_repo)) + self.app.output.write("On branch %s, root %s\n" % (branch, root)) has_uncommitted_changes = False - for d in self.iterate_branch_repos(branch_path, root_repo_path): + for gd in sorted(sb.list_git_directories(), key=lambda x: x.dirname): try: - repo = self.get_repo_config(d, 'morph.repository') + repo = gd.get_config('morph.repository') except cliapp.AppException: self.app.output.write( - ' %s: not part of system branch\n' % d) - continue - head = self.get_head(d) + ' %s: not part of system branch\n' % gd.dirname) + # TODO: make this less vulnerable to a branch using + # refs/heads/foo instead of foo + head = gd.HEAD if head != branch: self.app.output.write( - ' %s: unexpected ref checked out "%s"\n' % (repo, head)) - if len(self.get_uncommitted_changes(d)) > 0: + ' %s: unexpected ref checked out %r\n' % (repo, head)) + if any(gd.get_index().get_uncommitted_changes()): has_uncommitted_changes = True self.app.output.write(' %s: uncommitted changes\n' % repo) if not has_uncommitted_changes: self.app.output.write("\nNo repos have outstanding changes.\n") - def foreach(self, args): - '''Run a command in each repository checked out in a system branch. + def branch_from_image(self, args): + '''Produce a branch of an existing system image. - Use -- before specifying the command to separate its arguments from - Morph's own arguments. + 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. - Command line arguments: + If --metadata-dir is not specified, it defaults to your currently + running system. - * `--` indicates the end of option processing for Morph. - * `COMMAND` is a command to run. - * `ARGS` is a list of arguments or options to be passed onto - `COMMAND`. + ''' + 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']) - This runs the given `COMMAND` in each git repository belonging - to the current system branch that exists locally in the current - workspace. This can be a handy way to do the same thing in all - the local git repositories. + self._require_git_user_config() - For example: + ws = morphlib.workspace.open('.') - morph foreach -- git push + system, metadata = self._load_system_metadata(metadata_path) + resolved_refs = dict(self._resolve_refs_from_metadata(alias_resolver, + metadata)) + self.app.status(msg='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'] - The above command would push any committed changes in each - repository to the git server. + lrc, rrc = morphlib.util.new_repo_caches(self.app) + cached_repo = lrc.get_updated_repo(root_url) - ''' - # For simplicity, this simply iterates repositories in the directory - # rather than walking through the morphologies as 'morph merge' does. + with self._initializing_system_branch( + ws, root_url, system_branch, cached_repo, base_ref) as (sb, gd): - if len(args) == 0: - raise cliapp.AppException('morph foreach expects a command to run') + # 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) - workspace = self.deduce_workspace() - branch, branch_path = self.deduce_system_branch() + loader = morphlib.morphloader.MorphologyLoader() + morphs = self._load_all_sysbranch_morphologies(sb, loader) - root_repo = self.get_branch_config(branch_path, 'branch.root') - root_repo_path = self.find_repository(branch_path, root_repo) + morphs.repoint_refs(sb.root_repository_url, + sb.system_branch_name) - for d in self.iterate_branch_repos(branch_path, root_repo_path): - try: - repo = self.get_repo_config(d, 'morph.repository') - except cliapp.AppException: - continue + morphs.petrify_chunks(resolved_refs) - if d != root_repo_path: - self.app.output.write('\n') - self.app.output.write('%s\n' % repo) + self._save_dirty_morphologies(loader, sb, morphs.morphologies) - status, output, error = self.app.runcmd_unchecked(args, cwd=d) - self.app.output.write(output) - if status != 0: - self.app.output.write(error) - raise cliapp.AppException( - 'Command failed at repo %s: %s' % (repo, ' '.join(args))) + @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']) |