/* Class to draw the revision graph. * * Changes Copyright (C) 2010, 2011 Darrell Karbott (see below). * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public * License as published by the Free Software Foundation; either * version 2.0 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks (changes) * * This file was developed as component of * "fniki" (a wiki implementation running over Freenet). * * This file is a derived work based on the graphlog.py * from rev:643b8212813e in the mercurial hg-stable repo. * * sha1sum graphlog.py * a0e599d4bd0b483025f0b5f182e737d8682a88ba graphlog.py * * Original header: * # ASCII graph log extension for Mercurial * # * # Copyright 2007 Joel Rosdahl <joel@rosdahl.net> * # * # This software may be used and distributed according to the terms of the * # GNU General Public License version 2 or any later version. * */ package fniki.wiki; import java.io.FileReader; import java.io.IOException; import java.io.LineNumberReader; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.Arrays; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; public class GraphLog { //////////////////////////////////////////////////////////// // A quick and dirty port of the first ~= 200 lines of // graphlog.py from the mercurial codebase. //////////////////////////////////////////////////////////// public static ColumnData asciiedges(List<Integer> seen, int rev, List<Integer> parents) { // """adds edge info to changelog DAG walk suitable for ascii()""" if (!seen.contains(rev)) { seen.add(rev); } int nodeidx = seen.indexOf(rev); // i.e. just looking up the index of the rev // seen maps revs -> ordinals List<Integer> known_parents = new ArrayList<Integer>(); List<Integer> new_parents = new ArrayList<Integer>(); for (int parent : parents) { if (seen.contains(parent)) { known_parents.add(parent); } else { new_parents.add(parent); } } int ncols = seen.size(); seen.remove(nodeidx); // Correct? trying to get python slice assignement semantics seen.addAll(nodeidx, new_parents); List<AsciiEdge> edges = new ArrayList<AsciiEdge>(); for (int parent : known_parents) { edges.add(new AsciiEdge(nodeidx, seen.indexOf(parent))); } if (new_parents.size() > 0) { edges.add(new AsciiEdge(nodeidx, nodeidx)); } if (new_parents.size() > 1) { edges.add(new AsciiEdge(nodeidx, nodeidx + 1)); } int nmorecols = seen.size() - ncols; String edgeDump = ""; for (AsciiEdge edge : edges) { edgeDump += String.format("(%d, %d), ", edge.mStart, edge.mEnd); } return new ColumnData(nodeidx, edges, ncols, nmorecols); } static void fix_long_right_edges(List<AsciiEdge> edges) { int index = 0; for (AsciiEdge edge : edges) { if (edge.mEnd > edge.mStart) { edges.set(index, new AsciiEdge(edge.mStart, edge.mEnd + 1)); } index++; } } static List<String> get_nodeline_edges_tail(int node_index, int p_node_index, int n_columns, int n_columns_diff, int p_diff, boolean fix_tail) { if (fix_tail && n_columns_diff == p_diff && n_columns_diff != 0) { //# Still going in the same non-vertical direction. if (n_columns_diff == -1) { int start = Math.max(node_index + 1, p_node_index); List<String> tail = repeat(BAR_SPACE, (start - node_index - 1)); tail.addAll(repeat(SLASH_SPACE, (n_columns - start))); return tail; } else { return repeat(BACKSLASH_SPACE, (n_columns - node_index - 1)); } } else { return repeat(BAR_SPACE, (n_columns - node_index - 1)); } } static void draw_edges(List<AsciiEdge> edges, List<String> nodeline, List<String> interline) { int index = 0; for (AsciiEdge edge : edges) { if (edge.mStart == edge.mEnd + 1) { interline.set(2 * edge.mEnd + 1, "/"); } else if (edge.mStart == edge.mEnd - 1) { interline.set(2 * edge.mStart + 1, "\\"); } else if (edge.mStart == edge.mEnd) { interline.set(2 * edge.mStart, "|"); } else { nodeline.set(2 * edge.mEnd, "+"); if (edge.mStart > edge.mEnd) { edges.set(index, new AsciiEdge(edge.mEnd, edge.mStart)); } for (int j = 2 * edge.mStart + 1; j < 2 * edge.mEnd; j++) { if (!nodeline.get(j).equals("+")) { nodeline.set(j, "-"); } } } index++; } } static List<String> get_padding_line(int ni, int n_columns, List<AsciiEdge> edges) { List<String> line = new ArrayList<String>(); line.addAll(repeat(BAR_SPACE, ni)); String c = null; //if (ni, ni - 1) in edges or (ni, ni) in edges: if (edges.contains(new AsciiEdge(ni, ni - 1)) || edges.contains(new AsciiEdge(ni, ni))) { //# (ni, ni - 1) (ni, ni) //# | | | | | | | | //# +---o | | o---+ //# | | c | | c | | //# | |/ / | |/ / //# | | | | | | c = "|"; } else { c = " "; } line.add(c); line.add(" "); line.addAll(repeat(BAR_SPACE, (n_columns - ni - 1))); return line; } public static AsciiState asciistate() { //"""returns the initial value for the "state" argument to ascii()""" return new AsciiState(0, 0); } static boolean should_add_padding_line(List<String> text, int coldiff, List<AsciiEdge> edges) { //add_padding_line = (len(text) > 2 and coldiff == -1 and // [x for (x, y) in edges if x + 1 < y]) if (!(text.size() > 2 && coldiff == -1)) { return false; } for (AsciiEdge edge : edges) { if (edge.mStart + 1 < edge.mEnd) { return true; } } return false; } // # Called once for each entry in the dag list public static void ascii(Writer ui, AsciiState state, /* type, */ String nodeChar, List<String> text, ColumnData coldata) throws IOException { //"""prints an ASCII graph of the DAG // //takes the following arguments (one call per node in the graph): //- ui to write to //- Somewhere to keep the needed state in (init to asciistate()) //- Column of the current node in the set of ongoing edges. //- Type indicator of node data == ASCIIDATA. //- Payload: (char, lines): //- Character to use as node's symbol. //- List of lines to display as the node's text. //- Edges; a list of (col, next_col) indicating the edges between // the current node and its parents. //- Number of columns (ongoing edges) in the current revision. //- The difference between the number of columns (ongoing edges) // in the next revision and the number of columns (ongoing edges) // in the current revision. That is: -1 means one column removed; // 0 means no columns added or removed; 1 means one column added. // """ int idx = coldata.mNodeIdx; List<AsciiEdge> edges = coldata.mEdges; int ncols = coldata.mNCols; int coldiff = coldata.mMoreCols; assert_(coldiff > -2 && coldiff < 2); if (coldiff == -1) { //# Transform //# //# | | | | | | //# o | | into o---+ //# |X / |/ / //# | | | | fix_long_right_edges(edges); } //# add_padding_line says whether to rewrite //# //# | | | | | | | | //# | o---+ into | o---+ //# | / / | | | # <--- padding line //# o | | | / / //# o | | //add_padding_line = (len(text) > 2 and coldiff == -1 and // [x for (x, y) in edges if x + 1 < y]) boolean add_padding_line = should_add_padding_line(text, coldiff, edges); //# fix_nodeline_tail says whether to rewrite //# //# | | o | | | | o | | //# | | |/ / | | |/ / //# | o | | into | o / / # <--- fixed nodeline tail //# | |/ / | |/ / //# o | | o | | boolean fix_nodeline_tail = (text.size() <= 2) && (!add_padding_line); // # nodeline is the line containing the node character (typically o) List<String> nodeline = repeat(BAR_SPACE, idx); nodeline.add(nodeChar); nodeline.add(" "); nodeline.addAll(get_nodeline_edges_tail(idx, state.mIdx, ncols, coldiff, state.mColDiff, fix_nodeline_tail)); //# shift_interline is the line containing the non-vertical //# edges between this entry and the next List<String> shift_interline = repeat(BAR_SPACE, idx); String edge_ch = null; int n_spaces = -1; if (coldiff == -1) { n_spaces = 1; edge_ch = "/"; } else if (coldiff == 0) { n_spaces = 2; edge_ch = "|"; } else { n_spaces = 3; edge_ch = "\\"; } shift_interline.addAll(repeat(ONE_SPACE, n_spaces)); List<String> valueToRepeat = new ArrayList<String>(); valueToRepeat.add(edge_ch); valueToRepeat.add(" "); shift_interline.addAll(repeat(valueToRepeat, (ncols - idx - 1))); //# draw edges from the current node to its parents draw_edges(edges, nodeline, shift_interline); //# lines is the list of all graph lines to print List<String> lines = new ArrayList<String>(Arrays.asList(list_to_string(nodeline))); if (add_padding_line) { lines.add(list_to_string(get_padding_line(idx, ncols, edges))); } lines.add(list_to_string(shift_interline)); //# make sure that there are as many graph lines as there are //# log strings while (text.size() < lines.size()) { text.add(""); } if (lines.size() < text.size()) { String extra_interline = list_to_string(repeat(BAR_SPACE, (ncols + coldiff))); while (lines.size() < text.size()) { lines.add(extra_interline); } } //# print lines int indentation_level = Math.max(ncols, ncols + coldiff); assert_(lines.size() == text.size()); int index = 0; for (String line : lines) { String logstr = text.get(index); index++; // "%-*s => means "left justify string with width specified in the tuple" // ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr) String fmt = "%-" + ("" + (2 * indentation_level)).trim() + "s %s"; ui.write(rstrip(String.format(fmt, line, logstr))); ui.write("\n"); } state.mColDiff = coldiff; state.mIdx = idx; } //////////////////////////////////////////////////////////// // Helper data structures and functions used to port // to Java. //////////////////////////////////////////////////////////// static final class AsciiEdge { public final int mStart; public final int mEnd; AsciiEdge(int start, int end) { mStart = start; mEnd = end; } } public static final class ColumnData { public final int mNodeIdx; public final int mNCols; public final List<AsciiEdge> mEdges; public final int mMoreCols; public ColumnData(int nodeidx, List<AsciiEdge> edges, int ncols, int nmorecols) { mNodeIdx = nodeidx; mEdges = edges; mNCols = ncols; mMoreCols = nmorecols; } } // NOT immutable. Hrmmm.. public static class AsciiState { public int mColDiff; public int mIdx; public AsciiState(int coldiff, int idx) { mColDiff = coldiff; mIdx = idx; } } // A directed graph node. public static class DAGNode { public final String mTag; public final int mId; public final List<Integer> mParentIds; DAGNode(String tag, int id, List<Integer> parentIds) { mTag = tag; mId = id; mParentIds = parentIds; } public String asString() { return "{" + mTag + ", " + mId + ", " + GraphLog.prettyString_(mParentIds) + "}"; } } final static List<String> BAR_SPACE = Collections.unmodifiableList(Arrays.asList("|", " ")); final static List<String> SLASH_SPACE = Collections.unmodifiableList(Arrays.asList("/", " ")); final static List<String> BACKSLASH_SPACE = Collections.unmodifiableList(Arrays.asList("\\", " ")); final static List<String> ONE_SPACE = Collections.unmodifiableList(Arrays.asList(" ")); static List<String> repeat(List<String> value, int count) { List<String> ret = new ArrayList<String>(); while (count > 0) { ret.addAll(value); count--; } return ret; } static void assert_(boolean condition) { if (condition) { return; } throw new RuntimeException("Assertion Failure!"); } // PARANOID: Can get rid of the explicit iteration later if this is too slow. static String list_to_string(List<String> values) { StringBuilder sb = new StringBuilder(); for (String singleChar : values) { assert_(singleChar.length() == 1); sb.append(singleChar); } return sb.toString(); } static String rstrip(String text) { if (text.length() == 0) { return ""; } int pos = text.length() - 1; while (pos >= 0 && text.charAt(pos) == ' ') { pos--; } if (pos == text.length() - 1) { return text; } return text.substring(0, pos + 1); } //////////////////////////////////////////////////////////// // New code written on top of the ported mercurial stuff. //////////////////////////////////////////////////////////// public static class GraphEdge { public final String mFrom; public final String mTo; public GraphEdge(String from, String to) { mFrom = from; mTo = to; } public boolean equals(Object obj) { if (obj == null || (!(obj instanceof GraphEdge))) { return false; } GraphEdge other = (GraphEdge)obj; return mFrom.equals(other.mFrom) && mTo.equals(other.mTo); } public int hashCode() { return (mFrom + mTo).hashCode(); } // Hmmm... fast enough? } static class ParentInfo { public final Set<String> mAllParents; public final Set<String> mUnseenParents; public final Set<String> mRootIds; public ParentInfo(Set<String> allParents, Set<String> unseenParents, Set<String> rootIds) { mAllParents = allParents; mUnseenParents = unseenParents; mRootIds = rootIds; } } static class OrdinalMapper { private final Map<String, Integer> mLut = new HashMap<String, Integer>(); private int mCount; public OrdinalMapper() {} public int ordinal(String for_value) { if (!mLut.keySet().contains(for_value)) { mLut.put(for_value, mCount); mCount++; } return mLut.get(for_value); } public void dump() { List<String> keys = new ArrayList<String>(mLut.keySet()); Collections.sort(keys); for (String key : keys) { System.err.println(String.format("%s -> %d", key, mLut.get(key))); } } } static Set<String> allDescendants(Map<String, Set<String>> graph, String leaf_id) { Set<String> seen = new HashSet<String>(); List<String> stack = new ArrayList<String>(); stack.add(leaf_id); while (stack.size() > 0) { String edge_id = stack.remove(stack.size() - 1); if (seen.contains(edge_id)) { continue; } seen.add(edge_id); if (!graph.keySet().contains(edge_id)) { continue; } for (String parent : graph.get(edge_id)) { stack.add(parent); } } return seen; // # Hmmmm... includes leaf_id } static List<Map<String, Set<String>>> find_connected_subgraphs(Map<String, Set<String>> graph, Set<String> root_ids, Set<String> leaf_node_ids) { List<Set<String>> subgraph_id_sets = new ArrayList<Set<String>>(); for (String leaf_id : leaf_node_ids) { subgraph_id_sets.add(allDescendants(graph, leaf_id)); } //# Coalesce subgraphs which have common root nodes for(String key : root_ids) { Set<String> coalesced = new HashSet<String>(); // # Deep copy the list so I can mutate while iterating List<Set<String>> copyForMutatingIteration = new ArrayList<Set<String>>(); copyForMutatingIteration.addAll(subgraph_id_sets); for (Set<String> subgraph_id_set : copyForMutatingIteration) { if (!subgraph_id_set.contains(key)) { continue; } coalesced.addAll(subgraph_id_set); subgraph_id_sets.remove(subgraph_id_set); } if (coalesced.size() > 0) { subgraph_id_sets.add(coalesced); } if (subgraph_id_sets.size() < 2) { break; //# Bail out if there is only one graph } } List<Map<String, Set<String>>> subgraphs = new ArrayList<Map<String, Set<String>>>(); for (Set<String> subgraph_id_set : subgraph_id_sets) { Map<String, Set<String>>copy = new HashMap<String, Set<String>>(); for (String id_value : subgraph_id_set) { if (graph.keySet().contains(id_value)) { // # handle unseen parents Set<String> deepCopy = new HashSet<String>(); deepCopy.addAll(graph.get(id_value)); copy.put(id_value, deepCopy); } } //dumpGraph("subgraph", copy); subgraphs.add(copy); } return subgraphs; } // graph is a map of node identifiers -> set of parent nodes. public static Map<String, Set<String>> buildRawGraph(List<GraphEdge> edges) { Map<String, Set<String>> graph = new HashMap<String, Set<String>>(); for (GraphEdge edge : edges) { Set<String> parents = graph.get(edge.mTo); if (parents == null) { parents = new HashSet<String> (); graph.put(edge.mTo, parents); } parents.add(edge.mFrom); } return graph; } // IMPORTANT: The "reversed" graph is what you need to traverse to build the DAG. // Take a graph which maps node ids -> set of parents and create a graph // which maps node ids -> set of children from it. static Map<String, Set<String>> get_reversed_graph(Map<String, Set<String>> graph, Set<String> root_ids, OrdinalMapper ordinals) { Map<String, Set<String>> reversed_graph = new HashMap<String, Set<String>>(); for(Map.Entry<String, Set<String>> entry : graph.entrySet()) { String key = entry.getKey(); Set<String> parents = entry.getValue(); for (String parent : parents) { if (!graph.keySet().contains(parent)) { continue; } if (!reversed_graph.keySet().contains(parent)) { reversed_graph.put(parent, new HashSet<String>()); } //# i.e. is a child of the parent reversed_graph.get(parent).add(key); } //# For leaf nodes. if (!reversed_graph.keySet().contains(key)) { reversed_graph.put(key, new HashSet<String>()); } } return reversed_graph; } public static ParentInfo getParentInfo(Map<String, Set<String>> graph) { Set<String> allParents = new HashSet<String>(); for (Set<String> parents : graph.values()) { allParents.addAll(parents); } Set<String> unseenParents = new HashSet<String>(); unseenParents.addAll(allParents); unseenParents.removeAll(graph.keySet()); Set<String> rootIds = new HashSet<String>(); for(Map.Entry<String, Set<String>> entry : graph.entrySet()) { Set<String> parents = entry.getValue(); if (parents == null || parents.size() == 0) { rootIds.add(entry.getKey()); continue; } assert_(parents.size() > 0); Set<String> unknownParents = new HashSet<String>(); unknownParents.addAll(parents); unknownParents.removeAll(unseenParents); // BUG: ??? Revisit. what if only some parents are unknown? //# All parents are unknown if (unknownParents.size() == 0) { rootIds.add(entry.getKey()); } } return new ParentInfo(allParents, unseenParents, rootIds); } // LATER: Grok harder. Possible to get rid of this? //# Breadth first traversal of the graph to force creation of ordinals //# in an order that won't freak out the drawing code. static void traverse_in_plausible_commit_order(Map<String, Set<String>> reversed_graph, Set<String> root_ids, OrdinalMapper ordinals) { LinkedList<String> queue = new LinkedList<String>(); queue.addAll(root_ids); Collections.sort(queue); //# Force creation of ordinals for root nodes for (String id_value : queue) { ordinals.ordinal(id_value); } Set<String> seen = new HashSet<String>(); while (queue.size() > 0) { String id_value = queue.removeFirst(); if (seen.contains(id_value)) { continue; } seen.add(id_value); // Tricky: Sort to force creation of new ordinals in "natural order" // of child ids. List<String> child_ids = new ArrayList<String>(); child_ids.addAll(reversed_graph.get(id_value)); Collections.sort(child_ids); for (String child_id : child_ids) { ordinals.ordinal(child_id); queue.add(child_id); } } } // Create a list of graph nodes in the correct order so that // they can be used to draw the graph with the ascii() function. static List<DAGNode> build_dag(Map<String, Set<String>> graph, String null_rev_name) { ParentInfo parentInfo = getParentInfo(graph); Set<String> all_parents = parentInfo.mAllParents; Set<String> unseen_parents = parentInfo.mUnseenParents; Set<String> root_ids = parentInfo.mRootIds; OrdinalMapper ordinals = new OrdinalMapper(); Map<String, Set<String>> reversed_graph = get_reversed_graph(graph, root_ids, ordinals); //# Important: This sets the graph ordinals correctly. traverse_in_plausible_commit_order(reversed_graph, root_ids, ordinals); int null_rev_ordinal = ordinals.ordinal(null_rev_name); LinkedList<String> queue = new LinkedList<String>(); queue.addAll(root_ids); Collections.sort(queue); List<DAGNode> dag = new ArrayList<DAGNode>(); Set<String> seen = new HashSet<String>(); while (queue.size() > 0) { String id_value = queue.removeFirst(); if (seen.contains(id_value)) { continue; } seen.add(id_value); List<Integer> parents = new ArrayList<Integer>(); for (String parent_id : graph.get(id_value)) { // This accesses ordinals in parent order, but the DAG requires them // to be in child order. That's why we need the call to // traverse_in_plausible_commit_order above. parents.add(ordinals.ordinal(parent_id)); } Collections.sort(parents); if (parents.size() == 1 && parents.get(0) == null_rev_ordinal) { // Causes ascii() not to draw a line down from the 'o'. parents = new ArrayList<Integer>(); } dag.add(new DAGNode(id_value, ordinals.ordinal(id_value), parents)); if (!reversed_graph.keySet().contains(id_value)) { continue; } // Tricky: Must traverse in order or the dag nodes won't be right. List<String> child_ids = new ArrayList<String>(); child_ids.addAll(reversed_graph.get(id_value)); Collections.sort(child_ids); for (String child_id : child_ids) { queue.add(child_id); } } Collections.reverse(dag); return dag; } // BUG: detect cylces? // REQUIRES: edges form an acyclic directed graph. public static List<List<DAGNode>> build_dags(List<GraphEdge> edges, String null_rev_name) { //""" Creates a list of DAG lists that can be printed with the ascii() // method, from an unordered list of edges. """ Map<String, Set<String>> graph = buildRawGraph(edges); ParentInfo parentInfo = getParentInfo(graph); Set<String> all_parents = parentInfo.mAllParents; Set<String> root_ids = parentInfo.mRootIds; Set<String> leaf_node_ids = new HashSet<String>(); leaf_node_ids.addAll(graph.keySet()); leaf_node_ids.removeAll(all_parents); List<Map<String, Set<String>>> subgraphs = find_connected_subgraphs(graph, root_ids, leaf_node_ids); List<List<DAGNode>> dags = new ArrayList<List<DAGNode>>(); for (Map<String, Set<String>> subgraph : subgraphs) { dags.add(build_dag(subgraph, null_rev_name)); } return dags; } private final static String prettyString(Set<String> set) { List<String> list = new ArrayList<String>(); list.addAll(set); Collections.sort(list); return prettyString(list); } private final static String prettyString(List<String> list) { String ret = "{"; for (String value : list) { ret += value + ", "; } ret = ret.trim() + "}"; return ret; } // GRRRR... Colliding type erasure. Java is a poor man's C++ :-( static String prettyString_(List<Integer> values) { List<String> list = new ArrayList<String>(); for (Integer value : values) { list.add(value.toString()); } return prettyString(list); } private final static void dumpGraph(String msg, Map<String, Set<String>> graph) { System.err.println("--- graph: " + msg); List<String> keys = new ArrayList<String>(); keys.addAll(graph.keySet()); Collections.sort(keys); for (String key : keys) { System.err.println(String.format(" [%s] -> [%s]", key, prettyString(graph.get(key)))); } } // File format: // <child_rev> <parent_rev> // one entry per line, lines starting with # and // are ignored. private final static List<GraphEdge> readEdgesFromFile(String fileName) throws IOException { List<GraphEdge> list = new ArrayList<GraphEdge>(); LineNumberReader reader = new LineNumberReader(new FileReader(fileName)); String line = reader.readLine(); while(line != null) { if (line.trim().equals("") || line.startsWith("#") || line.startsWith("//")) { line = reader.readLine(); continue; } String[] fields = line.trim().split(" "); list.add(new GraphEdge(fields[1], fields[0])); line = reader.readLine(); } return list; } public static void test_dumping_graph(List<DAGNode> dag) throws IOException { List<Integer> seen = new ArrayList<Integer>(); AsciiState state = asciistate(); Writer out = new OutputStreamWriter(System.out); for (DAGNode value : dag) { List<String> lines = new ArrayList<String>(); lines.add(String.format("id: %s (%d)", value.mTag, value.mId)); ascii(out, state, "o", lines, asciiedges(seen, value.mId, value.mParentIds)); } out.flush(); } public final static void main(String[] args) throws Exception { System.out.println(String.format("Reading edges from: %s", args[0])); List<GraphEdge> edges = readEdgesFromFile(args[0]); List<List<DAGNode>> dags = build_dags(edges, "null_rev"); for (List<DAGNode> dag : dags) { System.out.println("--- dag ---"); test_dumping_graph(dag); } } }