mist 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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