├── Rakefile ├── Gemfile ├── LICENSE.markdown ├── Gemfile.lock ├── .gitignore ├── jekyll-staging.gemspec ├── README.markdown └── bin └── stage /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /LICENSE.markdown: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution-Sharealike 2 | 3 | http://creativecommons.org/licenses/by-sa/4.0/ 4 | 5 | 6 | 7 | I'd also appreciate hearing from anyone who uses this. Contact info is in the README file. 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | jekyll-staging (1.0.6) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | rake (10.4.2) 10 | 11 | PLATFORMS 12 | ruby 13 | 14 | DEPENDENCIES 15 | bundler (~> 1.7) 16 | jekyll-staging! 17 | rake (~> 10.0) 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | 12 | ## Specific to RubyMotion: 13 | .dat* 14 | .repl_history 15 | build/ 16 | 17 | ## Documentation cache and generated files: 18 | /.yardoc/ 19 | /_yardoc/ 20 | /doc/ 21 | /rdoc/ 22 | 23 | ## Environment normalisation: 24 | /.bundle/ 25 | /lib/bundler/man/ 26 | 27 | # for a library or gem, you might want to ignore these files since the code is 28 | # intended to run in multiple environments; otherwise, check them in: 29 | # Gemfile.lock 30 | # .ruby-version 31 | # .ruby-gemset 32 | 33 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 34 | .rvmrc 35 | -------------------------------------------------------------------------------- /jekyll-staging.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "jekyll-staging" 5 | spec.version = "1.0.6" 6 | spec.authors = ["Matt Gemmell"] 7 | spec.email = ["matt@mattgemmell.com"] 8 | spec.summary = %q{Stage and unstage draft posts in Jekyll.} 9 | spec.homepage = "https://github.com/mattgemmell/Jekyll-Staging" 10 | spec.license = "MIT" 11 | 12 | spec.files = `git ls-files -z`.split("\x0") 13 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 14 | spec.require_paths = ["lib"] 15 | 16 | spec.add_development_dependency "bundler", "~> 1.7" 17 | spec.add_development_dependency "rake", "~> 10.0" 18 | end 19 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Jekyll-Staging 2 | 3 | by [Matt Gemmell](http://mattgemmell.com/) 4 | 5 | 6 | ## What is it? 7 | 8 | It's a Ruby gem that stages and unstages draft posts for Jekyll's internal server. 9 | 10 | 11 | ## What are its requirements? 12 | 13 | All you need is Ruby. Grab it by running `gem install jekyll-staging` in your terminal. 14 | 15 | 16 | ## What does it do? 17 | 18 | This gem is for people who: 19 | 20 | 1. Use [Jekyll](http://jekyllrb.com) to build their static sites. 21 | 22 | 2. Keep their drafts _outside_ of Jekyll's folder structure. 23 | 24 | 3. Like to work on posts using Jekyll's built-in web server. 25 | 26 | If you have a lot of posts, Jekyll's rebuild process can take a while, so it can be handy to temporarily move all of your existing posts aside while you work on a draft. That's what this gem does. 27 | 28 | It lets you specify a given draft post, and it'll move it into Jekyll's folder structure (appropriately prefixing its filename with today's date). Then, it moves all other posts aside temporarily. That way, Jekyll's build and regeneration process will be super-fast while you're working on the draft. 29 | 30 | It can also reverse the process when you're done, of course, putting the draft back into your drafts folder and restoring all your existing posts. 31 | 32 | Note: It's assumed that your drafts' filenames _don't_ already have date-prefixes. Personally, I only add the date-prefixes when I'm ready to publish a new post. I often work on posts for several days, so it's easier that way. 33 | 34 | 35 | ## How do I use it? 36 | 37 | Run the gem without any arguments to see usage instructions. Be sure to run the gem from your Jekyll site's root directory. 38 | 39 | On the first run (per site), you'll be asked three configuration questions. The appropriate options will be stored in a `.jekyll-stagingrc` file in your site's root. 40 | 41 | Basically: 42 | 43 | - **`stage FILENAME_GLOB`** stages the first matching draft. 44 | - **`stage -u`** unstages the first staged post. 45 | - **`stage -u FILENAME_GLOB`** unstages the first matching staged post. 46 | 47 | You don't need to specify full paths, because it'll be looking in your drafts folder anyway. Feel free to use partial filenames, and shell glob patterns. If there's more than one match, it'll use the first one, and it'll output the full list of matches as well as the filename it chose. 48 | 49 | Let's say you had a draft whose filename was `my-new-iphone.markdown`. A typical workflow would be as follows. It assumes you're already in your Jekyll site's root directory. 50 | 51 | 1. **`stage iphone`** (stage the draft for Jekyll) 52 | 53 | 2. **`jekyll serve`** (start the built-in web server) 54 | 55 | 3. Edit your post as you see fit, and view it in your browser. When you're done, kill Jekyll's web server. 56 | 57 | 4. **`stage -u`** (unstage the draft, putting it back in your drafts folder) 58 | 59 | You can then decide whether to publish the post, and build and deploy your site as usual. 60 | 61 | 62 | ## Where does it temporarily put my other posts? 63 | 64 | In a directory called "`_stash`", in your Jekyll site's local root directory. The directory will be created if necessary. 65 | 66 | 67 | ## Should I run it on my server? 68 | 69 | Nope. I run it on my local machine, and so should you. 70 | 71 | 72 | ## Who made it? 73 | 74 | Matt Gemmell (that's me). 75 | 76 | - My website is at [mattgemmell.com](http://mattgemmell.com) 77 | 78 | - I'm on Twitter as [@mattgemmell](http://twitter.com/mattgemmell) 79 | 80 | - This code is on github at [github.com/mattgemmell/Jekyll-Staging](http://github.com/mattgemmell/Jekyll-Staging) 81 | 82 | 83 | ## What license is the code released under? 84 | 85 | Creative Commons [Attribution-Sharealike](http://creativecommons.org/licenses/by-sa/4.0/). 86 | 87 | 88 | ## Can you provide support? 89 | 90 | Nope. If you find a bug, please fix it and [submit a pull request on github](https://github.com/mattgemmell/Jekyll-Staging/pulls). 91 | 92 | 93 | ## I have a feature request or bug report. 94 | 95 | Please [create an issue on github](https://github.com/mattgemmell/Jekyll-Staging/issues). 96 | 97 | 98 | ## How can I thank you? 99 | 100 | You can: 101 | 102 | - [Support my writing](http://mattgemmell.com/support-me/). 103 | 104 | - Check out [my Amazon wishlist](http://www.amazon.co.uk/registry/wishlist/1BGIQ6Z8GT06F). 105 | 106 | - Say thanks [on Twitter](http://twitter.com/mattgemmell). 107 | -------------------------------------------------------------------------------- /bin/stage: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Staging/unstaging gem for Jekyll blog posts. 4 | # 5 | # by Matt Gemmell 6 | # 7 | # Web: http://mattgemmell.com 8 | # Twitter: http://twitter.com/mattgemmell 9 | 10 | 11 | # This gem moves a specified draft post into Jekyll's folder structure, 12 | # and temporarily stashes all other posts so the site will build quickly. 13 | # 14 | # This is for people who keep their drafts outside of Jekyll's folder structure. 15 | # 16 | # It can also reverse the process when you're done, of course. 17 | # 18 | # This is useful when you're working on a post using Jekyll's server. 19 | 20 | 21 | # Requirements: just Ruby itself. 22 | 23 | 24 | # Usage: Run the stage command with no arguments to see usage instructions. 25 | # Note: Run the command from your Jekyll site's root directory. 26 | # You'll be asked three configuration questions (for each site) on the first run. 27 | 28 | 29 | # License: CC Attribution-ShareAlike 30 | # http://creativecommons.org/licenses/by-sa/4.0/ 31 | 32 | 33 | require 'fileutils' 34 | 35 | 36 | # ======================================== 37 | # Configuration 38 | # ======================================== 39 | 40 | # Load in configuration file, or ask and populate. 41 | def read_or_ask_for_configuration 42 | require 'yaml' 43 | 44 | # Check we're in a Jekyll root directory. 45 | jekyll_conf_file = File.join(Dir.pwd, '_config.yml') 46 | if !File.exist?(jekyll_conf_file) 47 | puts "Please run the stage command from your Jekyll site's root directory." 48 | exit 49 | end 50 | 51 | # Look for config file in the current working directory. 52 | conf_file = File.join(Dir.pwd, '.jekyll-stagingrc') 53 | if File.exist?(conf_file) 54 | YAML.load_file(conf_file) 55 | else 56 | conf = {} 57 | print "Where is your Jekyll site located? (full path, '~' is ok) " 58 | conf['jekyll_root'] = STDIN.gets.strip 59 | print "Where are your drafts located? (full path, '~' is ok) " 60 | conf['drafts_dir'] = STDIN.gets.strip 61 | print "What file extension do you use for your drafts and posts? (no period) " 62 | conf['file_extension'] = STDIN.gets.strip 63 | File.open(conf_file, 'wb') { |f| f.write(YAML.dump(conf)) } 64 | conf 65 | end 66 | end 67 | 68 | 69 | configuration = read_or_ask_for_configuration 70 | 71 | # Your Jekyll site's local root directory (it has the "_config.yml" file in it). 72 | $jekyll_root = configuration['jekyll_root'].strip 73 | 74 | # Wherever you keep your local in-progress drafts, outside of your Jekyll site. 75 | $drafts_dir = configuration['drafts_dir'].strip 76 | 77 | # File-extension for your drafts and post files. 78 | $file_extension = configuration['file_extension'].strip 79 | 80 | # Note: It's assumed that your drafts' filenames DON'T have date-prefixes. 81 | 82 | 83 | # ======================================== 84 | # You probably DON'T need to touch anything below here! 85 | # ======================================== 86 | 87 | 88 | # Other directories within Jekyll's root. The defaults should be fine. 89 | $posts_dir_name = "_posts" # Should already be in the root directory. 90 | $stash_dir_name = "_stash" # Will be created if necessary. 91 | 92 | # Filename details for posts. The defaults should be fine. 93 | $file_date_prefix_format = "%Y\-%m\-%d\-" # for Ruby's DateTime strftime 94 | $file_date_prefix_regexp = /\d+-\d+-\d+-/ # regexp to match date format above 95 | 96 | 97 | # === Start of functions === 98 | 99 | 100 | def first_matching_file_in_dir(the_filename_glob, the_dir_path) 101 | Dir.chdir(the_dir_path) 102 | matches = Dir.glob(the_filename_glob) 103 | if matches.count == 0 104 | puts "No matches found in #{the_dir_path}." 105 | return false 106 | elsif matches.count > 1 107 | puts "Found #{matches.count} matching files (#{matches.join(", ")})" 108 | end 109 | 110 | filename = matches[0] 111 | puts "Using file \"#{filename}\"." 112 | return filename 113 | end 114 | 115 | 116 | def directory_exists(the_dir_path) 117 | # Check for a directory at path. 118 | 119 | if File.exist?(the_dir_path) 120 | # Something's there. Check if it's a directory. 121 | #puts "Found something at given path." 122 | if File.directory?(the_dir_path) 123 | # It exists, and it's a directory. We're done. 124 | #puts "It's a directory." 125 | return true 126 | else 127 | # Abort 128 | #puts "It's not a directory" 129 | end 130 | end 131 | 132 | return false 133 | end 134 | 135 | 136 | def create_dir_if_necessary(the_dir_path) 137 | # Check for a directory at path, creating it if necessary. 138 | 139 | # Check to see if an item exists at that path. 140 | if directory_exists(the_dir_path) 141 | return true 142 | else 143 | # It doesn't exist. Create it. 144 | #puts "Nothing found at given path." 145 | begin 146 | Dir.mkdir(the_dir_path, 0755) 147 | #puts "Created the directory: #{the_dir_path}" 148 | return true 149 | rescue SystemCallError 150 | # If we failed to create the directory, abort. 151 | #puts "Failed to create the directory." 152 | return false 153 | end 154 | end 155 | 156 | return false 157 | end 158 | 159 | 160 | def isolate(the_filename_glob) 161 | # Moves all files from posts to stash, except those matching the glob. 162 | 163 | # Move all files from posts into stash. 164 | files = Dir.glob(File.join($posts_dir, "*.#{$file_extension}")) 165 | result = FileUtils.move(files, $stash_dir) 166 | 167 | # Move the matching files back into posts. 168 | files = Dir.glob(File.join($stash_dir, "*#{the_filename_glob}*")) 169 | result = FileUtils.move(files, $posts_dir) 170 | end 171 | 172 | 173 | def integrate() 174 | # Moves all files from stash back into posts. 175 | 176 | # Move all files from stash into posts. 177 | files = Dir.glob(File.join($stash_dir, "*.#{$file_extension}")) 178 | result = FileUtils.move(files, $posts_dir) 179 | end 180 | 181 | 182 | def stage_file(the_filename_glob) 183 | # Stages the given file. 184 | 185 | puts "Staging \"#{the_filename_glob}\"..." 186 | 187 | # Grab specified file (glob) from drafts folder. 188 | full_glob = "*#{the_filename_glob}*.#{$file_extension}" 189 | filename = first_matching_file_in_dir(full_glob, $drafts_dir) 190 | if filename == false 191 | puts "Couldn't find a file to stage. Aborting." 192 | exit 193 | end 194 | 195 | # Move file to posts directory, prefixing its filename with today's date. 196 | require "Date" 197 | today = DateTime.now() 198 | prefix = today.strftime($file_date_prefix_format) 199 | new_filename = "#{prefix}#{filename}" 200 | 201 | result = FileUtils.move(File.join($drafts_dir, filename), File.join($posts_dir, new_filename)) 202 | 203 | # Isolate the staged file in posts, moving all other posts to stash. 204 | isolate(new_filename) 205 | 206 | puts "File staged as \"#{new_filename}\"." 207 | end 208 | 209 | 210 | def unstage_file(the_filename_glob) 211 | # Unstages the given file, or the first staged file if none is specified. 212 | 213 | if (the_filename_glob) 214 | puts "Unstaging \"#{the_filename_glob}\"..." 215 | else 216 | puts "Unstaging first staged file..." 217 | end 218 | 219 | # Grab specified file (glob) from posts folder. 220 | if (the_filename_glob) 221 | full_glob = "*#{the_filename_glob}*.#{$file_extension}" 222 | else 223 | full_glob = "*.#{$file_extension}" 224 | end 225 | filename = first_matching_file_in_dir(full_glob, $posts_dir) 226 | if filename == false 227 | puts "Couldn't find a file to unstage. Aborting." 228 | exit 229 | end 230 | 231 | # Move file to drafts directory, removing date from its filename. 232 | new_filename = filename.sub($file_date_prefix_regexp, "") 233 | 234 | result = FileUtils.move(File.join($posts_dir, filename), File.join($drafts_dir, new_filename)) 235 | 236 | # Move all stashed files back into posts. 237 | integrate() 238 | 239 | puts "Unstaging complete." 240 | end 241 | 242 | 243 | def staging_in_progress() 244 | # Try to determine if file(s) are already staged. 245 | 246 | in_progress = false 247 | if directory_exists($stash_dir) 248 | Dir.chdir($stash_dir) 249 | matches = Dir.glob("*.#{$file_extension}") 250 | if matches.count > 0 251 | # Posts are in the stash directory. We're probably staging. 252 | in_progress = true 253 | end 254 | end 255 | 256 | return in_progress 257 | end 258 | 259 | 260 | def show_help() 261 | puts "`stage FILENAME_GLOB` stages the first matching draft." 262 | puts "`stage #{$unstage_flag}` unstages the first staged post." 263 | puts "`stage #{$unstage_flag} FILENAME_GLOB` unstages the first matching staged post." 264 | end 265 | 266 | 267 | # === End of functions === 268 | 269 | 270 | # Handle input 271 | $unstage_flag = "-u" # flag passed to the command to request unstaging 272 | filename_glob = nil 273 | unstage = false 274 | 275 | if ARGV.count == 0 276 | puts "No filename or arguments specified." 277 | show_help() 278 | exit 279 | elsif ARGV.count == 1 280 | if ARGV[0] == $unstage_flag 281 | # This is the "unstage" flag without a filename. 282 | unstage = true 283 | else 284 | # This is the "stage" command with a filename. 285 | filename_glob = ARGV[0] 286 | end 287 | elsif ARGV.count == 2 288 | # If there are two arguments, the second one is the filename. 289 | filename_glob = ARGV[1] 290 | if ARGV[0] == $unstage_flag 291 | unstage = true 292 | else 293 | puts "Input not recognised." 294 | show_help() 295 | exit 296 | end 297 | end 298 | 299 | # All paths MUST be expanded before we begin. 300 | $drafts_dir = File.expand_path($drafts_dir) 301 | $jekyll_root = File.expand_path($jekyll_root) 302 | $stash_dir = "#{$jekyll_root}/#{$stash_dir_name}" 303 | $posts_dir = "#{$jekyll_root}/#{$posts_dir_name}" 304 | 305 | # Check that drafts, root, and posts directories exist. 306 | if directory_exists($drafts_dir) == false 307 | puts "Drafts directory doesn't exist: #{$drafts_dir}" 308 | exit 309 | end 310 | if directory_exists($jekyll_root) == false 311 | puts "Jekyll root directory doesn't exist: #{$jekyll_root}" 312 | exit 313 | end 314 | if directory_exists($posts_dir) == false 315 | puts "Jekyll posts directory doesn't exist: #{$posts_dir}" 316 | exit 317 | end 318 | 319 | # Ensure stash_dir exists, creating it if necessary. 320 | if directory_exists($stash_dir) == false 321 | if create_dir_if_necessary($stash_dir) == false 322 | # Something went wrong. Abort. 323 | puts "Couldn't create stash directory: #{$stash_dir}" 324 | puts "Aborting." 325 | exit 326 | else 327 | puts "Created stash directory: #{$stash_dir}" 328 | end 329 | end 330 | 331 | # Check whether we should proceed. 332 | if !unstage and staging_in_progress() 333 | puts "It looks like files are already staged. You should unstage them first (stage #{$unstage_flag})." 334 | puts "Do you want to proceed anyway? (y/n) [n]" 335 | proceed = STDIN.gets.strip.downcase.slice(0,1) 336 | if !proceed or proceed != "y" 337 | puts "Aborting." 338 | exit 339 | else 340 | puts "Proceeding anyway." 341 | end 342 | end 343 | 344 | # Get the job done. It's a bit anticlimactic, really. 345 | if unstage 346 | unstage_file(filename_glob) 347 | else 348 | stage_file(filename_glob) 349 | end 350 | --------------------------------------------------------------------------------