diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..4ce1f56 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,11 @@ +Metrics/LineLength: + Max: 160 + +Style/FormatStringToken: + Enabled: false + +Metrics/MethodLength: + Enabled: false + +Style/MutableConstant: + Enabled: false \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..76a1936 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,41 @@ +PATH + remote: . + specs: + plusbump (2.0.0.beta) + docopt (~> 0.6.1) + rugged (~> 0.26) + semver (~> 1.0) + +GEM + remote: https://rubygems.org/ + specs: + diff-lcs (1.3) + docopt (0.6.1) + rake (10.5.0) + rspec (3.7.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-core (3.7.1) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.7.0) + rspec-mocks (3.7.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.7.0) + rspec-support (3.7.1) + rugged (0.27.4) + semver (1.0.1) + +PLATFORMS + ruby + +DEPENDENCIES + bundler (~> 1.14) + plusbump! + rake (~> 10.0) + rspec (~> 3.7) + +BUNDLED WITH + 1.16.2 diff --git a/Rakefile b/Rakefile index 9969434..66ad4fa 100644 --- a/Rakefile +++ b/Rakefile @@ -1,11 +1,15 @@ -require "bundler/gem_tasks" -require "rspec/core/rake_task" +require 'bundler/gem_tasks' +require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) -task :default => :spec +task default: :spec -desc "Verbose test output" -task :doc do +desc 'Verbose test output' +task :doc do puts `rspec --format doc` -end \ No newline at end of file +end + +task :lint do + puts `rubocop` +end diff --git a/bin/console b/bin/console index df67edc..43518f9 100755 --- a/bin/console +++ b/bin/console @@ -1,7 +1,7 @@ #!/usr/bin/env ruby -require "bundler/setup" -require "plusbump" +require 'bundler/setup' +require 'plusbump' # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. @@ -10,5 +10,5 @@ require "plusbump" # require "pry" # Pry.start -require "irb" +require 'irb' IRB.start(__FILE__) diff --git a/bin/plusbump b/bin/plusbump index f338b62..13e0dc4 100755 --- a/bin/plusbump +++ b/bin/plusbump @@ -1,49 +1,15 @@ #!/usr/bin/env ruby -#encoding: utf-8 require 'docopt' require 'plusbump' -doc = < [options] - plusbump --latest= [options] - plusbump --version - -Arguments: - A git reference. If specified, PlusBump will search for bumps from HEAD back to this instead of searching for a tag. - -Options: - -h --help Show this screen. - - -l --latest= - - Specify a glob pattern to search for last matching tag instead of - providing a specific ref. - Will attempt to use everything after as the version string - so be sure to provide _entire_ prefix. - E.g. use "R_" if your versions are "R_1.2.3" - - --version Shows current version of PlusBump - --debug Debug flag - -DOCOPT - -# Note: If you are reading the above usage in the source code and not using --help, -# then ignore the double escapes in the usage examples. -# On the command line you have to write --majorpattern='\+major' -# The extra escape is to make it print that way in the usage message. - begin # Parse Commandline Arguments - input = Docopt::docopt(doc, version: PlusBump::VERSION) - puts input if input['--debug'] + input = Docopt.docopt(PlusBump::DOCOPT, version: PlusBump::VERSION) + puts input if input['--debug'] rescue Docopt::Exit => e puts e.message exit end -#TODO: input['--latest'].flatten[0] -puts PlusBump.bump(input[''], input['--latest'], debug: input['--debug']) - - +result = PlusBump.bump(input) +PlusBump::Tag.create(result) if input['--create-tag'] && result diff --git a/lib/plusbump.rb b/lib/plusbump.rb index b5a8ee9..05f47bf 100644 --- a/lib/plusbump.rb +++ b/lib/plusbump.rb @@ -1,101 +1,172 @@ -require "plusbump/version" +require 'plusbump/version' +require 'plusbump/usage' +require 'plusbump/config' require 'semver' require 'rugged' +# PlusBump main module module PlusBump - def self.bump(ref, latest, debug: false) - - # Defaults - major = /\+major/ - minor = /\+minor/ - patch = /\+patch/ - base = '0.0.0' - prefix = '' - # Init Repo from current directory - repository = Rugged::Repository.new(Dir.pwd) - tagcollection = Rugged::TagCollection.new(repository) + @repo = nil + + def self.repo + @repo ||= Rugged::Repository.new(Dir.pwd) + @repo + end + + class Tag + def self.create(tag_name) + target = PlusBump.repo.head.target + ref = PlusBump.repo.tags.create(tag_name, target) + puts "Created tag #{tag_name}" if ref + end + + def self.delete(tag_name) + tag_to_delete = PlusBump.repo.tags[tag_name] + PlusBump.repo.references.delete(tag_to_delete) if tag_to_delete + end + end + + def self.extract_number(partial) + /\d+/.match(partial)[0] if /\d+/ =~ partial + end + + def self.extract_prefix(partial) + /\D+/.match(partial)[0] if /\D+/ =~ partial + end + def self.transform(input) + Hash.new( input.map { |k,v| + [k.replace('-','').to_s,v] + }) + end + def self.bump(input) + if input['--from-tag'] + bump_by_tag(glob: input['--from-tag'], + base: input['--base-version'], + prefix: input['--new-prefix'] || '', + tag_replacement: input['--base-version-from-tag'], + major_pattern: input['--major-pattern'], + minor_pattern: input['--minor-pattern'], + patch_pattern: input['--patch-pattern'], + debug: input['--debug']) + elsif input['--from-ref'] + bump_by_ref(ref: input['--from-ref'], + semver: input['--base-version'], + prefix: input['--new-prefix'] || '', + major_pattern: input['--major-pattern'], + minor_pattern: input['--minor-pattern'], + patch_pattern: input['--patch-pattern'], + debug: input['--debug']) + end + end + + def self.create_walker(repository) w = Rugged::Walker.new(repository) # Initialise the walker to start at current HEAD head = repository.lookup(repository.head.target.oid) w.push(head) + w + end - if latest.nil? - tail = repository.rev_parse(ref) - w.hide(tail) - else - candidates = [] - puts "Searching for at tag that matches the glob pattern: " + latest if debug - tagcollection.each(latest+'*') do |tag| - unless repository.merge_base(tag.target, head).nil? - puts "Found matching tag on correct branch: " + tag.name if debug - candidates << tag - end - end + # TODO: Need fixing! + def self.current_semver() - if candidates.empty? - puts "No matching tag found for "+latest - else - candidates.sort! {|a,b| a.target.time <=> b.target.time } - latest_match = candidates.last - puts "Newest matching tag: #{latest_match.name}" if debug - puts "Newest matching tag sha: #{latest_match.target.oid}" if debug - #set target of matching commit as the tail of our walker - w.hide(latest_match.target) - #unless input[''] - base = latest_match.name.sub(latest,'') - puts "latest: #{latest}" if debug - puts "match.Name: #{latest_match.name}" if debug - puts "Base: #{base}" if debug - #end - - end + end + + def self.bump_by_ref(args = {}) + semver_string = args[:semver] || PlusBump::BASE + w = create_walker(PlusBump.repo) + tail = PlusBump.repo.rev_parse(args[:ref]) + w.hide(tail) + + v_number = semver_string.split('.') + v_special = semver_string.split('-').size > 1 ? semver_string.split('-')[-1] : '' + + # Current semver string + result = SemVer.new(extract_number(v_number[0]).to_i, v_number[1].to_i, v_number[2].to_i, v_special) # TODO: Fix special + + # Logic bump + bump_action(w, result, major_p: args[:major_pattern], minor_p: args[:minor_pattern], patch_p: args[:patch_pattern]) + final_res = extract_prefix(v_number[0]) || '' + (result.format "%M.%m.%p%s") + end + + # Should return a Rugged::Tag object + def self.find_newest_matching_tag(candidates) + candidates.sort! { |a,b| a.target.time <=> b.target.time } + candidates.last + end + + # The justification for this method is to split a semver tag into it's composite parts. Major.Minor.Patch[+-].... + # We need these parts to construct the SemVer object + def self.parse_semver_parts(base) + main = base.split(/[\+\-]/) # Should split something like Release_1.2.3-beta1+001 into two parts. + version_part = main[0].split('.') + # Clean up the version part...extract all numbers excluding non-numerical characters + version_part.map! { |elem| extract_number(elem) } + special_part = main[1..-1] || '' + return { :version_part => version_part, :special_part => special_part.join('') } + end + + def self.bump_by_tag(args = {}) + base = '0.0.0' + # Init Repo from current directory + tagcollection = Rugged::TagCollection.new(PlusBump.repo) + w = create_walker(PlusBump.repo) + head = PlusBump.repo.lookup(PlusBump.repo.head.target.oid) + candidates = [] + tagcollection.each(args[:glob] + '*') do |tag| + candidates << tag if !PlusBump.repo.merge_base(tag.target, head).nil? end - # Handle X.Y.Z-SPECIAL by saving SPECIAL part for later - split = base.split('-') - v_number = split[0].split('.') - special = '' + if candidates.empty? + puts 'No matching tag found for ' + args[:glob] + else + latest_match = find_newest_matching_tag(candidates) + # Set target of matching commit as the tail of our walker + w.hide(latest_match.target) + base = latest_match.name + puts "Found matching tag #{latest_match.name}" if args[:debug] + end - #TODO: Above could probably be re-written to use the semver gem for parsing. + # Current semver string + unless args[:tag_replacement].nil? + replacer = args[:tag_replacement] || '' + parts = parse_semver_parts(base) + result = SemVer.new(parts[:version_part][0].sub(replacer, '').to_i, parts[:version_part][1].to_i, parts[:version_part][2].to_i, parts[:special_part]) + else + parts = parse_semver_parts(args[:base]) + result = SemVer.new(parts[:version_part][0].to_i, parts[:version_part][1].to_i, parts[:version_part][2].to_i, parts[:special_part]) # TODO: FIX SPECIAL + end + # Logic bumps + bump_action(w, result, major_p: args[:major_pattern], minor_p: args[:minor_pattern], patch_p: args[:patch_pattern]) + args[:prefix] + (result.format "%M.%m.%p%s") + end - major_bump = false + def self.bump_action(walker, semver, **args) + # Defaults + major = Regexp.new(Regexp.quote(args[:major_p] || PlusBump::MAJOR)) + minor = Regexp.new(Regexp.quote(args[:minor_p] || PlusBump::MINOR)) + patch = Regexp.new(Regexp.quote(args[:patch_p] || PlusBump::PATCH)) minor_bump = false - patch_bump = false - #walk through all commits looking for version bump requests - w.each do |commit| - puts "Commit: " + commit.oid if debug + walker.each do |commit| if major =~ commit.message - puts "bumps major" if debug - major_bump = true - elsif minor =~ commit.message - puts "bump minor" if debug - minor_bump = true - else - patch_bump = true + semver.major += 1 + semver.minor = 0 + semver.patch = 0 + return :major end + minor_bump = true if minor =~ commit.message end - - result = SemVer.new(v_number[0].to_i, v_number[1].to_i, v_number[2].to_i, special) - - if major_bump - result.major += 1 - result.minor = 0 - result.patch = 0 - elsif minor_bump - result.minor += 1 - result.patch = 0 - elsif patch_bump - result.patch += 1 + if minor_bump + semver.minor += 1 + semver.patch = 0 + return :minor else - puts "No version increment" + semver.patch += 1 + return :patch end - - final_res = prefix + (result.format "%M.%m.%p%s") - - return final_res end end diff --git a/lib/plusbump/config.rb b/lib/plusbump/config.rb new file mode 100644 index 0000000..37b0184 --- /dev/null +++ b/lib/plusbump/config.rb @@ -0,0 +1,7 @@ +module PlusBump + # Module defaults + BASE = '0.0.0' + MAJOR = ENV['plusbump_major'] || '+major' + MINOR = ENV['plusbump_minor'] || '+minor' + PATCH = ENV['plusbump_patch'] || '+patch' +end diff --git a/lib/plusbump/usage.rb b/lib/plusbump/usage.rb new file mode 100644 index 0000000..8ffbb78 --- /dev/null +++ b/lib/plusbump/usage.rb @@ -0,0 +1,37 @@ +module PlusBump + DOCOPT = < --base-version= [options] + plusbump --from-tag --base-version= [--new-prefix=] [--create-tag] [options] + plusbump --from-tag --base-version-from-tag= [--new-prefix=] [--create-tag] [options] + +Options: + -h --help Show this screen. + --version Shows current version of PlusBump + -d --debug Debug flag + + --from-ref Specify a git ref (tree'ish) to use as start of commit interval. + PlusBump will search for bump declarations from HEAD back to, but not including this ref. + + --from-tag Specify a glob pattern (same as git tag -l ). PlusBump will find the latest tag matching this pattern + and use as the start of commit interval to analyse. + + --base-version= Take semver version as argument and use as base for computed new version + --base-version-from-tag= Find semver base version from the found tag. Optionally strip a prefix (e.g. "R_"). [default: ""] + + + --new-prefix= Optionally specify a prefix for the output computed SemVer. (e.g. "R_", or "WOULD_BE_"). + + --create-tag PlusBump tags the HEAD commit with the computed new SemVer (incl. optional prefix). This will not do a "git push". + + --patch-pattern= Specify regex pattern for bumping patch version + --minor-pattern= Specify regex pattern for bumping minor version + --major-pattern= Specify regex pattern for bumping major version +DOCOPT + +end diff --git a/lib/plusbump/version.rb b/lib/plusbump/version.rb index a9697ba..b35eb82 100644 --- a/lib/plusbump/version.rb +++ b/lib/plusbump/version.rb @@ -1,3 +1,3 @@ module PlusBump - VERSION = "2.0.0.beta" + VERSION = '2.0.0.beta2'.freeze end diff --git a/plusbump.gemspec b/plusbump.gemspec index 3f11b7c..f6b5c01 100644 --- a/plusbump.gemspec +++ b/plusbump.gemspec @@ -1,31 +1,30 @@ -# coding: utf-8 -lib = File.expand_path('../lib', __FILE__) +lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'plusbump/version' Gem::Specification.new do |spec| - spec.name = "plusbump" - spec.version = PlusBump::VERSION - spec.authors = ["Jan Krag", "Mads Nielsen"] - spec.email = ["jak@praqma.net", "man@praqma.net"] + spec.name = 'plusbump' + spec.version = PlusBump::VERSION + spec.authors = ['Jan Krag', 'Mads Nielsen'] + spec.email = ['jak@praqma.net', 'man@praqma.net'] - spec.summary = %q{PlusBump ruby gem} - spec.description = %q{Use this gem to automate the automation of version bumping in git} - spec.homepage = "https://github.com/Praqma/PlusBump" - spec.license = "MIT" + spec.summary = 'PlusBump ruby gem' + spec.description = 'Use this gem to automate the automation of version bumping in git' + spec.homepage = 'https://github.com/Praqma/PlusBump' + spec.license = 'MIT' - spec.files = `git ls-files -z`.split("\x0").reject do |f| + spec.files = `git ls-files -z`.split("\x0").reject do |f| f.match(%r{^(test|spec|features|jenkins-pipeline)/}) end - spec.bindir = "bin" + spec.bindir = 'bin' spec.executables << 'plusbump' - spec.require_paths = ["lib"] + spec.require_paths = ['lib'] - spec.add_development_dependency "bundler", "~> 1.14" - spec.add_development_dependency "rake", "~> 10.0" - spec.add_development_dependency "rspec", "~> 3.7" - spec.add_runtime_dependency 'docopt', "~> 0.6.1" - spec.add_runtime_dependency 'rugged', "~> 0.26" - spec.add_runtime_dependency 'semver', "~> 1.0" + spec.add_development_dependency 'bundler', '~> 1.14' + spec.add_development_dependency 'rake', '~> 10.0' + spec.add_development_dependency 'rspec', '~> 3.7' + spec.add_runtime_dependency 'docopt', '~> 0.6.1' + spec.add_runtime_dependency 'rugged', '~> 0.26' + spec.add_runtime_dependency 'semver', '~> 1.0' end diff --git a/spec/plusbump_spec.rb b/spec/plusbump_spec.rb index 894da0b..245d216 100644 --- a/spec/plusbump_spec.rb +++ b/spec/plusbump_spec.rb @@ -1,30 +1,78 @@ require 'plusbump' +require 'docopt' -RSpec.describe PlusBump, "#bump" do - specify { - expect { PlusBump.bump(nil, "not_found") }.to output(/No matching tag found for not_found/).to_stdout - } - specify { - expect(`ruby bin/plusbump -blaha`).to match(/Usage:/) - } - context "self smoke test" do - it "should correctly increment minor to 0.1.0" do - expect(PlusBump.bump("5a3cba405f73778b487d56fad3fd4083cfb112b5", nil)).to eq("0.1.0") - end - it "should increment major from first commit" do - expect(PlusBump.bump("e318c48368febb79309e7c371d99bb49fdd5f900", nil)).to eq("1.0.0") - end - it "should increment to major when used against 0.1.* and not be 0.1.0" do - expect(PlusBump.bump(nil, "0.1.")).not_to eq("0.1.0") - end - it "should increment to major when used against 0.1.*" do - expect(PlusBump.bump(nil, "0.1.")).to eq("1.0.0") - end - it "should increment to 1.0.0 when no tag found" do - expect(PlusBump.bump(nil, "not_found")).to eq("1.0.0") - end - it "should incremment to 3.0.0 when semver is prefix" do - expect(PlusBump.bump(nil, "2")).to eq("3.0.0") - end - end -end \ No newline at end of file +RSpec.configure do |config| + config.before(:all) do + PlusBump::Tag.delete('Test_1.0.0') + end +end + +def build_input(commandline) + Docopt.docopt(PlusBump::DOCOPT, version: PlusBump::VERSION, argv: commandline.split(' ')) +end + +RSpec.describe PlusBump, "bump" do + context '--from-ref used' do + it 'should correctly increment minor to 0.1.0' do + input = build_input("--from-ref 5a3cba405f73778b487d56fad3fd4083cfb112b5 --base-version 0.0.0") + expect(PlusBump.bump(input)).to eq('0.1.0') + end + it 'should increment major from to 1.0.0' do + input = build_input("--from-ref e318c48368febb79309e7c371d99bb49fdd5f900 --base-version 0.0.0") + expect(PlusBump.bump(input)).to eq('1.0.0') + end + end + + context '--base-version used with --from-ref' do + it 'should correctly increment minor so version becomes 1.1.0' do + input = build_input("--from-ref 5a3cba405f73778b487d56fad3fd4083cfb112b5 --base-version 1.0.0") + expect(PlusBump.bump(input)).to eq('1.1.0') + end + it 'should correctly increment major so version becomes 2.0.0' do + input = build_input("--from-ref e318c48368febb79309e7c371d99bb49fdd5f900 --base-version 1.0.0") + expect(PlusBump.bump(input)).to eq('2.0.0') + end + end + +# context '--semver and --prefix specified in ref' do +# it 'should correctly increment major so new semver v2.0.0 with manually added prefix' do +# expect(PlusBump.bump_by_ref(ref: 'e318c48368febb79309e7c371d99bb49fdd5f900', semver: '1.0.0', prefix: 'v')).to eq('v2.0.0') +# end +# end + + context '--from-tag with --base-version' do + it 'should increment to major when used against 0.1.* and not be 0.1.0' do + input = build_input("--from-tag 0.1. --base-version 0.0.0") + expect(PlusBump.bump(input)).not_to eq('0.1.0') + end + it 'should increment to major when used against 0.1.*' do + input = build_input("--from-tag 0.1. --base-version 0.0.0") + expect(PlusBump.bump(input)).not_to eq('0.1.0') + expect(PlusBump.bump(input)).to eq('1.0.0') + end + end + + context '--from-tag with with --base-version-from-tag' do + it 'should increment to 3.0.0 when used with 2.0.0 as tag glob' do + input = build_input("--from-tag 2.0.0 --base-version-from-tag='' --new-prefix=Test_") + expect(PlusBump.bump(input)).to eq('Test_3.0.0') + end + it 'should increment to 2.1.0 when used with 2.0.0 as tag glob and no matching major pattern (minor matches)' do + input = build_input("--from-tag 2.0.0 --base-version-from-tag='' --new-prefix=Test_ --major-pattern=not_there") + expect(PlusBump.bump(input)).to eq('Test_2.1.0') + end + it 'should increment to correctly with tag prefix' do + input = build_input("--from-tag R_ --base-version-from-tag=R_") + expect(PlusBump.bump(input)).to eq('2.1.0') + end + it 'should increment correctly with empty base-version-from-tag' do + input = build_input("--from-tag R_ --base-version-from-tag=''") + expect(PlusBump.bump(input)).to eq('2.1.0') + end + end + + context 'tag should be created' do + specify { expect { PlusBump::Tag.create('Test_1.0.0') }.to output(/Created tag Test_1.0.0/).to_stdout } + end + +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 251aa51..0cb0392 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -43,58 +43,4 @@ # inherited by the metadata hash of host groups and examples, rather than # triggering implicit auto-inclusion in groups with matching metadata. config.shared_context_metadata_behavior = :apply_to_host_groups - -# The settings below are suggested to provide a good initial experience -# with RSpec, but feel free to customize to your heart's content. -=begin - # This allows you to limit a spec run to individual examples or groups - # you care about by tagging them with `:focus` metadata. When nothing - # is tagged with `:focus`, all examples get run. RSpec also provides - # aliases for `it`, `describe`, and `context` that include `:focus` - # metadata: `fit`, `fdescribe` and `fcontext`, respectively. - config.filter_run_when_matching :focus - - # Allows RSpec to persist some state between runs in order to support - # the `--only-failures` and `--next-failure` CLI options. We recommend - # you configure your source control system to ignore this file. - config.example_status_persistence_file_path = "spec/examples.txt" - - # Limits the available syntax to the non-monkey patched syntax that is - # recommended. For more details, see: - # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ - # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ - # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode - config.disable_monkey_patching! - - # This setting enables warnings. It's recommended, but in some cases may - # be too noisy due to issues in dependencies. - config.warnings = true - - # Many RSpec users commonly either run the entire suite or an individual - # file, and it's useful to allow more verbose output when running an - # individual spec file. - if config.files_to_run.one? - # Use the documentation formatter for detailed output, - # unless a formatter has already been configured - # (e.g. via a command-line flag). - config.default_formatter = "doc" - end - - # Print the 10 slowest examples and example groups at the - # end of the spec run, to help surface which specs are running - # particularly slow. - config.profile_examples = 10 - - # Run specs in random order to surface order dependencies. If you find an - # order dependency and want to debug it, you can fix the order by providing - # the seed, which is printed after each run. - # --seed 1234 - config.order = :random - - # Seed global randomization in this process using the `--seed` CLI option. - # Setting this allows you to use `--seed` to deterministically reproduce - # test failures related to randomization by passing the same `--seed` value - # as the one that triggered the failure. - Kernel.srand config.seed -=end end