diff --git a/.config/cucumber.yml b/.config/cucumber.yml new file mode 100644 index 00000000..fea5edc1 --- /dev/null +++ b/.config/cucumber.yml @@ -0,0 +1 @@ +default: --publish-quiet diff --git a/.rubocop.yml b/.rubocop.yml index 84fb5455..8c30206a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -25,6 +25,8 @@ Layout/IndentHeredoc: # sane line length Metrics/LineLength: Max: 120 + Exclude: + - 'features/**/*' Style/FrozenStringLiteralComment: Enabled: false diff --git a/README.md b/README.md index 83dc61db..fd9624fa 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Table of Contents 4. [Installing](#installing) 5. [Workflow](#workflow) 6. [The Templates](#the-templates) +7. [Additional features](#additional-features) Usage TLDR ---------- @@ -436,3 +437,38 @@ The Templates See [Puppet's modulesync\_configs](https://github.com/puppetlabs/modulesync_configs) and [Vox Pupuli's modulesync\_config](https://github.com/voxpupuli/modulesync_config) repositories for different templates currently in use. + +Additional features +------------------- + +## Execute a custom script on each module + +During puppet modules maintenance, you may want to run a custom script to modify, add or deleted files then commit modifications. + +As example, if you want to generate REFERENCES.md, CHANGELOG.md or update your module to be PDK compliant, this command could be useful. + +```shell +msync exec /path/to/script --branch my-new-feature +``` + +**Important**: Please note that it is up to your script to modify correctly the repository (e.g. `git mv old-path new-path`, `git add my-file`) and performs the commits (ie. `git commit -m'Set a smart commit message here'`). + +Like `update` command, you can push and submit a PR/MR using the same options: + +```shell +msync exec /path/to/script --branch my-new-feature --push --pr --pr-title "Add a new super feature" +``` + +## Push manually a branch for each module + +After a regular `msync update` without push, you may want to add some commits manually then push. + +```shell +msync push --branch my-specific-branch +``` + +Like `update` command, you can submit a PR/MR using the same options: + +```shell +msync push --branch my-specific-feature-branch --pr --pr-title "Add a new super feature" +``` diff --git a/features/cli.feature b/features/cli.feature index 509ba47c..e920521a 100644 --- a/features/cli.feature +++ b/features/cli.feature @@ -18,18 +18,11 @@ Feature: CLI And the output should match /Commands:/ Scenario: When overriding a setting from the config file on the command line - Given a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: 'git@github.com:' - """ + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a directory named "moduleroot" - When I run `msync update --noop --git-base https://github.com/` + When I run `msync update --noop --namespace fakenamespace` Then the exit status should be 0 - And the output should not match /git@github.com:/ + And the stdout should contain: + """ + Syncing 'fakenamespace/puppet-test' + """ diff --git a/features/execute.feature b/features/execute.feature new file mode 100644 index 00000000..51cb7738 --- /dev/null +++ b/features/execute.feature @@ -0,0 +1,25 @@ +Feature: execute + Use ModuleSync to execute a custom script on a specified branch for each repos and optionaly push and submit a PR/MR + + Scenario: Running command without required 'branch' option + When I run `msync exec` + Then the output should match /^No value provided for required options '--branch'$/ + + Scenario: Cloning modules before running command when modules/ dir is empty + Given a basic setup with a puppet module "puppet-test" from "awesome" + Then the file "modules/awesome/puppet-test/metadata.json" should not exist + When I run `msync exec -b master /bin/true` + Then the exit status should be 0 + And the file "modules/awesome/puppet-test/metadata.json" should exist + + @no-clobber + Scenario: Hard-reset on specified branch before running command when modules have already been cloned + Given the file "modules/awesome/puppet-test/metadata.json" should exist + And the file "modules/awesome/puppet-test/hello" should not exist + And the puppet module "puppet-test" from "awesome" have a branch "execute" + And the puppet module "puppet-test" from "awesome" have a file named "hello" on branch "execute" with: + """ + Hello + """ + When I run `msync exec -b master --reset-hard origin/execute /bin/true` + And the file "modules/awesome/puppet-test/hello" should exist diff --git a/features/hook.feature b/features/hook.feature index 10e97909..cae008e9 100644 --- a/features/hook.feature +++ b/features/hook.feature @@ -5,8 +5,7 @@ Feature: hook Given a directory named ".git/hooks" When I run `msync hook activate` Then the exit status should be 0 - Given I run `cat .git/hooks/pre-push` - Then the output should contain "bash" + And the file named ".git/hooks/pre-push" should contain "bash" Scenario: Deactivating a hook Given a file named ".git/hooks/pre-push" with: @@ -21,8 +20,7 @@ Feature: hook Given a directory named ".git/hooks" When I run `msync hook activate -a '--foo bar --baz quux' -b master` Then the exit status should be 0 - Given I run `cat .git/hooks/pre-push` - Then the output should match: + And the file named ".git/hooks/pre-push" should contain: """ - "\$message" -n puppetlabs -b master --foo bar --baz quux + "$message" -n puppetlabs -b master --foo bar --baz quux """ diff --git a/features/push.feature b/features/push.feature new file mode 100644 index 00000000..625b0ac2 --- /dev/null +++ b/features/push.feature @@ -0,0 +1,15 @@ +Feature: push + Use ModuleSync to push a specified branch from each repos and optionaly submit a PR/MR + + Scenario: When 'branch' option is missing + When I run `msync push` + Then the output should match /^No value provided for required options '--branch'$/ + + Scenario: Report the need to clone repositories if puppet module have not been fetch before + Given a basic setup with a puppet module "puppet-test" from "awesome" + When I run `msync push --branch hello` + Then the exit status should be 1 + And the stderr should contain: + """ + Repository must be locally available before trying to push + """ diff --git a/features/step_definitions/git_steps.rb b/features/step_definitions/git_steps.rb index 36c1e903..c860eff5 100644 --- a/features/step_definitions/git_steps.rb +++ b/features/step_definitions/git_steps.rb @@ -1,48 +1,85 @@ -Given 'a mocked git configuration' do - steps %( - Given a mocked home directory - And I run `git config --global user.name Test` - And I run `git config --global user.email test@example.com` - ) -end +require_relative '../../spec/helpers/faker/puppet_module_remote_repo' -Given 'a remote module repository' do +Given 'a basic setup with a puppet module {string} from {string}' do |name, namespace| steps %( - Given a directory named "sources" - And I run `git clone https://github.com/maestrodev/puppet-test sources/puppet-test` + Given a mocked git configuration + And a puppet module "#{name}" from "#{namespace}" And a file named "managed_modules.yml" with: """ --- - - puppet-test + - #{name} """ + And a file named "modulesync.yml" with: + """ + --- + namespace: #{namespace} + """ + And a git_base option appended to "modulesync.yml" for local tests ) - write_file('modulesync.yml', <<~CONFIG) - --- - namespace: sources - git_base: file://#{expand_path('.')}/ - CONFIG end -Given Regexp.new(/a remote module repository with "(.+?)" as the default branch/) do |branch| +Given 'a mocked git configuration' do steps %( - Given a directory named "sources" - And I run `git clone --mirror https://github.com/maestrodev/puppet-test sources/puppet-test` - And a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ + Given a mocked home directory + And I run `git config --global user.name Aruba` + And I run `git config --global user.email aruba@example.com` ) - write_file('modulesync.yml', <<~CONFIG) - --- - namespace: sources - git_base: file://#{expand_path('.')}/ - CONFIG - - cd('sources/puppet-test') do - steps %( - And I run `git branch -M master #{branch}` - And I run `git symbolic-ref HEAD refs/heads/#{branch}` - ) - end +end + +Given 'a puppet module {string} from {string}' do |name, namespace| + pmrr = Faker::PuppetModuleRemoteRepo.new(name, namespace) + pmrr.populate +end + +Given 'a git_base option appended to "modulesync.yml" for local tests' do + File.write "#{Aruba.config.working_directory}/modulesync.yml", "\ngit_base: #{Faker::PuppetModuleRemoteRepo.git_base}", mode: 'a' +end + +Given 'the puppet module {string} from {string} is read-only' do |name, namespace| + pmrr = Faker::PuppetModuleRemoteRepo.new(name, namespace) + pmrr.read_only = true +end + +Then 'the puppet module {string} from {string} have no commit between {string} and {string}' do |name, namespace, commit1, commit2| + pmrr = Faker::PuppetModuleRemoteRepo.new(name, namespace) + expect(pmrr.commit_count_between(commit1, commit2)).to eq 0 +end + +Then 'the puppet module {string} from {string} have( only) {int} commit(s) made by {string}' do |name, namespace, commit_count, author| + pmrr = Faker::PuppetModuleRemoteRepo.new(name, namespace) + expect(pmrr.commit_count_by(author)).to eq commit_count +end + +Then 'the puppet module {string} from {string} have( only) {int} commit(s) made by {string} in branch {string}' do |name, namespace, commit_count, author, branch| + pmrr = Faker::PuppetModuleRemoteRepo.new(name, namespace) + expect(pmrr.commit_count_by(author, branch)).to eq commit_count +end + +Then 'the puppet module {string} from {string} have no commit made by {string}' do |name, namespace, author| + step "the puppet module \"#{name}\" from \"#{namespace}\" have 0 commit made by \"#{author}\"" +end + +Given 'the puppet module {string} from {string} have a file named {string} with:' do |name, namespace, filename, content| + pmrr = Faker::PuppetModuleRemoteRepo.new(name, namespace) + pmrr.add_file(filename, content) +end + +Given 'the puppet module {string} from {string} have a file named {string} on branch {string} with:' do |name, namespace, filename, branch, content| + pmrr = Faker::PuppetModuleRemoteRepo.new(name, namespace) + pmrr.add_file(filename, content, branch) +end + +Then 'the puppet module {string} from {string} should have a branch {string} with a file named {string} which contains:' do |name, namespace, branch, filename, content| + pmrr = Faker::PuppetModuleRemoteRepo.new(name, namespace) + expect(pmrr.read_file(filename, branch)).to include(content) +end + +Given 'the puppet module {string} from {string} have the default branch named {string}' do |name, namespace, default_branch| + pmrr = Faker::PuppetModuleRemoteRepo.new(name, namespace) + pmrr.default_branch = default_branch +end + +Given 'the puppet module {string} from {string} have a branch {string}' do |name, namespace, branch| + pmrr = Faker::PuppetModuleRemoteRepo.new(name, namespace) + pmrr.create_branch(branch) end diff --git a/features/support/env.rb b/features/support/env.rb index f3f544d6..f391dde2 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -1,5 +1,9 @@ require 'aruba/cucumber' +require_relative '../../spec/helpers/faker' + +Faker.working_directory = File.expand_path("#{Aruba.config.working_directory}/faker") + Before do @aruba_timeout_seconds = 5 end diff --git a/features/update.feature b/features/update.feature index 5dedf977..8078a04f 100644 --- a/features/update.feature +++ b/features/update.feature @@ -2,17 +2,7 @@ Feature: update ModuleSync needs to update module boilerplate Scenario: Adding a new file - Given a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: https://github.com/ - """ + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a file named "config_defaults.yml" with: """ --- @@ -31,21 +21,11 @@ Feature: update Files added: test """ - Given I run `cat modules/maestrodev/puppet-test/test` - Then the output should contain "aruba" + And the file named "modules/fakenamespace/puppet-test/test" should contain "aruba" Scenario: Using skip_broken option and adding a new file to repo without write access - Given a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: 'git@github.com:' - """ + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" + And the puppet module "puppet-test" from "fakenamespace" is read-only And a file named "config_defaults.yml" with: """ --- @@ -59,19 +39,11 @@ Feature: update """ When I run `msync update -s -m "Add test"` Then the exit status should be 0 + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Adding a new file to repo without write access - Given a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: 'git@github.com:' - """ + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" + And the puppet module "puppet-test" from "fakenamespace" is read-only And a file named "config_defaults.yml" with: """ --- @@ -85,19 +57,10 @@ Feature: update """ When I run `msync update -m "Add test" -r` Then the exit status should be 1 + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Adding a new file, without the .erb suffix - Given a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: https://github.com/ - """ + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a file named "config_defaults.yml" with: """ --- @@ -120,21 +83,11 @@ Feature: update Files added: test """ - Given I run `cat modules/maestrodev/puppet-test/test` - Then the output should contain "aruba" + And the file named "modules/fakenamespace/puppet-test/test" should contain "aruba" + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Adding a new file using global values - Given a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: https://github.com/ - """ + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a file named "config_defaults.yml" with: """ --- @@ -153,21 +106,11 @@ Feature: update Files added: test """ - Given I run `cat modules/maestrodev/puppet-test/test` - Then the output should contain "aruba" + And the file named "modules/fakenamespace/puppet-test/test" should contain "aruba" + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Adding a new file overriding global values - Given a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: https://github.com/ - """ + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a file named "config_defaults.yml" with: """ --- @@ -189,21 +132,11 @@ Feature: update Files added: test """ - Given I run `cat modules/maestrodev/puppet-test/test` - Then the output should contain "aruba" + And the file named "modules/fakenamespace/puppet-test/test" should contain "aruba" + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Adding a new file ignoring global values - Given a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: https://github.com/ - """ + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a file named "config_defaults.yml" with: """ --- @@ -225,21 +158,11 @@ Feature: update Files added: test """ - Given I run `cat modules/maestrodev/puppet-test/test` - Then the output should contain "aruba" + And the file named "modules/fakenamespace/puppet-test/test" should contain "aruba" + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Adding a file that ERB can't parse - Given a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: https://github.com/ - """ + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a file named "config_defaults.yml" with: """ --- @@ -255,19 +178,10 @@ Feature: update """ When I run `msync update --noop` Then the exit status should be 1 + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Using skip_broken option with invalid files - Given a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: https://github.com/ - """ + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a file named "config_defaults.yml" with: """ --- @@ -283,19 +197,10 @@ Feature: update """ When I run `msync update --noop -s` Then the exit status should be 0 + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Using skip_broken and fail_on_warnings options with invalid files - Given a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: https://github.com/ - """ + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a file named "config_defaults.yml" with: """ --- @@ -311,18 +216,13 @@ Feature: update """ When I run `msync update --noop --skip_broken --fail_on_warnings` Then the exit status should be 1 + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Modifying an existing file - Given a file named "managed_modules.yml" with: + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" + And the puppet module "puppet-test" from "fakenamespace" have a file named "Gemfile" with: """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: https://github.com/ + source 'https://example.com' """ And a file named "config_defaults.yml" with: """ @@ -342,15 +242,18 @@ Feature: update Files changed: +diff --git a/Gemfile b/Gemfile """ - Given I run `cat modules/maestrodev/puppet-test/Gemfile` - Then the output should contain: + And the file named "modules/fakenamespace/puppet-test/Gemfile" should contain: """ source 'https://somehost.com' """ + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Modifying an existing file and committing the change - Given a mocked git configuration - And a remote module repository + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" + And the puppet module "puppet-test" from "fakenamespace" have a file named "Gemfile" with: + """ + source 'https://example.com' + """ And a file named "config_defaults.yml" with: """ --- @@ -364,30 +267,25 @@ Feature: update """ When I run `msync update -m "Update Gemfile" -r test` Then the exit status should be 0 - Given I cd to "sources/puppet-test" - And I run `git checkout test` - Then the file "Gemfile" should contain: + And the puppet module "puppet-test" from "fakenamespace" have only 1 commit made by "Aruba" + And the puppet module "puppet-test" from "fakenamespace" have 1 commit made by "Aruba" in branch "test" + And the puppet module "puppet-test" from "fakenamespace" should have a branch "test" with a file named "Gemfile" which contains: """ source 'https://somehost.com' """ Scenario: Setting an existing file to unmanaged - Given a file named "managed_modules.yml" with: + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" + And the puppet module "puppet-test" from "fakenamespace" have a file named "Gemfile" with: """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: https://github.com/ + source 'https://rubygems.org' """ And a file named "config_defaults.yml" with: """ --- Gemfile: unmanaged: true + gem_source: https://somehost.com """ And a directory named "moduleroot" And a file named "moduleroot/Gemfile.erb" with: @@ -405,24 +303,14 @@ Feature: update Not managing Gemfile in puppet-test """ And the exit status should be 0 - Given I run `cat modules/maestrodev/puppet-test/Gemfile` - Then the output should contain: + And the file named "modules/fakenamespace/puppet-test/Gemfile" should contain: """ source 'https://rubygems.org' """ + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Setting an existing file to deleted - Given a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: https://github.com/ - """ + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a file named "config_defaults.yml" with: """ --- @@ -434,6 +322,10 @@ Feature: update """ source '<%= @configs['gem_source'] %>' """ + And the puppet module "puppet-test" from "fakenamespace" have a file named "Gemfile" with: + """ + source 'https://rubygems.org' + """ When I run `msync update --noop` Then the output should match: """ @@ -442,19 +334,10 @@ Feature: update deleted file mode 100644 """ And the exit status should be 0 + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Setting a non-existent file to deleted - Given a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: https://github.com/ - """ + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a file named "config_defaults.yml" with: """ --- @@ -464,19 +347,10 @@ Feature: update And a directory named "moduleroot" When I run `msync update -m 'deletes a file that doesnt exist!' -f puppet-test` And the exit status should be 0 + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Setting a directory to unmanaged - Given a file named "managed_modules.yml" with: - """ - --- - - puppetlabs-apache - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: puppetlabs - git_base: https://github.com/ - """ + Given a basic setup with a puppet module "puppet-apache" from "puppetlabs" And a file named "config_defaults.yml" with: """ --- @@ -488,36 +362,26 @@ Feature: update """ some spec_helper fud """ - And a directory named "modules/puppetlabs/puppetlabs-apache/spec" - And a file named "modules/puppetlabs/puppetlabs-apache/spec/spec_helper.rb" with: + And a directory named "modules/puppetlabs/puppet-apache/spec" + And a file named "modules/puppetlabs/puppet-apache/spec/spec_helper.rb" with: """ This is a fake spec_helper! """ When I run `msync update --offline` Then the output should contain: """ - Not managing spec/spec_helper.rb in puppetlabs-apache + Not managing spec/spec_helper.rb in puppet-apache """ And the exit status should be 0 - Given I run `cat modules/puppetlabs/puppetlabs-apache/spec/spec_helper.rb` - Then the output should contain: + And the file named "modules/puppetlabs/puppet-apache/spec/spec_helper.rb" should contain: """ This is a fake spec_helper! """ And the exit status should be 0 + And the puppet module "puppet-apache" from "puppetlabs" have no commit made by "Aruba" Scenario: Adding a new file in a new subdirectory - Given a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: https://github.com/ - """ + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a file named "config_defaults.yml" with: """ --- @@ -538,24 +402,14 @@ Feature: update Files added: spec/spec_helper.rb """ - Given I run `cat modules/maestrodev/puppet-test/spec/spec_helper.rb` - Then the output should contain: + And the file named "modules/fakenamespace/puppet-test/spec/spec_helper.rb" should contain: """ require 'puppetlabs_spec_helper/module_helper' """ + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Updating offline - Given a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: https://github.com/ - """ + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a file named "config_defaults.yml" with: """ --- @@ -572,19 +426,14 @@ Feature: update When I run `msync update --offline` Then the exit status should be 0 And the output should not match /Files (changed|added|deleted):/ + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Pulling a module that already exists in the modules directory - Given a file named "managed_modules.yml" with: - """ - --- - - maestrodev/puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - git_base: https://github.com/ - """ - And a file named "config_defaults.yml" with: + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" + When I run `msync update` + Then the exit status should be 0 + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" + Given a file named "config_defaults.yml" with: """ --- spec/spec_helper.rb: @@ -597,20 +446,6 @@ Feature: update require '<%= required %>' <% end %> """ - Given I run `git init modules/maestrodev/puppet-test` - Given a file named "modules/maestrodev/puppet-test/.git/config" with: - """ - [core] - repositoryformatversion = 0 - filemode = true - bare = false - logallrefupdates = true - ignorecase = true - precomposeunicode = true - [remote "origin"] - url = https://github.com/maestrodev/puppet-test.git - fetch = +refs/heads/*:refs/remotes/origin/* - """ When I run `msync update --noop` Then the exit status should be 0 And the output should match: @@ -618,37 +453,23 @@ Feature: update Files added: spec/spec_helper.rb """ + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: When running update with no changes - Given a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: https://github.com/ - """ + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a directory named "moduleroot" - When I run `msync update --noop` + When I run `msync update` Then the exit status should be 0 - And the output should not match /diff/ + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: When specifying configurations in managed_modules.yml - Given a file named "managed_modules.yml" with: + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" + And a file named "managed_modules.yml" with: """ --- puppet-test: module_name: test """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: https://github.com/ - """ And a file named "config_defaults.yml" with: """ --- @@ -667,11 +488,14 @@ Feature: update Files added: test """ - Given I run `cat modules/maestrodev/puppet-test/test` - Then the output should contain "aruba" + And the file named "modules/fakenamespace/puppet-test/test" should contain "aruba" + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: When specifying configurations in managed_modules.yml and using a filter - Given a file named "managed_modules.yml" with: + Given a mocked git configuration + And a puppet module "puppet-test" from "fakenamespace" + And a puppet module "puppet-blacksmith" from "fakenamespace" + And a file named "managed_modules.yml" with: """ --- puppet-blacksmith: @@ -681,9 +505,9 @@ Feature: update And a file named "modulesync.yml" with: """ --- - namespace: maestrodev - git_base: https://github.com/ + namespace: fakenamespace """ + And a git_base option appended to "modulesync.yml" for local tests And a file named "config_defaults.yml" with: """ --- @@ -702,12 +526,15 @@ Feature: update Files added: test """ - Given I run `cat modules/maestrodev/puppet-test/test` - Then the output should contain "aruba" - And a directory named "modules/maestrodev/puppet-blacksmith" should not exist + And the file named "modules/fakenamespace/puppet-test/test" should contain "aruba" + And a directory named "modules/fakenamespace/puppet-blacksmith" should not exist + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: When specifying configurations in managed_modules.yml and using a negative filter - Given a file named "managed_modules.yml" with: + Given a mocked git configuration + And a puppet module "puppet-test" from "fakenamespace" + And a puppet module "puppet-blacksmith" from "fakenamespace" + And a file named "managed_modules.yml" with: """ --- puppet-blacksmith: @@ -717,9 +544,9 @@ Feature: update And a file named "modulesync.yml" with: """ --- - namespace: maestrodev - git_base: https://github.com/ + namespace: fakenamespace """ + And a git_base option appended to "modulesync.yml" for local tests And a file named "config_defaults.yml" with: """ --- @@ -738,21 +565,12 @@ Feature: update Files added: test """ - Given I run `cat modules/maestrodev/puppet-test/test` - Then the output should contain "aruba" - And a directory named "modules/maestrodev/puppet-blacksmith" should not exist + And the file named "modules/fakenamespace/puppet-test/test" should contain "aruba" + And a directory named "modules/fakenamespace/puppet-blacksmith" should not exist + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Updating a module with a .sync.yml file - Given a file named "managed_modules.yml" with: - """ - --- - - maestrodev/puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - git_base: https://github.com/ - """ + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a file named "config_defaults.yml" with: """ --- @@ -775,21 +593,7 @@ Feature: update <%= @configs['global-to-overwrite'] %> <%= @configs['module-default'] %> """ - Given I run `git init modules/maestrodev/puppet-test` - Given a file named "modules/maestrodev/puppet-test/.git/config" with: - """ - [core] - repositoryformatversion = 0 - filemode = true - bare = false - logallrefupdates = true - ignorecase = true - precomposeunicode = true - [remote "origin"] - url = https://github.com/maestrodev/puppet-test.git - fetch = +refs/heads/*:refs/remotes/origin/* - """ - Given a file named "modules/maestrodev/puppet-test/.sync.yml" with: + And the puppet module "puppet-test" from "fakenamespace" have a file named ".sync.yml" with: """ --- :global: @@ -804,16 +608,19 @@ Feature: update """ Not managing spec/spec_helper.rb in puppet-test """ - Given I run `cat modules/maestrodev/puppet-test/global-test.md` - Then the output should match: + And the file named "modules/fakenamespace/puppet-test/global-test.md" should contain: """ some-default it-is-overwritten some-value """ + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Module with custom namespace - Given a file named "managed_modules.yml" with: + Given a mocked git configuration + And a puppet module "puppet-test" from "fakenamespace" + And a puppet module "puppet-lib-file_concat" from "electrical" + And a file named "managed_modules.yml" with: """ --- - puppet-test @@ -822,9 +629,9 @@ Feature: update And a file named "modulesync.yml" with: """ --- - namespace: maestrodev - git_base: https://github.com/ + namespace: fakenamespace """ + And a git_base option appended to "modulesync.yml" for local tests And a file named "config_defaults.yml" with: """ --- @@ -843,23 +650,13 @@ Feature: update Files added: test """ - Given I run `cat modules/maestrodev/puppet-test/.git/config` - Then the output should contain "url = https://github.com/maestrodev/puppet-test.git" - Given I run `cat modules/electrical/puppet-lib-file_concat/.git/config` - Then the output should contain "url = https://github.com/electrical/puppet-lib-file_concat.git" + And the file named "modules/fakenamespace/puppet-test/.git/config" should match /^\s+url = .*fakenamespace.puppet-test$/ + And the file named "modules/electrical/puppet-lib-file_concat/.git/config" should match /^\s+url = .*electrical.puppet-lib-file_concat$/ + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" + And the puppet module "puppet-lib-file_concat" from "electrical" have no commit made by "Aruba" Scenario: Modifying an existing file with values exposed by the module - Given a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: https://github.com/ - """ + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a file named "config_defaults.yml" with: """ --- @@ -868,7 +665,12 @@ Feature: update And a directory named "moduleroot" And a file named "moduleroot/README.md.erb" with: """ - echo '<%= @configs[:git_base] + @configs[:namespace] %>' + module: <%= @configs[:puppet_module] %> + namespace: <%= @configs[:namespace] %> + """ + And the puppet module "puppet-test" from "fakenamespace" have a file named "README.md" with: + """ + Hello world! """ When I run `msync update --noop` Then the exit status should be 0 @@ -877,15 +679,15 @@ Feature: update Files changed: +diff --git a/README.md b/README.md """ - Given I run `cat modules/maestrodev/puppet-test/README.md` - Then the output should contain: + And the file named "modules/fakenamespace/puppet-test/README.md" should contain: """ - echo 'https://github.com/maestrodev' + module: puppet-test + namespace: fakenamespace """ + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Running the same update twice and pushing to a remote branch - Given a mocked git configuration - And a remote module repository + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a file named "config_defaults.yml" with: """ --- @@ -899,15 +701,18 @@ Feature: update """ When I run `msync update -m "Update Gemfile" -r test` Then the exit status should be 0 + And the puppet module "puppet-test" from "fakenamespace" have only 1 commit made by "Aruba" + And the puppet module "puppet-test" from "fakenamespace" have 1 commit made by "Aruba" in branch "test" Given I remove the directory "modules" When I run `msync update -m "Update Gemfile" -r test` Then the exit status should be 0 Then the output should not contain "error" Then the output should not contain "rejected" + And the puppet module "puppet-test" from "fakenamespace" have only 1 commit made by "Aruba" + And the puppet module "puppet-test" from "fakenamespace" have 1 commit made by "Aruba" in branch "test" Scenario: Creating a GitHub PR with an update - Given a mocked git configuration - And a remote module repository + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a directory named "moduleroot" And I set the environment variables to: | variable | value | @@ -915,10 +720,10 @@ Feature: update When I run `msync update --noop --branch managed_update --pr` Then the output should contain "Would submit PR " And the exit status should be 0 + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Creating a GitLab MR with an update - Given a mocked git configuration - And a remote module repository + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a directory named "moduleroot" And I set the environment variables to: | variable | value | @@ -926,10 +731,11 @@ Feature: update When I run `msync update --noop --branch managed_update --pr` Then the output should contain "Would submit MR " And the exit status should be 0 + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" Scenario: Repository with a default branch other than master - Given a mocked git configuration - And a remote module repository with "develop" as the default branch + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" + And the puppet module "puppet-test" from "fakenamespace" have the default branch named "develop" And a file named "config_defaults.yml" with: """ --- @@ -943,24 +749,16 @@ Feature: update """ When I run `msync update -m "Update Gemfile"` Then the exit status should be 0 - Then the output should contain "Using repository's default branch: develop" + And the output should contain "Using repository's default branch: develop" + And the puppet module "puppet-test" from "fakenamespace" have only 1 commit made by "Aruba" + And the puppet module "puppet-test" from "fakenamespace" have 1 commit made by "Aruba" in branch "develop" - Scenario: Adding a new file from a template using meta data + Scenario: Adding a new file from a template using metadata + Given a basic setup with a puppet module "puppet-test" from "fakenamespace" And a file named "config_defaults.yml" with: """ --- """ - Given a file named "managed_modules.yml" with: - """ - --- - - puppet-test - """ - And a file named "modulesync.yml" with: - """ - --- - namespace: maestrodev - git_base: https://github.com/ - """ And a directory named "moduleroot" And a file named "moduleroot/test.erb" with: """ @@ -970,10 +768,10 @@ Feature: update """ When I run `msync update --noop` Then the exit status should be 0 - Given I run `cat modules/maestrodev/puppet-test/test` - Then the output should contain: + And the file named "modules/fakenamespace/puppet-test/test" should contain: """ module: puppet-test - target: modules/maestrodev/puppet-test/test - workdir: modules/maestrodev/puppet-test + target: modules/fakenamespace/puppet-test/test + workdir: modules/fakenamespace/puppet-test """ + And the puppet module "puppet-test" from "fakenamespace" have no commit made by "Aruba" diff --git a/lib/modulesync.rb b/lib/modulesync.rb index 101d4197..e1e10a21 100644 --- a/lib/modulesync.rb +++ b/lib/modulesync.rb @@ -1,5 +1,6 @@ require 'fileutils' require 'pathname' +require 'English' require 'modulesync/cli' require 'modulesync/constants' require 'modulesync/git' @@ -10,6 +11,8 @@ require 'monkey_patches' module ModuleSync # rubocop:disable Metrics/ModuleLength + class Error < StandardError; end + include Constants def self.config_defaults @@ -62,9 +65,17 @@ def self.managed_modules(config_file, filter, negative_filter) managed_modules end - def self.module_name(module_name, default_namespace) - return [default_namespace, module_name] unless module_name.include?('/') - ns, mod = module_name.split('/') + def self.compute_module_naming_attributes(module_name, options, module_options) + namespace = module_options[:namespace] || options[:namespace] + name = module_name + namespace, name = module_name.split('/') if module_name.include?('/') + fullname = "#{namespace}/#{name}" + + { + name: name, + namespace: namespace, + fullname: fullname, + } end def self.hook(options) @@ -105,26 +116,28 @@ def self.manage_file(filename, settings, options) end def self.manage_module(puppet_module, module_files, module_options, defaults, options) - default_namespace = options[:namespace] - if module_options.is_a?(Hash) && module_options.key?(:namespace) - default_namespace = module_options[:namespace] - end - namespace, module_name = module_name(puppet_module, default_namespace) - git_repo = File.join(namespace, module_name) + git_repo = module_options[:fullname] + unless options[:offline] - Git.pull(options[:git_base], git_repo, options[:branch], options[:project_root], module_options || {}) + git_options = { + override_changes: true, + remote: module_options[:remote], + reset_hard: options[:reset_hard], + } + Git.pull(options[:git_base], git_repo, options[:branch], options[:project_root], git_options) end - module_configs = Util.parse_config(module_file(options[:project_root], namespace, module_name, MODULE_CONF_FILE)) + module_configs = Util.parse_config(module_file(options[:project_root], module_options[:namespace], module_options[:name], MODULE_CONF_FILE)) settings = Settings.new(defaults[GLOBAL_DEFAULTS_KEY] || {}, defaults, module_configs[GLOBAL_DEFAULTS_KEY] || {}, module_configs, - :puppet_module => module_name, + :puppet_module => module_options[:name], :git_base => options[:git_base], - :namespace => namespace) + :namespace => module_options[:namespace]) + settings.unmanaged_files(module_files).each do |filename| - $stdout.puts "Not managing #{filename} in #{module_name}" + $stdout.puts "Not managing #{filename} in #{module_options[:name]}" end files_to_manage = settings.managed_files(module_files) @@ -132,10 +145,10 @@ def self.manage_module(puppet_module, module_files, module_options, defaults, op if options[:noop] Git.update_noop(git_repo, options) - options[:pr] && pr(module_options).manage(namespace, module_name, options) + options[:pr] && pr(module_options).manage(module_options[:namespace], module_options[:name], options) elsif !options[:offline] pushed = Git.update(git_repo, files_to_manage, options) - pushed && options[:pr] && pr(module_options).manage(namespace, module_name, options) + pushed && options[:pr] && pr(module_options).manage(module_options[:namespace], module_options[:name], options) end end @@ -148,8 +161,63 @@ def config_path(file, options) self.class.config_path(file, options) end - def self.update(options) + def self.update(cli_options) + prepare = lambda { |options| + local_template_dir = config_path(MODULE_FILES_DIR, options) + local_files = find_template_files(local_template_dir) + module_files = relative_names(local_files, local_template_dir) + return options.merge(_module_files: module_files) + } + + job = lambda { |puppet_module, module_options, defaults, options| + manage_module(puppet_module, options[:_module_files], module_options, defaults, options) + } + + run(cli_options, job, prepare) + end + + def self.push(cli_options) + job = lambda { |puppet_module, module_options, _defaults, options| + repo_dir = File.join(options[:project_root], module_options[:fullname]) + + begin + repo = ::Git.open repo_dir + rescue ArgumentError => e + raise unless e.message == 'path does not exist' + raise ModuleSync::Error, 'Repository must be locally available before trying to push' + end + Git.push(repo, options) + options[:pr] && pr(module_options).manage(module_options[:namespace], module_options[:name], options) + } + + run(cli_options, job) + end + + def self.execute(cli_options) + job = lambda { |puppet_module, module_options, _defaults, options| + repo_dir = File.join(options[:project_root], module_options[:fullname]) + + git_options = { + remote: module_options[:remote], + reset_hard: options[:reset_hard], + } + repo = Git.pull(options[:git_base], module_options[:fullname], options[:branch], options[:project_root], git_options) + + FileUtils.chdir(repo_dir) do + result = system(options[:script]) + raise "Error during script execution (#{$CHILD_STATUS})" unless result + + options[:push] && Git.push(repo, options) + options[:push] && options[:pr] && pr(module_options).manage(module_options[:namespace], module_options[:name], options) + end + } + + run(cli_options, job) + end + + def self.run(options, job, prepare = nil) options = config_defaults.merge(options) + options[:remote_branch] ||= options[:branch] defaults = Util.parse_config(config_path(CONF_FILE, options)) if options[:pr] unless options[:branch] @@ -160,9 +228,7 @@ def self.update(options) @pr = create_pr_manager if options[:pr] end - local_template_dir = config_path(MODULE_FILES_DIR, options) - local_files = find_template_files(local_template_dir) - module_files = relative_names(local_files, local_template_dir) + options = prepare.call(options) unless prepare.nil? managed_modules = self.managed_modules(config_path(options[:managed_modules_conf], options), options[:filter], @@ -171,14 +237,19 @@ def self.update(options) errors = false # managed_modules is either an array or a hash managed_modules.each do |puppet_module, module_options| + module_options ||= {} + module_options = Util.symbolize_keys(module_options) + module_options.merge! compute_module_naming_attributes(puppet_module, options, module_options) + begin - mod_options = module_options.nil? ? nil : Util.symbolize_keys(module_options) - manage_module(puppet_module, module_files, mod_options, defaults, options) - rescue # rubocop:disable Lint/RescueWithoutErrorClass - $stderr.puts "Error while updating #{puppet_module}" + job.call(puppet_module, module_options, defaults, options) + rescue StandardError => e + message = e.message || "Error during '#{options[:command]}'" + message = "#{puppet_module}: #{message}" + $stderr.puts message raise unless options[:skip_broken] errors = true - $stdout.puts "Skipping #{puppet_module} as update process failed" + $stdout.puts "Skipping #{puppet_module} as '#{options[:command]}' process failed" end end exit 1 if errors && options[:fail_on_warnings] diff --git a/lib/modulesync/cli.rb b/lib/modulesync/cli.rb index 72602219..64a50895 100644 --- a/lib/modulesync/cli.rb +++ b/lib/modulesync/cli.rb @@ -33,7 +33,7 @@ def deactivate end end - class Base < Thor + class Base < Thor # rubocop:disable Metrics/ClassLength class_option :project_root, :aliases => '-c', :desc => 'Path used by git to clone modules into.', @@ -85,6 +85,8 @@ class Base < Thor :type => :boolean, :desc => 'Force push amended commit', :default => false + option :reset_hard, + :desc => 'Hard-reset on specified branch (e.g. origin/master) before updating modules' option :noop, :type => :boolean, :desc => 'No-op mode', @@ -141,6 +143,123 @@ def update ModuleSync.update(config) end + desc 'push', 'Push specified branch for each modules and submit PR if requested' + option :configs, + :aliases => '-c', + :desc => 'The local directory or remote repository to define the list of managed modules,' \ + ' the file templates, and the default values for template variables.' + option :managed_modules_conf, + :desc => 'The file name to define the list of managed modules' + option :skip_broken, + :type => :boolean, + :aliases => '-s', + :desc => 'Process remaining modules if an error is found', + :default => false + option :fail_on_warnings, + :type => :boolean, + :aliases => '-F', + :desc => 'Produce a failure exit code when there are warnings' \ + ' (only has effect when --skip_broken is enabled)', + :default => false + option :branch, + :aliases => '-b', + :desc => 'Branch name to push', + :required => true + option :remote_branch, + :aliases => '-r', + :desc => 'Remote branch name to push. Defaults to the branch name.' + option :force, + :type => :boolean, + :desc => 'Force push', + :default => false + option :pr, + :type => :boolean, + :desc => 'Submit pull/merge request', + :default => false + option :pr_title, + :desc => 'Title of pull/merge request' + option :pr_labels, + :type => :array, + :desc => 'Labels to add to the pull/merge request', + :default => CLI.defaults[:pr_labels] || [] + option :pr_target_branch, + :desc => 'Target branch for the pull/merge request', + :default => CLI.defaults[:pr_target_branch] || 'master' + def push + config = { :command => 'push' }.merge(options) + config = Util.symbolize_keys(config) + raise Thor::Error, 'No value provided for required option "--pr-title"' if config[:pr] && !config[:pr_title] + ModuleSync.push(config) + end + + desc 'execute SCRIPT', 'Execute a script on each managed modules then push/PR if requested' + option :configs, + :aliases => '-c', + :desc => 'The local directory or remote repository to define the list of managed modules,' \ + ' the file templates, and the default values for template variables.' + option :managed_modules_conf, + :desc => 'The file name to define the list of managed modules' + option :skip_broken, + :type => :boolean, + :aliases => '-s', + :desc => 'Process remaining modules if an error is found', + :default => false + option :fail_on_warnings, + :type => :boolean, + :aliases => '-F', + :desc => 'Produce a failure exit code when there are warnings' \ + ' (only has effect when --skip_broken is enabled)', + :default => false + option :branch, + :aliases => '-b', + :desc => 'Branch name to make the changes in.', + :required => true + option :reset_hard, + :desc => 'Hard-reset on specified branch (e.g. origin/master) between fetch and script execution' + option :push, + :desc => 'Push the changes. Please note that it is up to the script to carry out commits.', + :type => :boolean, + :default => false + option :remote_branch, + :aliases => '-r', + :desc => 'Remote branch name to push the changes to. Defaults to the branch name.' + option :force, + :type => :boolean, + :desc => 'Force push commit', + :default => false + option :pr, + :type => :boolean, + :desc => 'Submit pull/merge request', + :default => false + option :pr_title, + :desc => 'Title of pull/merge request' + option :pr_labels, + :type => :array, + :desc => 'Labels to add to the pull/merge request', + :default => CLI.defaults[:pr_labels] || [] + option :pr_target_branch, + :desc => 'Target branch for the pull/merge request', + :default => CLI.defaults[:pr_target_branch] || 'master' + def execute(script) + script = File.expand_path(script) + + unless File.executable? script + $stderr.puts 'Script does not exist nor executable' + raise + end + + config = { + :command => 'execute', + :script => script, + }.merge(options) + config = Util.symbolize_keys(config) + + raise Thor::Error, 'Option "--pr" requires "--push"' if config[:pr] && !config[:push] + raise Thor::Error, 'No value provided for required option "--pr-title"' if config[:pr] && !config[:pr_title] + + ModuleSync.execute(config) + end + desc 'hook', 'Activate or deactivate a git hook.' subcommand 'hook', ModuleSync::CLI::Hook end diff --git a/lib/modulesync/git.rb b/lib/modulesync/git.rb index e6f70b69..49c7cf13 100644 --- a/lib/modulesync/git.rb +++ b/lib/modulesync/git.rb @@ -45,30 +45,53 @@ def self.switch_branch(repo, branch) end end - def self.pull(git_base, name, branch, project_root, opts) - puts "Syncing #{name}" - Dir.mkdir(project_root) unless Dir.exist?(project_root) + def self.pull(git_base, name, branch, project_root, options) + puts "Syncing '#{name}'" + repo_dir = "#{project_root}/#{name}" # Repo needs to be cloned in the cwd - if !Dir.exist?("#{project_root}/#{name}") || !Dir.exist?("#{project_root}/#{name}/.git") - puts 'Cloning repository fresh' - remote = opts[:remote] || (git_base.start_with?('file://') ? "#{git_base}#{name}" : "#{git_base}#{name}.git") - local = "#{project_root}/#{name}" - puts "Cloning from #{remote}" - repo = ::Git.clone(remote, local) + if !Dir.exist?("#{repo_dir}/.git") + remote = options[:remote] || (git_base.start_with?('file://') ? "#{git_base}#{name}" : "#{git_base}#{name}.git") + puts "Cloning '#{remote}' to '#{repo_dir}'" + repo = ::Git.clone(remote, repo_dir) switch_branch(repo, branch) # Repo already cloned, check out master and override local changes else # Some versions of git can't properly handle managing a repo from outside the repo directory - Dir.chdir("#{project_root}/#{name}") do - puts "Overriding any local changes to repositories in #{project_root}" + Dir.chdir(repo_dir) do repo = ::Git.open('.') repo.fetch - repo.reset_hard + if options[:override_changes] + puts "Overriding any local changes in '#{repo_dir}'" + repo.reset_hard + end + switch_branch(repo, branch) - repo.pull('origin', branch) if remote_branch_exists?(repo, branch) + + if options[:reset_hard] + repo.reset_hard(options[:reset_hard]) + elsif options[:override_changes] + if remote_branch_exists?(repo, branch) + # FIXME: It does not honor --remote-branch option, should it? + puts "Pulling changes from 'origin/#{branch}' in '#{repo_dir}'" + # NOTE: It seems to be more efficient to hard-reset here + repo.pull('origin', branch) + end + end end end + + repo + end + + def self.push(repo, options) + git_options = {} + git_options = { :force => true } if options[:force] + + remote = 'origin' + remote_url = repo.remote('origin').url + $stdout.puts "Push branch '#{options[:branch]}' to '#{remote_url}' (#{remote}/#{options[:remote_branch]})" + repo.push(remote, "#{options[:branch]}:#{options[:remote_branch]}", git_options) end def self.update_changelog(repo, version, message, module_root) diff --git a/lib/monkey_patches.rb b/lib/monkey_patches.rb index f4818232..ca4fb440 100644 --- a/lib/monkey_patches.rb +++ b/lib/monkey_patches.rb @@ -1,43 +1,4 @@ module Git - class Diff - # Monkey patch process_full_diff until https://github.com/schacon/ruby-git/issues/326 is resolved - def process_full_diff - defaults = { - :mode => '', - :src => '', - :dst => '', - :type => 'modified' - } - final = {} - current_file = nil - full_diff_utf8_encoded = @full_diff.encode("UTF-8", "binary", { - :invalid => :replace, - :undef => :replace - }) - full_diff_utf8_encoded.split("\n").each do |line| - if m = /^diff --git a\/(.*?) b\/(.*?)/.match(line) - current_file = m[1] - final[current_file] = defaults.merge({:patch => line, :path => current_file}) - elsif !current_file.nil? - if m = /^index (.......)\.\.(.......)( ......)*/.match(line) - final[current_file][:src] = m[1] - final[current_file][:dst] = m[2] - final[current_file][:mode] = m[3].strip if m[3] - end - if m = /^([[:alpha:]]*?) file mode (......)/.match(line) - final[current_file][:type] = m[1] - final[current_file][:mode] = m[2] - end - if m = /^Binary files /.match(line) - final[current_file][:binary] = true - end - final[current_file][:patch] << "\n" + line - end - end - final.map { |e| [e[0], DiffFile.new(@base, e[1])] } - end - end - module LibMonkeyPatch # Monkey patch set_custom_git_env_variables due to our ::Git::GitExecuteError handling. # @@ -51,18 +12,5 @@ def set_custom_git_env_variables class Lib prepend LibMonkeyPatch - - # Monkey patch ls_files until https://github.com/ruby-git/ruby-git/pull/320 is resolved - def ls_files(location=nil) - location ||= '.' - hsh = {} - command_lines('ls-files', ['--stage', location]).each do |line| - (info, file) = line.split("\t") - (mode, sha, stage) = info.split - file = eval(file) if file =~ /^\".*\"$/ # This takes care of quoted strings returned from git - hsh[file] = {:path => file, :mode_index => mode, :sha_index => sha, :stage => stage} - end - hsh - end end end diff --git a/modulesync.gemspec b/modulesync.gemspec index c06ee84e..338a36eb 100644 --- a/modulesync.gemspec +++ b/modulesync.gemspec @@ -23,7 +23,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rspec' spec.add_development_dependency 'rubocop', '~> 0.50.0' - spec.add_runtime_dependency 'git', '~>1.3' + spec.add_runtime_dependency 'git', '~>1.7' spec.add_runtime_dependency 'gitlab', '~>4.0' spec.add_runtime_dependency 'octokit', '~>4.0' spec.add_runtime_dependency 'puppet-blacksmith', '>= 3.0', '< 7' diff --git a/spec/helpers/faker.rb b/spec/helpers/faker.rb new file mode 100644 index 00000000..4f628c57 --- /dev/null +++ b/spec/helpers/faker.rb @@ -0,0 +1,12 @@ +# Faker is a top-level module to keep global faker config +module Faker + def self.working_directory=(path) + @working_directory = path + end + + def self.working_directory + raise 'Working directory must be set' if @working_directory.nil? + FileUtils.mkdir_p @working_directory + @working_directory + end +end diff --git a/spec/helpers/faker/puppet_module_remote_repo.rb b/spec/helpers/faker/puppet_module_remote_repo.rb new file mode 100644 index 00000000..d8e58b2f --- /dev/null +++ b/spec/helpers/faker/puppet_module_remote_repo.rb @@ -0,0 +1,146 @@ +require 'open3' + +require_relative '../faker' + +# Fake a remote git repository that hold a puppet module +# +# This module allow to fake a remote repositiory using: +# - a bare repo +# - a temporary cloned repo to operate on the remote before exposition to modulesync +# +# Note: This module need to have working_directory sets before using it +module Faker + class PuppetModuleRemoteRepo + attr_reader :name + attr_reader :namespace + + def initialize(name, namespace) + @name = name + @namespace = namespace + end + + def populate + FileUtils.chdir(Faker.working_directory) do + run "git init --bare '#{bare_repo_dir}'" + run "git clone '#{bare_repo_dir}' '#{tmp_repo_dir}'" + + module_short_name = name.sub(/^puppet-/, '') + + FileUtils.chdir(tmp_repo_dir) do + metadata = { + name: "modulesync-#{module_short_name}", + version: '0.4.2', + author: 'ModuleSync team', + }.to_json + + File.write 'metadata.json', metadata + run 'git add metadata.json' + run "git commit -m'Initial commit'" + run 'git push' + end + end + end + + def read_only=(value) + mode = value ? '0444' : '0644' + FileUtils.chdir(bare_repo_dir) do + run "git config core.sharedRepository #{mode}" + end + end + + def default_branch=(value) + FileUtils.chdir(bare_repo_dir) do + run "git branch -M master #{value}" + run "git symbolic-ref HEAD refs/heads/#{value}" + end + end + + def default_branch + FileUtils.chdir(bare_repo_dir) do + stdout = run "git symbolic-ref --short HEAD" + return stdout.chomp + end + end + + def read_file(filename, branch = nil) + branch ||= default_branch + FileUtils.chdir(tmp_repo_dir) do + run "git fetch" + run "git checkout #{branch}" + run "git merge --ff-only" + return unless File.exists?(filename) + + return File.read filename + end + end + + def create_branch(branch, from = nil) + from ||= default_branch + FileUtils.chdir(tmp_repo_dir) do + run "git checkout -B #{branch} #{from}" + run "git push --set-upstream origin #{branch}" + end + end + + def add_file(filename, content, branch = nil) + FileUtils.chdir(tmp_repo_dir) do + run "git checkout #{branch}" unless branch.nil? + File.write filename, content + run "git add #{filename}" + run "git commit -m'Add file: \"#{filename}\"'" + run 'git push' + end + end + + def commit_count_between(commit1, commit2) + FileUtils.chdir(bare_repo_dir) do + stdout = run "git rev-list --count #{commit1}..#{commit2}" + return Integer(stdout) + end + end + + def commit_count_by(author, commit = nil) + FileUtils.chdir(bare_repo_dir) do + commit ||= '--all' + stdout = run "git rev-list #{commit} --author='#{author}' --count" + return Integer(stdout) + end + end + + def remote_url + "file://#{bare_repo_dir}" + end + + def self.git_base + "file://#{Faker.working_directory}/bare/" + end + + private + def tmp_repo_dir + "#{Faker.working_directory}/tmp/#{namespace}/#{name}" + end + + def bare_repo_dir + "#{Faker.working_directory}/bare/#{namespace}/#{name}.git" + end + + GIT_ENV = { + 'GIT_AUTHOR_NAME' => 'Faker', + 'GIT_AUTHOR_EMAIL' => 'faker@example.com', + 'GIT_COMMITTER_NAME' => 'Faker', + 'GIT_COMMITTER_EMAIL' => 'faker@example.com', + } + + def run(command) + stdout, stderr, status = Open3.capture3(GIT_ENV, command) + return stdout if status.success? + + $stderr.puts "Command '#{command}' failed: #{status}" + $stderr.puts ' STDOUT:' + $stderr.puts stdout + $stderr.puts ' STDERR:' + $stderr.puts stderr + raise + end + end +end