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

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