Permalink
Browse files

added optional incremental regeneration in auto mode for when only po…

…sts have been modified
  • Loading branch information...
1 parent c92eb56 commit 39ae8c7c3f4a3cffd095e3b7638cfa8025c5a67a @graysky committed Jan 10, 2010
Showing with 126 additions and 12 deletions.
  1. +23 −1 bin/jekyll
  2. +3 −0 lib/jekyll/convertible.rb
  3. +2 −1 lib/jekyll/layout.rb
  4. +1 −0 lib/jekyll/page.rb
  5. +5 −1 lib/jekyll/post.rb
  6. +41 −9 lib/jekyll/site.rb
  7. +51 −0 test/test_site.rb
View
@@ -30,6 +30,10 @@ opts = OptionParser.new do |opts|
opts.on("--no-auto", "No auto-regenerate") do
options['auto'] = false
end
+
+ opts.on("--no-incremental", "Disable incremental regeneration (only with auto)") do
+ options['incremental'] = false
+ end
opts.on("--server [PORT]", "Start web server (default port 4000)") do |port|
options['server'] = true
@@ -107,7 +111,11 @@ site = Jekyll::Site.new(options)
if options['auto']
require 'directory_watcher'
+ incremental = true
+ incremental = options['incremental'] unless options['incremental'].nil?
+
puts "Auto-regenerating enabled: #{source} -> #{destination}"
+ puts "Incremental regeneration enabled" if incremental
dw = DirectoryWatcher.new(source)
dw.interval = 1
@@ -116,7 +124,21 @@ if options['auto']
dw.add_observer do |*args|
t = Time.now.strftime("%Y-%m-%d %H:%M:%S")
puts "[#{t}] regeneration: #{args.size} files changed"
- site.process
+
+ st = Time.now
+ # Check if files were only modified (not added/removed)
+ # to attempt incremental regeneration
+ mod_paths = args.collect { |e| e.path if e.type == :modified }.compact
+
+ if incremental && (args.size == mod_paths.size)
+ site.incremental(mod_paths)
+ else
+ site.process
+ end
+
+ f = Time.now
+ elapsed = format("%.2f",f - st)
+ puts "[#{f.strftime("%Y-%m-%d %H:%M:%S")}] regeneration finished: #{elapsed} seconds"
end
dw.start
@@ -5,6 +5,9 @@
# self.site -> Jekyll::Site
module Jekyll
module Convertible
+
+ attr_accessor :dirty
+
# Return the contents as a string
def to_s
self.content || ''
@@ -5,7 +5,7 @@ class Layout
attr_accessor :site
attr_accessor :ext
- attr_accessor :data, :content
+ attr_accessor :data, :content, :src_path
# Initialize a new Layout.
# +site+ is the Site
@@ -17,6 +17,7 @@ def initialize(site, base, name)
@site = site
@base = base
@name = name
+ @src_path = File.join(@base, @name) # source path of the layout
self.data = {}
View
@@ -19,6 +19,7 @@ def initialize(site, base, dir, name)
@base = base
@dir = dir
@name = name
+ @dirty = true
self.process(name)
self.read_yaml(File.join(base, dir), name)
View
@@ -18,7 +18,7 @@ def self.valid?(name)
name =~ MATCHER
end
- attr_accessor :site, :date, :slug, :ext, :published, :data, :content, :output, :tags
+ attr_accessor :site, :date, :slug, :ext, :published, :data, :content, :output, :tags, :src_path
attr_writer :categories
def categories
@@ -36,6 +36,8 @@ def initialize(site, source, dir, name)
@site = site
@base = File.join(source, dir, '_posts')
@name = name
+ @src_path = File.join(@base, name) # source path of the post
+ @dirty = true
self.categories = dir.split('/').reject { |x| x.empty? }
self.process(name)
@@ -207,6 +209,8 @@ def write(dest)
File.open(path, 'w') do |f|
f.write(self.output)
end
+
+ self.dirty = false
end
# Convert this post into a Hash for use in Liquid templates.
View
@@ -22,13 +22,19 @@ def initialize(config)
self.setup
end
- def reset
+ def reset(modified_posts=nil)
self.layouts = {}
- self.posts = []
self.pages = []
self.static_files = []
self.categories = Hash.new { |hash, key| hash[key] = [] }
self.tags = Hash.new { |hash, key| hash[key] = [] }
+
+ if modified_posts.nil?
+ self.posts = [] # Clean everything
+ else
+ # Only remove modified posts
+ self.posts.delete_if {|p| modified_posts.include?(p) }
+ end
end
def setup
@@ -92,13 +98,35 @@ def textile(content)
# real deal. Now has 4 phases; reset, read, render, write. This allows
# rendering to have full site payload available.
#
+ # +modified_posts+ is optional array of modified Posts for incremental rebuild
# Returns nothing
- def process
- self.reset
+ def process(modified_posts=nil)
+ self.reset(modified_posts)
self.read
self.render
self.write
end
+
+ # Incrementally regenerate if only posts have been modified.
+ # Will also regenerate layouts, pages, static pages since they
+ # may have references to posts.
+ #
+ # +changed_files+ array of paths that were modified
+ # Returns nothing
+ def incremental(changed_files)
+ modified_posts = []
+ self.posts.each do |p|
+ modified_posts << p if changed_files.include? p.src_path
+ end
+
+ if modified_posts.size != changed_files.size
+ # Files other than just posts changed, do full regenerate
+ self.process
+ else
+ # Incremental rebuild of modified posts
+ self.process(modified_posts)
+ end
+ end
def read
self.read_layouts # existing implementation did this at top level only so preserved that
@@ -132,7 +160,11 @@ def read_posts(dir)
# first pass processes, but does not yet render post content
entries.each do |f|
- if Post.valid?(f)
+ # Check if post already has been created
+ full_path = File.join(base, f)
+ post_exists = self.posts.find {|p| p.src_path == full_path}
+
+ if Post.valid?(f) && !post_exists
post = Post.new(self, self.source, dir, f)
if post.published
@@ -146,9 +178,9 @@ def read_posts(dir)
self.posts.sort!
end
- def render
- [self.posts, self.pages].flatten.each do |convertible|
- convertible.render(self.layouts, site_payload)
+ def render(posts=self.posts)
+ [posts, self.pages].flatten.each do |convertible|
+ convertible.render(self.layouts, site_payload) if convertible.dirty
end
self.categories.values.map { |ps| ps.sort! { |a, b| b <=> a} }
@@ -162,7 +194,7 @@ def render
# Returns nothing
def write
self.posts.each do |post|
- post.write(self.dest)
+ post.write(self.dest) if post.dirty
end
self.pages.each do |page|
page.write(self.dest)
View
@@ -72,6 +72,57 @@ class TestSite < Test::Unit::TestCase
assert_equal includes, @site.filter_entries(excludes + includes)
end
+ context "incremental regeneration" do
+ setup do
+ # Start with a processed site
+ clear_dest
+ @site.process
+ end
+
+ should "do full regeneration when layout changes" do
+ # User modified a layout and post
+ mod_layout = @site.layouts["default"]
+ mod_post = @site.posts.first
+
+ mock(@site).process # Full-rebuild should be called
+
+ @site.incremental([mod_layout.src_path, mod_post.src_path])
+
+ @site.posts.each do |p|
+ assert !p.dirty # All posts should be clean
+ end
+ end
+
+ should "do incremental regeneration when only posts are modified" do
+ mod_post = @site.posts.first
+ mod_path = mod_post.src_path
+
+ orig_num_posts = @site.posts.size
+
+ unmod_post = @site.posts.last
+ # Unmodified post should not be touched
+ dont_allow(unmod_post).write(is_a(String))
+
+ orig_page = @site.pages.first
+
+ @site.incremental([mod_path])
+
+ assert_equal @site.posts.size, orig_num_posts
+
+ # Compare updated post with original post
+ updated_post = @site.posts.first
+ assert_equal mod_post, updated_post # preserve ordering
+ assert_not_nil updated_post
+ assert_equal mod_post, updated_post # same path
+ assert !mod_post.equal?(updated_post) # but different object
+
+ # Page should have been updated as well
+ updated_page = @site.pages.first
+ assert_equal orig_page.url, updated_page.url #
+ assert !updated_page.equal?(orig_page) #
+ end
+ end
+
context 'with an invalid markdown processor in the configuration' do
should 'give a meaningful error message' do

0 comments on commit 39ae8c7

Please sign in to comment.