From 36fadeeaa47b642c90ae84a7032cb87ce524ee25 Mon Sep 17 00:00:00 2001 From: David First Date: Mon, 8 May 2023 15:41:37 -0400 Subject: [PATCH] feat(cat-version-history): introduce --graph flag to generate a png file with the graph --- src/api/scope/lib/cat-version-history.ts | 18 ++++++-- .../private-cmds/cat-version-history-cmd.ts | 20 +++++++- src/scope/graph/vizgraph.ts | 46 ++++++++++++++++++- 3 files changed, 76 insertions(+), 8 deletions(-) diff --git a/src/api/scope/lib/cat-version-history.ts b/src/api/scope/lib/cat-version-history.ts index da870ef392c2..c2ae0ac901ce 100644 --- a/src/api/scope/lib/cat-version-history.ts +++ b/src/api/scope/lib/cat-version-history.ts @@ -1,12 +1,22 @@ import { BitId } from '../../../bit-id'; import { loadScope, Scope } from '../../../scope'; +import { VersionHistory } from '../../../scope/models'; export async function catVersionHistory(id: string) { - const scope: Scope = await loadScope(); - const bitId: BitId = await scope.getParsedId(id); - const component = await scope.getModelComponent(bitId); - const versionHistory = await component.GetVersionHistory(scope.objects); + const versionHistory = await getVersionHistory(id); const versionHistoryObj = versionHistory.toObject(); versionHistoryObj.hash = versionHistory.hash().toString(); return versionHistoryObj; } + +export async function generateVersionHistoryGraph(id: string) { + const versionHistory = await getVersionHistory(id); + return versionHistory.getGraph(); +} + +async function getVersionHistory(id: string): Promise { + const scope: Scope = await loadScope(); + const bitId: BitId = await scope.getParsedId(id); + const component = await scope.getModelComponent(bitId); + return component.GetVersionHistory(scope.objects); +} diff --git a/src/cli/commands/private-cmds/cat-version-history-cmd.ts b/src/cli/commands/private-cmds/cat-version-history-cmd.ts index 280d77b66a0b..1fd3f837d2b3 100644 --- a/src/cli/commands/private-cmds/cat-version-history-cmd.ts +++ b/src/cli/commands/private-cmds/cat-version-history-cmd.ts @@ -1,6 +1,13 @@ -import { catVersionHistory } from '../../../api/scope/lib/cat-version-history'; +import { catVersionHistory, generateVersionHistoryGraph } from '../../../api/scope/lib/cat-version-history'; +import VisualDependencyGraph from '../../../scope/graph/vizgraph'; import { CommandOptions, LegacyCommand } from '../../legacy-command'; +const colorPerEdgeType = { + parent: 'green', + unrelated: 'red', + squashed: 'blue', +}; + export class CatVersionHistoryCmd implements LegacyCommand { name = 'cat-version-history [id]'; description = 'cat version-history object by component-id'; @@ -9,9 +16,18 @@ export class CatVersionHistoryCmd implements LegacyCommand { opts = [ // json is also the default for this command. it's only needed to suppress the logger.console ['j', 'json', 'json format'], + ['g', 'graph', `generate graph image (arrows color: ${JSON.stringify(colorPerEdgeType)})`], ] as CommandOptions; - action([id]: [string]): Promise { + async action([id]: [string], { graph }: { graph: boolean }): Promise { + if (graph) { + const graphHistory = await generateVersionHistoryGraph(id); + const visualDependencyGraph = await VisualDependencyGraph.loadFromClearGraph(graphHistory, { + colorPerEdgeType, + }); + const result = await visualDependencyGraph.image(); + return `image created at ${result}`; + } return catVersionHistory(id); } diff --git a/src/scope/graph/vizgraph.ts b/src/scope/graph/vizgraph.ts index 56f21dbdb04b..348d01e01cfb 100644 --- a/src/scope/graph/vizgraph.ts +++ b/src/scope/graph/vizgraph.ts @@ -1,13 +1,15 @@ import execa from 'execa'; +import os from 'os'; import fs from 'fs-extra'; import { Graph } from 'graphlib'; import graphviz, { Digraph } from 'graphviz'; +import { Graph as ClearGraph } from '@teambit/graph.cleargraph'; import * as path from 'path'; import BitId from '../../bit-id/bit-id'; import BitIds from '../../bit-id/bit-ids'; import logger from '../../logger/logger'; -import { getLatestVersionNumber } from '../../utils'; +import { generateRandomStr, getLatestVersionNumber } from '../../utils'; // const Graph = GraphLib.Graph; // const Digraph = graphviz.digraph; @@ -23,6 +25,7 @@ type ConfigProps = { graphVizOptions?: Record; // null Custom GraphViz options graphVizPath?: string; // null Custom GraphViz path highlightColor?: string; + colorPerEdgeType?: { [edgeType: string]: string }; }; const defaultConfig: ConfigProps = { @@ -62,6 +65,18 @@ export default class VisualDependencyGraph { return new VisualDependencyGraph(graphlib, graph, mergedConfig); } + static async loadFromClearGraph( + clearGraph: ClearGraph, + config: ConfigProps = {} + ): Promise { + const mergedConfig = Object.assign({}, defaultConfig, config); + await checkGraphvizInstalled(config.graphVizPath); + // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX! + const graph: Digraph = VisualDependencyGraph.buildDependenciesGraphFromClearGraph(clearGraph, mergedConfig); + // @ts-ignore + return new VisualDependencyGraph(clearGraph, graph, mergedConfig); + } + /** * Creates the graphviz graph */ @@ -92,6 +107,29 @@ export default class VisualDependencyGraph { return graph; } + static buildDependenciesGraphFromClearGraph(clearGraph: ClearGraph, config: ConfigProps): Digraph { + const graph = graphviz.digraph('G'); + + if (config.graphVizPath) { + graph.setGraphVizPath(config.graphVizPath); + } + + const nodes = clearGraph.nodes; + const edges = clearGraph.edges; + + nodes.forEach((node) => { + graph.addNode(node); + }); + edges.forEach((edge) => { + const edgeType = edge.attr; + const vizEdge = graph.addEdge(edge.sourceId, edge.targetId); + const color = config.colorPerEdgeType?.[edgeType] || 'green'; + setEdgeColor(vizEdge, color); + }); + + return graph; + } + getNode(id: BitId) { if (id.hasVersion()) { return this.graph.getNode(id.toString()); @@ -108,12 +146,16 @@ export default class VisualDependencyGraph { setNodeColor(node, this.config.highlightColor); } + private getTmpFilename() { + return path.join(os.tmpdir(), `${generateRandomStr()}.png`); + } + /** * Creates an image from the module dependency graph. * @param {String} imagePath * @return {Promise} */ - async image(imagePath: string): Promise { + async image(imagePath: string = this.getTmpFilename()): Promise { const options: Record = createGraphvizOptions(this.config); const type: string = path.extname(imagePath).replace('.', '') || 'png'; // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX!