Permalink
Browse files

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

…sts have been modified
  • Loading branch information...
graysky committed Jan 10, 2010
1 parent c92eb56 commit 39ae8c7c3f4a3cffd095e3b7638cfa8025c5a67a
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
@@ -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 = {}

@@ -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)
@@ -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.
@@ -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)
@@ -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.