mist 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +1 -0
  3. data/.rvmrc +1 -0
  4. data/.travis.yml +13 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +192 -0
  7. data/README.md +108 -0
  8. data/Rakefile +9 -0
  9. data/app/assets/images/mist/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
  10. data/app/assets/images/mist/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
  11. data/app/assets/images/mist/ui-bg_flat_10_000000_40x100.png +0 -0
  12. data/app/assets/images/mist/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  13. data/app/assets/images/mist/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  14. data/app/assets/images/mist/ui-bg_glass_65_ffffff_1x400.png +0 -0
  15. data/app/assets/images/mist/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  16. data/app/assets/images/mist/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  17. data/app/assets/images/mist/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  18. data/app/assets/images/mist/ui-icons_222222_256x240.png +0 -0
  19. data/app/assets/images/mist/ui-icons_228ef1_256x240.png +0 -0
  20. data/app/assets/images/mist/ui-icons_ef8c08_256x240.png +0 -0
  21. data/app/assets/images/mist/ui-icons_ffd27a_256x240.png +0 -0
  22. data/app/assets/images/mist/ui-icons_ffffff_256x240.png +0 -0
  23. data/app/assets/javascripts/mist/jquery-ui-1.8.17.custom.min.js +356 -0
  24. data/app/assets/javascripts/mist/posts.js.coffee +3 -0
  25. data/app/assets/javascripts/mist_core.js +3 -0
  26. data/app/assets/stylesheets/mist/posts.css.sass +97 -0
  27. data/app/assets/stylesheets/mist/scaffolds.css.scss +56 -0
  28. data/app/assets/stylesheets/mist/ui-lightness/jquery-ui-1.8.17.custom.css +565 -0
  29. data/app/assets/stylesheets/mist_core.css +4 -0
  30. data/app/controllers/application_controller.rb +3 -0
  31. data/app/controllers/mist/posts_controller.rb +130 -0
  32. data/app/helpers/mist/posts_helper.rb +61 -0
  33. data/app/mailers/.gitkeep +0 -0
  34. data/app/models/.gitkeep +0 -0
  35. data/app/models/mist/post.rb +240 -0
  36. data/app/models/mist/post_sweeper.rb +63 -0
  37. data/app/views/layouts/mist/posts.html.erb +66 -0
  38. data/app/views/mist/posts/_form.html.erb +55 -0
  39. data/app/views/mist/posts/_post.html.erb +19 -0
  40. data/app/views/mist/posts/_sidebar_title.html.erb +5 -0
  41. data/app/views/mist/posts/edit.html.erb +16 -0
  42. data/app/views/mist/posts/feed.atom.builder +17 -0
  43. data/app/views/mist/posts/index.html.erb +31 -0
  44. data/app/views/mist/posts/new.html.erb +15 -0
  45. data/app/views/mist/posts/show.html.erb +3 -0
  46. data/config/cucumber.yml +8 -0
  47. data/config/environment.rb +1 -0
  48. data/config/routes.rb +7 -0
  49. data/features/atom_feed.feature +20 -0
  50. data/features/authorize_create_post.feature +14 -0
  51. data/features/authorize_destroy_post.feature +27 -0
  52. data/features/authorize_update_post.feature +28 -0
  53. data/features/authorize_view_unpublished.feature +22 -0
  54. data/features/create_post.feature +13 -0
  55. data/features/destroy_post.feature +13 -0
  56. data/features/format_with_markdown.feature +13 -0
  57. data/features/popular_posts.feature +35 -0
  58. data/features/recent_posts.feature +28 -0
  59. data/features/similar_posts.feature +43 -0
  60. data/features/step_definitions/authorization_steps.rb +17 -0
  61. data/features/step_definitions/filesystem_steps.rb +13 -0
  62. data/features/step_definitions/page_steps.rb +43 -0
  63. data/features/step_definitions/post_steps.rb +58 -0
  64. data/features/step_definitions/sidebar_steps.rb +21 -0
  65. data/features/support/env.rb +58 -0
  66. data/features/support/setup.rb +15 -0
  67. data/features/update_post.feature +19 -0
  68. data/gemfiles/common +20 -0
  69. data/gemfiles/rails-3.1.3 +4 -0
  70. data/gemfiles/rails-3.1.3.lock +195 -0
  71. data/gemfiles/rails-3.2.0 +4 -0
  72. data/lib/generators/mist/USAGE +13 -0
  73. data/lib/generators/mist/setup_generator.rb +21 -0
  74. data/lib/generators/mist/templates/initializer.rb +17 -0
  75. data/lib/generators/mist/templates/mist.css.scss +2 -0
  76. data/lib/generators/mist/templates/mist.js.coffee +1 -0
  77. data/lib/generators/mist/views_generator.rb +11 -0
  78. data/lib/mist.rb +26 -0
  79. data/lib/mist/code_example_parser.rb +86 -0
  80. data/lib/mist/configuration.rb +68 -0
  81. data/lib/mist/configuration/author.rb +8 -0
  82. data/lib/mist/engine.rb +12 -0
  83. data/lib/mist/git_file_system_history.rb +66 -0
  84. data/lib/mist/git_model.rb +154 -0
  85. data/lib/mist/git_model/attributes.rb +21 -0
  86. data/lib/mist/git_model/class_methods.rb +141 -0
  87. data/lib/mist/permalink.rb +5 -0
  88. data/lib/mist/repository.rb +15 -0
  89. data/lib/mist/version.rb +8 -0
  90. data/mist.gemspec +33 -0
  91. data/spec/controllers/posts_controller_spec.rb +263 -0
  92. data/spec/dummy_rails_app/app/assets/javascripts/mist.js.coffee +1 -0
  93. data/spec/dummy_rails_app/app/assets/stylesheets/mist.css.scss +2 -0
  94. data/spec/dummy_rails_app/app/views/layouts/mist/posts.html.erb +60 -0
  95. data/spec/dummy_rails_app/config.ru +4 -0
  96. data/spec/dummy_rails_app/config/application.rb +48 -0
  97. data/spec/dummy_rails_app/config/boot.rb +7 -0
  98. data/spec/dummy_rails_app/config/database.yml +28 -0
  99. data/spec/dummy_rails_app/config/environment.rb +5 -0
  100. data/spec/dummy_rails_app/config/environments/development.rb +32 -0
  101. data/spec/dummy_rails_app/config/environments/production.rb +60 -0
  102. data/spec/dummy_rails_app/config/environments/test.rb +42 -0
  103. data/spec/dummy_rails_app/config/initializers/active_gist_credentials.rb +8 -0
  104. data/spec/dummy_rails_app/config/initializers/backtrace_silencers.rb +7 -0
  105. data/spec/dummy_rails_app/config/initializers/inflections.rb +10 -0
  106. data/spec/dummy_rails_app/config/initializers/mime_types.rb +5 -0
  107. data/spec/dummy_rails_app/config/initializers/mist.rb +17 -0
  108. data/spec/dummy_rails_app/config/initializers/secret_token.rb +7 -0
  109. data/spec/dummy_rails_app/config/initializers/session_store.rb +8 -0
  110. data/spec/dummy_rails_app/config/initializers/wrap_parameters.rb +14 -0
  111. data/spec/dummy_rails_app/config/locales/en.yml +5 -0
  112. data/spec/dummy_rails_app/config/routes.rb +3 -0
  113. data/spec/dummy_rails_app/db/schema.rb +22 -0
  114. data/spec/dummy_rails_app/db/seeds.rb +7 -0
  115. data/spec/dummy_rails_app/doc/README_FOR_APP +2 -0
  116. data/spec/dummy_rails_app/lib/assets/.gitkeep +0 -0
  117. data/spec/dummy_rails_app/lib/tasks/.gitkeep +0 -0
  118. data/spec/dummy_rails_app/lib/tasks/cucumber.rake +65 -0
  119. data/spec/dummy_rails_app/public/404.html +26 -0
  120. data/spec/dummy_rails_app/public/422.html +26 -0
  121. data/spec/dummy_rails_app/public/500.html +26 -0
  122. data/spec/dummy_rails_app/public/favicon.ico +0 -0
  123. data/spec/dummy_rails_app/public/robots.txt +5 -0
  124. data/spec/dummy_rails_app/script/cucumber +10 -0
  125. data/spec/dummy_rails_app/script/rails +6 -0
  126. data/spec/dummy_rails_app/vendor/assets/stylesheets/.gitkeep +0 -0
  127. data/spec/dummy_rails_app/vendor/plugins/.gitkeep +0 -0
  128. data/spec/factories/posts.rb +8 -0
  129. data/spec/fixtures/gist_404 +6 -0
  130. data/spec/fixtures/gist_with_1_code_example +66 -0
  131. data/spec/helpers/posts_helper_spec.rb +64 -0
  132. data/spec/lib/mist/code_example_parser_spec.rb +135 -0
  133. data/spec/lib/mist/configuration_spec.rb +88 -0
  134. data/spec/lib/mist/permalink_spec.rb +17 -0
  135. data/spec/lib/mist/repository_spec.rb +20 -0
  136. data/spec/models/mist/git_model_spec.rb +260 -0
  137. data/spec/models/mist/post_spec.rb +575 -0
  138. data/spec/requests/posts_caching_spec.rb +152 -0
  139. data/spec/spec_helper.rb +20 -0
  140. data/spec/support/cache_helpers.rb +71 -0
  141. data/spec/support/config.rb +42 -0
  142. data/spec/support/fakeweb.rb +3 -0
  143. data/spec/views/mist/posts/edit.html.erb_spec.rb +11 -0
  144. data/spec/views/mist/posts/index.html.erb_spec.rb +27 -0
  145. data/spec/views/mist/posts/new.html.erb_spec.rb +15 -0
  146. data/spec/views/mist/posts/show.html.erb_spec.rb +11 -0
  147. metadata +371 -0
@@ -0,0 +1,4 @@
1
+ common = File.expand_path('common', File.dirname(__FILE__))
2
+ eval File.read(common), binding, common
3
+
4
+ gem 'rails', '= 3.2.0'
@@ -0,0 +1,13 @@
1
+ Description:
2
+ Installs Mist into your Rails application.
3
+
4
+ Next Steps:
5
+ After running this generator, edit config/initializers/mist.rb
6
+ and hook up your authentication so that you can control who is
7
+ or isn't allowed to change posts.
8
+
9
+ Mist extracts its views and assets into your app/ directory.
10
+ You should personalize the look and feel of the blog by editing
11
+ those views.
12
+
13
+
@@ -0,0 +1,21 @@
1
+ class Mist::SetupGenerator < Rails::Generators::Base
2
+ source_root File.expand_path('../../..', File.dirname(__FILE__))
3
+
4
+ def copy_views
5
+ copy_file 'app/views/layouts/mist/posts.html.erb'
6
+ end
7
+
8
+ def install_assets
9
+ copy_file template_path('mist.js.coffee'), 'app/assets/javascripts/mist.js.coffee'
10
+ copy_file template_path('mist.css.scss'), 'app/assets/stylesheets/mist.css.scss'
11
+ end
12
+
13
+ def create_initializer
14
+ copy_file template_path('initializer.rb'), 'config/initializers/mist.rb'
15
+ end
16
+
17
+ private
18
+ def template_path(relative_path)
19
+ File.expand_path(File.join('templates', relative_path), File.dirname(__FILE__))
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ # This hook is called whenever Mist needs to know if a user
2
+ # is allowed to do something. You can also pass in specific
3
+ # actions such as :create_post, :edit_post and so on. See
4
+ # documentation for details.
5
+ Mist.authorize :all do |controller|
6
+ true
7
+ end
8
+
9
+
10
+ Mist.title = "Blog Title"
11
+ Mist.author.name = "John Doe"
12
+ Mist.author.email = "john@example.com"
13
+
14
+ # GitHub credentails -- without these you can't edit Gists
15
+ # because they're anonymous
16
+ ActiveGist::API.username = ENV['GITHUB_USERNAME']
17
+ ActiveGist::API.password = ENV['GITHUB_PASSWORD']
@@ -0,0 +1,2 @@
1
+ // You can place stylesheets related to Mist here.
2
+ // Styles here will override the Mist defaults.
@@ -0,0 +1 @@
1
+ # You can place customized Mist JavaScripts here.
@@ -0,0 +1,11 @@
1
+ class Mist::ViewsGenerator < Rails::Generators::Base
2
+ source_root File.expand_path('../../..', File.dirname(__FILE__))
3
+
4
+ def copy_views
5
+ directory 'app/views/mist'
6
+ end
7
+
8
+ def install_assets
9
+ directory 'app/assets'
10
+ end
11
+ end
data/lib/mist.rb ADDED
@@ -0,0 +1,26 @@
1
+ require 'git'
2
+ require 'github/markup'
3
+ require 'active_gist'
4
+
5
+ module Mist
6
+ require 'mist/engine'
7
+ require 'mist/configuration'
8
+ require 'mist/repository'
9
+ require 'mist/version'
10
+ require 'mist/permalink'
11
+ require 'mist/git_model'
12
+ require "mist/code_example_parser"
13
+
14
+ extend Mist::Configuration
15
+ extend Mist::Repository
16
+
17
+ class << self
18
+ delegate :log, :to => :repository
19
+
20
+ # Returns an array of all possible actions which Mist will authorize against
21
+ # at one point or another
22
+ def authorized_actions
23
+ [ :create_post, :edit_post, :destroy_post, :view_drafts ]
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,86 @@
1
+ class Mist::CodeExampleParser
2
+ attr_reader :examples, :content
3
+
4
+ class Example < String
5
+ attr_accessor :start_offset
6
+ attr_accessor :whitespace_skipped
7
+ attr_writer :filename
8
+ FILENAME_REGEXP = /\A[\s\t\n]*file: (.*?)[\n\r]+/
9
+
10
+ def initialize(start_offset)
11
+ @whitespace_skipped = 0
12
+ @start_offset = start_offset
13
+ super()
14
+ end
15
+
16
+ def explicit_filename
17
+ @explicit_filename ||= if self =~ FILENAME_REGEXP
18
+ # omit filename from self
19
+ match = $~
20
+ @whitespace_skipped += match.offset(0)[1] - match.offset(0)[0]
21
+ sub! match[0], ''
22
+ match[1]
23
+ else
24
+ nil
25
+ end
26
+ end
27
+
28
+ def filename
29
+ @filename || explicit_filename
30
+ end
31
+
32
+ def offset
33
+ start_offset...(start_offset+length+whitespace_skipped)
34
+ end
35
+ end
36
+
37
+ def initialize(content)
38
+ @content = content.to_s.dup
39
+ @examples = parse
40
+ end
41
+
42
+ def parse
43
+ in_example = false
44
+ offset = 0
45
+
46
+ [].tap do |examples|
47
+ while match = /^ ([^\n]+(\n|\z))/m.match(content[offset..-1])
48
+ in_example = examples.last && match.offset(0)[0] == 0 && offset > 0
49
+ examples << Example.new(match.offset(0)[0]+offset) unless in_example
50
+ line = match[1]
51
+ examples.last.whitespace_skipped += 4
52
+ examples.last.concat match[1]
53
+
54
+ offset += match.offset(0)[1]
55
+ end
56
+
57
+ # detect multiple examples separated only by white space
58
+ # -- they are part of the same example
59
+ pass = proc do
60
+ call_again = false
61
+ examples.length.times do |index|
62
+ current = examples[index]
63
+ next if index == 0 or current.explicit_filename
64
+
65
+ previous = examples[index-1]
66
+ min = previous.offset.max
67
+ max = current.offset.min
68
+
69
+ if content[(min+1)...max] =~ /\A[\s\t\n]*\z/
70
+ previous.whitespace_skipped += current.whitespace_skipped
71
+ previous.concat $~[0] + current
72
+ examples.delete current
73
+ call_again = true
74
+ break
75
+ end
76
+ end
77
+
78
+ pass.call if call_again
79
+ end
80
+ pass.call
81
+
82
+ # Last pass: assign default filenames as needed
83
+ examples.each_with_index { |example, index| example.filename ||= "Example #{index+1}" }
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,68 @@
1
+ module Mist::Configuration
2
+ require 'mist/configuration/author'
3
+
4
+ def repository_location
5
+ @respository_location ||= default_repository_location
6
+ end
7
+
8
+ def repository_location=(dir)
9
+ @respository_location = dir
10
+ end
11
+
12
+ def default_repository_location
13
+ Rails.root.join("db/mist.repo.#{Rails.env}")
14
+ end
15
+
16
+ def reset_authorizations!
17
+ authorizations.clear
18
+ end
19
+
20
+ def authorizations
21
+ @authorizations ||= {}.with_indifferent_access
22
+ end
23
+
24
+ def commit_meta_data
25
+ @commit_meta_data = true if @commit_meta_data.nil?
26
+ end
27
+
28
+ def commit_meta_data=(a)
29
+ @commit_meta_data = a
30
+ end
31
+
32
+ def author
33
+ @author ||= Mist::Configuration::Author.new
34
+ end
35
+
36
+ def title=(title)
37
+ @title = title
38
+ end
39
+
40
+ def title
41
+ @title ||= "Blog Title"
42
+ end
43
+
44
+ # Register a block to be invoked whenever Mist needs to know if a user is allowed
45
+ # to perform some action, such as :create_post, :edit_post, :view_post, :destroy_post,
46
+ # or :all.
47
+ #
48
+ # Mist.authorize { |controller| ... } # invoke for any action not otherwise registered
49
+ # Mist.authorize(:all) { |controller| ... } # same as above
50
+ # Mist.authorize(:create_post) { |controller| ... } # use this block only for :create_post
51
+ #
52
+ # By default, all authorization requests will return false (denied).
53
+ #
54
+ def authorize(*types, &block)
55
+ raise ArgumentError, "Expected a block which will be evaluated at runtime" unless block_given?
56
+ types.push :all if types.empty?
57
+ types.each { |type| authorizations[type] = block }
58
+ end
59
+
60
+ # Invokes the blocks registered by Mist::Configuration#authorize to see if the specified
61
+ # action is authorized. Passes *args into the block.
62
+ def authorized?(action, *args)
63
+ if authorizations.key?(action) then authorizations[action].call *args
64
+ elsif authorizations.key?(:all) then authorizations[:all].call *args
65
+ else nil
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,8 @@
1
+ class Mist::Configuration::Author
2
+ attr_accessor :name, :email
3
+
4
+ def initialize
5
+ @name = "John Doe"
6
+ @email = "john@example.com"
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ require 'rails'
2
+
3
+ module Mist
4
+ class Engine < Rails::Engine
5
+ engine_name 'mist'
6
+ isolate_namespace Mist
7
+
8
+ initializer "mist.assets" do |app|
9
+ app.config.assets.precompile += ['mist_core.js', 'mist_core.css', 'mist.js', 'mist.css']
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,66 @@
1
+ class Mist::GitFileSystemHistory
2
+ def initialize(path_or_repo)
3
+ @files = ActiveSupport::OrderedHash.new
4
+ @repo = path_or_repo.kind_of?(Git::Base) ? path_or_repo : Git.open(path_or_repo)
5
+ end
6
+
7
+ # Returns files in order of discovery. Since they are discovered
8
+ # by traveling up the tree and backward in time, this is the
9
+ # REVERSE of the order of their actual creation.
10
+ def files
11
+ @files.keys.select { |filename| @files[filename] > 0 }
12
+ end
13
+
14
+ def mark_file(path, mode)
15
+ @files[path] ||= 0
16
+ case mode
17
+ when :created then @files[path] += 1
18
+ when :deleted then @files[path] -= 1
19
+ end
20
+ end
21
+
22
+ def mark_tree(tree, mode, path = /./)
23
+ tree.full_tree.each do |entry|
24
+ file = entry.split(/\t/)[1]
25
+ next unless file[path]
26
+ mark_file file, mode
27
+ end
28
+ end
29
+
30
+ # Finds and returns `count` files, in the order of creation,
31
+ # in descending order (most recent files first),
32
+ # after accounting for deletion.
33
+ #
34
+ # If count is nil, all matches will be returned.
35
+ def find(count, path = /./)
36
+ begin
37
+ commit = @repo.log(1).first
38
+ rescue Git::GitExecuteError => err
39
+ # empty repository, no commits = no matches
40
+ return [] if err.message =~ /bad default revision 'HEAD'/
41
+ raise err
42
+ end
43
+
44
+ while commit.parent
45
+ commit.diff_parent.each do |diff|
46
+ next if diff.type == 'modified' # only care about new or deleted files
47
+ next unless diff.path[path]
48
+
49
+ case diff.type # because parent is B, changes are reversed.
50
+ when 'deleted' then mark_file diff.path, :created # file was created in this commit
51
+ when 'new' then mark_file diff.path, :deleted # file was deleted in this commit
52
+ end
53
+
54
+ return files if count and files.length == count
55
+ end
56
+
57
+ commit = commit.parent
58
+ end
59
+
60
+ # if we made it this far then commit is the initial commit
61
+ # so at this stage, mark each file in the tree.
62
+ mark_tree commit.gtree, :created, path
63
+
64
+ count && files.length > count ? files[0...count] : files
65
+ end
66
+ end
@@ -0,0 +1,154 @@
1
+ class Mist::GitModel
2
+ require 'mist/git_model/attributes'
3
+ require 'mist/git_model/class_methods'
4
+ require 'mist/git_file_system_history'
5
+
6
+ extend ActiveModel::Callbacks
7
+ extend ActiveModel::Naming
8
+ include ActiveModel::Validations
9
+ include ActiveModel::Validations::Callbacks
10
+ include ActiveModel::Dirty
11
+ include ActiveModel::Conversion
12
+ include ActiveModel::Observing
13
+ extend Mist::GitModel::ClassMethods
14
+
15
+ define_model_callbacks :save, :create, :update, :initialize, :destroy
16
+
17
+ delegate :table_name, :record_path, :default_attributes, :to => 'self.class'
18
+ attribute :id, :default => proc { (self.class.count + 1).to_s }
19
+ attribute :id_on_record
20
+
21
+ validate do |record|
22
+ unless record.id.blank?
23
+ if record.id_changed? and self.class.find(record.id)
24
+ record.errors.add :id, "has already been taken"
25
+ end
26
+ end
27
+ end
28
+
29
+ def initialize(attributes = {})
30
+ run_callbacks :initialize do
31
+ default_attributes!
32
+ attributes.each do |key, value|
33
+ self.send(:"#{key}=", value)
34
+ end
35
+ end
36
+ end
37
+
38
+ def class_name
39
+ self.class.name
40
+ end
41
+
42
+ def ==(other)
43
+ id == (other.kind_of?(self.class) ? other.id : other)
44
+ end
45
+
46
+ def commit_message
47
+ if new_record?
48
+ "Post created"
49
+ else
50
+ "Post updated"
51
+ end
52
+ end
53
+
54
+ def commit(commit_message = self.commit_message)
55
+ Mist.repository.add path
56
+ Mist.repository.commit commit_message
57
+ end
58
+
59
+ def path
60
+ record_path id
61
+ end
62
+
63
+ def path_was
64
+ record_path id_was
65
+ end
66
+
67
+ def destroy
68
+ return if new_record?
69
+ run_callbacks :destroy do
70
+ Mist.repository.remove path, :recursive => true
71
+ Mist.repository.commit "Destroyed #{class_name} #{id.inspect}"
72
+ FileUtils.rm_rf path
73
+ end
74
+ end
75
+
76
+ def update_attributes(attributes)
77
+ attributes.each { |k,v| self.send(:"#{k}=", v) }
78
+ save
79
+ end
80
+
81
+ def attributes
82
+ @attributes ||= Mist::GitModel::Attributes.new(self)
83
+ end
84
+
85
+ def default_attributes!
86
+ default_attributes.each do |key, value|
87
+ if value.kind_of?(Proc)
88
+ attributes[key] = instance_eval &value
89
+ else
90
+ attributes[key] = value
91
+ end
92
+ end
93
+
94
+ changed_attributes.clear
95
+ end
96
+
97
+ def persisted?
98
+ !changed? && !new_record?
99
+ end
100
+
101
+ def new_record?
102
+ id_was.blank? || !File.file?(path_was)
103
+ end
104
+
105
+ def save
106
+ return false unless valid?
107
+
108
+ if new_record? || changed?
109
+ create_or_update_callback = new_record? ? :create : :update
110
+ run_callbacks create_or_update_callback do
111
+ run_callbacks :save do
112
+ Mist.repository.lib.mv path_was, path if !new_record? && id_changed?
113
+ save_record_file
114
+ commit
115
+ end
116
+ end
117
+ end
118
+
119
+ if !(files = Mist.repository.lib.diff_files).empty?
120
+ # if false, there's nothing we can reliably do to sync up unless we
121
+ # forcefully commit all changes, which the user may not want. However,
122
+ # if true, then this record was probably renamed from id_on_record
123
+ # to id. So, commit the deletion.
124
+ old_path = record_path(id_on_record).to_s.sub(/^#{Regexp::escape Mist.repository_location.to_s}\/?/, '')
125
+ if files.keys.include?(old_path) && files[old_path][:type] == 'D'
126
+ save_record_file
127
+ Mist.repository.lib.remove old_path
128
+ Mist.repository.lib.add path
129
+ Mist.repository.commit "Syncing to external filesystem changes"
130
+ else
131
+ Rails.logger.warn "Found uncommitted changes in git repo but didn't recognize them, so didn't commit them"
132
+ # no need to show the diff in the log, as the Git library has done that for us
133
+ end
134
+ end
135
+
136
+ @previously_changed = changes
137
+ changed_attributes.clear
138
+ true
139
+ end
140
+
141
+ def save_record_file
142
+ self.id_on_record = id
143
+ FileUtils.mkdir_p File.dirname(path)
144
+ File.open(path, "w") { |f| f.print attributes.to_yaml }
145
+ end
146
+
147
+ def save!
148
+ raise "Record not saved: #{errors.full_messages.join('; ')}" unless save
149
+ end
150
+
151
+ def inspect
152
+ "#<#{self.class.name} #{attributes.collect { |a| [a[0], a[1].inspect].join('=') }.join(' ')}>"
153
+ end
154
+ end