diff --git a/renga/cli/_ascii.py b/renga/cli/_ascii.py index 578101b44c..b3f2f258fa 100644 --- a/renga/cli/_ascii.py +++ b/renga/cli/_ascii.py @@ -27,6 +27,15 @@ from networkx.algorithms.dag import topological_sort +def _format_sha1(graph, key): + """Return formatted text with the submodule information.""" + submodule = ':'.join(graph.G.nodes[key].get('submodule', [])) + if submodule: + return click.style(submodule, fg='green') + '@' + click.style( + key[0][:8], fg='yellow') + return click.style(key[0][:8], fg='yellow') + + @attr.s class DAG(object): """Generate ASCII representation of a DAG.""" @@ -72,7 +81,7 @@ def __iter__(self): def node_text(self, node): """Return text for a given node.""" - return [click.style(node[0][:7], fg='yellow') + ' ' + node[1]] + return [_format_sha1(self.graph, node) + ' ' + node[1]] def iter_edges(self, node): """Yield edges for a node and update internal status.""" diff --git a/renga/cli/_graph.py b/renga/cli/_graph.py index 30a1abd3e3..316d2a6d35 100644 --- a/renga/cli/_graph.py +++ b/renga/cli/_graph.py @@ -24,12 +24,14 @@ import attr import networkx as nx import yaml -from git import IndexFile +from git import IndexFile, Submodule from renga._compat import Path from renga.models.cwl.command_line_tool import CommandLineTool from renga.models.cwl.workflow import Workflow +from ._repo import Repo + @attr.s class Graph(object): @@ -64,7 +66,7 @@ def find_cwl(self, commit): ] if len(files) == 1: - return os.path.relpath(Path(files[0]).resolve(), self.repo_path) + return files[0] def find_latest_cwl(self): """Return the latest CWL in the repository.""" @@ -88,9 +90,8 @@ def iter_file_inputs(self, tool, basedir): for input_ in tool.inputs: if input_.type == 'File' and input_.default: yield os.path.relpath( - (basedir / input_.default.path).resolve(), - self.repo_path - ), input_.id + (self.repo_path / basedir / input_.default.path).resolve(), + self.repo_path), input_.id def add_tool(self, commit, path): """Add a tool and its dependencies to the graph.""" @@ -124,7 +125,44 @@ def add_file(self, path, revision='HEAD'): if file_commits: #: Does not have a parent CWL. - return self.add_node(file_commits[0], path) + root_node = self.add_node(file_commits[0], path) + parent_commit, original_path = root_node + + #: Capture information about the submodule in a submodule. + root_submodule = self.G.nodes[root_node].get('submodule', []) + + #: Resolve Renga based submodules. + original_path = Path(original_path) + if original_path.is_symlink() or str(original_path).startswith( + '.renga/vendors'): + original_path = original_path.resolve() + + for submodule in Submodule.iter_items( + self.repo.git, parent_commit=parent_commit): + try: + subpath = original_path.relative_to( + Path(submodule.path).resolve()) + subgraph = Graph(repo=Repo(git_home=submodule.path)) + subnode = subgraph.add_file( + str(subpath), revision=submodule.hexsha) + + #: Extend node metadata. + for _, data in subgraph.G.nodes(data=True): + data['submodule'] = root_submodule + [ + submodule.name + ] + + #: Merge file node with it's symlinked version. + self.G = nx.contracted_nodes( + nx.compose(self.G, subgraph.G), + root_node, + subnode, + ) # TODO optionally it can be changed to an edge. + break + except ValueError: + continue + + return root_node @property def _output_keys(self): @@ -162,7 +200,7 @@ def build_status(self, revision='HEAD'): for filepath, _ in index.entries.keys(): if not filepath.startswith('.renga') and \ - filepath not in {'.gitignore', }: + filepath not in {'.gitignore', '.gitattributes'}: self.add_file(filepath, revision=revision) current_files.add(filepath) @@ -172,8 +210,7 @@ def build_status(self, revision='HEAD'): graph_files = sorted( ((commit, filepath) for (commit, filepath) in self.G if filepath in current_files), - key=itemgetter(1) - ) + key=itemgetter(1)) status = {'up-to-date': {}, 'outdated': {}, 'multiple-versions': {}} @@ -183,10 +220,9 @@ def build_status(self, revision='HEAD'): if len(keys) > 1: status['multiple-versions'][filepath] = keys - if any(len(self.G.nodes[key]['_need_update']) > 1 - for key in keys): - updates = list(self.G.nodes[key] - ['_need_update'] for key in keys) + if any(len(self.G.nodes[key]['_need_update']) > 1 for key in keys): + updates = list( + self.G.nodes[key]['_need_update'] for key in keys) status['outdated'][filepath] = updates elif len(keys) == 1: status['up-to-date'][filepath] = keys[0][0] diff --git a/renga/cli/status.py b/renga/cli/status.py index f8fe87a4c4..5b99ed0f07 100644 --- a/renga/cli/status.py +++ b/renga/cli/status.py @@ -19,6 +19,7 @@ import click +from ._ascii import _format_sha1 from ._git import with_git from ._graph import Graph from ._repo import pass_repo @@ -46,13 +47,12 @@ def status(ctx, repo, revision, path): for filepath, files in status['outdated'].items(): paths = (', '.join( - '{0}@{1}'.format( - click.style(p, fg='yellow', bold=True), - click.style(c[:8], fg='blue'), - ) - for c, p in stts - if not p.startswith('.renga') and p not in status['outdated'] - ) for stts in files) + '{0}#{1}'.format( + click.style(p, fg='blue', bold=True), + _format_sha1(graph, (c, p)), + ) for c, p in stts + if not p.startswith('.renga') and p not in status['outdated']) + for stts in files) click.echo('\t{0}: {1}'.format( click.style(filepath, fg='red', bold=True), ', '.join(paths))) @@ -71,10 +71,9 @@ def status(ctx, repo, revision, path): click.echo() for filepath, files in status['multiple-versions'].items(): - commits = (click.style(commit[:8], fg='blue') - for commit, _ in files) + commits = (_format_sha1(graph, key) for key in files) click.echo('\t{0}: {1}'.format( - click.style(filepath, fg='yellow', bold=True), + click.style(filepath, fg='blue', bold=True), ', '.join(commits))) click.echo()