Skip to content
Snippets Groups Projects
Commit 8ec7b37a authored by Steven Danna's avatar Steven Danna
Browse files

Merge pull request #439 from chef/ssd/changelog

Add `omnibus generate changelog` command
parents 539d420c f693370e
No related branches found
No related tags found
No related merge requests found
Showing
with 743 additions and 18 deletions
Loading
Loading
@@ -265,6 +265,24 @@ This will output a JSON-formatted manifest containing the resolved
version of every software definition.
 
 
Changelog
---------
STATUS: *EXPERIMENTAL*
`omnibus changelog generate` will generate a changelog for an omnibus
project. This command currently assumes:
- version-manifest.json is checked into the project root
- the project is a git repository
- each version is tagged with a SemVer compliant annotated tag
- Any git-based sources are checked out at ../COMPONENT_NAME
- Any commit message line prepended with ChangeLog-Entry: should be
added to the changelog.
These assumptions *will* change as we determine what works best for a
number of our projects.
Caveats
-------
### Overrides
Loading
Loading
Loading
Loading
@@ -74,11 +74,18 @@ module Omnibus
 
autoload :Manifest, 'omnibus/manifest'
autoload :ManifestEntry, 'omnibus/manifest_entry'
autoload :ManifestDiff, 'omnibus/manifest_diff'
autoload :ChangeLog, 'omnibus/changelog'
autoload :GitRepository, 'omnibus/git_repository'
autoload :SemanticVersion, 'omnibus/semantic_version'
 
module Command
autoload :Base, 'omnibus/cli/base'
autoload :Cache, 'omnibus/cli/cache'
autoload :Publish, 'omnibus/cli/publish'
autoload :ChangeLog, 'omnibus/cli/changelog'
end
 
class << self
Loading
Loading
require 'omnibus/git_repository'
module Omnibus
class ChangeLog
CHANGELOG_TAG = "ChangeLog-Entry"
attr_reader :end_ref
def initialize(start_ref=nil, end_ref="HEAD", git_repo=GitRepository.new('./'))
@start_ref = start_ref
@end_ref = end_ref
@git_repo = git_repo
end
def authors
git_repo.authors(start_ref, end_ref)
end
def changelog_entries
entries = []
current_entry = []
git_repo.commit_messages(start_ref, end_ref).each do |l|
if blank?(l)
entries << current_entry
current_entry = []
elsif tagged?(l)
entries << current_entry
current_entry = Array(l.sub(/^#{CHANGELOG_TAG}:[\s]*/, ""))
elsif !current_entry.empty?
current_entry << l
end
end
entries << current_entry
entries.reject(&:empty?).map(&:join)
end
def start_ref
@start_ref ||= git_repo.latest_tag
end
private
attr_reader :git_repo
def blank?(line)
line =~ /^[\s]*$/
end
def tagged?(line)
line =~ /^#{CHANGELOG_TAG}:/
end
end
end
module Omnibus
class ChangeLogPrinter
def initialize(changelog, diff, source_path="../")
@changelog = changelog
@diff = diff
@source_path = source_path
end
def print(new_version)
puts "## #{new_version} (#{Time.now.strftime('%Y-%m-%d')})"
print_changelog
if !diff.empty?
print_components
puts ""
end
print_contributors
end
private
attr_reader :changelog, :diff, :source_path
def print_changelog(cl=changelog, indent=0)
cl.changelog_entries.each do |entry|
puts "#{' ' * indent}* #{entry.sub("\n", "\n #{' ' * indent}")}\n"
end
end
def print_components
puts "### Components\n"
print_new_components
print_updated_components
print_removed_components
end
def print_new_components
return if diff.added.empty?
puts "New Components"
diff.added.each do |entry|
puts "* #{entry[:name]} (#{entry[:new_version]})"
end
puts ""
end
def print_updated_components
return if diff.updated.empty?
puts "Updated Components"
diff.updated.each do |entry|
puts sprintf("* %s (%.8s -> %.8s)",
entry[:name], entry[:old_version], entry[:new_version])
repo_path = ::File.join(source_path, entry[:name])
if entry[:source_type] == 'git' && ::File.directory?("#{repo_path}/.git")
cl = ChangeLog.new(entry[:old_version], entry[:new_version], GitRepository.new("#{repo_path}"))
print_changelog(cl, 2)
end
end
puts ""
end
def print_removed_components
return if diff.removed.empty?
puts "Removed Components"
diff.removed.each do |entry|
puts "* #{entry[:name]} (#{entry[:old_version]})"
end
puts ""
end
def print_contributors
puts "### Contributors\n"
changelog.authors.each do |author|
puts "* #{author}"
end
end
end
end
Loading
Loading
@@ -90,6 +90,10 @@ module Omnibus
end
end
 
register(Command::ChangeLog, 'changelog', 'changelog [COMMAND]', 'Create and view changelogs')
CLI.tasks['changelog'].options = Command::ChangeLog.class_options
#
# Generate a version manifest for the given project definition
#
Loading
Loading
#
# Copyright 2015 Chef Software, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
require 'omnibus/changelog'
require 'omnibus/changelog_printer'
require 'omnibus/manifest_diff'
require 'omnibus/semantic_version'
module Omnibus
class Command::ChangeLog < Command::Base
namespace :changelog
#
# Generate a Changelog
#
# $ omnibus changelog generate
#
method_option :source_path,
desc: "Path to local checkout of git dependencies",
type: :string,
default: "../"
method_option :starting_manifest,
desc: "Path to version-manifest from the last version (we attempt to pull it from the git history if not given)",
type: :string
method_option :ending_manifest,
desc: "Path to the version-manifest from the current version",
type: :string,
default: "version-manifest.json"
method_option :skip_components,
desc: "Don't include component changes in the changelog",
type: :boolean,
default: false
method_option :major,
desc: "Bump the major version",
type: :boolean,
default: false
method_option :minor,
desc: "Bump the minor version",
type: :boolean,
default: true
method_option :patch,
desc: "Bump the patch version",
type: :boolean,
default: false
method_option :version,
desc: "Explicit version for this changelog",
type: :string
desc 'generate', 'Generate a changelog for a new release'
def generate
g = GitRepository.new
if @options[:skip_components]
diff = Omnibus::EmptyManifestDiff.new
else
old_manifest = if @options[:starting_manifest]
Omnibus::Manifest.from_file(@options[:starting_manifest])
else
Omnibus::Manifest.from_hash(JSON.parse(g.file_at_revision("version-manifest.json",
g.latest_tag)))
end
new_manifest = Omnibus::Manifest.from_file(@options[:ending_manifest])
diff = Omnibus::ManifestDiff.new(old_manifest, new_manifest)
end
new_version = if @options[:version]
@options[:version]
elsif @options[:patch]
Omnibus::SemanticVersion.new(g.latest_tag).next_patch.to_s
elsif @options[:minor] && !@options[:major] # minor is the default so it will always be true
Omnibus::SemanticVersion.new(g.latest_tag).next_minor.to_s
elsif @options[:major]
Omnibus::SemanticVersion.new(g.latest_tag).next_major.to_s
end
Omnibus::ChangeLogPrinter.new(ChangeLog.new(),
diff,
@options[:source_path]).print(new_version)
end
end
end
Loading
Loading
@@ -309,4 +309,12 @@ Could not resolve `#{ref}' to a valid git SHA-1.
EOH
end
end
class InvalidVersion < Error
def initialize(version)
super <<-EOF
'#{version}' could not be parsed as a valid version.
EOF
end
end
end
require 'omnibus/util'
module Omnibus
class GitRepository
include Util
def initialize(path='./')
@repo_path = path
end
def authors(start_ref, end_ref)
formatted_log_between(start_ref, end_ref, '%aN').lines.map(&:chomp).uniq
end
def commit_messages(start_ref, end_ref)
formatted_log_between(start_ref, end_ref, '%B').lines.to_a
end
def latest_tag
git('describe --abbrev=0').chomp
end
def file_at_revision(path, revision)
git("show #{revision}:#{path}")
end
private
attr_reader :repo_path
def formatted_log_between(start_ref, end_ref, format)
git("log #{start_ref}..#{end_ref} --pretty=\"format:#{format}\"")
end
def git(cmd)
shellout!("git #{cmd}", :cwd => repo_path).stdout
end
end
end
Loading
Loading
@@ -51,6 +51,16 @@ module Omnibus
self
end
 
def each
@data.each do |key, entry|
yield entry
end
end
def entry_names
@data.keys
end
def to_hash
software_hash = @data.inject({}) do |memo, (k,v)|
memo[k] = v.to_hash
Loading
Loading
module Omnibus
class EmptyManifestDiff
def updated
[]
end
def added
[]
end
def removed
[]
end
def empty?
true
end
end
class ManifestDiff
def initialize(first, second)
@first = first
@second = second
end
def updated
@updated ||=
begin
(first.entry_names & second.entry_names).collect do |name|
diff(first.entry_for(name), second.entry_for(name))
end.compact
end
end
def removed
@removed ||=
begin
(first.entry_names - second.entry_names).collect do |name|
removed_entry(first.entry_for(name))
end
end
end
def added
@added ||=
begin
(second.entry_names - first.entry_names).collect do |name|
new_entry(second.entry_for(name))
end
end
end
def empty?
updated.empty? && removed.empty? && added.empty?
end
private
attr_reader :first, :second
def new_entry(entry)
{ name: entry.name,
new_version: entry.locked_version,
source_type: entry.source_type,
source: entry.locked_source }
end
def removed_entry(entry)
{ name: entry.name,
old_version: entry.locked_version,
source_type: entry.source_type,
source: entry.locked_source }
end
def diff(a, b)
if a == b
nil
else
{ name: b.name,
old_version: a.locked_version,
new_version: b.locked_version,
source_type: b.source_type,
source: b.locked_source }
end
end
end
end
#
# Copyright 2015 Chef Software, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
require 'mixlib/versioning'
module Omnibus
class SemanticVersion
def initialize(version_string)
@prefix = if version_string =~ /^v/
"v"
else
""
end
@version = Mixlib::Versioning.parse(version_string.gsub(/^v/, ""))
if @version.nil?
raise InvalidVersion, "#{version_string} could not be parsed as a valid version"
end
end
def to_s
"#{prefix}#{version}"
end
def next_patch
s = [version.major, version.minor, version.patch + 1].join(".")
self.class.new("#{prefix}#{s}")
end
def next_minor
s = [version.major, version.minor + 1, 0].join(".")
self.class.new("#{prefix}#{s}")
end
def next_major
s = [version.major + 1, 0, 0].join(".")
self.class.new("#{prefix}#{s}")
end
private
attr_reader :prefix, :version
end
end
Loading
Loading
@@ -24,6 +24,7 @@ Gem::Specification.new do |gem|
gem.add_dependency 'chef-sugar', '~> 2.2'
gem.add_dependency 'cleanroom', '~> 1.0'
gem.add_dependency 'mixlib-shellout', '~> 1.4'
gem.add_dependency 'mixlib-versioning'
gem.add_dependency 'ohai', '~> 7.2'
gem.add_dependency 'ruby-progressbar', '~> 1.7'
gem.add_dependency 'uber-s3'
Loading
Loading
Loading
Loading
@@ -9,28 +9,16 @@ module Omnibus
"file://#{fake_git_remote("git://github.com/omnibus/#{repo}.git", options)}/.git"
end
 
def remote_git_repo(name, options = {})
path = File.join(remotes, name)
remote_url = "file://#{path}"
# Create a bogus software
FileUtils.mkdir_p(path)
def local_git_repo(name, options={})
path = git_scratch
Dir.chdir(path) do
git %|init --bare|
git %|config core.sharedrepository 1|
git %|config receive.denyNonFastforwards true|
git %|config receive.denyCurrentBranch ignore|
end
Dir.chdir(git_scratch) do
# Create a bogus configure file
File.open('configure', 'w') { |f| f.write('echo "Done!"') }
 
git %|init .|
git %|add .|
git %|commit -am "Initial commit for #{name}..."|
git %|remote add origin "#{remote_url}"|
git %|remote add origin "#{options[:remote]}"| if options[:remote]
git %|push origin master|
 
options[:annotated_tags].each do |tag|
Loading
Loading
@@ -38,7 +26,7 @@ module Omnibus
git %|add tag|
git %|commit -am "Create tag #{tag}"|
git %|tag "#{tag}" -m "#{tag}"|
git %|push origin "#{tag}"|
git %|push origin "#{tag}"| if options[:remote]
end if options[:annotated_tags]
 
options[:tags].each do |tag|
Loading
Loading
@@ -46,7 +34,7 @@ module Omnibus
git %|add tag|
git %|commit -am "Create tag #{tag}"|
git %|tag "#{tag}"|
git %|push origin "#{tag}"|
git %|push origin "#{tag}"| if options[:remote]
end if options[:tags]
 
options[:branches].each do |branch|
Loading
Loading
@@ -54,11 +42,28 @@ module Omnibus
File.open('branch', 'w') { |f| f.write(branch) }
git %|add branch|
git %|commit -am "Create branch #{branch}"|
git %|push origin "#{branch}"|
git %|push origin "#{branch}"| if options[:remote]
git %|checkout master|
end if options[:branches]
end
path
end
def remote_git_repo(name, options = {})
path = File.join(remotes, name)
remote_url = "file://#{path}"
options[:remote] = remote_url
# Create a bogus software
FileUtils.mkdir_p(path)
Dir.chdir(path) do
git %|init --bare|
git %|config core.sharedrepository 1|
git %|config receive.denyNonFastforwards true|
git %|config receive.denyCurrentBranch ignore|
end
 
local_git_repo(name, options)
path
end
 
Loading
Loading
require 'spec_helper'
module Omnibus
describe ChangeLog do
describe "#new" do
it "sets the start_ref to the latest tag if none is set" do
repo = double(GitRepository, :latest_tag => "1.0")
expect(ChangeLog.new(nil, "2.0", repo).start_ref).to eq("1.0")
end
it "sets the end_ref to HEAD if none is set" do
expect(ChangeLog.new.end_ref).to eq("HEAD")
end
end
describe "#changelog_entries" do
it "returns any git log lines with the ChangeLog: tag, removing the tag" do
repo = double(GitRepository, :commit_messages => ["ChangeLog-Entry: foobar\n",
"ChangeLog-Entry: wombat\n"])
changelog = ChangeLog.new("0.0.1", "0.0.2", repo)
expect(changelog.changelog_entries).to eq(["foobar\n", "wombat\n"])
end
it "returns an empty array if there were no changelog entries" do
repo = double(GitRepository, :commit_messages => [])
changelog = ChangeLog.new("0.0.1", "0.0.2", repo)
expect(changelog.changelog_entries).to eq([])
end
it "does not return git messages without a ChangeLog: tag" do
repo = double(GitRepository, :commit_messages => ["foobar\n", "wombat\n"])
changelog = ChangeLog.new("0.0.1", "0.0.2", repo)
expect(changelog.changelog_entries).to eq([])
end
it "does not return blank lines" do
repo = double(GitRepository, :commit_messages => ["\n", "\n"])
changelog = ChangeLog.new("0.0.1", "0.0.2", repo)
expect(changelog.changelog_entries).to eq([])
end
it "can handle multi-line ChangeLog entries" do
repo = double(GitRepository, :commit_messages => ["ChangeLog-Entry: foobar\n", "foobaz\n"])
changelog = ChangeLog.new("0.0.1", "0.0.2", repo)
expect(changelog.changelog_entries).to eq(["foobar\nfoobaz\n"])
end
it "end a ChangeLog entry at the first blank line" do
repo = double(GitRepository, :commit_messages => ["ChangeLog-Entry: foobar\n", "\n", "foobaz\n"])
changelog = ChangeLog.new("0.0.1", "0.0.2", repo)
expect(changelog.changelog_entries).to eq(["foobar\n"])
end
end
end
end
require 'spec_helper'
module Omnibus
describe GitRepository do
let(:git_repo) do
path = local_git_repo("foobar", annotated_tags: ["1.0", "2.0", "3.0"])
Omnibus::GitRepository.new(path)
end
describe "#authors" do
it "returns an array of authors between two tags" do
expect(git_repo.authors("1.0", "2.0")).to eq(["omnibus"])
end
it "returns an empty array if start_ref == end_ref" do
expect(git_repo.authors("3.0", "3.0")).to eq([])
end
it "doesn't return duplicates" do
expect(git_repo.authors("1.0", "3.0")).to eq(["omnibus"])
end
it "returns an error if the tags don't exist" do
expect{git_repo.authors("1.0", "WUT")}.to raise_error
end
end
describe "#latest_tag" do
it "returns the latest annotated tag" do
expect(git_repo.latest_tag).to eq("3.0")
end
end
describe "#file_at_revision" do
it "returns the text of the specified file in a repository at a given revision" do
expect(git_repo.file_at_revision("configure", "1.0")).to eq("echo \"Done!\"")
end
end
describe "#commit_messages" do
it "returns the raw text from commits between two tags as an array of lines" do
expect(git_repo.commit_messages("1.0", "3.0")).to eq(["Create tag 3.0\n", "\n", "Create tag 2.0\n"])
end
it "returns lines with the newline attached" do
expect(git_repo.commit_messages("1.0", "3.0").first[-1]).to eq("\n")
end
it "returns an empty array if start_ref == end_ref" do
expect(git_repo.commit_messages("3.0", "3.0")).to eq([])
end
end
end
end
require 'spec_helper'
module Omnibus
describe ManifestDiff do
def manifest_entry_for(name, dv, lv)
Omnibus::ManifestEntry.new(name, {:described_version => dv,
:locked_version => lv,
:locked_source => {
:git => "git://#{name}@example.com"},
:source_type => :git
})
end
let(:manifest_one) do
m = Omnibus::Manifest.new()
m.add("foo", manifest_entry_for("foo", "1.2.4", "deadbeef"))
m.add("bar", manifest_entry_for("bar", "1.2.4", "deadbeef"))
m
end
let(:manifest_two) do
m = Omnibus::Manifest.new()
m.add("foo", manifest_entry_for("foo", "1.2.5", "deadbea0"))
m.add("baz", manifest_entry_for("baz", "1.2.4", "deadbeef"))
m
end
subject { described_class.new(manifest_one, manifest_two)}
describe "#updated" do
it "returns items that existed in the first manifest but have been changed" do
expect(subject.updated).to eq([{ :name => "foo",
:old_version => "deadbeef",
:new_version => "deadbea0",
:source_type => :git,
:source => {:git => "git://foo@example.com"}
}])
end
describe "#removed" do
it "returns items that existed in the first manfiest but don't exist in the second" do
expect(subject.removed).to eq([{ :name => "bar",
:old_version => "deadbeef",
:source_type => :git,
:source => {:git => "git://bar@example.com"}
}])
end
end
describe "#added" do
it "returns items that did not exist in the first manifest but do exist in the second" do
expect(subject.added).to eq([{ :name => "baz",
:new_version => "deadbeef",
:source_type => :git,
:source => {:git => "git://baz@example.com"}
}])
end
end
describe "#empty?" do
it "returns false if there have been changes" do
expect(subject.empty?).to eq(false)
end
it "returns true if nothing changed" do
diff = Omnibus::ManifestDiff.new(manifest_one, manifest_one)
expect(diff.empty?).to eq(true)
end
end
end
end
end
Loading
Loading
@@ -29,6 +29,26 @@ module Omnibus
end
end
 
describe "#each" do
it "yields each item to the block" do
first = ManifestEntry.new("foobar", {})
second = ManifestEntry.new("wombat", {})
subject.add("foobar", first)
subject.add("wombat", second)
expect{ |b| subject.each &b }.to yield_successive_args(first, second)
end
end
describe "#entry_names" do
it "returns an array of software names present in the manifest" do
first = ManifestEntry.new("foobar", {})
second = ManifestEntry.new("wombat", {})
subject.add("foobar", first)
subject.add("wombat", second)
expect(subject.entry_names).to eq(["foobar", "wombat"])
end
end
describe "#to_hash" do
it "returns a Hash containg the current manifest format" do
expect(subject.to_hash['manifest_format']).to eq(Manifest::LATEST_MANIFEST_FORMAT)
Loading
Loading
#
# Copyright 2015 Chef Software, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
require 'spec_helper'
module Omnibus
describe SemanticVersion do
it "raises an InvalidVersion error if it doesn't understand the format" do
expect {Omnibus::SemanticVersion.new("wut")}.to raise_error(Omnibus::InvalidVersion)
end
it "preserves leading the leading v when printing the string" do
v = Omnibus::SemanticVersion.new("v1.0.0")
expect(v.to_s).to eq("v1.0.0")
end
it "can bump the patch version" do
v = Omnibus::SemanticVersion.new("1.0.0")
expect(v.next_patch.to_s).to eq("1.0.1")
end
it "can bump the minor version" do
v = Omnibus::SemanticVersion.new("1.1.0")
expect(v.next_minor.to_s).to eq("1.2.0")
end
it "can bump the major version" do
v = Omnibus::SemanticVersion.new("1.0.0")
expect(v.next_major.to_s).to eq("2.0.0")
end
it "resets the patch version when bumping minor versions" do
v = Omnibus::SemanticVersion.new("1.1.1")
expect(v.next_minor.to_s).to eq("1.2.0")
end
it "resets the patch and minor version when bumping major versions" do
v = Omnibus::SemanticVersion.new("1.1.1")
expect(v.next_major.to_s).to eq("2.0.0")
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment