chandler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1b4381b69c5976f8de750c7756837e6da82031b1
4
+ data.tar.gz: ff127a8a9970f75de6922b618a913f2bd2fc815f
5
+ SHA512:
6
+ metadata.gz: ff2f30b2a72f70e11f27105e27c12c144e2540d173af10975b11bb554db2a2d5d4b341c578412e3a90923987be5ac7caf3834480d3802d7be52dbe68ec100fa0
7
+ data.tar.gz: 11e84ada787c62a276c9f9f9345c2b052d51eda75a38c3bc52b10f36189baa10153acf6fffa020f32797aa8a763ba4692ad9628c4a9265fbe65c1553379b8b0b
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
@@ -0,0 +1,33 @@
1
+ AllCops:
2
+ Exclude:
3
+ - "*.gemspec"
4
+
5
+ Metrics/AbcSize:
6
+ Exclude:
7
+ - "test/**/*"
8
+
9
+ Metrics/MethodLength:
10
+ Exclude:
11
+ - "test/**/*"
12
+
13
+ Metrics/ClassLength:
14
+ Exclude:
15
+ - "test/**/*"
16
+
17
+ Style/ClassAndModuleChildren:
18
+ Enabled: false
19
+
20
+ Style/Documentation:
21
+ Enabled: false
22
+
23
+ Style/DoubleNegation:
24
+ Enabled: false
25
+
26
+ Style/HashSyntax:
27
+ EnforcedStyle: hash_rockets
28
+
29
+ Style/SpaceAroundEqualsInParameterDefault:
30
+ EnforcedStyle: no_space
31
+
32
+ Style/StringLiterals:
33
+ EnforcedStyle: double_quotes
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1
4
+ - 2.2
5
+ before_install: gem install bundler -v 1.10.4
@@ -0,0 +1,16 @@
1
+ # chandler Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ chandler is in a pre-1.0 state. This means that its APIs and behavior are subject to breaking changes without deprecation notices. Until 1.0, version numbers will follow a [Semver][]-ish `0.y.z` format, where `y` is incremented when new features or breaking changes are introduced, and `z` is incremented for lesser changes or bug fixes.
6
+
7
+ ## [Unreleased]
8
+
9
+ * Your contribution here!
10
+
11
+ ## 0.1.0 (2015-06-19)
12
+
13
+ * Initial release
14
+
15
+ [Semver]: http://semver.org
16
+ [Unreleased]: https://github.com/mattbrictson/airbrussh/compare/v0.1.0...HEAD
@@ -0,0 +1,26 @@
1
+ # Contributing to chandler
2
+
3
+ Have a feature idea, bug fix, or refactoring suggestion? Contributions are welcome!
4
+
5
+ ## Pull requests
6
+
7
+ 1. Check [Issues][] to see if your contribution has already been discussed and/or implemented.
8
+ 2. If not, open an issue to discuss your contribution. I won't accept all changes and do not want to waste your time.
9
+ 3. Once you have the :thumbsup:, fork the repo, make your changes, and open a PR.
10
+ 4. Don't forget to add your contribution and credit yourself in `CHANGELOG.md`!
11
+
12
+ ## Coding guidelines
13
+
14
+ * This project has a coding style enforced by [rubocop][]. Use hash rockets and double-quoted strings, and otherwise try to follow the [Ruby style guide][style].
15
+ * Writing tests is strongly encouraged! This project uses Minitest.
16
+
17
+ ## Getting started
18
+
19
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment. To run your checked-out version of chandler, use `bundle exec chandler`.
20
+
21
+ To execute chandler's tests and rubocop checks, run `rake`.
22
+
23
+
24
+ [Issues]: https://github.com/mattbrictson/chandler/issues
25
+ [rubocop]: https://github.com/bbatsov/rubocop
26
+ [style]: https://github.com/bbatsov/ruby-style-guide
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in chandler.gemspec
4
+ gemspec
@@ -0,0 +1,6 @@
1
+ guard :minitest do
2
+ # with Minitest::Unit
3
+ watch(%r{^test/(.*)\/?(.*)_test\.rb$})
4
+ watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}#{m[2]}_test.rb" }
5
+ watch(%r{^test/minitest_helper\.rb$}) { "test" }
6
+ end
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Matt Brictson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,107 @@
1
+ # chandler
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/chandler.svg)](http://badge.fury.io/rb/chandler)
4
+ [![Build Status](https://travis-ci.org/mattbrictson/chandler.svg?branch=master)](https://travis-ci.org/mattbrictson/chandler)
5
+
6
+ **chandler syncs your CHANGELOG entries to GitHub's release notes so you don't have to enter release notes manually.** For Ruby projects, you can even add chandler to your gem's Rakefile to make this an automatic part of your release process!
7
+
8
+ ### How does it work?
9
+
10
+ chandler scans your git repository for version tags (e.g. `v1.0.2`), parses out the corresponding release notes for those tags from your CHANGELOG, and uploads those notes to the GitHub releases area via the GitHub API.
11
+
12
+ ### Why go through the trouble?
13
+
14
+ GitHub's releases feature is a nice UI for browsing the history of your project and downloading snapshots of each version. It is also structured data that can be queried via GitHub's API, making it a available for third-party integrations. For example, [Sibbell][] can automatically send the release notes out to interested parties whenever you publish a new version.
15
+
16
+ Of course, as a considerate developer you also want to have a plain text CHANGELOG that travels with the code, can be collaboratively edited in pull requests, and so on. But that means you need two copies of the same release notes!
17
+
18
+ chandler takes the hassle out of maintaining these two separate formats: your CHANGELOG is the authoritative source, and GitHub releases are updated with a simple `chandler` command.
19
+
20
+ ## Requirements
21
+
22
+ * Ruby 2.1 or higher
23
+ * Your project's CHANGELOG must be in Markdown with version numbers in the headings (similar to the format advocated by [keepachangelog.com](http://keepachangelog.com))
24
+ * You must be an owner or collaborator of the GitHub repository to update its releases
25
+
26
+ ## Installation
27
+
28
+ ### 1. Install the gem
29
+
30
+ ```
31
+ gem install chandler
32
+ ```
33
+
34
+ ### 2. Configure .netrc
35
+
36
+ In order to access the GitHub API on your behalf, you must provide chandler with your GitHub credentials. Do this by creating a `~/.netrc` file with your GitHub username and password, like this:
37
+
38
+ ```
39
+ machine api.github.com
40
+ login defunkt
41
+ password c0d3b4ssssss!
42
+ ```
43
+
44
+ For more security, you can use an OAuth access token in place of your password. [Here's how to generate one][access-token].
45
+
46
+
47
+ ## Usage
48
+
49
+ To push all CHANGELOG entries for all tags to GitHub, just run:
50
+
51
+ ```
52
+ chandler push
53
+ ```
54
+
55
+ chandler will make educated guesses as to what GitHub repository to use, the location of the CHANGELOG, and the tags that represent releases. To see what will happen without actually making changes, run:
56
+
57
+ ```
58
+ chandler push --dry-run
59
+ ```
60
+
61
+ To upload only a specific tag, `v1.0.2` for example:
62
+
63
+ ```
64
+ chandler push v1.0.2
65
+ ```
66
+
67
+ Other command-line options:
68
+
69
+ * `--git=/path/to/project/.git` – location of the local git repository (defaults to `.git`)
70
+ * `--github=username/repo` – GitHub repository to upload to (if unspecified, chandler will guess based on your git remotes)
71
+ * `--changelog=History.md` – location of the CHANGELOG (defaults to `CHANGELOG.md`)
72
+
73
+
74
+ ## Rakefile integration
75
+
76
+ If you maintain a Ruby gem and use Bundler's gem tasks (i.e. `rake release`) to publish your gem, then you can use chandler to automate update your GitHub release notes.
77
+
78
+ ### 1. Update the gemspec
79
+
80
+ ```ruby
81
+ spec.add_development_dependency "chandler"
82
+ ```
83
+
84
+ ### 2. Modify the Rakefile
85
+
86
+ ```ruby
87
+ require "bundler/gem_tasks"
88
+ require "chandler/tasks"
89
+
90
+ # Optional: override default chandler configuration
91
+ Chandler::Tasks.configure do |config|
92
+ config.changelog_path = "History.md"
93
+ config.github_repository = "mattbrictson/mygem"
94
+ end
95
+
96
+ # Add chandler as a prerequisite for `rake release`
97
+ task "release:rubygem_push" => "chandler:push"
98
+ ```
99
+
100
+ That's it! Now when you run `rake release`, your GitHub release notes will be updated automatically based on your CHANGELOG entries.
101
+
102
+ [Sibbell]: http://sibbell.com
103
+ [access-token]: https://help.github.com/articles/creating-an-access-token-for-command-line-use/
104
+
105
+ ## Contributing
106
+
107
+ Contributions are welcome! Read [CONTRIBUTING.md](CONTRIBUTING.md) to get started.
@@ -0,0 +1,17 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+ require "rubocop/rake_task"
4
+
5
+ $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
6
+ require "chandler/tasks"
7
+ task "release:rubygem_push" => "chandler:push"
8
+
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << "test"
11
+ t.libs << "lib"
12
+ t.test_files = FileList["test/**/*_test.rb"]
13
+ end
14
+
15
+ RuboCop::RakeTask.new
16
+
17
+ task :default => [:test, :rubocop]
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "chandler"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,36 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'chandler/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.required_ruby_version = ">= 2.1.0"
8
+
9
+ spec.name = "chandler"
10
+ spec.version = Chandler::VERSION
11
+ spec.authors = ["Matt Brictson"]
12
+ spec.email = ["chandler@mattbrictson.com"]
13
+
14
+ spec.summary = "Syncs CHANGELOG entries to GitHub's release notes"
15
+ spec.homepage = "https://github.com/mattbrictson/chandler"
16
+ spec.license = "MIT"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_dependency "netrc"
24
+ spec.add_dependency "octokit", ">= 2.2.0"
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.10"
27
+ spec.add_development_dependency "guard", ">= 2.2.2"
28
+ spec.add_development_dependency "guard-minitest"
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ spec.add_development_dependency "rb-fsevent"
31
+ spec.add_development_dependency "minitest"
32
+ spec.add_development_dependency "minitest-reporters"
33
+ spec.add_development_dependency "mocha"
34
+ spec.add_development_dependency "rubocop"
35
+ spec.add_development_dependency "terminal-notifier-guard"
36
+ end
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "chandler/cli"
4
+ Chandler::CLI.new.run
@@ -0,0 +1,4 @@
1
+ require "chandler/version"
2
+
3
+ module Chandler
4
+ end
@@ -0,0 +1,97 @@
1
+ require "chandler/refinements/version_format"
2
+
3
+ module Chandler
4
+ # Responsible for parsing a CHANGELOG into a hash of release notes keyed
5
+ # by version number. Release notes for a particular version or tag can be
6
+ # accessed using the `fetch` method.
7
+ class Changelog
8
+ using Chandler::Refinements::VersionFormat
9
+
10
+ NoMatchingVersion = Class.new(StandardError)
11
+
12
+ HEADING_PATTERNS = [
13
+ /^##\s+.*\n/, # Markdown "atx" style
14
+ /^###\s+.*\n/,
15
+ /^==\s+.*\n/, # Rdoc style
16
+ /^===\s+.*\n/,
17
+ /^\S.*\n-+\n/ # Markdown "Setext" style
18
+ ].freeze
19
+
20
+ attr_reader :path
21
+
22
+ def initialize(path:)
23
+ @path = path
24
+ end
25
+
26
+ # Fetch release notes for the given tag or version number.
27
+ #
28
+ # E.g.
29
+ # fetch("v1.0.1") # => "\nRelease notes for 1.0.1.\n"
30
+ # fetch("1.0.1") # => "\nRelease notes for 1.0.1.\n"
31
+ # fetch("blergh") # => Chandler::NoMatchingVersion
32
+ #
33
+ def fetch(tag)
34
+ versions.fetch(tag.version_number) do
35
+ fail NoMatchingVersion, "Couldn’t find #{tag} in #{path}"
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # Transforms the changelog into a hash where the keys are version numbers
42
+ # and the values are the release notes for those versions. The values are
43
+ # *not* stripped of whitespace.
44
+ #
45
+ # The version numbers are assumed to be contained at Markdown or Rdoc
46
+ # headings. The release notes for those version numbers are the text
47
+ # delimited by those headings. The algorithm tries various styles of these
48
+ # Markdown and Rdoc headings (see `HEADING_PATTERNS`) until it finds one
49
+ # that matches.
50
+ #
51
+ # The resulting hash entries look like:
52
+ # { "1.0.1" => "\nRelease notes for 1.0.1.\n" }
53
+ #
54
+ def versions
55
+ @versions ||= begin
56
+ versions = HEADING_PATTERNS.find do |heading_re|
57
+ found = versions_at_headings(heading_re)
58
+ break(found) unless found.empty?
59
+ end
60
+ versions || {}
61
+ end
62
+ end
63
+
64
+ # rubocop:disable Style/SymbolProc
65
+ def versions_at_headings(heading_re)
66
+ sections(heading_re).each_with_object({}) do |(heading, text), versions|
67
+ tokens = heading.gsub(/[\[\]\(\)`]/, " ").split.map(&:strip)
68
+ version = tokens.find { |t| t.version? }
69
+ versions[version.version_number] = text if version
70
+ end
71
+ end
72
+ # rubocop:enable Style/SymbolProc
73
+
74
+ # Parses the changelog into a hash, where the keys of the hash are the
75
+ # Markdown/rdoc headings matching the specified heading regexp, and values
76
+ # are the content delimited by those headings.
77
+ #
78
+ # E.g.
79
+ # { "## v1.0.1\n" => "\nRelease notes for 1.0.1.\n" }
80
+ #
81
+ def sections(heading_re)
82
+ hash = {}
83
+ heading = ""
84
+ remainder = text
85
+
86
+ until remainder.empty?
87
+ hash[heading], heading, remainder = remainder.partition(heading_re)
88
+ end
89
+
90
+ hash
91
+ end
92
+
93
+ def text
94
+ @text ||= IO.read(path)
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,49 @@
1
+ require "chandler/cli/parser"
2
+ require "chandler/commands/push"
3
+ require "chandler/logging"
4
+ require "forwardable"
5
+
6
+ module Chandler
7
+ # Handles constructing and invoking the appropriate chandler command
8
+ # based on command line arguments and options provided by the CLI::Parser.
9
+ # Essentially this is the "router" for the command-line app.
10
+ #
11
+ class CLI
12
+ include Logging
13
+ extend Forwardable
14
+ def_delegator :@parser, :args
15
+ def_delegator :@parser, :config
16
+
17
+ def initialize(parser: Chandler::CLI::Parser.new(ARGV))
18
+ @parser = parser
19
+ end
20
+
21
+ def run
22
+ command.call
23
+ end
24
+
25
+ def command # rubocop:disable Metrics/MethodLength
26
+ case (command = args.shift)
27
+ when "push"
28
+ push
29
+ when nil
30
+ error("Please specify a command")
31
+ info(@parser.usage)
32
+ exit(1)
33
+ else
34
+ error("Unrecognized command: #{command}")
35
+ info(@parser.usage)
36
+ exit(1)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def push
43
+ Chandler::Commands::Push.new(
44
+ :tags => args.empty? ? config.git.version_tags : args,
45
+ :config => config
46
+ )
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,86 @@
1
+ require "chandler/configuration"
2
+ require "chandler/version"
3
+ require "optparse"
4
+
5
+ module Chandler
6
+ class CLI
7
+ class Parser
8
+ attr_reader :args, :config
9
+
10
+ def initialize(args, config=Chandler::Configuration.new)
11
+ @args = args
12
+ @config = config
13
+ parse_options
14
+ end
15
+
16
+ def usage
17
+ option_parser.to_s
18
+ end
19
+
20
+ private
21
+
22
+ def parse_options
23
+ unprocessed = []
24
+ until args.empty?
25
+ option_parser.order!(args)
26
+ unprocessed << args.shift
27
+ end
28
+ @args = unprocessed.compact
29
+ end
30
+
31
+ def option_parser # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
32
+ OptionParser.new do |opts|
33
+ opts.banner = "Usage: chandler push [tag] [options]"
34
+ opts.separator("")
35
+ opts.separator(summary)
36
+ opts.separator("")
37
+
38
+ opts.on("--git=[PATH]", "Path to .git directory") do |p|
39
+ config.git_path = p
40
+ end
41
+
42
+ opts.on("--github=[URL]",
43
+ "GitHub repository URL or owner/repo") do |u|
44
+ config.github_repository = u
45
+ end
46
+
47
+ opts.on("--changelog=[PATH]",
48
+ "Path to CHANGELOG Markdown file") do |p|
49
+ config.changelog_path = p
50
+ end
51
+
52
+ opts.on("--dry-run",
53
+ "Simulate, but don’t actually push to GitHub") do |d|
54
+ config.dry_run = d
55
+ end
56
+
57
+ opts.on("--debug", "Enable debug output") do |d|
58
+ config.logger.verbose = d
59
+ end
60
+
61
+ opts.on("-h", "--help", "Show this help message") do
62
+ puts(opts)
63
+ exit
64
+ end
65
+
66
+ opts.on("-v", "--version", "Print the chandler version number") do
67
+ puts("chandler version #{Chandler::VERSION}")
68
+ exit
69
+ end
70
+ end
71
+ end
72
+
73
+ def summary
74
+ <<-SUMMARY
75
+ chandler scans your git repository for version tags (e.g. `v1.0.2`), parses out
76
+ the corresponding release notes for those tags from your CHANGELOG, and uploads
77
+ those notes to the GitHub releases area via the GitHub API.
78
+
79
+ chandler will use reasonable defaults and inferences to configure itself.
80
+ If chandler doesn’t work for you out of the box, override the configuration
81
+ using these options.
82
+ SUMMARY
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,47 @@
1
+ require "chandler/logging"
2
+ require "chandler/refinements/color"
3
+ require "chandler/refinements/version_format"
4
+
5
+ module Chandler
6
+ module Commands
7
+ # Iterates over a given array of tags, fetches the corresponding notes
8
+ # from the CHANGELOG, and creates (or updates) the release notes for that
9
+ # tag on GitHub.
10
+ class Push
11
+ include Logging
12
+ using Chandler::Refinements::Color
13
+ using Chandler::Refinements::VersionFormat
14
+
15
+ attr_reader :github, :changelog, :tags, :config
16
+
17
+ def initialize(tags:, config:)
18
+ @tags = tags
19
+ @github = config.github
20
+ @changelog = config.changelog
21
+ @config = config
22
+ end
23
+
24
+ def call
25
+ benchmarking_each_tag do |tag|
26
+ github.create_or_update_release(
27
+ :tag => tag,
28
+ :title => tag.version_number,
29
+ :description => changelog.fetch(tag).strip
30
+ )
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def benchmarking_each_tag
37
+ width = tags.map(&:length).max
38
+ tags.each do |tag|
39
+ ellipsis = "…".ljust(1 + width - tag.length)
40
+ benchmark("Push #{tag.blue}#{ellipsis}") do
41
+ yield(tag)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,39 @@
1
+ require "chandler/changelog"
2
+ require "chandler/git"
3
+ require "chandler/github"
4
+ require "chandler/logger"
5
+
6
+ module Chandler
7
+ class Configuration
8
+ attr_accessor :changelog_path, :git_path, :github_repository, :dry_run
9
+ attr_accessor :logger
10
+
11
+ def initialize
12
+ @changelog_path = "CHANGELOG.md"
13
+ @git_path = ".git"
14
+ @logger = Chandler::Logger.new
15
+ @dry_run = false
16
+ end
17
+
18
+ def dry_run?
19
+ dry_run
20
+ end
21
+
22
+ def git
23
+ @git ||= Chandler::Git.new(:path => git_path)
24
+ end
25
+
26
+ def github
27
+ @github ||=
28
+ Chandler::GitHub.new(:repository => github_repository, :config => self)
29
+ end
30
+
31
+ def changelog
32
+ @changelog ||= Chandler::Changelog.new(:path => changelog_path)
33
+ end
34
+
35
+ def github_repository
36
+ @github_repository || git.origin_remote
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,58 @@
1
+ require "chandler/refinements/version_format"
2
+ require "open3"
3
+
4
+ module Chandler
5
+ # Uses the shell to execute git commands against a given .git directory.
6
+ class Git
7
+ using Chandler::Refinements::VersionFormat
8
+
9
+ Error = Class.new(StandardError)
10
+ attr_reader :path
11
+
12
+ # Initializes the Git object with the path to the `.git` directory of the
13
+ # desired git repository.
14
+ #
15
+ # Chandler::Git.new(:path => "/path/to/my/project/.git")
16
+ #
17
+ def initialize(path:)
18
+ @path = path
19
+ end
20
+
21
+ # Uses `git tag -l` to obtain the list of tags, then returns the subset of
22
+ # those tags that appear to be version numbers.
23
+ #
24
+ # version_tags # => ["v0.0.1", "v0.2.0", "v0.2.1", "v0.3.0"]
25
+ #
26
+ # rubocop:disable Style/SymbolProc
27
+ def version_tags
28
+ tags = git("tag", "-l").lines.map(&:strip).select { |v| v.version? }
29
+ tags.sort_by { |t| Gem::Version.new(t.version_number) }
30
+ end
31
+ # rubocop:enable Style/SymbolProc
32
+
33
+ # Uses `git remote -v` to list the remotes and returns the URL of the
34
+ # first one labeled "origin".
35
+ #
36
+ # origin_remote # => "git@github.com:mattbrictson/chandler.git"
37
+ #
38
+ def origin_remote
39
+ origin = git("remote", "-v").lines.grep(/^origin\s/).first
40
+ origin && origin.split[1]
41
+ end
42
+
43
+ private
44
+
45
+ def git(*args)
46
+ capture("git", "--git-dir", path, *args)
47
+ end
48
+
49
+ def capture(*args)
50
+ out, err, status = Open3.capture3(*args)
51
+ return out if status.success?
52
+
53
+ message = "Failed to execute: #{args.join(' ')}"
54
+ message << "\n#{err}" unless err.nil?
55
+ fail Error, message
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,67 @@
1
+ require "octokit"
2
+
3
+ module Chandler
4
+ # A facade for performing GitHub API operations on a given GitHub repository
5
+ # (specified as a git URL or as `owner/repo` format). Requires that
6
+ # "~/.netrc" is properly configured with GitHub credentials.
7
+ #
8
+ class GitHub
9
+ MissingCredentials = Class.new(StandardError)
10
+
11
+ attr_reader :repository, :config
12
+
13
+ def initialize(repository:, config:)
14
+ @repository = parse_repository(repository)
15
+ @config = config
16
+ end
17
+
18
+ def create_or_update_release(tag:, title:, description:)
19
+ return if config.dry_run?
20
+
21
+ release = existing_release(tag)
22
+ return update_release(release, title, description) if release
23
+
24
+ create_release(tag, title, description)
25
+ end
26
+
27
+ private
28
+
29
+ def parse_repository(repo)
30
+ repo[%r{(git@github.com:|://github.com/)(.*)\.git}, 2] || repo
31
+ end
32
+
33
+ def existing_release(tag)
34
+ release = client.release_for_tag(repository, tag)
35
+ release.id.nil? ? nil : release
36
+ rescue Octokit::NotFound
37
+ nil
38
+ end
39
+
40
+ def update_release(release, title, desc)
41
+ return if release_unchanged?(release, title, desc)
42
+ client.update_release(release.url, :name => title, :body => desc)
43
+ end
44
+
45
+ def release_unchanged?(release, title, desc)
46
+ release.name == title && release.body.to_s.strip == desc.strip
47
+ end
48
+
49
+ def create_release(tag, title, desc)
50
+ client.create_release(repository, tag, :name => title, :body => desc)
51
+ end
52
+
53
+ def client
54
+ @client ||= begin
55
+ octokit = Octokit::Client.new(:netrc => true)
56
+ octokit.login ? octokit : fail_missing_credentials
57
+ end
58
+ end
59
+
60
+ def fail_missing_credentials
61
+ message = "Couldn’t load GitHub credentials from ~/.netrc.\n"
62
+ message << "For .netrc instructions, see: "
63
+ message << "https://github.com/octokit/octokit.rb#using-a-netrc-file"
64
+ fail MissingCredentials, message
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,86 @@
1
+ require "chandler/refinements/color"
2
+
3
+ module Chandler
4
+ # Similar to Ruby's standard Logger, but automatically removes ANSI color
5
+ # from the logged messages if stdout and stderr do not support it.
6
+ #
7
+ class Logger
8
+ using Chandler::Refinements::Color
9
+ attr_accessor :stderr, :stdout, :verbose
10
+
11
+ def initialize(stderr: $stderr, stdout: $stdout)
12
+ @stderr = stderr
13
+ @stdout = stdout
14
+ @verbose = false
15
+ end
16
+
17
+ def verbose?
18
+ verbose
19
+ end
20
+
21
+ # Logs a message to stderr. Unless otherwise specified, the message will
22
+ # be printed in red.
23
+ def error(message)
24
+ message = message.red unless message.color?
25
+ puts(stderr, message)
26
+ end
27
+
28
+ # Logs a message to stdout.
29
+ def info(message)
30
+ puts(stdout, message)
31
+ end
32
+
33
+ # Logs a message to stdout, but only if `verbose?` is true.
34
+ def debug(message=nil)
35
+ return unless verbose?
36
+ return puts(stdout, yield) if block_given?
37
+ puts(stdout, message)
38
+ end
39
+
40
+ # Logs a message to stdout, runs the given block, and then prints the time
41
+ # it took to run the block.
42
+ def benchmark(message)
43
+ start = Time.now
44
+ print(stdout, "#{message} ")
45
+ debug("\n")
46
+ result = yield
47
+ duration = Time.now - start
48
+ info("✔".green + format(" %0.3fs", duration).gray)
49
+ result
50
+ rescue
51
+ info("✘".red)
52
+ raise
53
+ end
54
+
55
+ private
56
+
57
+ def print(io, message)
58
+ message = message.strip_color unless color_enabled?
59
+ io.print(message)
60
+ end
61
+
62
+ def puts(io, message)
63
+ message = message.strip_color unless color_enabled?
64
+ io.puts(message)
65
+ end
66
+
67
+ def color_enabled?
68
+ @color_enabled = determine_color_support if @color_enabled.nil?
69
+ @color_enabled
70
+ end
71
+
72
+ def determine_color_support
73
+ if ENV["CLICOLOR_FORCE"] == "1"
74
+ true
75
+ elsif ENV["TERM"] == "dumb"
76
+ false
77
+ else
78
+ tty?(stdout) && tty?(stderr)
79
+ end
80
+ end
81
+
82
+ def tty?(io)
83
+ io.respond_to?(:tty?) && io.tty?
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,17 @@
1
+ module Chandler
2
+ # Assuming self responds to `config`, this mixin provides easy access to
3
+ # logging methods by delegating to the configured Logger.
4
+ #
5
+ module Logging
6
+ def self.included(target)
7
+ target.instance_exec do
8
+ extend Forwardable
9
+ private def_delegator :config, :logger
10
+ private def_delegator :logger, :benchmark
11
+ private def_delegator :logger, :debug
12
+ private def_delegator :logger, :error
13
+ private def_delegator :logger, :info
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ module Chandler
2
+ module Refinements
3
+ # Monkey patch String to provide basic ANSI color support.
4
+ #
5
+ # "hello".color? # => false
6
+ # "hello".blue # => "\e[0;34;49mhello\e[0m"
7
+ # "hello".blue.color? # => true
8
+ # "hello".blue.strip_color # "hello"
9
+ #
10
+ module Color
11
+ ANSI_CODES = {
12
+ :red => 31,
13
+ :green => 32,
14
+ :blue => 34,
15
+ :gray => 90,
16
+ :grey => 90
17
+ }.freeze
18
+
19
+ refine String do
20
+ # Returns `true` if this String contains ANSI color sequences.
21
+ def color?
22
+ self != strip_color
23
+ end
24
+
25
+ # Returns a new String with ANSI color sequences removed.
26
+ def strip_color
27
+ gsub(/\e\[[0-9;]*m/, "")
28
+ end
29
+
30
+ # Define red, green, blue, etc. methods that return a copy of the
31
+ # String that is wrapped in the corresponding ANSI color escape
32
+ # sequence.
33
+ ANSI_CODES.each do |name, code|
34
+ define_method(name) do
35
+ "\e[0;#{code};49m#{self}\e[0m"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,46 @@
1
+ module Chandler
2
+ module Refinements
3
+ # Monkey patch String to provide conveniences for identifying strings that
4
+ # represent a version, and converting between between tags (e.g "v1.0.2")
5
+ # and version numbers ("1.0.2").
6
+ #
7
+ module VersionFormat
8
+ refine String do
9
+ # Does this string represent a version?
10
+ #
11
+ # "1.0.2".version? # => true
12
+ # "v1.0.2".version? # => true
13
+ # "nope".version? # => false
14
+ # "".version? # => false
15
+ #
16
+ def version?
17
+ !!version_number
18
+ end
19
+
20
+ # The version number portion of the string, with the optional "v"
21
+ # prefix removed.
22
+ #
23
+ # "1.0.2".version_number # => "1.0.2"
24
+ # "v1.0.2".version_number # => "1.0.2"
25
+ # "nope".version_number # => nil
26
+ # "".version_number # => nil
27
+ #
28
+ def version_number
29
+ self[/^v?(#{Gem::Version::VERSION_PATTERN})$/, 1]
30
+ end
31
+
32
+ # The version number reformatted as a tag, by prefixing "v".
33
+ #
34
+ # "1.0.2".version_tag # => "v1.0.2"
35
+ # "v1.0.2".version_tag # => "v1.0.2"
36
+ # "nope".version_tag # => nil
37
+ # "".version_tag # => nil
38
+ #
39
+ def version_tag
40
+ number = version_number
41
+ number && "v#{version_number}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ require "bundler/gem_helper"
2
+ require "chandler/configuration"
3
+ require "chandler/refinements/version_format"
4
+ require "chandler/commands/push"
5
+
6
+ using Chandler::Refinements::VersionFormat
7
+
8
+ namespace :chandler do
9
+ desc "Push release notes for the current version to GitHub"
10
+ task "push" do
11
+ gemspec = Bundler::GemHelper.gemspec
12
+ push = Chandler::Commands::Push.new(
13
+ :tags => [gemspec.version.to_s.version_tag],
14
+ :config => Chandler::Tasks.config
15
+ )
16
+ push.call
17
+ end
18
+ end
19
+
20
+ module Chandler
21
+ module Tasks
22
+ def self.config
23
+ @configuration ||= Chandler::Configuration.new
24
+ end
25
+
26
+ def self.configure
27
+ yield(config)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ module Chandler
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,241 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chandler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matt Brictson
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-06-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: netrc
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: octokit
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 2.2.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 2.2.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.10'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.10'
55
+ - !ruby/object:Gem::Dependency
56
+ name: guard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.2.2
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 2.2.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: guard-minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rb-fsevent
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: minitest
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: minitest-reporters
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: mocha
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rubocop
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: terminal-notifier-guard
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ description:
182
+ email:
183
+ - chandler@mattbrictson.com
184
+ executables:
185
+ - chandler
186
+ extensions: []
187
+ extra_rdoc_files: []
188
+ files:
189
+ - ".gitignore"
190
+ - ".rubocop.yml"
191
+ - ".travis.yml"
192
+ - CHANGELOG.md
193
+ - CONTRIBUTING.md
194
+ - Gemfile
195
+ - Guardfile
196
+ - LICENSE.txt
197
+ - README.md
198
+ - Rakefile
199
+ - bin/console
200
+ - bin/setup
201
+ - chandler.gemspec
202
+ - exe/chandler
203
+ - lib/chandler.rb
204
+ - lib/chandler/changelog.rb
205
+ - lib/chandler/cli.rb
206
+ - lib/chandler/cli/parser.rb
207
+ - lib/chandler/commands/push.rb
208
+ - lib/chandler/configuration.rb
209
+ - lib/chandler/git.rb
210
+ - lib/chandler/github.rb
211
+ - lib/chandler/logger.rb
212
+ - lib/chandler/logging.rb
213
+ - lib/chandler/refinements/color.rb
214
+ - lib/chandler/refinements/version_format.rb
215
+ - lib/chandler/tasks.rb
216
+ - lib/chandler/version.rb
217
+ homepage: https://github.com/mattbrictson/chandler
218
+ licenses:
219
+ - MIT
220
+ metadata: {}
221
+ post_install_message:
222
+ rdoc_options: []
223
+ require_paths:
224
+ - lib
225
+ required_ruby_version: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: 2.1.0
230
+ required_rubygems_version: !ruby/object:Gem::Requirement
231
+ requirements:
232
+ - - ">="
233
+ - !ruby/object:Gem::Version
234
+ version: '0'
235
+ requirements: []
236
+ rubyforge_project:
237
+ rubygems_version: 2.4.8
238
+ signing_key:
239
+ specification_version: 4
240
+ summary: Syncs CHANGELOG entries to GitHub's release notes
241
+ test_files: []