aboutsummaryrefslogtreecommitdiffstats
path: root/git_deps/html/js
diff options
context:
space:
mode:
Diffstat (limited to 'git_deps/html/js')
-rw-r--r--git_deps/html/js/.gitignore1
-rw-r--r--git_deps/html/js/fullscreen.js48
-rw-r--r--git_deps/html/js/git-deps-data.coffee108
-rw-r--r--git_deps/html/js/git-deps-graph.coffee595
-rw-r--r--git_deps/html/js/git-deps-layout.coffee253
-rw-r--r--git_deps/html/js/git-deps-noty.coffee32
6 files changed, 1037 insertions, 0 deletions
diff --git a/git_deps/html/js/.gitignore b/git_deps/html/js/.gitignore
new file mode 100644
index 0000000..0e804e3
--- /dev/null
+++ b/git_deps/html/js/.gitignore
@@ -0,0 +1 @@
+bundle.js
diff --git a/git_deps/html/js/fullscreen.js b/git_deps/html/js/fullscreen.js
new file mode 100644
index 0000000..6d8f3d8
--- /dev/null
+++ b/git_deps/html/js/fullscreen.js
@@ -0,0 +1,48 @@
+
+function endFullScreen(oncancel) {
+ if (!RunPrefixMethod(document, "FullScreen") && !RunPrefixMethod(document, "IsFullScreen")) {
+ oncancel();
+ }
+}
+function fullScreen(e, oncancel) {
+ if (RunPrefixMethod(document, "FullScreen") || RunPrefixMethod(document, "IsFullScreen")) {
+ RunPrefixMethod(document, "CancelFullScreen");
+ }
+ else {
+ RunPrefixMethod(e, "RequestFullScreen");
+ e.setAttribute("width", screen.width);
+ e.setAttribute("height", screen.height);
+ }
+ if (arguments.length > 1) {
+ var f = function () { endFullScreen(oncancel); };
+ document.addEventListener("fullscreenchange", f, false);
+ document.addEventListener("mozfullscreenchange", f, false);
+ document.addEventListener("webkitfullscreenchange", f, false);
+ }
+}
+
+var pfx = ["webkit", "moz", "ms", "o", ""];
+function RunPrefixMethod(obj, method) {
+
+ var p = 0, m, t;
+ while (p < pfx.length && !obj[m]) {
+ m = method;
+ if (pfx[p] == "") {
+ m = m.substr(0, 1).toLowerCase() + m.substr(1);
+ }
+ m = pfx[p] + m;
+ t = typeof obj[m];
+ if (t != "undefined") {
+ pfx = [pfx[p]];
+ return (t == "function" ? obj[m]() : obj[m]);
+ }
+ p++;
+ }
+}
+
+function isFullScreen() {
+ var fullscreenEnabled = document.fullscreenEnabled || document.mozFullScreenEnabled || document.webkitFullscreenEnabled;
+ return fullscreenEnabled;
+}
+
+module.exports = fullScreen;
diff --git a/git_deps/html/js/git-deps-data.coffee b/git_deps/html/js/git-deps-data.coffee
new file mode 100644
index 0000000..34715a3
--- /dev/null
+++ b/git_deps/html/js/git-deps-data.coffee
@@ -0,0 +1,108 @@
+# The list of nodes and links to feed into WebCola.
+# These will be dynamically built as we retrieve them via XHR.
+nodes = []
+links = []
+
+# WebCola requires links to refer to nodes by index within the
+# nodes array, so as nodes are dynamically added, we need to
+# be able to retrieve their index efficiently in order to add
+# links to/from them. This also allows us to avoid adding the
+# same node twice.
+node_index = {}
+
+# Track dependencies in a hash of hashes which maps parents to
+# children to booleans. Constraints will be added to try to keep
+# siblings at the same y position. For this we need to track
+# siblings, which we do by mapping each parent to an array of its
+# siblings in this hash. It also enables us to deduplicate links
+# across multiple XHRs.
+deps = {}
+
+# Track dependences in reverse in a hash of hashes which maps children
+# to parents to booleans. This allows us to highlight parents when
+# the mouse hovers over a child, and know when we can safely remove
+# a commit due to its sole parent being deleted.
+rdeps = {}
+
+# Returns 1 iff a node was added, otherwise 0.
+add_node = (commit) ->
+ if commit.sha1 of node_index
+ n = node commit.sha1
+ n.explored ||= commit.explored
+ return 0
+
+ nodes.push commit
+ node_index[commit.sha1] = nodes.length - 1
+ return 1
+
+# Returns 1 iff a dependency was added, otherwise 0.
+add_dependency = (parent_sha1, child_sha1) ->
+ deps[parent_sha1] = {} unless parent_sha1 of deps
+
+ # We've already got this link, presumably
+ # from a previous XHR.
+ return 0 if child_sha1 of deps[parent_sha1]
+ deps[parent_sha1][child_sha1] = true
+ add_link parent_sha1, child_sha1
+ return 1
+
+# Returns 1 iff a reverse dependency was added, otherwise 0.
+add_rev_dependency = (child_sha1, parent_sha1) ->
+ rdeps[child_sha1] = {} unless child_sha1 of rdeps
+
+ # We've already got this link, presumably
+ # from a previous XHR.
+ return 0 if parent_sha1 of rdeps[child_sha1]
+ rdeps[child_sha1][parent_sha1] = true
+ return 1
+
+add_link = (parent_sha1, child_sha1) ->
+ pi = node_index[parent_sha1]
+ ci = node_index[child_sha1]
+ link =
+ source: pi
+ target: ci
+ value: 1 # no idea what WebCola needs this for
+
+ links.push link
+ return
+
+# Returns true iff new data was added.
+add_data = (data) ->
+ new_nodes = 0
+ new_deps = 0
+ for commit in data.commits
+ new_nodes += add_node(commit)
+
+ for dep in data.dependencies
+ new_deps += add_dependency(dep.parent, dep.child)
+ add_rev_dependency(dep.child, dep.parent)
+
+ if new_nodes > 0 or new_deps > 0
+ return [
+ new_nodes
+ new_deps
+ data.query
+ ]
+
+ return false
+
+node = (sha1) ->
+ i = node_index[sha1]
+ unless i?
+ console.error "No index for SHA1 '#{sha1}'"
+ return null
+ return nodes[i]
+
+module.exports =
+ # Variables (N.B. if these variables are reinitialised at any
+ # point, the values here will become stale and require updating)
+ nodes: nodes
+ links: links
+ node_index: node_index
+ deps: deps
+ rdeps: rdeps
+
+ # Functions
+ add: add_data
+ node: node
diff --git a/git_deps/html/js/git-deps-graph.coffee b/git_deps/html/js/git-deps-graph.coffee
new file mode 100644
index 0000000..7ad6827
--- /dev/null
+++ b/git_deps/html/js/git-deps-graph.coffee
@@ -0,0 +1,595 @@
+jQuery = require "jquery"
+$ = jQuery
+d3 = require "d3"
+d3tip = require "d3-tip"
+d3tip d3
+
+# Hacky workaround:
+# https://github.com/tgdwyer/WebCola/issues/145#issuecomment-271316856
+window.d3 = d3
+
+cola = require "webcola"
+
+global.gdn = require "./git-deps-noty.coffee"
+global.gdd = require "./git-deps-data.coffee"
+global.gdl = require "./git-deps-layout.coffee"
+
+fullScreen = require "./fullscreen"
+
+SVG_MARGIN = 2 # space around <svg>, matching #svg-container border
+RECT_MARGIN = 14 # space in between <rects>
+PADDING = 5 # space in between <text> label and <rect> border
+EDGE_ROUTING_MARGIN = 3
+PLUS_ICON_WIDTH = 14
+
+svg_width = 960
+svg_height = 800
+old_svg_height = undefined
+old_svg_width = undefined
+
+color = d3.scale.category20()
+
+global.d3cola = cola.d3adaptor()
+d3cola
+ .flowLayout("y", 100)
+ .avoidOverlaps(true)
+ #.linkDistance(60)
+ #.symmetricDiffLinkLengths(30)
+ #.jaccardLinkLengths(100)
+
+# d3 visualization elements
+container = undefined
+svg = undefined
+fg = undefined
+nodes = undefined
+paths = undefined
+tip = undefined
+tip_template = undefined
+zoom = undefined
+
+options = undefined # Options will be retrieved from web server
+
+jQuery ->
+ d3.json "options", (error, data) ->
+ options = data
+ gdl.debug = options.debug
+
+ d3.html "tip-template.html", (error, html) ->
+ tip_template = html
+
+ #setup_default_form_values();
+ $("form.commitish").submit (event) ->
+ event.preventDefault()
+ add_commitish $(".commitish input").val()
+
+ init_svg()
+
+setup_default_form_values = ->
+ $("input[type=text]").each(->
+ $(this).val $(this).attr("defaultValue")
+ $(this).css color: "grey"
+ ).focus(->
+ if $(this).val() is $(this).attr("defaultValue")
+ $(this).val ""
+ $(this).css color: "black"
+ ).blur ->
+ if $(this).val() is ""
+ $(this).val $(this).attr("defaultValue")
+ $(this).css color: "grey"
+
+resize_window = ->
+ calculate_svg_size_from_container()
+ fit_svg_to_container()
+ redraw true
+
+redraw = (transition) ->
+ # if mouse down then we are dragging not panning
+ # if nodeMouseDown
+ # return
+ ((if transition then fg.transition() else fg))
+ .attr "transform",
+ "translate(#{zoom.translate()}) scale(#{zoom.scale()})"
+
+graph_bounds = ->
+ x = Number.POSITIVE_INFINITY
+ X = Number.NEGATIVE_INFINITY
+ y = Number.POSITIVE_INFINITY
+ Y = Number.NEGATIVE_INFINITY
+ fg.selectAll(".node").each (d) ->
+ x = Math.min(x, d.x - d.width / 2)
+ y = Math.min(y, d.y - d.height / 2)
+ X = Math.max(X, d.x + d.width / 2)
+ Y = Math.max(Y, d.y + d.height / 2)
+ return {} =
+ x: x
+ X: X
+ y: y
+ Y: Y
+
+fit_svg_to_container = ->
+ svg.attr("width", svg_width).attr("height", svg_height)
+
+full_screen_cancel = ->
+ svg_width = old_svg_width
+ svg_height = old_svg_height
+ fit_svg_to_container()
+ #zoom_to_fit();
+ resize_window()
+
+full_screen_click = ->
+ fullScreen container.node(), full_screen_cancel
+ fit_svg_to_container()
+ resize_window()
+ #zoom_to_fit();
+ return false
+
+zoom_to_fit = ->
+ b = graph_bounds()
+ w = b.X - b.x
+ h = b.Y - b.y
+ cw = svg.attr("width")
+ ch = svg.attr("height")
+ s = Math.min(cw / w, ch / h)
+ tx = -b.x * s + (cw / s - w) * s / 2
+ ty = -b.y * s + (ch / s - h) * s / 2
+ zoom.translate([tx, ty]).scale s
+ redraw true
+ return false
+
+window.full_screen_click = full_screen_click
+window.zoom_to_fit = zoom_to_fit
+
+add_commitish = (commitish) ->
+ tip.hide() if tip?
+ draw_graph commitish
+
+calculate_svg_size_from_container = ->
+ old_svg_width = svg_width
+ old_svg_height = svg_height
+ svg_width = container.node().offsetWidth - SVG_MARGIN
+ svg_height = container.node().offsetHeight - SVG_MARGIN
+
+init_svg = ->
+ container = d3.select("#svg-container")
+ calculate_svg_size_from_container()
+ svg = container.append("svg")
+ .attr("width", svg_width)
+ .attr("height", svg_height)
+ d3cola.size [svg_width, svg_height]
+
+ d3.select(window).on "resize", resize_window
+
+ zoom = d3.behavior.zoom()
+
+ svg.append("rect")
+ .attr("class", "background")
+ .attr("width", "100%")
+ .attr("height", "100%")
+ .call(zoom.on("zoom", redraw))
+ .on("dblclick.zoom", zoom_to_fit)
+
+ fg = svg.append("g")
+ svg_defs fg
+
+update_cola = ->
+ d3cola
+ .nodes(gdd.nodes)
+ .links(gdd.links)
+ .constraints(gdl.constraints)
+
+draw_graph = (commitish) ->
+ d3.json "deps.json/" + commitish, (error, data) ->
+ if error
+ details = JSON.parse(error.responseText)
+ gdn.error details.message
+ return
+
+ new_data = gdd.add(data)
+
+ unless new_data
+ gdn.warn "No new commits or dependencies found!"
+ update_rect_explored()
+ return
+ new_data_notification new_data
+ focus_commitish_input()
+
+ gdl.build_constraints()
+ update_cola()
+
+ paths = fg.selectAll(".link")
+ .data(gdd.links, link_key)
+ paths.enter().append("svg:path")
+ .attr("class", "link")
+ .attr("stroke", (d) -> color(link_key(d)))
+ nodes = fg.selectAll(".node")
+ .data(gdd.nodes, (d) -> d.sha1)
+ global.nodes = nodes
+
+ g_enter = nodes.enter().append("g")
+ .attr("class", "node")
+ # Questionable attempt to use dagre layout as starting positions
+ # https://github.com/tgdwyer/WebCola/issues/63
+ nodes.each (d, i) ->
+ n = gdl.node d.sha1
+ d.x = n.x
+ d.y = n.y
+ nodes.attr "transform", (d) ->
+ translate d.x, d.y
+
+ # N.B. has to be done on the update selection, i.e. *after* the enter!
+ nodes.call(d3cola.drag)
+
+ init_tip() unless tip?
+ # Event handlers need to be updated every time new nodes are added.
+ init_tip_event_handlers(nodes)
+
+ [rects, labels] = draw_new_nodes fg, g_enter
+ position_nodes(rects, labels)
+ update_rect_explored()
+
+focus_commitish_input = () ->
+ d3.select('.commitish input').node().focus()
+
+# Required for object constancy: http://bost.ocks.org/mike/constancy/ ...
+link_key = (link) ->
+ source = sha1_of_link_pointer(link.source)
+ target = sha1_of_link_pointer(link.target)
+ return source + " " + target
+
+# ... but even though link sources and targets are initially fed in
+# as indices into the nodes array, webcola then replaces the indices
+# with references to the node objects. So we have to deal with both
+# cases when ensuring we are uniquely identifying each link.
+sha1_of_link_pointer = (pointer) ->
+ return pointer.sha1 if typeof (pointer) is "object"
+ return gdd.nodes[pointer].sha1
+
+init_tip = () ->
+ tip = d3.tip().attr("class", "d3-tip").html(tip_html)
+ global.tip = tip
+ fg.call tip
+
+# A wrapper around tip.show is required to perform multiple visual
+# actions when the mouse hovers over a node; however even if the only
+# action required was to show the tool tip, the wrapper would still be
+# required in order to work around something which looks like a bug in
+# d3 or d3-tip. tip.show is defined as:
+#
+# function() {
+# var args = Array.prototype.slice.call(arguments)
+# if(args[args.length - 1] instanceof SVGElement) target = args.pop()
+# ...
+#
+# and there's also:
+#
+# function getScreenBBox() {
+# var targetel = target || d3.event.target;
+# ...
+#
+# which I'm guessing normally uses d3.event.target. However for some
+# reason when using tip.show as the dragend handler, d3.event.target
+# points to a function rather than the expected DOM element, which
+# appears to be exactly the same problem described here:
+#
+# http://stackoverflow.com/questions/12934731/d3-event-targets
+#
+# However I tried rects.call ... instead of nodes.call as suggested in
+# that SO article, but it resulted in the callback not being triggered
+# at all. By *always* providing the exact SVGElement the tip is
+# supposed to target, the desired behaviour is obtained. If
+# node_mouseover is only used in tip_dragend_handler then the target
+# gets memoised, and a normal hover-based tip.show shows the target
+# last shown by a drag, rather than the node being hovered over.
+# Weird, and annoying.
+node_mouseover = (d, i) ->
+ tip.show d, i, nodes[0][i]
+ highlight_nodes d3.select(nodes[0][i]), false
+ highlight_parents(d, i, true)
+ highlight_children(d, i, true)
+
+node_mouseout = (d, i) ->
+ tip.hide d, i, nodes[0][i]
+ highlight_nodes d3.select(nodes[0][i]), false
+ highlight_parents(d, i, false)
+ highlight_children(d, i, false)
+
+highlight_parents = (d, i, highlight) ->
+ sha1 = gdd.nodes[i].sha1
+ parents = nodes.filter (d, i) ->
+ d.sha1 of (gdd.rdeps[sha1] || {})
+ highlight_nodes parents, highlight, 'rgb(74, 200, 148)'
+
+highlight_children = (d, i, highlight) ->
+ sha1 = gdd.nodes[i].sha1
+ children = nodes.filter (d, i) ->
+ d.sha1 of (gdd.deps[sha1] || {})
+ highlight_nodes children, highlight, 'rgb(128, 197, 247)'
+
+highlight_nodes = (selection, highlight, colour='#c0c0c0') ->
+ selection.selectAll('rect')
+ .transition()
+ .ease('cubic-out')
+ .duration(200)
+ .style('stroke', if highlight then colour else '#e5e5e5')
+ .style('stroke-width', if highlight then '4px' else '2px')
+
+tip_dragend_handler = (d, i, elt) ->
+ focus_commitish_input()
+ node_mouseover d, i
+
+init_tip_event_handlers = (selection) ->
+ # We have to reuse the same drag object, otherwise only one
+ # of the event handlers will work.
+ drag = d3cola.drag()
+ hide_tip_on_drag = drag.on("drag", tip.hide)
+ on_dragend = drag.on("dragend", tip_dragend_handler)
+ selection.call hide_tip_on_drag
+ selection.call on_dragend
+
+draw_new_nodes = (fg, g_enter) ->
+ rects = g_enter.append('rect')
+ .attr('rx', 5)
+ .attr('ry', 5)
+ .on('dblclick', (d) -> launch_viewer d)
+
+ labels = g_enter.append('text').text((d) ->
+ d.name
+ ).each((d) ->
+ b = @getBBox()
+
+ # Calculate width/height of rectangle from text bounding box.
+ d.rect_width = b.width + 2 * PADDING
+ d.rect_height = b.height + 2 * PADDING
+
+ # Now set the node width/height as used by cola for
+ # positioning. This has to include the margin
+ # outside the rectangle.
+ d.width = d.rect_width + 2 * RECT_MARGIN
+ d.height = d.rect_height + 2 * RECT_MARGIN
+ )
+
+ return [rects, labels]
+
+explore_node = (d) ->
+ if d.explored
+ gdn.warn "Commit #{d.name} already explored"
+ else
+ add_commitish d.sha1
+
+launch_viewer = (d) ->
+ window.location.assign "gitfile://#{options.repo_path}##{d.sha1}"
+
+new_data_notification = (new_data) ->
+ new_nodes = new_data[0]
+ new_deps = new_data[1]
+ query = new_data[2]
+ notification =
+ if query.revspec == query.tip_sha1
+ "Analysed dependencies of #{query.revspec}"
+ else if query.revisions.length == 1
+ "<span class=\"commit-ref\">#{query.revspec}</span>
+ resolved as #{query.tip_abbrev}"
+ else
+ "<span class=\"commit-ref\">#{query.revspec}</span>
+ expanded; tip is #{query.tip_abbrev}"
+ notification += "<p>#{new_nodes} new commit"
+ notification += "s" unless new_nodes == 1
+ notification += "; #{new_deps} new " +
+ (if new_deps == 1 then "dependency" else "dependencies")
+ notification += "</p>"
+
+ gdn.success notification
+
+svg_defs = () ->
+ # define arrow markers for graph links
+ defs = svg.insert("svg:defs")
+
+ defs.append("svg:marker")
+ .attr("id", "end-arrow")
+ .attr("viewBox", "0 -5 10 10")
+ .attr("refX", 6)
+ .attr("markerWidth", 6)
+ .attr("markerHeight", 6)
+ .attr("orient", "auto")
+ .append("svg:path")
+ .attr("d", "M0,-5L10,0L0,5")
+ .attr("fill", "#000")
+
+ plus_icon = defs.append("svg:symbol")
+ .attr("id", "plus-icon")
+ .attr("viewBox", "-51 -51 102 102") # allow for stroke-width 1
+ # border
+ plus_icon.append("svg:rect")
+ .attr("width", 100)
+ .attr("height", 100)
+ .attr("fill", "#295b8c")
+ .attr("stroke", "rgb(106, 136, 200)")
+ .attr("x", -50)
+ .attr("y", -50)
+ .attr("rx", 20)
+ .attr("ry", 20)
+ # plus sign
+ plus_icon.append("svg:path")
+ .attr("d", "M-30,0 H30 M0,-30 V30")
+ .attr("stroke", "white")
+ .attr("stroke-width", 10)
+ .attr("stroke-linecap", "round")
+
+ # Uncomment to see a large version:
+ # fg.append("use")
+ # .attr("class", "plus-icon")
+ # .attr("xlink:href", "#plus-icon")
+ # .attr("width", "200")
+ # .attr("height", "200")
+ # .attr("x", 400)
+ # .attr("y", 200)
+
+position_nodes = (rects, labels) ->
+ rects
+ .attr("width", (d, i) -> d.rect_width)
+ .attr("height", (d, i) -> d.rect_height)
+ .on("mouseover", node_mouseover)
+ .on("mouseout", node_mouseout)
+
+ # Centre labels
+ labels
+ .attr("x", (d) -> d.rect_width / 2)
+ .attr("y", (d) -> d.rect_height / 2)
+ .on("mouseover", node_mouseover)
+ .on("mouseout", node_mouseout)
+
+ d3cola.start 10, 20, 20
+ d3cola.on "tick", tick_handler
+
+ # d3cola.on "end", routeEdges
+
+ # turn on overlap avoidance after first convergence
+ # d3cola.on("end", () ->
+ # unless d3cola.avoidOverlaps
+ # gdd.nodes.forEach((v) ->
+ # v.width = v.height = 10
+ # d3cola.avoidOverlaps true
+ # d3cola.start
+
+update_rect_explored = () ->
+ d3.selectAll(".node rect").attr "class", (d) ->
+ if d.explored then "explored" else "unexplored"
+ nodes.each (d) ->
+ existing_icon = d3.select(this).select("use.plus-icon")
+ if d.explored
+ existing_icon.remove()
+ else if existing_icon.empty()
+ add_plus_icon this
+
+add_plus_icon = (node_element) ->
+ n = d3.select(node_element)
+ rw = node_element.__data__.rect_width
+ rh = node_element.__data__.rect_height
+
+ icon = n.insert('use')
+ .attr('class', 'plus-icon')
+ .attr('xlink:href', '#plus-icon')
+ .attr('x', rw/2)
+ .attr('y', rh - PLUS_ICON_WIDTH/2)
+ .attr('width', 0)
+ .attr('height', 0)
+ icon
+ .on('mouseover', (d, i) -> icon_ease_in icon, rw)
+ .on('mouseout', (d, i) -> icon_ease_out icon, rw)
+ .on('click', (d) -> explore_node d)
+
+ n
+ .on('mouseover', (d, i) -> icon_ease_in icon, rw)
+ .on('mouseout', (d, i) -> icon_ease_out icon, rw)
+
+icon_ease_in = (icon, rw) ->
+ icon.transition()
+ .ease('cubic-out')
+ .duration(200)
+ .attr('width', PLUS_ICON_WIDTH)
+ .attr('height', PLUS_ICON_WIDTH)
+ .attr('x', rw/2 - PLUS_ICON_WIDTH/2)
+
+icon_ease_out = (icon, rw) ->
+ icon.transition()
+ .attr(rw/2 - PLUS_ICON_WIDTH/2)
+ .ease('cubic-out')
+ .duration(200)
+ .attr('width', 0)
+ .attr('height', 0)
+ .attr('x', rw/2)
+
+tip_html = (d) ->
+ fragment = $(tip_template).clone()
+ top = fragment.find("#fragment")
+ title = top.find("p.commit-title")
+ title.text d.title
+
+ if d.refs
+ title.append " <span />"
+ refs = title.children().first()
+ refs.addClass("commit-describe commit-ref")
+ .text(d.refs.join(" "))
+
+ top.find("span.commit-author").text(d.author_name)
+ date = new Date(d.author_time * 1000)
+ top.find("time.commit-time")
+ .attr("datetime", date.toISOString())
+ .text(date)
+ pre = top.find(".commit-body pre").text(d.body)
+
+ if options.debug
+ # deps = gdd.deps[d.sha1]
+ # if deps
+ # sha1s = [gdd.node(sha1).name for name, bool of deps]
+ # top.append("<br />Dependencies: " + sha1s.join(", "));
+ index = gdd.node_index[d.sha1]
+ debug = "<br />node index: " + index
+ dagre_node = gdl.graph.node(d.sha1)
+ debug += "<br />dagre: (#{dagre_node.x}, #{dagre_node.y})"
+ top.append debug
+
+ # Javascript *sucks*. There's no way to get the outerHTML of a
+ # document fragment, so you have to wrap the whole thing in a
+ # single parent and then look that up via children[0].
+ return fragment[0].children[0].outerHTML
+
+translate = (x, y) ->
+ "translate(#{x},#{y})"
+
+tick_handler = ->
+ nodes.each (d) ->
+ # cola sets the bounds property which is a Rectangle
+ # representing the space which other nodes should not
+ # overlap. The innerBounds property seems to tell
+ # cola the Rectangle which is the visible part of the
+ # node, minus any blank margin.
+ d.innerBounds = d.bounds.inflate(-RECT_MARGIN)
+
+ nodes.attr "transform", (d) ->
+ translate d.innerBounds.x, d.innerBounds.y
+
+ paths.each (d) ->
+ @parentNode.insertBefore this, this if isIE()
+
+ paths.attr "d", (d) ->
+ # Undocumented: https://github.com/tgdwyer/WebCola/issues/52
+ route = cola.makeEdgeBetween \
+ d.source.innerBounds,
+ d.target.innerBounds,
+ # This value is related to but not equal to the
+ # distance of arrow tip from object it points at:
+ 5
+
+ lineData = [
+ {x: route.sourceIntersection.x, y: route.sourceIntersection.y},
+ {x: route.arrowStart.x, y: route.arrowStart.y}
+ ]
+ return lineFunction lineData
+
+lineFunction = d3.svg.line()
+ .x((d) -> d.x)
+ .y((d) -> d.y)
+ .interpolate("linear")
+
+routeEdges = ->
+ d3cola.prepareEdgeRouting EDGE_ROUTING_MARGIN
+ paths.attr "d", (d) ->
+ lineFunction d3cola.routeEdge(d)
+ # show visibility graph
+ # (g) ->
+ # if d.source.id == 10 and d.target.id === 11
+ # g.E.forEach (e) =>
+ # vis.append("line").attr("x1", e.source.p.x).attr("y1", e.source.p.y)
+ # .attr("x2", e.target.p.x).attr("y2", e.target.p.y)
+ # .attr("stroke", "green")
+
+ if isIE()
+ paths.each (d) ->
+ @parentNode.insertBefore this, this
+
+isIE = ->
+ (navigator.appName is "Microsoft Internet Explorer") or
+ ((navigator.appName is "Netscape") and
+ ((new RegExp "Trident/.*rv:([0-9]{1,}[.0-9]{0,})")
+ .exec(navigator.userAgent)?))
diff --git a/git_deps/html/js/git-deps-layout.coffee b/git_deps/html/js/git-deps-layout.coffee
new file mode 100644
index 0000000..8b8cd05
--- /dev/null
+++ b/git_deps/html/js/git-deps-layout.coffee
@@ -0,0 +1,253 @@
+DEBUG = false
+
+MIN_ROW_GAP = 60
+MIN_NODE_X_GAP = 100 # presumably includes the node width
+MAX_NODE_X_GAP = 300
+MAX_NODE_Y_GAP = 80
+
+dagre = require "dagre"
+
+gdd = require "./git-deps-data.coffee"
+
+# The list of constraints to feed into WebCola.
+constraints = []
+
+# Group nodes by row, as assigned by the y coordinates returned from
+# dagre's layout(). This will map a y coordinate onto all nodes
+# within that row.
+row_groups = {}
+
+debug = (msg) ->
+ if exports.debug
+ console.log msg
+
+dagre_layout = ->
+ g = new dagre.graphlib.Graph()
+ exports.graph = g
+
+ # Set an object for the graph label
+ g.setGraph {}
+
+ # Default to assigning a new object as a label for each new edge.
+ g.setDefaultEdgeLabel -> {}
+
+ for node in gdd.nodes
+ g.setNode node.sha1,
+ label: node.name
+ width: node.rect_width or 70
+ height: node.rect_height or 30
+
+ for parent_sha1, children of gdd.deps
+ for child_sha1, bool of children
+ g.setEdge parent_sha1, child_sha1
+
+ dagre.layout g
+ return g
+
+dagre_row_groups = ->
+ g = dagre_layout()
+ row_groups = {}
+ exports.row_groups = row_groups
+ for sha1 in g.nodes()
+ x = g.node(sha1).x
+ y = g.node(sha1).y
+ row_groups[y] = [] unless y of row_groups
+ row_groups[y].push
+ sha1: sha1
+ x: x
+
+ for y, nodes of row_groups
+ nodes.sort (n) -> -n.x
+
+ return row_groups
+
+build_constraints = ->
+ row_groups = dagre_row_groups()
+ debug "build_constraints"
+ for y, row_nodes of row_groups
+ debug y
+ debug row_nodes
+
+ constraints.length = 0 # FIXME: only rebuild constraints which changed
+
+ # We want alignment constraints between all nodes which dagre
+ # assigned the same y value.
+ #row_alignment_constraints(row_groups)
+
+ # We need separation constraints ensuring that the left-to-right
+ # ordering within each row assigned by dagre is preserved.
+ for y, row_nodes of row_groups
+ # No point having an alignment group with only one node in.
+ continue if row_nodes.length <= 1
+
+ # Multiple constraints per row.
+ debug "ordering for row y=#{y}"
+ row_node_ordering_constraints(row_nodes)
+ debug_constraints()
+
+ # We need separation constraints ensuring that the top-to-bottom
+ # ordering assigned by dagre is preserved. Since all nodes within
+ # a single row are already constrained to the same y coordinate
+ # from above, one would have hoped it would be enough to only have
+ # separation between a single node in adjacent rows:
+ #
+ # row_ordering_constraints(row_groups)
+
+ # However, due to https://github.com/tgdwyer/WebCola/issues/61
+ # there is more flexibility for y-coordinates within a row than we
+ # want, so instead we order rows using dependencies.
+ dependency_ordering_constraints()
+
+debug_constraints = (cs = constraints) ->
+ for c in cs
+ debug c
+ return
+
+row_alignment_constraints = (row_groups) ->
+ row_alignment_constraint(row_nodes) \
+ for y, row_nodes of row_groups when row_nodes.length > 1
+
+row_alignment_constraint = (row_nodes) ->
+ debug 'row_alignment_constraint'
+ # A standard alignment constraint (one per row) is too strict
+ # because it doesn't give cola enough "wiggle room":
+ #
+ # constraint =
+ # axis: "y"
+ # type: "alignment"
+ # offsets: []
+ #
+ # for node in row_nodes
+ # constraint.offsets.push
+ # node: gdd.node_index[node.sha1],
+ # offset: 0
+ #
+ # constraints.push constraint
+ #
+ # So instead we use vertical min/max separation constraints:
+ i = 0
+ while i < row_nodes.length - 1
+ left = row_nodes[i]
+ right = row_nodes[i+1]
+ mm = max_unordered_separation_constraints \
+ 'y', MAX_NODE_Y_GAP,
+ gdd.node_index[left.sha1],
+ gdd.node_index[right.sha1]
+ exports.constraints = constraints = constraints.concat mm
+ i++
+ debug_constraints()
+ return
+
+row_node_ordering_constraints = (row_nodes) ->
+ debug 'row_node_ordering_constraints'
+ i = 0
+ while i < row_nodes.length - 1
+ left = row_nodes[i]
+ right = row_nodes[i+1]
+ left_i = gdd.node_index[left.sha1]
+ right_i = gdd.node_index[right.sha1]
+ debug " #{left_i} < #{right_i} (#{left.x} < #{right.x})"
+ # mm = min_max_ordered_separation_constraints \
+ # 'x', MIN_NODE_X_GAP, MAX_NODE_X_GAP, left_i, right_i
+ min = min_separation_constraint \
+ 'x', MIN_NODE_X_GAP, left_i, right_i
+ exports.constraints = constraints = constraints.concat min
+ i++
+ return
+
+row_ordering_constraints = (row_groups) ->
+ debug 'row_ordering_constraints'
+ row_y_coords = Object.keys(row_groups).sort()
+
+ i = 0
+ while i < row_y_coords.length - 1
+ upper_y = row_y_coords[i]
+ lower_y = row_y_coords[i + 1]
+ upper_node = row_groups[upper_y][0]
+ lower_node = row_groups[lower_y][0]
+ constraints.push \
+ min_separation_constraint \
+ 'y', MIN_ROW_GAP,
+ gdd.node_index[upper_node.sha1],
+ gdd.node_index[lower_node.sha1]
+
+ i++
+ debug_constraints()
+ return
+
+dependency_ordering_constraints = () ->
+ debug 'dependency_ordering_constraints'
+
+ for parent_sha1, children of gdd.deps
+ child_sha1s = Object.keys(children).sort (sha1) -> node(sha1).x
+ dependency_ordering_constraint(parent_sha1, child_sha1s[0])
+ len = child_sha1s.length
+ if len > 1
+ dependency_ordering_constraint(parent_sha1, child_sha1s[len-1])
+ if len > 2
+ middle = Math.floor(len / 2)
+ dependency_ordering_constraint(parent_sha1, child_sha1s[middle])
+
+ debug_constraints()
+ return
+
+dependency_ordering_constraint = (parent_sha1, child_sha1) ->
+ constraints.push \
+ min_separation_constraint \
+ 'y', MIN_ROW_GAP,
+ gdd.node_index[parent_sha1],
+ gdd.node_index[child_sha1]
+
+##################################################################
+# helpers
+
+# Uses approach explained here:
+# https://github.com/tgdwyer/WebCola/issues/62#issuecomment-69571870
+min_max_ordered_separation_constraints = (axis, min, max, left, right) ->
+ return [
+ min_separation_constraint(axis, min, left, right),
+ max_separation_constraint(axis, max, left, right)
+ ]
+
+# https://github.com/tgdwyer/WebCola/issues/66
+max_unordered_separation_constraints = (axis, max, left, right) ->
+ return [
+ max_separation_constraint(axis, max, left, right),
+ max_separation_constraint(axis, max, right, left)
+ ]
+
+min_separation_constraint = (axis, gap, left, right) ->
+ {} =
+ axis: axis
+ gap: gap
+ left: left
+ right: right
+
+# We use a negative gap and reverse the inequality, in order to
+# achieve a maximum rather than minimum separation gap. However this
+# does not prevent the nodes from overlapping or even swapping order.
+# For that you also need a min_separation_constraint, but it's more
+# convenient to use min_max_ordered_separation_constraints. See
+# https://github.com/tgdwyer/WebCola/issues/62#issuecomment-69571870
+# for more details.
+max_separation_constraint = (axis, gap, left, right) ->
+ {} =
+ axis: axis
+ gap: -gap
+ left: right
+ right: left
+
+node = (sha1) ->
+ exports.graph.node sha1
+
+module.exports = exports =
+ # Variables have to be exported every time they're assigned,
+ # since assignment creates a new object and associated reference
+
+ # Functions
+ build_constraints: build_constraints
+ debug_constraints: debug_constraints
+ node: node
+
+ # Variables
+ debug: DEBUG
diff --git a/git_deps/html/js/git-deps-noty.coffee b/git_deps/html/js/git-deps-noty.coffee
new file mode 100644
index 0000000..cec2b08
--- /dev/null
+++ b/git_deps/html/js/git-deps-noty.coffee
@@ -0,0 +1,32 @@
+noty = require "noty"
+
+# Different noty types:
+# alert, success, error, warning, information, confirmation
+noty_error = (text) -> notyfication "error", text
+noty_warn = (text) -> notyfication "warning", text
+noty_success = (text) -> notyfication "success", text
+noty_info = (text) -> notyfication "information", text
+noty_debug = (text) -> notyfication "information", text
+
+# "notyfication" - haha, did you see what I did there?
+notyfication = (type, text) ->
+ noty(
+ text: text
+ type: type
+ layout: "topRight"
+ theme: "relax"
+ maxVisible: 15
+ timeout: 30000 # ms
+ animation:
+ open: "animated bounceInUp" # Animate.css class names
+ close: "animated bounceOutUp" # Animate.css class names
+ easing: "swing" # unavailable - no need
+ speed: 500 # unavailable - no need
+ )
+
+module.exports =
+ error: noty_error
+ warn: noty_warn
+ success: noty_success
+ info: noty_info
+ debug: noty_debug