gemstash 1.0.0.pre.1 → 1.0.0.pre.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a7a9b80d9f2ba68627a8d7fff3311e0fcaeca9ed
4
- data.tar.gz: ea7e01d9fc2c9390ddf93f04a5623418f92408e7
3
+ metadata.gz: 323e0282020fd74fa61840b2492370fd1e9b4241
4
+ data.tar.gz: 3e3d5e4569c9e46f81fcb383f3001d01421f3cf2
5
5
  SHA512:
6
- metadata.gz: d7813e9f3b7ecfa1878456519437e8191cfac94f90294dcab6778a6b5e33a268db876d60987c567bba07ba60d02b2d16a05e3ebac0713b06e2f4c374b24d6bb5
7
- data.tar.gz: 64ba438731606e20c748af9b561012604ae4d700686bc1555d5de08fd5666f40a5ce26ff2ed726c91372a739d26b9468d544f925488d0e77cb42bb03a339b281
6
+ metadata.gz: 4132d846fbbc750db4934f61acf625bb8fd5fe96c5b9b902f48e8a0630725b6af14dc1128fdcf12f875b242fc97a035c54ed782c163fc8f70b2deb0c27b02729
7
+ data.tar.gz: d67634e744f569a2fc3e677c8a7e0db65dfecab5d67804fb5aacf4e49c9c5071cc9b0e8d5581816b9d118fd070819531bac077f2c9ede20d40f66a1d3741a1ce
data/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ ## 1.0.0.pre.2 (2015-12-14)
2
+
3
+ ### Upgrade Notes
4
+
5
+ - If you pushed any private gems to your Gemstash instance, you will need to run: https://gist.github.com/smellsblue/53f5a6757dcc91ad10bc
6
+
7
+ ### Bugfixes
8
+
9
+ - Add --pre option to gemstash installation documentation ([#54](https://github.com/bundler/gemstash/pull/54), [@farukaydin](https://github.com/farukaydin))
10
+ - Fix docs for `gemstash authorize` ([#59](https://github.com/bundler/gemstash/pull/59), [@farukaydin](https://github.com/farukaydin))
11
+ - Refactoring, changed resource metadata `:gemstash_storage_version` to use `:gemstash_resource_version` ([#60](https://github.com/bundler/gemstash/pull/60), [@smellsblue](https://github.com/smellsblue))
12
+ - Fix migrations for utf8 on MySQL >= 5.5 ([#64](https://github.com/bundler/gemstash/pull/64), [@chriseckhardt](https://github.com/chriseckhardt))
13
+
14
+ ### Features
15
+
16
+ - Support MySQL as DB backend ([#52](https://github.com/bundler/gemstash/pull/52), [@pcarranza](https://github.com/pcarranza))
17
+ - Add start/stop output ([#58](https://github.com/bundler/gemstash/pull/58), [@farukaydin](https://github.com/farukaydin))
18
+ - Add `gemstash --version` ([#62](https://github.com/bundler/gemstash/pull/62), [@smellsblue](https://github.com/smellsblue))
19
+ - Create the CHANGELOG ([#63](https://github.com/bundler/gemstash/pull/63), [@smellsblue](https://github.com/smellsblue))
20
+
21
+ ## 1.0.0.pre.1 (2015-11-30)
22
+
23
+ ### Features
24
+
25
+ - Cache gems from multiple sources
26
+ - Push, yank, and unyank private gems
27
+ - Zero setup dependencies
28
+ - Optionally use Memcached for caching or PostgreSQL for the database
data/README.md CHANGED
@@ -28,12 +28,14 @@ Quickstart Guide, you will be able to bundle stashed gems from public sources
28
28
  against a Gemstash server running on your machine.
29
29
 
30
30
  Install Gemstash to get started:
31
+
31
32
  ```
32
- $ gem install gemstash
33
+ $ gem install gemstash --pre
33
34
  ```
34
35
 
35
36
  After it is installed, starting Gemstash requires no additional steps. Simply
36
37
  start the Gemstash server with the `gemstash` command:
38
+
37
39
  ```
38
40
  $ gemstash start
39
41
  ```
@@ -43,17 +45,24 @@ will run the server in the background by default. The server runs on port 9292.
43
45
 
44
46
  ### Bundling
45
47
 
46
- With the server running, you can bundle against it. Create a simple `Gemfile`
47
- like the following:
48
+ With the server running, you can bundle against it. Tell Bundler that you want
49
+ to use Gemstash to find gems from RubyGems.org:
50
+
51
+ ```
52
+ $ bundle config mirror.https://rubygems.org http://localhost:9292
53
+ ```
54
+
55
+ Now you can create a Gemfile and install gems through Gemstash:
56
+
48
57
  ```ruby
49
58
  # ./Gemfile
50
- source "http://localhost:9292"
59
+ source "https://rubygems.org"
51
60
  gem "rubywarrior"
52
61
  ```
53
62
 
54
- The first line is important, as it will tell Bundler to use your new Gemstash
55
- server. The gems you include should be gems you don't yet have installed,
63
+ The gems you include should be gems you don't yet have installed,
56
64
  otherwise Gemstash will have nothing to stash. Now bundle:
65
+
57
66
  ```
58
67
  $ bundle install --path .bundle
59
68
  ```
@@ -63,6 +72,7 @@ cached them for you! To prove this, you can disable your Internet connection and
63
72
  try again. The gem dependencies from https://www.rubygems.org are cached for 30
64
73
  minutes, so if you bundle again before that, you can successfully bundle without
65
74
  an Internet connection:
75
+
66
76
  ```
67
77
  $ # Disable your Internet first!
68
78
  $ rm -rf Gemfile.lock .bundle
@@ -73,10 +83,18 @@ $ bundle
73
83
 
74
84
  Once you've finish using your Gemstash server, you can stop it just as easily as
75
85
  you started it:
86
+
76
87
  ```
77
88
  $ gemstash stop
78
89
  ```
79
90
 
91
+ You'll also want to tell Bundler that it can go back to getting gems from
92
+ RubyGems.org directly, instead of going through Gemstash:
93
+
94
+ ```
95
+ $ bundle config --delete mirror.https://rubygems.org
96
+ ```
97
+
80
98
  ### Under the Hood
81
99
 
82
100
  You might wonder where the gems are stored. After running the commands above,
@@ -118,6 +136,10 @@ For an anatomy of various configuration and commands, follow the links:
118
136
  * [Stop](docs/reference.md#stop)
119
137
  * [Status](docs/reference.md#status)
120
138
  * [Setup](docs/reference.md#setup)
139
+ * [Version](docs/reference.md#version)
140
+
141
+ To see what has changed in recent versions of Gemstash, see the
142
+ [CHANGELOG](CHANGELOG.md).
121
143
 
122
144
  ## Development
123
145
 
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
3
  require "rubocop/rake_task"
4
+ require_relative "rake/changelog.rb"
5
+ require_relative "rake/table_of_contents.rb"
4
6
 
5
7
  RuboCop::RakeTask.new
6
8
 
@@ -9,27 +11,15 @@ RSpec::Core::RakeTask.new(:spec) do |t|
9
11
  t.rspec_opts = %w(--color)
10
12
  end
11
13
 
12
- task :spec => :rubocop
13
- task :default => :spec
14
+ task spec: :rubocop
15
+ task default: :spec
14
16
 
15
17
  desc "Generate Table of Contents for certain docs"
16
18
  task :toc do
17
- toc_dir = File.expand_path("../tmp/", __FILE__)
18
- toc = File.join(toc_dir, "gh-md-toc")
19
-
20
- unless File.exist?(toc)
21
- require "open-uri"
22
- toc_contents = open("https://raw.githubusercontent.com/ekalinin/github-markdown-toc/master/gh-md-toc", &:read)
23
- Dir.mkdir(toc_dir) unless Dir.exist?(toc_dir)
24
- File.write(toc, toc_contents)
25
- File.chmod(0776, toc)
26
- end
19
+ TableOfContents.new.run
20
+ end
27
21
 
28
- doc = File.expand_path("../docs/reference.md", __FILE__)
29
- old_contents = File.read(doc)
30
- old_contents.sub!(/\A.*?^---$/m, "---")
31
- File.write(doc, old_contents)
32
- toc_contents = `"#{toc}" "#{doc}"`
33
- toc_contents.sub!(/Created by.*$/, "")
34
- File.write(doc, "#{toc_contents}\n#{old_contents}")
22
+ desc "Update ChangeLog based on commits in master"
23
+ task :changelog do
24
+ Changelog.new.run
35
25
  end
data/docs/private_gems.md CHANGED
@@ -14,7 +14,7 @@ key against your server. Instead of the key value here, use whatever key is
14
14
  generated from running the commands.
15
15
 
16
16
  In order to push a gem to your Gemstash server, you need to first create an API
17
- key. Utilize the `gemstash` command to create the API key:
17
+ key. Utilize the `gemstash authorize` command to create the API key:
18
18
  ```
19
19
  $ gemstash authorize
20
20
  Your new key is: e374e237fdf5fa5718d2a21bd63dc911
data/docs/reference.md CHANGED
@@ -37,6 +37,8 @@ Table of Contents
37
37
  * [--redo](#--redo)
38
38
  * [--debug](#--debug)
39
39
  * [--config-file](#--config-file-4)
40
+ * [Version](#version)
41
+ * [Usage](#usage-5)
40
42
 
41
43
 
42
44
 
@@ -182,6 +184,7 @@ Specify the API key to affect. This should be the actual key value, not a name.
182
184
  This option is required when using `--remove` but is optional otherwise. If
183
185
  adding an authorization, using this will either create or update the permissions
184
186
  for the specified API key. If missing, a new API key will always be generated.
187
+ Note that a key can only have a maximum length of 255 chars.
185
188
 
186
189
  #### --remove
187
190
 
@@ -303,6 +306,18 @@ Specify the config file to write to. Without this option, your configuration
303
306
  will be written to `~/.gemstash/config.yml`. If you write to a custom location,
304
307
  you will need to pass the `--config-file` option to all Gemstash commands.
305
308
 
309
+ ## Version
310
+
311
+ Show what version of Gemstash you are using.
312
+
313
+ ### Usage
314
+
315
+ ```
316
+ gemstash version
317
+ gemstash --version
318
+ gemstash -v
319
+ ```
320
+
306
321
  ---
307
322
 
308
323
  Table of contents thanks to [gh-md-toc](https://github.com/ekalinin/github-markdown-toc).
data/gemstash.gemspec CHANGED
@@ -40,6 +40,8 @@ you push your own private gems as well."
40
40
  end
41
41
 
42
42
  spec.add_development_dependency "bundler", "~> 1.10"
43
+ spec.add_development_dependency "citrus", "~> 3.0"
44
+ spec.add_development_dependency "octokit", "~> 4.2"
43
45
  spec.add_development_dependency "rack-test", "~> 0.6"
44
46
  spec.add_development_dependency "rake", "~> 10.0"
45
47
  spec.add_development_dependency "rspec", "~> 3.3"
data/lib/gemstash.rb CHANGED
@@ -17,6 +17,7 @@ module Gemstash
17
17
  autoload :LruReduxClient, "gemstash/cache"
18
18
  autoload :NotAuthorizedError, "gemstash/authorization"
19
19
  autoload :RackEnvRewriter, "gemstash/rack_env_rewriter"
20
+ autoload :Resource, "gemstash/storage"
20
21
  autoload :SpecsBuilder, "gemstash/specs_builder"
21
22
  autoload :Storage, "gemstash/storage"
22
23
  autoload :Upstream, "gemstash/upstream"
data/lib/gemstash/cli.rb CHANGED
@@ -67,5 +67,11 @@ module Gemstash
67
67
  def stop
68
68
  Gemstash::CLI::Stop.new(self).run
69
69
  end
70
+
71
+ desc "version", "Prints gemstash version information"
72
+ def version
73
+ say "Gemstash version #{Gemstash::VERSION}"
74
+ end
75
+ map %w(-v --version) => :version
70
76
  end
71
77
  end
@@ -35,7 +35,9 @@ module Gemstash
35
35
  def check_gemstash_version
36
36
  version = Gem::Version.new(Gemstash::Storage.metadata[:gemstash_version])
37
37
  return if Gem::Requirement.new("<= #{Gemstash::VERSION}").satisfied_by?(Gem::Version.new(version))
38
- raise Gemstash::CLI::Error.new(@cli, "Gemstash version is too old")
38
+ raise Gemstash::CLI::Error.new(@cli, "Gemstash version #{Gemstash::VERSION} does not support version " \
39
+ "#{version}.\nIt appears you may have downgraded Gemstash, please " \
40
+ "install version #{version} or later.")
39
41
  end
40
42
 
41
43
  def pidfile_args
@@ -77,27 +77,27 @@ module Gemstash
77
77
 
78
78
  def ask_database
79
79
  say_current_config(:db_adapter, "Current database adapter")
80
- options = %w(sqlite3 postgres)
80
+ options = %w(sqlite3 postgres mysql)
81
81
  database = nil
82
82
 
83
83
  until database
84
- database = @cli.ask "What database adapter? [SQLITE3, postgres]"
84
+ database = @cli.ask "What database adapter? [SQLITE3, postgres, mysql]"
85
85
  database = database.downcase
86
86
  database = "sqlite3" if database.empty?
87
87
  database = nil unless options.include?(database)
88
88
  end
89
89
 
90
90
  @config[:db_adapter] = database
91
- ask_postgres_details if database == "postgres"
91
+ ask_database_details(database) unless database == "sqlite3"
92
92
  end
93
93
 
94
- def ask_postgres_details
94
+ def ask_database_details(database)
95
95
  say_current_config(:db_url, "Current database url")
96
96
 
97
97
  if RUBY_PLATFORM == "java"
98
- default_value = "jdbc:postgres:///gemstash"
98
+ default_value = "jdbc:#{database}:///gemstash"
99
99
  else
100
- default_value = "postgres:///gemstash"
100
+ default_value = "#{database}:///gemstash"
101
101
  end
102
102
 
103
103
  url = @cli.ask "Where is the database? [#{default_value}]"
@@ -10,6 +10,7 @@ module Gemstash
10
10
  prepare
11
11
  setup_logging
12
12
  store_daemonized
13
+ @cli.say("Starting gemstash!", :green)
13
14
  Puma::CLI.new(args, Gemstash::Logging::StreamLogger.puma_events).run
14
15
  end
15
16
 
@@ -9,6 +9,7 @@ module Gemstash
9
9
  def run
10
10
  prepare
11
11
  Puma::ControlCLI.new(args).run
12
+ @cli.say("Gemstash stopped successfully!", :green)
12
13
  end
13
14
 
14
15
  private
data/lib/gemstash/env.rb CHANGED
@@ -113,7 +113,7 @@ module Gemstash
113
113
  else
114
114
  db = Sequel.connect("sqlite://#{URI.escape(db_path)}", max_connections: 1)
115
115
  end
116
- when "postgres"
116
+ when "postgres", "mysql"
117
117
  db = Sequel.connect(config[:db_url])
118
118
  else
119
119
  raise "Unsupported DB adapter: '#{config[:db_adapter]}'"
@@ -71,7 +71,7 @@ module Gemstash
71
71
  gem = fetch_gem(gem_full_name)
72
72
  halt 404 unless gem.exist?(:spec)
73
73
  content_type "application/octet-stream"
74
- gem.load(:spec).content(:spec)
74
+ gem.content(:spec)
75
75
  end
76
76
 
77
77
  def serve_actual_gem(id)
@@ -130,7 +130,6 @@ module Gemstash
130
130
  def fetch_gem(gem_full_name)
131
131
  gem = storage.resource(gem_full_name)
132
132
  halt 404 unless gem.exist?(:gem)
133
- gem.load(:gem)
134
133
  halt 403, "That gem has been yanked" unless gem.properties[:indexed]
135
134
  gem
136
135
  end
@@ -118,10 +118,10 @@ module Gemstash
118
118
 
119
119
  private
120
120
 
121
- def serve_cached(id, key)
122
- gem = fetch_gem(id, key)
123
- headers.update(gem.properties[:headers][key]) if gem.properties[:headers] && gem.properties[:headers][key]
124
- gem.content(key)
121
+ def serve_cached(id, resource_type)
122
+ gem = fetch_gem(id, resource_type)
123
+ headers.update(gem.properties[:headers][resource_type]) if gem.property?(:headers, resource_type)
124
+ gem.content(resource_type)
125
125
  rescue Gemstash::WebError => e
126
126
  halt e.code
127
127
  end
@@ -142,25 +142,25 @@ module Gemstash
142
142
  @gem_fetcher ||= Gemstash::GemFetcher.new(http_client_for(upstream))
143
143
  end
144
144
 
145
- def fetch_gem(id, key)
145
+ def fetch_gem(id, resource_type)
146
146
  gem_name = Gemstash::Upstream::GemName.new(upstream, id)
147
147
  gem_resource = storage.resource(gem_name.name)
148
- if gem_resource.exist?(key)
149
- fetch_local_gem(gem_name, gem_resource, key)
148
+ if gem_resource.exist?(resource_type)
149
+ fetch_local_gem(gem_name, gem_resource, resource_type)
150
150
  else
151
- fetch_remote_gem(gem_name, gem_resource, key)
151
+ fetch_remote_gem(gem_name, gem_resource, resource_type)
152
152
  end
153
153
  end
154
154
 
155
- def fetch_local_gem(gem_name, gem_resource, key)
156
- log.info "Gem #{gem_name.name} exists, returning cached #{key}"
157
- gem_resource.load(key)
155
+ def fetch_local_gem(gem_name, gem_resource, resource_type)
156
+ log.info "Gem #{gem_name.name} exists, returning cached #{resource_type}"
157
+ gem_resource
158
158
  end
159
159
 
160
- def fetch_remote_gem(gem_name, gem_resource, key)
161
- log.info "Gem #{gem_name.name} is not cached, fetching #{key}"
162
- gem_fetcher.fetch(gem_name.id, key) do |content, properties|
163
- gem_resource.save({ key => content }, headers: { key => properties })
160
+ def fetch_remote_gem(gem_name, gem_resource, resource_type)
161
+ log.info "Gem #{gem_name.name} is not cached, fetching #{resource_type}"
162
+ gem_fetcher.fetch(gem_name.id, resource_type) do |content, properties|
163
+ gem_resource.save({ resource_type => content }, headers: { resource_type => properties })
164
164
  end
165
165
  end
166
166
  end
@@ -2,7 +2,7 @@ Sequel.migration do
2
2
  change do
3
3
  create_table :rubygems do
4
4
  primary_key :id
5
- String :name, :size => 255, :null => false
5
+ String :name, :size => 191, :null => false
6
6
  DateTime :created_at, :null => false
7
7
  DateTime :updated_at, :null => false
8
8
  index [:name], :unique => true
@@ -11,10 +11,10 @@ Sequel.migration do
11
11
  create_table :versions do
12
12
  primary_key :id
13
13
  Integer :rubygem_id, :null => false
14
- String :storage_id, :size => 255, :null => false
15
- String :number, :size => 255, :null => false
16
- String :platform, :size => 255, :null => false
17
- String :full_name, :size => 255, :null => false
14
+ String :storage_id, :size => 191, :null => false
15
+ String :number, :size => 191, :null => false
16
+ String :platform, :size => 191, :null => false
17
+ String :full_name, :size => 191, :null => false
18
18
  TrueClass :indexed, :default => true, :null => false
19
19
  TrueClass :prerelease, :null => false
20
20
  DateTime :created_at, :null => false
@@ -30,8 +30,8 @@ Sequel.migration do
30
30
  create_table :dependencies do
31
31
  primary_key :id
32
32
  Integer :version_id, :null => false
33
- String :rubygem_name, :size => 255, :null => false
34
- String :requirements, :size => 255, :null => false
33
+ String :rubygem_name, :size => 191, :null => false
34
+ String :requirements, :size => 191, :null => false
35
35
  DateTime :created_at, :null => false
36
36
  DateTime :updated_at, :null => false
37
37
  index [:version_id]
@@ -2,8 +2,8 @@ Sequel.migration do
2
2
  change do
3
3
  create_table :authorizations do
4
4
  primary_key :id
5
- String :auth_key, :size => 2056, :null => false
6
- String :permissions, :size => 255, :null => false
5
+ String :auth_key, :size => 191, :null => false
6
+ String :permissions, :size => 191, :null => false
7
7
  DateTime :created_at, :null => false
8
8
  DateTime :updated_at, :null => false
9
9
  index [:auth_key], :unique => true
@@ -5,34 +5,57 @@ require "fileutils"
5
5
  require "yaml"
6
6
 
7
7
  module Gemstash
8
- #:nodoc:
8
+ # The entry point into the storage engine for storing cached gems, specs, and
9
+ # private gems.
9
10
  class Storage
10
11
  extend Gemstash::Env::Helper
11
12
  VERSION = 1
12
13
 
13
- # If the storage engine detects something that was stored with a newer
14
- # version of the storage engine, this error will be thrown.
14
+ # If the storage engine detects the base cache directory was originally
15
+ # initialized with a newer version, this error is thrown.
15
16
  class VersionTooNew < StandardError
17
+ def initialize(folder, version)
18
+ super("Gemstash storage version #{Gemstash::Storage::VERSION} does " \
19
+ "not support version #{version} found at #{folder}")
20
+ end
16
21
  end
17
22
 
23
+ # This object should not be constructed directly, but instead via
24
+ # {for} and {#for}.
18
25
  def initialize(folder, root: true)
19
- check_engine if root
20
26
  @folder = folder
27
+ check_storage_version if root
21
28
  FileUtils.mkpath(@folder) unless Dir.exist?(@folder)
22
29
  end
23
30
 
31
+ # Fetch the resource with the given +id+ within this storage.
32
+ #
33
+ # @param id [String] the id of the resource to fetch
34
+ # @return [Gemstash::Resource] a new resource instance from the +id+
24
35
  def resource(id)
25
36
  Resource.new(@folder, id)
26
37
  end
27
38
 
39
+ # Fetch a nested entry from this instance in the storage engine.
40
+ #
41
+ # @param child [String] the name of the nested entry to load
42
+ # @return [Gemstash::Storage] a new storage instance for the +child+
28
43
  def for(child)
29
44
  Storage.new(File.join(@folder, child), root: false)
30
45
  end
31
46
 
47
+ # Fetch a base entry in the storage engine.
48
+ #
49
+ # @param name [String] the name of the entry to load
50
+ # @return [Gemstash::Storage] a new storage instance for the +name+
32
51
  def self.for(name)
33
52
  new(gemstash_env.base_file(name))
34
53
  end
35
54
 
55
+ # Read the global metadata for Gemstash and the storage engine. If the
56
+ # metadata hasn't been stored yet, it will be created.
57
+ #
58
+ # @return [Hash] the metadata about Gemstash and the storage engine
36
59
  def self.metadata
37
60
  file = gemstash_env.base_file("metadata.yml")
38
61
 
@@ -46,10 +69,10 @@ module Gemstash
46
69
 
47
70
  private
48
71
 
49
- def check_engine
72
+ def check_storage_version
50
73
  version = Gemstash::Storage.metadata[:storage_version]
51
74
  return if version <= Gemstash::Storage::VERSION
52
- raise Gemstash::Storage::VersionTooNew, "Storage engine is out of date: #{version}"
75
+ raise Gemstash::Storage::VersionTooNew.new(@folder, version)
53
76
  end
54
77
 
55
78
  def path_valid?(path)
@@ -59,10 +82,25 @@ module Gemstash
59
82
  end
60
83
  end
61
84
 
62
- #:nodoc:
85
+ # A resource within the storage engine. The resource may have 1 or more files
86
+ # associated with it along with a metadata Hash that is stored in a YAML file.
63
87
  class Resource
64
88
  include Gemstash::Logging
65
89
  attr_reader :name, :folder
90
+ VERSION = 1
91
+
92
+ # If the storage engine detects a resource was originally saved from a newer
93
+ # version, this error is thrown.
94
+ class VersionTooNew < StandardError
95
+ def initialize(name, folder, version)
96
+ super("Gemstash resource version #{Gemstash::Resource::VERSION} does " \
97
+ "not support version #{version} for resource #{name.inspect} " \
98
+ "found at #{folder}")
99
+ end
100
+ end
101
+
102
+ # This object should not be constructed directly, but instead via
103
+ # {Gemstash::Storage#resource}.
66
104
  def initialize(folder, name)
67
105
  @base_path = folder
68
106
  @name = name
@@ -78,6 +116,13 @@ module Gemstash
78
116
  @folder = File.join(@base_path, *trie_parents, child_folder)
79
117
  end
80
118
 
119
+ # When +key+ is nil, this will test if this resource exists with any
120
+ # content. If a +key+ is provided, this will test that the resource exists
121
+ # with at least the given +key+ file. The +key+ corresponds to the +content+
122
+ # key provided to {#save}.
123
+ #
124
+ # @param key [Symbol, nil] the key of the content to check existence
125
+ # @return [Boolean] true if the indicated content exists
81
126
  def exist?(key = nil)
82
127
  if key
83
128
  File.exist?(properties_filename) && File.exist?(content_filename(key))
@@ -86,6 +131,25 @@ module Gemstash
86
131
  end
87
132
  end
88
133
 
134
+ # Save one or more files for this resource given by the +content+ hash.
135
+ # Metadata properties about the file(s) may be provided in the optional
136
+ # +properties+ parameter. The keys in the content hash correspond to the
137
+ # file name for this resource, while the values will be the content stored
138
+ # for that key.
139
+ #
140
+ # Separate calls to save for the same resource will replace existing files,
141
+ # and add new ones. Properties on additional calls will be merged with
142
+ # existing properties.
143
+ #
144
+ # Examples:
145
+ #
146
+ # Gemstash::Storage.for("foo").resource("bar").save(baz: "qux")
147
+ # Gemstash::Storage.for("foo").resource("bar").save(baz: "one", qux: "two")
148
+ # Gemstash::Storage.for("foo").resource("bar").save({ baz: "qux" }, meta: true)
149
+ #
150
+ # @param content [Hash{Symbol => String}] files to save, *must not be nil*
151
+ # @param properties [Hash, nil] metadata properties related to this resource
152
+ # @return [Gemstash::Resource] self for chaining purposes
89
153
  def save(content, properties = nil)
90
154
  content.each do |key, value|
91
155
  save_content(key, value)
@@ -95,28 +159,74 @@ module Gemstash
95
159
  self
96
160
  end
97
161
 
162
+ # Fetch the content for the given +key+. This will load and cache the
163
+ # properties and the content of the +key+. The +key+ corresponds to the
164
+ # +content+ key provided to {#save}.
165
+ #
166
+ # @param key [Symbol] the key of the content to load
167
+ # @return [String] the content stored in the +key+
98
168
  def content(key)
169
+ @content ||= {}
170
+ load(key) unless @content.include?(key)
99
171
  @content[key]
100
172
  end
101
173
 
174
+ # Fetch the metadata properties for this resource. The properties will be
175
+ # cached for future calls.
176
+ #
177
+ # @return [Hash] the metadata properties for this resource
102
178
  def properties
179
+ load_properties
103
180
  @properties || {}
104
181
  end
105
182
 
183
+ # Update the metadata properties of this resource. The +props+ will be
184
+ # merged with any existing properties.
185
+ #
186
+ # @param props [Hash] the properties to add
187
+ # @return [Gemstash::Resource] self for chaining purposes
106
188
  def update_properties(props)
107
- load_properties
189
+ load_properties(true)
108
190
  save_properties(properties.merge(props || {}))
109
191
  self
110
192
  end
111
193
 
112
- def load(key)
113
- raise "Resource #{@name} has no content to load" unless exist?(key)
114
- load_properties
115
- @content ||= {}
116
- @content[key] = read_file(content_filename(key))
117
- self
194
+ # Check if the metadata properties includes the +keys+. The +keys+ represent
195
+ # a nested path in the properties to check.
196
+ #
197
+ # Examples:
198
+ #
199
+ # resource = Gemstash::Storage.for("x").resource("y")
200
+ # resource.save({ file: "content" }, foo: "one", bar: { baz: "qux" })
201
+ # resource.has_property?(:foo) # true
202
+ # resource.has_property?(:bar, :baz) # true
203
+ # resource.has_property?(:missing) # false
204
+ # resource.has_property?(:foo, :bar) # false
205
+ #
206
+ # @param keys [Array<Object>] one or more keys pointing to a property
207
+ # @return [Boolean] whether the nested keys points to a valid property
208
+ def property?(*keys)
209
+ keys.inject(node: properties, result: true) do |memo, key|
210
+ if memo[:result]
211
+ memo[:result] = memo[:node].is_a?(Hash) && memo[:node].include?(key)
212
+ memo[:node] = memo[:node][key] if memo[:result]
213
+ end
214
+
215
+ memo
216
+ end[:result]
118
217
  end
119
218
 
219
+ # Delete the content for the given +key+. If the +key+ is the last one for
220
+ # this resource, the metadata properties will be deleted as well. The +key+
221
+ # corresponds to the +content+ key provided to {#save}.
222
+ #
223
+ # The resource will be reset afterwards, clearing any cached content or
224
+ # properties.
225
+ #
226
+ # Does nothing if the key doesn't {#exist?}.
227
+ #
228
+ # @param key [Symbol] the key of the content to delete
229
+ # @return [Gemstash::Resource] self for chaining purposes
120
230
  def delete(key)
121
231
  return self unless exist?(key)
122
232
 
@@ -139,17 +249,25 @@ module Gemstash
139
249
 
140
250
  private
141
251
 
142
- def load_properties
252
+ def load(key)
253
+ raise "Resource #{@name} has no #{key.inspect} content to load" unless exist?(key)
254
+ load_properties # Ensures storage version is checked
255
+ @content ||= {}
256
+ @content[key] = read_file(content_filename(key))
257
+ end
258
+
259
+ def load_properties(force = false)
260
+ return if @properties && !force
143
261
  return unless File.exist?(properties_filename)
144
- @properties = YAML.load_file(properties_filename)
145
- check_version
262
+ @properties = YAML.load_file(properties_filename) || {}
263
+ check_resource_version
146
264
  end
147
265
 
148
- def check_version
149
- version = @properties[:gemstash_storage_version]
150
- return if version <= Gemstash::Storage::VERSION
266
+ def check_resource_version
267
+ version = @properties[:gemstash_resource_version]
268
+ return if version <= Gemstash::Resource::VERSION
151
269
  reset
152
- raise Gemstash::Storage::VersionTooNew, "Resource was stored with a newer storage: #{version}"
270
+ raise Gemstash::Resource::VersionTooNew.new(name, folder, version)
153
271
  end
154
272
 
155
273
  def reset
@@ -159,8 +277,8 @@ module Gemstash
159
277
 
160
278
  def content?
161
279
  return false unless Dir.exist?(@folder)
162
- entries = Dir.entries(@folder).reject {|file| file =~ /\A\.\.?\z/ }
163
- !entries.empty? && entries != %w(properties.yaml)
280
+ entries = Dir.entries(@folder).reject {|file| file =~ /\A\.\.?\z/ || file == "properties.yaml" }
281
+ !entries.empty?
164
282
  end
165
283
 
166
284
  def sanitize(name)
@@ -175,7 +293,7 @@ module Gemstash
175
293
 
176
294
  def save_properties(props)
177
295
  props ||= {}
178
- props = { gemstash_storage_version: Gemstash::Storage::VERSION }.merge(props)
296
+ props = { gemstash_resource_version: Gemstash::Resource::VERSION }.merge(props)
179
297
  store(properties_filename, props.to_yaml)
180
298
  @properties = props
181
299
  end
@@ -1,4 +1,4 @@
1
1
  #:nodoc:
2
2
  module Gemstash
3
- VERSION = "1.0.0.pre.1"
3
+ VERSION = "1.0.0.pre.2"
4
4
  end
@@ -0,0 +1,157 @@
1
+ # Resulting structure:
2
+ #
3
+ # versions[]
4
+ # number
5
+ # date
6
+ # description
7
+ # sections[]
8
+ # title
9
+ # description
10
+ # changes[]
11
+ # comment
12
+ # pull_requests[]
13
+ # number
14
+ # url
15
+ # authors[]
16
+ # username
17
+ # url
18
+ grammar Changelog::Grammar
19
+ rule versions
20
+ (version+) {
21
+ def versions
22
+ captures[:version]
23
+ end
24
+ }
25
+ end
26
+
27
+ rule version
28
+ (version_header version_section+) {
29
+ def number
30
+ capture(:version_header).capture(:version_number).value
31
+ end
32
+
33
+ def date
34
+ capture(:version_header).capture(:date).value
35
+ end
36
+
37
+ def description
38
+ result = capture(:version_header).capture(:description)
39
+ result.value if result
40
+ end
41
+
42
+ def sections
43
+ captures[:version_section]
44
+ end
45
+
46
+ def pull_requests
47
+ sections.map(&:changes).flatten.map(&:pull_requests).flatten
48
+ end
49
+ }
50
+ end
51
+
52
+ rule version_header
53
+ "## " version_number:(/\d+(\.\d+)*(\.pre\.\d+)?/) " (" date:(/\d{4}-\d{2}-\d{2}/) ")\n\n"
54
+ description?
55
+ end
56
+
57
+ rule version_section
58
+ ("### " title:(/[^\n]*/) "\n\n" description? changes?) {
59
+ def title
60
+ capture(:title).value
61
+ end
62
+
63
+ def heading
64
+ "### #{title}\n\n#{description}"
65
+ end
66
+
67
+ def description
68
+ capture(:description).value if capture(:description)
69
+ end
70
+
71
+ def changes
72
+ if capture(:changes)
73
+ capture(:changes).captures[:change]
74
+ else
75
+ []
76
+ end
77
+ end
78
+ }
79
+ end
80
+
81
+ rule description
82
+ paragraph+
83
+ end
84
+
85
+ rule paragraph
86
+ (" " !"- " /\w[^\n]*/ "\n")+ "\n"
87
+ end
88
+
89
+ rule changes
90
+ change+ "\n"?
91
+ end
92
+
93
+ rule change
94
+ (" - " comment:(/[^()\n]*/) pull_requests_and_authors? "\n") {
95
+ def comment
96
+ capture(:comment).value
97
+ end
98
+
99
+ def pull_requests
100
+ pull_requests_and_authors(:pull_requests)
101
+ end
102
+
103
+ def authors
104
+ pull_requests_and_authors(:authors)
105
+ end
106
+
107
+ private
108
+
109
+ def pull_requests_and_authors(type)
110
+ return [] unless capture(:pull_requests_and_authors)
111
+ return [] unless capture(:pull_requests_and_authors).capture(type)
112
+ singular = type.to_s.sub(/s$/, "").to_sym
113
+ capture(:pull_requests_and_authors).capture(type).capture(singular) || []
114
+ end
115
+ }
116
+ end
117
+
118
+ rule pull_requests_and_authors
119
+ "(" pull_requests ", " authors ")"
120
+ end
121
+
122
+ rule pull_requests
123
+ pull_request (", " pull_request)*
124
+ end
125
+
126
+ rule pull_request
127
+ ("[#" number:(/\d+/) "](" github_url ")") {
128
+ def number
129
+ capture(:number).value
130
+ end
131
+
132
+ def url
133
+ capture(:github_url).value
134
+ end
135
+ }
136
+ end
137
+
138
+ rule authors
139
+ author (", " author)*
140
+ end
141
+
142
+ rule author
143
+ ("[@" username:(/\w+/) "](" github_url ")") {
144
+ def username
145
+ capture(:username).value
146
+ end
147
+
148
+ def url
149
+ capture(:github_url).value
150
+ end
151
+ }
152
+ end
153
+
154
+ rule github_url
155
+ "https://github.com" path:(/\/\w*/)*
156
+ end
157
+ end
data/rake/changelog.rb ADDED
@@ -0,0 +1,201 @@
1
+ require "set"
2
+
3
+ # Helper class for updating CHANGELOG.md
4
+ class Changelog
5
+ attr_reader :changelog_file, :parsed, :parsed_current_version, :parsed_last_version, :missing_pull_requests
6
+
7
+ def initialize
8
+ @changelog_file = File.expand_path("../../CHANGELOG.md", __FILE__)
9
+ end
10
+
11
+ def run
12
+ ensure_new_version_specified
13
+ parse_changelog
14
+ fetch_missing_pull_requests
15
+ update_changelog
16
+ end
17
+
18
+ def ensure_new_version_specified
19
+ tags = `git tag -l`
20
+ return unless tags.include? Changelog.current_version
21
+ Changelog.error("Please update lib/gemstash/version.rb with the new version first!")
22
+ end
23
+
24
+ def parse_changelog
25
+ require "citrus"
26
+ Citrus.load(File.expand_path("../changelog.citrus", __FILE__))
27
+ @parsed = Changelog::Grammar.parse(File.read(changelog_file))
28
+ @parsed_current_version = @parsed.versions.find {|version| version.number == Changelog.current_version }
29
+
30
+ if @parsed_current_version
31
+ index = @parsed.versions.index(@parsed_current_version)
32
+ @parsed_last_version = @parsed.versions[index + 1]
33
+ else
34
+ @parsed_last_version = @parsed.versions.first
35
+ end
36
+ end
37
+
38
+ def last_version
39
+ @last_version ||= begin
40
+ version = parsed_last_version.number
41
+
42
+ unless version =~ /\A\d+(\.\d+)*(\.pre\.\d+)?\z/
43
+ error("Invalid last version: #{version}, instead use something like 1.1.0, or 1.1.0.pre.2")
44
+ end
45
+
46
+ version
47
+ end
48
+ end
49
+
50
+ def octokit
51
+ @octokit ||= begin
52
+ require "octokit"
53
+ token_path = File.expand_path("../../.rake_github_token", __FILE__)
54
+
55
+ if File.exist?(token_path)
56
+ options = { access_token: File.read(token_path).strip }
57
+ else
58
+ puts "\e[31mWARNING:\e[0m You do not have a GitHub OAuth token configured"
59
+ puts "Please generate one at: https://github.com/settings/tokens"
60
+ puts "And store it at: #{token_path}"
61
+ puts "Otherwise you might hit rate limits while running this"
62
+ print "Continue without token? [yes/no] "
63
+ abort("Please create your token and retry") unless STDIN.gets.strip.downcase == "yes"
64
+ options = {}
65
+ end
66
+
67
+ client = Octokit::Client.new(options)
68
+ client.auto_paginate = true
69
+ client
70
+ end
71
+ end
72
+
73
+ def fetch_missing_pull_requests
74
+ @missing_pull_requests = missing_pull_request_numbers.map {|pr| fetch_pull_request(pr) }
75
+ end
76
+
77
+ def fetch_pull_request(number)
78
+ puts "Fetching pull request ##{number}"
79
+ octokit.pull_request("bundler/gemstash", number)
80
+ end
81
+
82
+ def missing_pull_request_numbers
83
+ @missing_pull_request_numbers ||= begin
84
+ commits = `git log --oneline HEAD ^v#{last_version} --grep "^Merge pull request"`.split("\n")
85
+ pull_requests = commits.map {|commit| commit[/Merge pull request #(\d+)/, 1].to_i }
86
+ documented = Set.new
87
+
88
+ if parsed_current_version
89
+ parsed_current_version.pull_requests.each do |pr|
90
+ documented << pr.number.to_i
91
+ end
92
+ end
93
+
94
+ pull_requests.sort.reject {|pr| documented.include?(pr) }
95
+ end
96
+ end
97
+
98
+ def update_changelog
99
+ return if missing_pull_requests.empty?
100
+
101
+ File.open(changelog_file, "w") do |file|
102
+ begin
103
+ write_current_version(file)
104
+ ensure
105
+ parsed.versions.each do |version|
106
+ next if version == parsed_current_version
107
+ file.write version.value
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ def write_current_version(file)
114
+ pull_requests_by_section = missing_pull_requests.group_by {|pr| section_for(pr) }
115
+ file.puts "## #{Changelog.current_version} (#{current_date})"
116
+ file.puts
117
+
118
+ if parsed_current_version
119
+ file.puts parsed_current_version.description if parsed_current_version.description
120
+
121
+ parsed_current_version.sections.each do |section|
122
+ if pull_requests_by_section[section.title].to_a.empty?
123
+ file.write section.value
124
+ else
125
+ file.write section.heading
126
+ section.changes.each {|change| file.write change.value }
127
+ write_pull_requests(file, pull_requests_by_section[section.title])
128
+ file.puts
129
+ pull_requests_by_section.delete(section.title)
130
+ end
131
+ end
132
+ end
133
+
134
+ pull_requests_by_section.keys.sort.each do |section_title|
135
+ file.puts "### #{section_title}"
136
+ file.puts
137
+ write_pull_requests(file, pull_requests_by_section[section_title])
138
+ file.puts
139
+ end
140
+ end
141
+
142
+ def section_for(pull_request)
143
+ puts "Fetching issue for ##{pull_request.number}"
144
+ issue = pull_request.rels[:issue].get.data
145
+ labels = issue.labels.map(&:name)
146
+
147
+ if labels.include?("bug")
148
+ "Bugfixes"
149
+ elsif labels.include?("enhancement")
150
+ "Features"
151
+ else
152
+ "Changes"
153
+ end
154
+ end
155
+
156
+ def write_pull_requests(file, pull_requests)
157
+ pull_requests.each do |pr|
158
+ puts "Fetching commits for ##{pr.number}"
159
+ commits = pr.rels[:commits].get.data
160
+ authors = commits.map {|commit| author_link(commit) }.uniq
161
+ file.puts " - #{pr.title} ([##{pr.number}](#{pr.html_url}), #{authors.join(", ")})"
162
+ end
163
+ end
164
+
165
+ def author_link(commit)
166
+ @author_links ||= {}
167
+ author = commit.author
168
+
169
+ if author
170
+ "[@#{author.login}](#{author.html_url})"
171
+ elsif @author_links[commit.commit.author.name]
172
+ @author_links[commit.commit.author.name]
173
+ else
174
+ puts "Cannot find GitHub link for author: #{commit.commit.author.name}"
175
+ print "What is their GitHub username? "
176
+ username = STDIN.gets.strip
177
+ @author_links[commit.commit.author.name] = "[@#{username}](https://github.com/#{username})"
178
+ end
179
+ end
180
+
181
+ def current_date
182
+ @current_date ||= Time.now.strftime("%Y-%m-%d")
183
+ end
184
+
185
+ def self.error(msg)
186
+ STDERR.puts(msg)
187
+ exit(false)
188
+ end
189
+
190
+ def self.current_version
191
+ @current_version ||= begin
192
+ require_relative "../lib/gemstash/version.rb"
193
+
194
+ unless Gemstash::VERSION =~ /\A\d+(\.\d+)*(\.pre\.\d+)?\z/
195
+ error("Invalid version: #{Gemstash::VERSION}, instead use something like 1.1.0, or 1.1.0.pre.2")
196
+ end
197
+
198
+ Gemstash::VERSION
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,36 @@
1
+ require "pathname"
2
+
3
+ # Helper class for generating the table of contents in markdown files.
4
+ class TableOfContents
5
+ attr_reader :toc_dir, :toc, :docs_dir
6
+
7
+ def initialize
8
+ @toc_dir = Pathname.new(File.expand_path("../../tmp", __FILE__))
9
+ @toc = @toc_dir.join("gh-md-toc")
10
+ @docs_dir = Pathname.new(File.expand_path("../../docs", __FILE__))
11
+ end
12
+
13
+ def run
14
+ cache_toc_script
15
+ update_toc("reference.md")
16
+ end
17
+
18
+ def update_toc(doc)
19
+ doc = docs_dir.join(doc)
20
+ old_contents = File.read(doc)
21
+ old_contents.sub!(/\A.*?^---$/m, "---")
22
+ File.write(doc, old_contents)
23
+ toc_contents = `"#{toc}" "#{doc}"`
24
+ toc_contents.sub!(/Created by.*$/, "")
25
+ File.write(doc, "#{toc_contents}\n#{old_contents}")
26
+ end
27
+
28
+ def cache_toc_script
29
+ return if toc.exist?
30
+ require "open-uri"
31
+ toc_contents = open("https://raw.githubusercontent.com/ekalinin/github-markdown-toc/master/gh-md-toc", &:read)
32
+ Dir.mkdir(toc_dir) unless toc_dir.exist?
33
+ File.write(toc, toc_contents)
34
+ File.chmod(0776, toc)
35
+ end
36
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gemstash
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre.1
4
+ version: 1.0.0.pre.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andre Arko
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-11-30 00:00:00.000000000 Z
11
+ date: 2015-12-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dalli
@@ -150,6 +150,34 @@ dependencies:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
152
  version: '1.10'
153
+ - !ruby/object:Gem::Dependency
154
+ name: citrus
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '3.0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '3.0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: octokit
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '4.2'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '4.2'
153
181
  - !ruby/object:Gem::Dependency
154
182
  name: rack-test
155
183
  requirement: !ruby/object:Gem::Requirement
@@ -222,6 +250,7 @@ files:
222
250
  - ".rubocop-relax.yml"
223
251
  - ".rubocop.yml"
224
252
  - ".travis.yml"
253
+ - CHANGELOG.md
225
254
  - CODE_OF_CONDUCT.md
226
255
  - Gemfile
227
256
  - LICENSE.txt
@@ -279,6 +308,9 @@ files:
279
308
  - lib/gemstash/upstream.rb
280
309
  - lib/gemstash/version.rb
281
310
  - lib/gemstash/web.rb
311
+ - rake/changelog.citrus
312
+ - rake/changelog.rb
313
+ - rake/table_of_contents.rb
282
314
  homepage: https://github.com/bundler/gemstash
283
315
  licenses:
284
316
  - MIT