modulesync 1.2.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,25 +15,39 @@ module ModuleSync
15
15
 
16
16
  def manage(namespace, module_name, options)
17
17
  repo_path = File.join(namespace, module_name)
18
+ branch = options[:remote_branch] || options[:branch]
19
+ head = "#{namespace}:#{branch}"
20
+ target_branch = options[:pr_target_branch] || 'master'
21
+
22
+ if options[:noop]
23
+ $stdout.puts \
24
+ "Using no-op. Would submit MR '#{options[:pr_title]}' to #{repo_path} " \
25
+ "- merges #{branch} into #{target_branch}"
26
+ return
27
+ end
18
28
 
19
- head = "#{namespace}:#{options[:branch]}"
20
29
  merge_requests = @api.merge_requests(repo_path,
21
30
  :state => 'opened',
22
31
  :source_branch => head,
23
- :target_branch => 'master')
24
- if merge_requests.empty?
25
- mr_labels = ModuleSync::Util.parse_list(options[:pr_labels])
26
- mr = @api.create_merge_request(repo_path, options[:pr_title],
27
- :source_branch => options[:branch],
28
- :target_branch => 'master',
29
- :labels => mr_labels)
30
- $stdout.puts "Submitted MR '#{options[:pr_title]}' to #{repo_path} - merges #{options[:branch]} into master"
31
- return if mr_labels.empty?
32
- $stdout.puts "Attached the following labels to MR #{mr.iid}: #{mr_labels.join(', ')}"
33
- else
32
+ :target_branch => target_branch)
33
+ unless merge_requests.empty?
34
34
  # Skip creating the MR if it exists already.
35
- $stdout.puts "Skipped! #{merge_requests.length} MRs found for branch #{options[:branch]}"
35
+ $stdout.puts "Skipped! #{merge_requests.length} MRs found for branch #{branch}"
36
+ return
36
37
  end
38
+
39
+ mr_labels = ModuleSync::Util.parse_list(options[:pr_labels])
40
+ mr = @api.create_merge_request(repo_path,
41
+ options[:pr_title],
42
+ :source_branch => branch,
43
+ :target_branch => target_branch,
44
+ :labels => mr_labels)
45
+ $stdout.puts \
46
+ "Submitted MR '#{options[:pr_title]}' to #{repo_path} " \
47
+ "- merges #{branch} into #{target_branch}"
48
+
49
+ return if mr_labels.empty?
50
+ $stdout.puts "Attached the following labels to MR #{mr.iid}: #{mr_labels.join(', ')}"
37
51
  end
38
52
  end
39
53
  end
@@ -0,0 +1,37 @@
1
+ require 'puppet_blacksmith'
2
+
3
+ require 'modulesync/source_code'
4
+
5
+ module ModuleSync
6
+ # Provide methods to manipulate puppet module code
7
+ class PuppetModule < SourceCode
8
+ def update_changelog(version, message)
9
+ changelog = path('CHANGELOG.md')
10
+ if File.exist?(changelog)
11
+ puts "Updating #{changelog} for version #{version}"
12
+ changes = File.readlines(changelog)
13
+ File.open(changelog, 'w') do |f|
14
+ date = Time.now.strftime('%Y-%m-%d')
15
+ f.puts "## #{date} - Release #{version}\n\n"
16
+ f.puts "#{message}\n\n"
17
+ # Add old lines again
18
+ f.puts changes
19
+ end
20
+ repository.git.add('CHANGELOG.md')
21
+ else
22
+ puts 'No CHANGELOG.md file found, not updating.'
23
+ end
24
+ end
25
+
26
+ def bump(message, changelog = false)
27
+ m = Blacksmith::Modulefile.new path('metadata.json')
28
+ new = m.bump!
29
+ puts "Bumped to version #{new}"
30
+ repository.git.add('metadata.json')
31
+ update_changelog(new, message) if changelog
32
+ repository.git.commit("Release version #{new}")
33
+ repository.git.push
34
+ new
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,158 @@
1
+ require 'git'
2
+
3
+ module ModuleSync
4
+ # Wrapper for Git in ModuleSync context
5
+ class Repository
6
+ def initialize(directory:, remote:)
7
+ @directory = directory
8
+ @remote = remote
9
+ end
10
+
11
+ def git
12
+ @git ||= Git.open @directory
13
+ end
14
+
15
+ # This is an alias to minimize code alteration
16
+ def repo
17
+ git
18
+ end
19
+
20
+ def remote_branch_exists?(branch)
21
+ repo.branches.remote.collect(&:name).include?(branch)
22
+ end
23
+
24
+ def local_branch_exists?(branch)
25
+ repo.branches.local.collect(&:name).include?(branch)
26
+ end
27
+
28
+ def remote_branch_differ?(local_branch, remote_branch)
29
+ !remote_branch_exists?(remote_branch) ||
30
+ repo.diff("#{local_branch}..origin/#{remote_branch}").any?
31
+ end
32
+
33
+ def default_branch
34
+ symbolic_ref = repo.branches.find { |b| b.full =~ %r{remotes/origin/HEAD} }
35
+ return unless symbolic_ref
36
+ %r{remotes/origin/HEAD\s+->\s+origin/(?<branch>.+?)$}.match(symbolic_ref.full)[:branch]
37
+ end
38
+
39
+ def switch_branch(branch)
40
+ unless branch
41
+ branch = default_branch
42
+ puts "Using repository's default branch: #{branch}"
43
+ end
44
+ return if repo.current_branch == branch
45
+
46
+ if local_branch_exists?(branch)
47
+ puts "Switching to branch #{branch}"
48
+ repo.checkout(branch)
49
+ elsif remote_branch_exists?(branch)
50
+ puts "Creating local branch #{branch} from origin/#{branch}"
51
+ repo.checkout("origin/#{branch}")
52
+ repo.branch(branch).checkout
53
+ else
54
+ repo.checkout('origin/master')
55
+ puts "Creating new branch #{branch}"
56
+ repo.branch(branch).checkout
57
+ end
58
+ end
59
+
60
+ def prepare_workspace(branch)
61
+ # Repo needs to be cloned in the cwd
62
+ if !Dir.exist?("#{@directory}/.git")
63
+ puts 'Cloning repository fresh'
64
+ puts "Cloning from '#{@remote}'"
65
+ @git = Git.clone(@remote, @directory)
66
+ switch_branch(branch)
67
+ # Repo already cloned, check out master and override local changes
68
+ else
69
+ # Some versions of git can't properly handle managing a repo from outside the repo directory
70
+ Dir.chdir(@directory) do
71
+ puts "Overriding any local changes to repository in '#{@directory}'"
72
+ @git = Git.open('.')
73
+ repo.fetch
74
+ repo.reset_hard
75
+ switch_branch(branch)
76
+ git.pull('origin', branch) if remote_branch_exists?(branch)
77
+ end
78
+ end
79
+ end
80
+
81
+ def tag(version, tag_pattern)
82
+ tag = tag_pattern % version
83
+ puts "Tagging with #{tag}"
84
+ repo.add_tag(tag)
85
+ repo.push('origin', tag)
86
+ end
87
+
88
+ def checkout_branch(branch)
89
+ selected_branch = branch || repo.current_branch || 'master'
90
+ repo.branch(selected_branch).checkout
91
+ selected_branch
92
+ end
93
+
94
+ # Git add/rm, git commit, git push
95
+ def submit_changes(files, options)
96
+ message = options[:message]
97
+ branch = checkout_branch(options[:branch])
98
+ files.each do |file|
99
+ if repo.status.deleted.include?(file)
100
+ repo.remove(file)
101
+ elsif File.exist?("#{@directory}/#{file}")
102
+ repo.add(file)
103
+ end
104
+ end
105
+ begin
106
+ opts_commit = {}
107
+ opts_push = {}
108
+ opts_commit = { :amend => true } if options[:amend]
109
+ opts_push = { :force => true } if options[:force]
110
+ if options[:pre_commit_script]
111
+ script = "#{File.dirname(File.dirname(__FILE__))}/../contrib/#{options[:pre_commit_script]}"
112
+ `#{script} #{@directory}`
113
+ end
114
+ repo.commit(message, opts_commit)
115
+ if options[:remote_branch]
116
+ if remote_branch_differ?(branch, options[:remote_branch])
117
+ repo.push('origin', "#{branch}:#{options[:remote_branch]}", opts_push)
118
+ end
119
+ else
120
+ repo.push('origin', branch, opts_push)
121
+ end
122
+ rescue Git::GitExecuteError => e
123
+ raise unless e.message.match?(/working (directory|tree) clean/)
124
+
125
+ puts "There were no changes in '#{@directory}'. Not committing."
126
+ return false
127
+ end
128
+
129
+ true
130
+ end
131
+
132
+ # Needed because of a bug in the git gem that lists ignored files as
133
+ # untracked under some circumstances
134
+ # https://github.com/schacon/ruby-git/issues/130
135
+ def untracked_unignored_files
136
+ ignore_path = "#{@directory}/.gitignore"
137
+ ignored = File.exist?(ignore_path) ? File.read(ignore_path).split : []
138
+ repo.status.untracked.keep_if { |f, _| ignored.none? { |i| File.fnmatch(i, f) } }
139
+ end
140
+
141
+ def show_changes(options)
142
+ checkout_branch(options[:branch])
143
+
144
+ puts 'Files changed:'
145
+ repo.diff('HEAD', '--').each do |diff|
146
+ puts diff.patch
147
+ end
148
+
149
+ puts 'Files added:'
150
+ untracked_unignored_files.each_key do |file|
151
+ puts file
152
+ end
153
+
154
+ puts "\n\n"
155
+ puts '--------------------------------'
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,57 @@
1
+ require 'modulesync'
2
+ require 'modulesync/repository'
3
+ require 'modulesync/util'
4
+
5
+ module ModuleSync
6
+ # Provide methods to retrieve source code attributes
7
+ class SourceCode
8
+ attr_reader :given_name
9
+ attr_reader :options
10
+
11
+ def initialize(given_name, options)
12
+ @options = Util.symbolize_keys(options || {})
13
+
14
+ @given_name = given_name
15
+
16
+ return unless given_name.include?('/')
17
+
18
+ @repository_name = given_name.split('/').last
19
+ @repository_namespace = given_name.split('/')[0...-1].join('/')
20
+ end
21
+
22
+ def repository
23
+ @repository ||= Repository.new directory: working_directory, remote: repository_remote
24
+ end
25
+
26
+ def repository_name
27
+ @repository_name ||= given_name
28
+ end
29
+
30
+ def repository_namespace
31
+ @repository_namespace ||= @options[:namespace] || ModuleSync.options[:namespace]
32
+ end
33
+
34
+ def repository_path
35
+ @repository_path ||= "#{repository_namespace}/#{repository_name}"
36
+ end
37
+
38
+ def repository_remote
39
+ @repository_remote ||= @options[:remote] || _repository_remote
40
+ end
41
+
42
+ def working_directory
43
+ @working_directory ||= File.join(ModuleSync.options[:project_root], repository_path)
44
+ end
45
+
46
+ def path(*parts)
47
+ File.join(working_directory, *parts)
48
+ end
49
+
50
+ private
51
+
52
+ def _repository_remote
53
+ git_base = ModuleSync.options[:git_base]
54
+ git_base.start_with?('file://') ? "#{git_base}#{repository_path}" : "#{git_base}#{repository_path}.git"
55
+ end
56
+ end
57
+ end
@@ -3,7 +3,10 @@ require 'yaml'
3
3
  module ModuleSync
4
4
  module Util
5
5
  def self.symbolize_keys(hash)
6
- hash.inject({}) { |memo, (k, v)| memo[k.to_sym] = v; memo }
6
+ hash.inject({}) do |memo, (k, v)|
7
+ memo[k.to_sym] = v.is_a?(Hash) ? symbolize_keys(v) : v
8
+ memo
9
+ end
7
10
  end
8
11
 
9
12
  def self.parse_config(config_file)
@@ -1,55 +1,16 @@
1
1
  module Git
2
- class Diff
3
- # Monkey patch process_full_diff until https://github.com/schacon/ruby-git/issues/326 is resolved
4
- def process_full_diff
5
- defaults = {
6
- :mode => '',
7
- :src => '',
8
- :dst => '',
9
- :type => 'modified'
10
- }
11
- final = {}
12
- current_file = nil
13
- full_diff_utf8_encoded = @full_diff.encode("UTF-8", "binary", {
14
- :invalid => :replace,
15
- :undef => :replace
16
- })
17
- full_diff_utf8_encoded.split("\n").each do |line|
18
- if m = /^diff --git a\/(.*?) b\/(.*?)/.match(line)
19
- current_file = m[1]
20
- final[current_file] = defaults.merge({:patch => line, :path => current_file})
21
- elsif !current_file.nil?
22
- if m = /^index (.......)\.\.(.......)( ......)*/.match(line)
23
- final[current_file][:src] = m[1]
24
- final[current_file][:dst] = m[2]
25
- final[current_file][:mode] = m[3].strip if m[3]
26
- end
27
- if m = /^([[:alpha:]]*?) file mode (......)/.match(line)
28
- final[current_file][:type] = m[1]
29
- final[current_file][:mode] = m[2]
30
- end
31
- if m = /^Binary files /.match(line)
32
- final[current_file][:binary] = true
33
- end
34
- final[current_file][:patch] << "\n" + line
35
- end
36
- end
37
- final.map { |e| [e[0], DiffFile.new(@base, e[1])] }
2
+ module LibMonkeyPatch
3
+ # Monkey patch set_custom_git_env_variables due to our ::Git::GitExecuteError handling.
4
+ #
5
+ # We rescue on the GitExecuteError and proceed differently based on the output of git.
6
+ # This way makes code language-dependent, so here we ensure that Git gem throw git commands with the "C" language
7
+ def set_custom_git_env_variables
8
+ super
9
+ ENV['LANG'] = 'C.UTF-8'
38
10
  end
39
11
  end
40
12
 
41
13
  class Lib
42
- # Monkey patch ls_files until https://github.com/ruby-git/ruby-git/pull/320 is resolved
43
- def ls_files(location=nil)
44
- location ||= '.'
45
- hsh = {}
46
- command_lines('ls-files', ['--stage', location]).each do |line|
47
- (info, file) = line.split("\t")
48
- (mode, sha, stage) = info.split
49
- file = eval(file) if file =~ /^\".*\"$/ # This takes care of quoted strings returned from git
50
- hsh[file] = {:path => file, :mode_index => mode, :sha_index => sha, :stage => stage}
51
- end
52
- hsh
53
- end
14
+ prepend LibMonkeyPatch
54
15
  end
55
16
  end
data/modulesync.gemspec CHANGED
@@ -3,14 +3,14 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
 
4
4
  Gem::Specification.new do |spec|
5
5
  spec.name = 'modulesync'
6
- spec.version = '1.2.0'
6
+ spec.version = '2.1.0'
7
7
  spec.authors = ['Vox Pupuli']
8
8
  spec.email = ['voxpupuli@groups.io']
9
9
  spec.summary = 'Puppet Module Synchronizer'
10
10
  spec.description = 'Utility to synchronize common files across puppet modules in Github.'
11
11
  spec.homepage = 'http://github.com/voxpupuli/modulesync'
12
12
  spec.license = 'Apache-2.0'
13
- spec.required_ruby_version = '>= 2.0.0'
13
+ spec.required_ruby_version = '>= 2.5.0'
14
14
 
15
15
  spec.files = `git ls-files -z`.split("\x0")
16
16
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
@@ -23,9 +23,9 @@ Gem::Specification.new do |spec|
23
23
  spec.add_development_dependency 'rspec'
24
24
  spec.add_development_dependency 'rubocop', '~> 0.50.0'
25
25
 
26
- spec.add_runtime_dependency 'git', '~>1.3'
26
+ spec.add_runtime_dependency 'git', '~>1.7'
27
27
  spec.add_runtime_dependency 'gitlab', '~>4.0'
28
28
  spec.add_runtime_dependency 'octokit', '~>4.0'
29
- spec.add_runtime_dependency 'puppet-blacksmith', '~>3.0'
29
+ spec.add_runtime_dependency 'puppet-blacksmith', '>= 3.0', '< 7'
30
30
  spec.add_runtime_dependency 'thor'
31
31
  end
@@ -0,0 +1,14 @@
1
+ module ModuleSync
2
+ # Faker is a top-level module to keep global faker config
3
+ module Faker
4
+ def self.working_directory=(path)
5
+ @working_directory = path
6
+ end
7
+
8
+ def self.working_directory
9
+ raise 'Working directory must be set' if @working_directory.nil?
10
+ FileUtils.mkdir_p @working_directory
11
+ @working_directory
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,146 @@
1
+ require 'open3'
2
+
3
+ require_relative '../faker'
4
+
5
+ module ModuleSync
6
+ # Fake a remote git repository that holds a puppet module
7
+ #
8
+ # This module allows to fake a remote repositiory using:
9
+ # - a bare repo
10
+ # - a temporary cloned repo to operate on the remote before exposing to modulesync
11
+ #
12
+ # Note: This module needs to have working_directory sets before using it
13
+ module Faker
14
+ class PuppetModuleRemoteRepo
15
+ class CommandExecutionError < StandardError; end
16
+
17
+ attr_reader :name, :namespace
18
+
19
+ def initialize(name, namespace)
20
+ @name = name
21
+ @namespace = namespace
22
+ end
23
+
24
+ def populate
25
+ FileUtils.chdir(Faker.working_directory) do
26
+ run %W[git init --bare #{bare_repo_dir}]
27
+ run %W[git clone #{bare_repo_dir} #{tmp_repo_dir}]
28
+
29
+ module_short_name = name.split('-').last
30
+
31
+ FileUtils.chdir(tmp_repo_dir) do
32
+ metadata = {
33
+ name: "modulesync-#{module_short_name}",
34
+ version: '0.4.2',
35
+ author: 'ModuleSync team',
36
+ }
37
+
38
+ File.write 'metadata.json', metadata.to_json
39
+ run %w[git add metadata.json]
40
+ run %w[git commit --message] << 'Initial commit'
41
+ run %w[git push]
42
+ end
43
+ end
44
+ end
45
+
46
+ def read_only=(value)
47
+ mode = value ? '0444' : '0644'
48
+ FileUtils.chdir(bare_repo_dir) do
49
+ run %W[git config core.sharedRepository #{mode}]
50
+ end
51
+ end
52
+
53
+ def default_branch
54
+ FileUtils.chdir(bare_repo_dir) do
55
+ stdout = run %w[git symbolic-ref --short HEAD]
56
+ return stdout.chomp
57
+ end
58
+ end
59
+
60
+ def default_branch=(value)
61
+ FileUtils.chdir(bare_repo_dir) do
62
+ run %W[git branch -M #{default_branch} #{value}]
63
+ run %W[git symbolic-ref HEAD refs/heads/#{value}]
64
+ end
65
+ end
66
+
67
+ def read_file(filename, branch = nil)
68
+ branch ||= default_branch
69
+ FileUtils.chdir(bare_repo_dir) do
70
+ return run %W[git show #{branch}:#{filename}]
71
+ rescue CommandExecutionError
72
+ return nil
73
+ end
74
+ end
75
+
76
+ def add_file(filename, content, branch = nil)
77
+ branch ||= default_branch
78
+ FileUtils.chdir(tmp_repo_dir) do
79
+ run %W[git checkout #{branch}]
80
+ File.write filename, content
81
+ run %W[git add #{filename}]
82
+ run %w[git commit --message] << "Add file: '#{filename}'"
83
+ run %w[git push]
84
+ end
85
+ end
86
+
87
+ def commit_count_between(commit1, commit2)
88
+ FileUtils.chdir(bare_repo_dir) do
89
+ stdout = run %W[git rev-list --count #{commit1}..#{commit2}]
90
+ return Integer(stdout)
91
+ end
92
+ end
93
+
94
+ def commit_count_by(author, commit = nil)
95
+ FileUtils.chdir(bare_repo_dir) do
96
+ commit ||= '--all'
97
+ stdout = run %W[git rev-list #{commit} --author #{author} --count]
98
+ return Integer(stdout)
99
+ end
100
+ end
101
+
102
+ def tags
103
+ FileUtils.chdir(bare_repo_dir) do
104
+ return run %w{git tag --list}
105
+ end
106
+ end
107
+
108
+ def remote_url
109
+ "file://#{bare_repo_dir}"
110
+ end
111
+
112
+ def self.git_base
113
+ "file://#{Faker.working_directory}/bare/"
114
+ end
115
+
116
+ private
117
+
118
+ def tmp_repo_dir
119
+ File.join Faker.working_directory, 'tmp', namespace, name
120
+ end
121
+
122
+ def bare_repo_dir
123
+ File.join Faker.working_directory, 'bare', namespace, "#{name}.git"
124
+ end
125
+
126
+ GIT_ENV = {
127
+ 'GIT_AUTHOR_NAME' => 'Faker',
128
+ 'GIT_AUTHOR_EMAIL' => 'faker@example.com',
129
+ 'GIT_COMMITTER_NAME' => 'Faker',
130
+ 'GIT_COMMITTER_EMAIL' => 'faker@example.com',
131
+ }.freeze
132
+
133
+ def run(command)
134
+ stdout, stderr, status = Open3.capture3(GIT_ENV, *command)
135
+ return stdout if status.success?
136
+
137
+ warn "Command '#{command}' failed: #{status}"
138
+ warn ' STDOUT:'
139
+ warn stdout
140
+ warn ' STDERR:'
141
+ warn stderr
142
+ raise CommandExecutionError, "Command '#{command}' failed: #{status}"
143
+ end
144
+ end
145
+ end
146
+ end