diff --git a/.gitignore b/.gitignore
index ac8828d1..24f54284 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,8 @@ pkg
.sass-cache
.sassc
.tmp
+graph.dot
+graph.jpg
docs
.rbenv-*
.ruby-version
diff --git a/Gemfile.lock b/Gemfile.lock
index feae474c..bc35a7ad 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -26,6 +26,7 @@ PATH
padrino-helpers (~> 0.13.0)
parallel
rack (>= 1.4.5, < 3)
+ rgl (~> 0.5.3)
sass (>= 3.4)
sassc (~> 2.0)
servolux
@@ -113,6 +114,7 @@ GEM
jaro_winkler (1.5.1)
json (2.1.0)
kramdown (1.17.0)
+ lazy_priority_queue (0.1.1)
libv8 (6.7.288.46.1)
liquid (4.0.1)
listen (3.1.5)
@@ -166,6 +168,9 @@ GEM
rb-inotify (0.10.0)
ffi (~> 1.0)
redcarpet (3.4.0)
+ rgl (0.5.3)
+ lazy_priority_queue (~> 0.1.0)
+ stream (~> 0.5.0)
rspec (3.8.0)
rspec-core (~> 3.8.0)
rspec-expectations (~> 3.8.0)
@@ -215,6 +220,7 @@ GEM
slim (4.0.1)
temple (>= 0.7.6, < 0.9)
tilt (>= 2.0.6, < 2.1)
+ stream (0.5)
stylus (1.0.2)
execjs
stylus-source
diff --git a/middleman-cli/lib/middleman-cli/build.rb b/middleman-cli/lib/middleman-cli/build.rb
index 65c088e3..204ba6f3 100644
--- a/middleman-cli/lib/middleman-cli/build.rb
+++ b/middleman-cli/lib/middleman-cli/build.rb
@@ -44,6 +44,10 @@ module Middleman::Cli
type: :boolean,
default: false,
desc: 'Track file dependencies'
+ class_option :visualize_graph,
+ type: :boolean,
+ default: false,
+ desc: 'Generate a visual of the dependency graph'
class_option :only_changed,
type: :boolean,
default: false,
@@ -95,7 +99,8 @@ module Middleman::Cli
parallel: options['parallel'],
only_changed: options['only_changed'],
missing_and_changed: missing_and_changed,
- track_dependencies: should_track_dependencies)
+ track_dependencies: should_track_dependencies,
+ visualize_graph: options['visualize_graph'])
builder.thor = self
builder.on_build_event(&method(:on_event))
end
diff --git a/middleman-core/features/incremental_builds.feature b/middleman-core/features/incremental_builds.feature
new file mode 100644
index 00000000..511d9f3b
--- /dev/null
+++ b/middleman-core/features/incremental_builds.feature
@@ -0,0 +1,174 @@
+Feature: Incremental builds
+
+ Scenario: Changing a page should only rebuild that page
+ Given an empty app
+ When a file named "config.rb" with:
+ """
+ """
+ When a file named "source/standalone.html.erb" with:
+ """
+ Initial
+ """
+ When a file named "source/other.html.erb" with:
+ """
+ Some other file
+ """
+ Then build the app tracking dependencies
+ Then the output should contain "create build/standalone.html"
+ Then the following files should exist:
+ | build/standalone.html |
+ And the file "build/standalone.html" should contain "Initial"
+ When a file named "source/standalone.html.erb" with:
+ """
+ Updated
+ """
+ Then build app with only changed
+ Then there are "0" files which are " create "
+ Then there are "1" files which are " updated "
+ Then the output should contain "updated build/standalone.html"
+ Then the following files should exist:
+ | build/standalone.html |
+ And the file "build/standalone.html" should contain "Updated"
+
+ Scenario: Changing a layout should only rebuild pages which use that layout
+ Given an empty app
+ When a file named "config.rb" with:
+ """
+ """
+ When a file named "source/layout.erb" with:
+ """
+ Initial
+ <%= yield %>
+ """
+ When a file named "source/page1.html.erb" with:
+ """
+ Page 1
+ """
+ When a file named "source/page2.html.erb" with:
+ """
+ Page 2
+ """
+ When a file named "source/no-layout.html.erb" with:
+ """
+ ---
+ layout: false
+ ---
+
+ Another page
+ """
+ Then build the app tracking dependencies
+ Then the output should contain "create build/page1.html"
+ Then the output should contain "create build/page2.html"
+ Then the following files should exist:
+ | build/page1.html |
+ | build/page2.html |
+ And the file "build/page1.html" should contain "Initial"
+ And the file "build/page1.html" should contain "Page 1"
+ And the file "build/page2.html" should contain "Initial"
+ And the file "build/page2.html" should contain "Page 2"
+ When a file named "source/layout.erb" with:
+ """
+ Updated
+ <%= yield %>
+ """
+ Then build app with only changed
+ Then there are "0" files which are " create "
+ Then there are "2" files which are " updated "
+ Then the output should contain "updated build/page1.html"
+ Then the output should contain "updated build/page2.html"
+ Then the following files should exist:
+ | build/page1.html |
+ | build/page2.html |
+ And the file "build/page1.html" should contain "Updated"
+ And the file "build/page1.html" should contain "Page 1"
+ And the file "build/page2.html" should contain "Updated"
+ And the file "build/page2.html" should contain "Page 2"
+
+ Scenario: Changing a piece of data only rebuilds the pages which use it
+ Given an empty app
+ When a file named "config.rb" with:
+ """
+ data.people.each do |p|
+ proxy "/person-#{p.slug}.html", '/person.html', ignore: true, locals: { person: p }
+ end
+ """
+ When a file named "data/people.yml" with:
+ """
+ -
+ slug: "one"
+ name: "Person One"
+ age: 5
+ -
+ slug: "two"
+ name: "Person Two"
+ age: 10
+ """
+ When a file named "source/person.html.erb" with:
+ """
+ <%= person.name %>
+ """
+ Then build the app tracking dependencies
+ Then the output should contain "create build/person-one.html"
+ Then the output should contain "create build/person-two.html"
+ Then the following files should exist:
+ | build/person-one.html |
+ | build/person-two.html |
+ And the file "build/person-one.html" should contain "Person One"
+ And the file "build/person-two.html" should contain "Person Two"
+ When a file named "data/people.yml" with:
+ """
+ -
+ slug: "one"
+ name: "Person One"
+ age: 15
+ -
+ slug: "two"
+ name: "Person Two"
+ age: 20
+ """
+ Then build app with only changed
+ Then there are "0" files which are " create "
+ Then there are "0" files which are " updated "
+ Then the following files should exist:
+ | build/person-one.html |
+ | build/person-two.html |
+ When a file named "data/people.yml" with:
+ """
+ -
+ slug: "one"
+ name: "Person One"
+ age: 5
+ -
+ slug: "two"
+ name: "Person Updated"
+ age: 10
+ """
+ Then build app with only changed
+ Then there are "0" files which are " create "
+ Then there are "1" files which are " updated "
+ Then the output should contain "updated build/person-two.html"
+ Then the following files should exist:
+ | build/person-one.html |
+ | build/person-two.html |
+ And the file "build/person-two.html" should contain "Person Updated"
+ When a file named "data/people.yml" with:
+ """
+ -
+ slug: "updated-slug"
+ name: "Person New Slug"
+ age: 5
+ -
+ slug: "two"
+ name: "Person Updated"
+ age: 10
+ """
+ Then build app with only changed
+ Then there are "1" files which are " create "
+ Then there are "1" files which are " remove "
+ Then there are "0" files which are " updated "
+ Then the output should contain "create build/person-updated-slug.html"
+ Then the output should contain "remove build/person-one.html"
+ Then the following files should exist:
+ | build/person-updated-slug.html |
+ | build/person-two.html |
+ And the file "build/person-updated-slug.html" should contain "Person New Slug"
\ No newline at end of file
diff --git a/middleman-core/fixtures/data-app/data/pages.yml b/middleman-core/fixtures/data-app/data/pages.yml
index 18f63665..0cd5973b 100644
--- a/middleman-core/fixtures/data-app/data/pages.yml
+++ b/middleman-core/fixtures/data-app/data/pages.yml
@@ -1,12 +1,16 @@
-
from: "/test1.html"
to: "/index.html"
+ guid: 0
-
from: "/test2.html"
to: "/index.html"
+ guid: 1
-
from: "/test3.html"
to: "/index.html"
+ guid: 2
-
from: "/test4.html"
- to: "/index.html"
\ No newline at end of file
+ to: "/index.html"
+ guid: 3
\ No newline at end of file
diff --git a/middleman-core/fixtures/data-app/source/first.html.erb b/middleman-core/fixtures/data-app/source/first.html.erb
new file mode 100644
index 00000000..70c600c4
--- /dev/null
+++ b/middleman-core/fixtures/data-app/source/first.html.erb
@@ -0,0 +1,3 @@
+<%= data.pages.first %>
+<%#= data.pages.first(2) %>
+<%#= data.pages.first(2).first %>
\ No newline at end of file
diff --git a/middleman-core/fixtures/data-app/source/index.html.erb b/middleman-core/fixtures/data-app/source/index.html.erb
index 4cd6464e..f47aba51 100755
--- a/middleman-core/fixtures/data-app/source/index.html.erb
+++ b/middleman-core/fixtures/data-app/source/index.html.erb
@@ -1,10 +1 @@
-
Welcome
-
-
-<%= data.pages.first %>
-<%= data.pages.first(2) %>
-<%= data.pages.first(2).first %>
---
-<%= data.pages.last %>
-<%= data.pages.last(2) %>
-<%= data.pages.last(2).last %>
\ No newline at end of file
+Welcome
\ No newline at end of file
diff --git a/middleman-core/fixtures/data-app/source/last.html.erb b/middleman-core/fixtures/data-app/source/last.html.erb
new file mode 100644
index 00000000..e42a594e
--- /dev/null
+++ b/middleman-core/fixtures/data-app/source/last.html.erb
@@ -0,0 +1,3 @@
+<%= data.pages.last %>
+<%#= data.pages.last(2) %>
+<%#= data.pages.last(2).last %>
\ No newline at end of file
diff --git a/middleman-core/fixtures/data-app/source/layout.erb b/middleman-core/fixtures/data-app/source/layout.erb
index 0520e80c..764e2955 100644
--- a/middleman-core/fixtures/data-app/source/layout.erb
+++ b/middleman-core/fixtures/data-app/source/layout.erb
@@ -1,5 +1,6 @@
+ <%#= data.pages[1].guid %>
<%= yield %>
\ No newline at end of file
diff --git a/middleman-core/fixtures/data-app/source/no-layout.html.erb b/middleman-core/fixtures/data-app/source/no-layout.html.erb
new file mode 100644
index 00000000..7469e6ea
--- /dev/null
+++ b/middleman-core/fixtures/data-app/source/no-layout.html.erb
@@ -0,0 +1,5 @@
+---
+layout: false
+---
+
+No layout
\ No newline at end of file
diff --git a/middleman-core/fixtures/data-app/source/random b/middleman-core/fixtures/data-app/source/random
new file mode 100644
index 00000000..e69de29b
diff --git a/middleman-core/fixtures/data-app/source/two.html.erb b/middleman-core/fixtures/data-app/source/two.html.erb
new file mode 100644
index 00000000..d8263ee9
--- /dev/null
+++ b/middleman-core/fixtures/data-app/source/two.html.erb
@@ -0,0 +1 @@
+2
\ No newline at end of file
diff --git a/middleman-core/fixtures/data-path-dependency-app/config.rb b/middleman-core/fixtures/data-path-dependency-app/config.rb
deleted file mode 100644
index 16ee9410..00000000
--- a/middleman-core/fixtures/data-path-dependency-app/config.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-data.people.each do |p|
- proxy "/#{p.slug}.html", '/person.html', ignore: true, locals: { person: p }
-end
diff --git a/middleman-core/fixtures/incremental-build-app/config.rb b/middleman-core/fixtures/incremental-build-app/config.rb
new file mode 100644
index 00000000..98c47ee9
--- /dev/null
+++ b/middleman-core/fixtures/incremental-build-app/config.rb
@@ -0,0 +1,3 @@
+# data.people.each do |p|
+# proxy "/#{p.slug}.html", '/person.html', ignore: true, locals: { person: p }
+# end
diff --git a/middleman-core/fixtures/data-path-dependency-app/data/people.yml b/middleman-core/fixtures/incremental-build-app/data/people.yml
similarity index 100%
rename from middleman-core/fixtures/data-path-dependency-app/data/people.yml
rename to middleman-core/fixtures/incremental-build-app/data/people.yml
diff --git a/middleman-core/fixtures/data-path-dependency-app/source/person.html.erb b/middleman-core/fixtures/incremental-build-app/source/person.html.erb
similarity index 100%
rename from middleman-core/fixtures/data-path-dependency-app/source/person.html.erb
rename to middleman-core/fixtures/incremental-build-app/source/person.html.erb
diff --git a/middleman-core/lib/middleman-core/builder.rb b/middleman-core/lib/middleman-core/builder.rb
index 308e2b3b..664c3f1c 100644
--- a/middleman-core/lib/middleman-core/builder.rb
+++ b/middleman-core/lib/middleman-core/builder.rb
@@ -40,6 +40,7 @@ module Middleman
@only_changed = options_hash.fetch(:only_changed, false)
@missing_and_changed = options_hash.fetch(:missing_and_changed, false)
@track_dependencies = options_hash.fetch(:track_dependencies, false)
+ @visualize_graph = @track_dependencies && options_hash.fetch(:visualize_graph, false)
@dry_run = options_hash.fetch(:dry_run)
@cleaning = !@dry_run && options_hash.fetch(:clean)
@@ -94,7 +95,7 @@ module Middleman
prerender_css.tap do |resources|
if @track_dependencies
resources.each do |r|
- @graph.add_edge(r[1]) unless r[1].nil?
+ @graph.add_edge_set(r[1]) unless r[1].nil?
end
end
end
@@ -106,7 +107,7 @@ module Middleman
output_files.tap do |resources|
if @track_dependencies
resources.each do |r|
- @graph.add_edge(r[1]) unless r[1].nil?
+ @graph.add_edge_set(r[1]) unless r[1].nil?
end
end
end
@@ -115,9 +116,12 @@ module Middleman
::Middleman::Profiling.report('build')
unless @has_error
- partial_update_with_no_changes = (@only_changed || @missing_and_changed) && !@invalidated_files.empty?
+ partial_update_with_no_changes = (@only_changed || @missing_and_changed) && @invalidated_files.empty?
+
::Middleman::Dependencies.serialize_and_save(@app, @graph) if @track_dependencies && !partial_update_with_no_changes
+ ::Middleman::Dependencies.visualize_graph(@app, @graph) if @track_dependencies && @visualize_graph
+
::Middleman::Util.instrument 'builder.clean' do
clean! if @cleaning
end
@@ -326,12 +330,11 @@ module Middleman
else
content = resource.render({}, {})
- unless resource.vertices.empty?
- vertices = ::Middleman::Dependencies::Edge.new(
- ::Middleman::Dependencies::FileVertex.from_resource(resource),
- resource.vertices
- )
- end
+ self_vertex = ::Middleman::Dependencies::FileVertex.from_resource(resource)
+ vertices = ::Middleman::Dependencies::Edge.new(
+ self_vertex,
+ resource.vertices << self_vertex
+ )
export_file!(output_file, binary_encode(content))
end
diff --git a/middleman-core/lib/middleman-core/core_extensions/data/controller.rb b/middleman-core/lib/middleman-core/core_extensions/data/controller.rb
index e59f5df3..91c822a8 100644
--- a/middleman-core/lib/middleman-core/core_extensions/data/controller.rb
+++ b/middleman-core/lib/middleman-core/core_extensions/data/controller.rb
@@ -47,12 +47,6 @@ module Middleman
end
end
- def vertices_for_key(k)
- @data_stores.reduce(::Hamster::Set.empty) do |sum, s|
- sum | s.vertices_for_key(k)
- end
- end
-
def enhanced_data(k)
value = key(k)
diff --git a/middleman-core/lib/middleman-core/core_extensions/data/proxies/array.rb b/middleman-core/lib/middleman-core/core_extensions/data/proxies/array.rb
index f1b74c2a..12672bd4 100644
--- a/middleman-core/lib/middleman-core/core_extensions/data/proxies/array.rb
+++ b/middleman-core/lib/middleman-core/core_extensions/data/proxies/array.rb
@@ -54,10 +54,12 @@ module Middleman
end
def to_s
+ log_access(:__full_access__)
@data.to_s
end
def to_json
+ log_access(:__full_access__)
@data.to_a.to_json
end
diff --git a/middleman-core/lib/middleman-core/core_extensions/data/proxies/hash.rb b/middleman-core/lib/middleman-core/core_extensions/data/proxies/hash.rb
index bdc21164..bfa4ae8f 100644
--- a/middleman-core/lib/middleman-core/core_extensions/data/proxies/hash.rb
+++ b/middleman-core/lib/middleman-core/core_extensions/data/proxies/hash.rb
@@ -23,10 +23,12 @@ module Middleman
end
def to_s
+ log_access(:__full_access__)
@data.to_s
end
def to_json
+ log_access(:__full_access__)
@data.to_h.to_json
end
end
diff --git a/middleman-core/lib/middleman-core/core_extensions/data/stores/base.rb b/middleman-core/lib/middleman-core/core_extensions/data/stores/base.rb
index 1a2ac5d9..5edb7901 100644
--- a/middleman-core/lib/middleman-core/core_extensions/data/stores/base.rb
+++ b/middleman-core/lib/middleman-core/core_extensions/data/stores/base.rb
@@ -29,11 +29,6 @@ module Middleman
Hamster::Set.empty
end
- Contract Symbol => ImmutableSetOf[::Middleman::Dependencies::Vertex]
- def vertices_for_key(_k)
- Hamster::Set.empty
- end
-
Contract Hash
def to_h
keys.each_with_object({}) do |k, sum|
diff --git a/middleman-core/lib/middleman-core/core_extensions/data/stores/in_memory.rb b/middleman-core/lib/middleman-core/core_extensions/data/stores/in_memory.rb
index cd92272b..246e319f 100644
--- a/middleman-core/lib/middleman-core/core_extensions/data/stores/in_memory.rb
+++ b/middleman-core/lib/middleman-core/core_extensions/data/stores/in_memory.rb
@@ -27,11 +27,6 @@ module Middleman
Hamster::Set.new(@keys_to_vertex.values.flatten(1))
end
- Contract Symbol => ImmutableSetOf[::Middleman::Dependencies::Vertex]
- def vertices_for_key(k)
- @keys_to_vertex[k] || ::Hamster::Set.empty
- end
-
# Store static data hash
#
# @param [Symbol] name Name of the data, used for namespacing
diff --git a/middleman-core/lib/middleman-core/core_extensions/data/stores/local_file.rb b/middleman-core/lib/middleman-core/core_extensions/data/stores/local_file.rb
index e7a9bdb2..4461a731 100644
--- a/middleman-core/lib/middleman-core/core_extensions/data/stores/local_file.rb
+++ b/middleman-core/lib/middleman-core/core_extensions/data/stores/local_file.rb
@@ -26,12 +26,6 @@ module Middleman
@app = app
@local_data = {}
- @paths_to_vertex = {}
- end
-
- Contract Symbol => ImmutableSetOf[::Middleman::Dependencies::Vertex]
- def vertices_for_key(k)
- @paths_to_vertex[k] || ::Hamster::Set.empty
end
Contract ArrayOf[IsA['Middleman::SourceFile']], ArrayOf[IsA['Middleman::SourceFile']] => Any
@@ -69,16 +63,6 @@ module Middleman
data_branch = data_branch[dir.to_sym]
end
- # For now, all files nested under a folder in `data/` will invalidate
- # the whole folder.
- if paths.empty?
- @paths_to_vertex[basename.to_sym] ||= ::Hamster::Set.empty
- @paths_to_vertex[basename.to_sym] <<= ::Middleman::Dependencies::FileVertex.from_source_file(@app, file)
- else
- @paths_to_vertex[paths.first.to_sym] ||= ::Hamster::Set.empty
- @paths_to_vertex[paths.first.to_sym] <<= ::Middleman::Dependencies::FileVertex.from_source_file(@app, file)
- end
-
data_branch[basename.to_sym] = data
end
diff --git a/middleman-core/lib/middleman-core/data_proxy.rb b/middleman-core/lib/middleman-core/data_proxy.rb
index fc471848..4fa98649 100644
--- a/middleman-core/lib/middleman-core/data_proxy.rb
+++ b/middleman-core/lib/middleman-core/data_proxy.rb
@@ -56,11 +56,7 @@ module Middleman
end
def method_missing(method, *args, &block)
- if @ctx.internal_data_store.key?(method)
- @ctx.vertices |= @ctx.internal_data_store.vertices_for_key(method)
-
- return @ctx.internal_data_store.proxied_data(method, self)
- end
+ return @ctx.internal_data_store.proxied_data(method, self) if @ctx.internal_data_store.key?(method)
super
end
diff --git a/middleman-core/lib/middleman-core/dependencies.rb b/middleman-core/lib/middleman-core/dependencies.rb
index 8930288c..b8ba8441 100644
--- a/middleman-core/lib/middleman-core/dependencies.rb
+++ b/middleman-core/lib/middleman-core/dependencies.rb
@@ -24,8 +24,16 @@ module Middleman
end
end
+ Contract IsA['::Middleman::Application'], Graph => Any
+ def visualize_graph(_app, graph)
+ require 'rgl/dot'
+ graph.graph.write_to_graphic_file('jpg', 'graph')
+ end
+
Contract IsA['::Middleman::Application'], Graph => String
def serialize(app, graph)
+ serialized = graph.serialize
+
ruby_files = Dir.glob(RUBY_FILES).reduce([]) do |sum, file|
sum << {
file: Pathname(File.expand_path(file)).relative_path_from(app.root_path).to_s,
@@ -33,21 +41,10 @@ module Middleman
}
end
- edges = graph.dependency_map.reduce([]) do |sum, (vertex, depended_on_by)|
- sum << {
- key: vertex.key,
- depended_on_by: depended_on_by.delete(vertex).to_a.map(&:key).sort
- }
- end
-
- vertices = graph.dependency_map.reduce([]) do |sum, (vertex, _depended_on_by)|
- sum << vertex.serialize
- end
-
::YAML.dump(
- ruby_files: ruby_files.sort_by { |d| d[:file] },
- edges: edges.sort_by { |d| d[:key] },
- vertices: vertices.sort_by { |d| d[:key] }
+ {
+ ruby_files: ruby_files.sort_by { |d| d[:file] }
+ }.merge(serialized)
)
end
@@ -105,17 +102,18 @@ module Middleman
end
end
- graph = Graph.new(vertices)
+ graph = Graph.new
+ vertices.values.each { |v| graph.add_vertex(v) }
- Contract ImmutableHashOf[Vertex, ImmutableSetOf[Vertex]]
-
- edges = data[:edges]
- graph.dependency_map = edges.reduce(::Hamster::Hash.empty) do |sum, row|
- vertex = graph.vertices[row[:key]]
- depended_on_by = row[:depended_on_by].map { |k| graph.vertices[k] }
- sum.put(vertex, ::Hamster::Set.new(depended_on_by) << vertex)
+ data[:edges].each do |e|
+ graph.add_edge_by_key(e[:depends_on], e[:key])
end
+ graph.invalidate_changes!
+
+ # require 'rgl/dot'
+ # graph.graph.write_to_graphic_file('jpg', 'valid')
+
graph
rescue StandardError
raise InvalidDepsYAML
diff --git a/middleman-core/lib/middleman-core/dependencies/graph.rb b/middleman-core/lib/middleman-core/dependencies/graph.rb
index 599cc196..2d1af09c 100644
--- a/middleman-core/lib/middleman-core/dependencies/graph.rb
+++ b/middleman-core/lib/middleman-core/dependencies/graph.rb
@@ -1,87 +1,121 @@
require 'set'
+require 'rgl/adjacency'
require 'middleman-core/contracts'
require 'middleman-core/dependencies/vertices/vertex'
require 'middleman-core/dependencies/edge'
module Middleman
module Dependencies
+ class DirectedAdjacencyGraph < ::RGL::DirectedAdjacencyGraph
+ def add_vertex(v)
+ super(merged_vertex_or_new(v))
+ end
+
+ def add_edge(u, v)
+ super(merged_vertex_or_new(u), merged_vertex_or_new(v))
+ end
+
+ def remove_vertex(vertex)
+ each_adjacent(vertex) do |v|
+ remove_vertex(v) unless v == vertex
+ end
+
+ super(vertex)
+ end
+
+ def find_vertex_by_key(key)
+ vertices.find { |v| v.key == key }
+ end
+
+ protected
+
+ def merged_vertex_or_new(vertex)
+ found_vertex = find_vertex_by_key(vertex.key)
+
+ if found_vertex
+ found_vertex.merge!(vertex)
+ found_vertex
+ else
+ vertex
+ end
+ end
+ end
+
class Graph
include Contracts
- Contract HashOf[Vertex::VERTEX_KEY, Vertex]
- attr_reader :vertices
+ Contract DirectedAdjacencyGraph
+ attr_reader :graph
- Contract ImmutableHashOf[Vertex, ImmutableSetOf[Vertex]]
- attr_accessor :dependency_map
-
- def initialize(vertices = {})
- @vertices = vertices
- @dependency_map = ::Hamster::Hash.empty
+ def initialize(_vertices = {})
+ @graph = DirectedAdjacencyGraph.new
end
- Contract Vertex => Vertex
- def merged_vertex_or_new(v)
- if @vertices[v.key]
- @vertices[v.key].merge!(v)
- else
- @vertices[v.key] = v
- end
+ def invalidate_vertex!(vertex)
+ @graph.remove_vertex(vertex)
+ end
- @vertices[v.key]
+ Contract Vertex => Any
+ def add_vertex(vertex)
+ @graph.add_vertex(vertex)
+ end
+
+ Contract Vertex, Vertex => Any
+ def add_edge(source, target)
+ if source == target
+ add_vertex(source)
+ else
+ @graph.add_edge(source, target)
+ end
+ end
+
+ Contract Symbol, Symbol => Any
+ def add_edge_by_key(source, target)
+ a = @graph.find_vertex_by_key(source)
+ b = @graph.find_vertex_by_key(target)
+
+ add_edge(a, b)
end
Contract Edge => Any
- def add_edge(edge)
- deduped_vertex = merged_vertex_or_new edge.vertex
-
- # FIXME
- # Depending on yourself (<< deduped_vertex)
- # is only useful for files in source/ that can be depended on and also
- # be their own route
- @dependency_map = @dependency_map.put(deduped_vertex) do |v|
- (v || ::Hamster::Set.empty) << deduped_vertex
- end
-
+ def add_edge_set(edge)
return if edge.depends_on.nil?
edge.depends_on.each do |depended_on|
- deduped_depended_on = merged_vertex_or_new depended_on
-
- @dependency_map = @dependency_map.put(deduped_depended_on) do |v|
- (v || ::Hamster::Set.empty) << deduped_depended_on << deduped_vertex
- end
+ add_edge(depended_on, edge.vertex)
end
end
- Contract String => Bool
- def exists?(file_path)
- @dependency_map.key?(file_path)
+ def serialize
+ edges = @graph.edges.map do |edge|
+ {
+ key: edge.target.key,
+ depends_on: edge.source.key
+ }
+ end
+
+ vertices = @graph.vertices.map(&:serialize)
+
+ {
+ edges: edges.sort_by { |d| [d[:key], d[:depends_on]] },
+ vertices: vertices.sort_by { |d| d[:key] }
+ }
+ end
+
+ Contract Any
+ def invalidate_changes!
+ @invalidated = @graph.vertices.reject(&:valid?)
+ @invalidated.each { |v| @graph.remove_vertex(v) }
end
Contract ImmutableSetOf[Vertex]
def invalidated
- @_invalidated_cache ||= begin
- invalidated_vertices = @dependency_map.keys.select do |vertex|
- # Either "Missing from known vertices"
- # Or invalided by the class
- !@vertices.key?(vertex.key) || !vertex.valid?
- end
-
- invalidated_vertices.reduce(::Hamster::Set.empty) do |sum, vertex|
- sum | invalidated_with_parents(vertex)
- end
- end
- end
-
- Contract Vertex => ImmutableSetOf[Vertex]
- def invalidated_with_parents(vertex)
- # TODO, recurse more?
- @dependency_map[vertex] << vertex
+ ::Hamster::Set.new(@invalidated)
end
Contract IsA['::Middleman::Sitemap::Resource'] => Bool
def invalidates_resource?(resource)
- invalidated.any? { |d| d.invalidates_resource?(resource) }
+ @graph.vertices.none? { |d| d.matches_resource?(resource) }
end
end
end
diff --git a/middleman-core/lib/middleman-core/dependencies/vertices/file_vertex.rb b/middleman-core/lib/middleman-core/dependencies/vertices/file_vertex.rb
index b56a68de..45cd4d04 100644
--- a/middleman-core/lib/middleman-core/dependencies/vertices/file_vertex.rb
+++ b/middleman-core/lib/middleman-core/dependencies/vertices/file_vertex.rb
@@ -38,7 +38,7 @@ module Middleman
end
Contract IsA['Middleman::Sitemap::Resource'] => Bool
- def invalidates_resource?(resource)
+ def matches_resource?(resource)
resource.file_descriptor[:full_path].to_s == @full_path
end
diff --git a/middleman-core/lib/middleman-core/dependencies/vertices/vertex.rb b/middleman-core/lib/middleman-core/dependencies/vertices/vertex.rb
index 10655742..ac72de1f 100644
--- a/middleman-core/lib/middleman-core/dependencies/vertices/vertex.rb
+++ b/middleman-core/lib/middleman-core/dependencies/vertices/vertex.rb
@@ -42,7 +42,7 @@ module Middleman
end
Contract IsA['Middleman::Sitemap::Resource'] => Bool
- def invalidates_resource?(_resource)
+ def matches_resource?(_resource)
false
end
diff --git a/middleman-core/lib/middleman-core/step_definitions/builder_steps.rb b/middleman-core/lib/middleman-core/step_definitions/builder_steps.rb
index a111ca8e..ee0d8fad 100644
--- a/middleman-core/lib/middleman-core/step_definitions/builder_steps.rb
+++ b/middleman-core/lib/middleman-core/step_definitions/builder_steps.rb
@@ -6,21 +6,27 @@ end
Given /^a built app at "([^\"]*)"$/ do |path|
step %(a fixture app "#{path}")
+ step %(I run `middleman build --verbose --no-parallel`)
+end
- cwd = File.expand_path(aruba.current_directory)
- step %(I set the environment variable "MM_ROOT" to "#{cwd}")
+Then /^build the app tracking dependencies$/ do
+ step %(I run `middleman build --track-dependencies --no-parallel`)
+ step %(was successfully built)
+end
- step %(I run `middleman build --verbose`)
+Then /^build app with only changed$/ do
+ step %(I run `middleman build --track-dependencies --only-changed --no-parallel`)
+ step %(was successfully built)
end
Given /^was successfully built$/ do
- # step %(the output should contain "Project built successfully.")
+ step %(the output should contain "Project built successfully.")
step %(the exit status should be 0)
step %(a directory named "build" should exist)
end
Given /^was not successfully built$/ do
- # step %(the output should not contain "Project built successfully.")
+ step %(the output should not contain "Project built successfully.")
step %(the exit status should not be 0)
step %(a directory named "build" should not exist)
end
@@ -36,7 +42,7 @@ Given /^a built app at "([^\"]*)" with flags "([^\"]*)"$/ do |path, flags|
cwd = File.expand_path(aruba.current_directory)
step %(I set the environment variable "MM_ROOT" to "#{cwd}")
- step %(I run `middleman build #{flags}`)
+ step %(I run `middleman build --no-parallel #{flags}`)
end
Given /^a successfully built app at "([^\"]*)" with flags "([^\"]*)"$/ do |path, flags|
@@ -55,3 +61,8 @@ Given /^I run the interactive middleman server$/ do
step %(I set the environment variable "MM_ROOT" to "#{cwd}")
step %(I run `middleman server` interactively)
end
+
+Then('there are {string} files which are {string}') do |num, str|
+ # $stderr.puts last_command_started.output
+ expect(last_command_started.output.scan(str).length).to be num.to_i
+end
diff --git a/middleman-core/lib/middleman-core/step_definitions/middleman_steps.rb b/middleman-core/lib/middleman-core/step_definitions/middleman_steps.rb
index 7b51a111..41dc57ce 100644
--- a/middleman-core/lib/middleman-core/step_definitions/middleman_steps.rb
+++ b/middleman-core/lib/middleman-core/step_definitions/middleman_steps.rb
@@ -7,9 +7,17 @@ Given /^app "([^\"]*)" is using config "([^\"]*)"$/ do |_path, config_name|
end
Given /^an empty app$/ do
+ ENV['MM_ROOT'] = nil
+
+ # This step can be reentered from several places but we don't want
+ # to keep re-copying and re-cd-ing into ever-deeper directories
+ next if File.basename(expand_path('.')) == 'empty_app'
+
step %(a directory named "empty_app")
step %(I cd to "empty_app")
- ENV['MM_ROOT'] = nil
+
+ cwd = File.expand_path(aruba.current_directory)
+ step %(I set the environment variable "MM_ROOT" to "#{cwd}")
end
Given /^a fixture app "([^\"]*)"$/ do |path|
@@ -25,6 +33,9 @@ Given /^a fixture app "([^\"]*)"$/ do |path|
FileUtils.cp_r(target_path, expand_path('.'))
step %(I cd to "#{path}")
+
+ cwd = File.expand_path(aruba.current_directory)
+ step %(I set the environment variable "MM_ROOT" to "#{cwd}")
end
Then /^the file "([^\"]*)" has the contents$/ do |path, contents|
diff --git a/middleman-core/middleman-core.gemspec b/middleman-core/middleman-core.gemspec
index 232688e1..5bee8451 100644
--- a/middleman-core/middleman-core.gemspec
+++ b/middleman-core/middleman-core.gemspec
@@ -26,6 +26,7 @@ Gem::Specification.new do |s|
s.add_dependency('parallel')
s.add_dependency('servolux')
s.add_dependency('dotenv')
+ s.add_dependency('rgl', ['~> 0.5.3'])
# Helpers
s.add_dependency('activesupport', ['>= 4.2', '< 5.2'])