modulesync 2.0.1 → 2.2.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.
@@ -0,0 +1,26 @@
1
+ Feature: Run `msync update` without a good context
2
+
3
+ Scenario: Run `msync update` without any module
4
+ Given a directory named "moduleroot"
5
+ When I run `msync update --message "In a bad context"`
6
+ Then the exit status should be 1
7
+ And the stderr should contain:
8
+ """
9
+ No modules found
10
+ """
11
+
12
+ Scenario: Run `msync update` without the "moduleroot" directory
13
+ Given a basic setup with a puppet module "puppet-test" from "fakenamespace"
14
+ When I run `msync update --message "In a bad context"`
15
+ Then the exit status should be 1
16
+ And the stderr should contain "moduleroot"
17
+
18
+ Scenario: Run `msync update` without commit message
19
+ Given a basic setup with a puppet module "puppet-test" from "fakenamespace"
20
+ And a directory named "moduleroot"
21
+ When I run `msync update`
22
+ Then the exit status should be 1
23
+ And the stderr should contain:
24
+ """
25
+ No value provided for required option "--message"
26
+ """
@@ -0,0 +1,87 @@
1
+ Feature: Bump a new version after an update
2
+ Scenario: Bump the module version, update changelog and tag it after an update that produces changes
3
+ Given a basic setup with a puppet module "puppet-test" from "fakenamespace"
4
+ And the puppet module "puppet-test" from "fakenamespace" has a file named "CHANGELOG.md" with:
5
+ """
6
+ ## 1965-04-14 - Release 0.4.2
7
+ """
8
+ And a file named "config_defaults.yml" with:
9
+ """
10
+ ---
11
+ new-file:
12
+ content: aruba
13
+ """
14
+ And a directory named "moduleroot"
15
+ And a file named "moduleroot/new-file.erb" with:
16
+ """
17
+ <%= @configs['content'] %>
18
+ """
19
+ When I run `msync update --message "Add new-file" --bump --changelog --tag`
20
+ Then the exit status should be 0
21
+ And the file named "modules/fakenamespace/puppet-test/new-file" should contain "aruba"
22
+ And the stdout should contain:
23
+ """
24
+ Bumped to version 0.4.3
25
+ """
26
+ And the stdout should contain:
27
+ """
28
+ Tagging with 0.4.3
29
+ """
30
+ And the file named "modules/fakenamespace/puppet-test/CHANGELOG.md" should contain "0.4.3"
31
+ And the puppet module "puppet-test" from "fakenamespace" should have 2 commits made by "Aruba"
32
+ And the puppet module "puppet-test" from "fakenamespace" should have a tag named "0.4.3"
33
+
34
+ Scenario: Bump the module version after an update that produces changes
35
+ Given a basic setup with a puppet module "puppet-test" from "fakenamespace"
36
+ And a file named "config_defaults.yml" with:
37
+ """
38
+ ---
39
+ new-file:
40
+ content: aruba
41
+ """
42
+ And a directory named "moduleroot"
43
+ And a file named "moduleroot/new-file.erb" with:
44
+ """
45
+ <%= @configs['content'] %>
46
+ """
47
+ When I run `msync update --message "Add new-file" --bump`
48
+ Then the exit status should be 0
49
+ And the file named "modules/fakenamespace/puppet-test/new-file" should contain "aruba"
50
+ And the stdout should contain:
51
+ """
52
+ Bumped to version 0.4.3
53
+ """
54
+ And the puppet module "puppet-test" from "fakenamespace" should have 2 commits made by "Aruba"
55
+ And the puppet module "puppet-test" from "fakenamespace" should not have a tag named "0.4.3"
56
+
57
+ Scenario: Bump the module version with changelog update when no CHANGELOG.md is available
58
+ Given a basic setup with a puppet module "puppet-test" from "fakenamespace"
59
+ And a file named "config_defaults.yml" with:
60
+ """
61
+ ---
62
+ new-file:
63
+ content: aruba
64
+ """
65
+ And a directory named "moduleroot"
66
+ And a file named "moduleroot/new-file.erb" with:
67
+ """
68
+ <%= @configs['content'] %>
69
+ """
70
+ When I run `msync update --message "Add new-file" --bump --changelog`
71
+ Then the exit status should be 0
72
+ And the file named "modules/fakenamespace/puppet-test/new-file" should contain "aruba"
73
+ And the stdout should contain:
74
+ """
75
+ Bumped to version 0.4.3
76
+ No CHANGELOG.md file found, not updating.
77
+ """
78
+ And the file named "modules/fakenamespace/puppet-test/CHANGELOG.md" should not exist
79
+ And the puppet module "puppet-test" from "fakenamespace" should have 2 commits made by "Aruba"
80
+
81
+ Scenario: Dont bump the module version after an update that produces no changes
82
+ Given a basic setup with a puppet module "puppet-test" from "fakenamespace"
83
+ And a directory named "moduleroot"
84
+ When I run `msync update --message "Add new-file" --bump --tag`
85
+ Then the exit status should be 0
86
+ And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba"
87
+ And the puppet module "puppet-test" from "fakenamespace" should not have a tag named "0.4.3"
data/lib/modulesync.rb CHANGED
@@ -1,15 +1,19 @@
1
1
  require 'fileutils'
2
2
  require 'pathname'
3
+
3
4
  require 'modulesync/cli'
4
5
  require 'modulesync/constants'
5
- require 'modulesync/git'
6
6
  require 'modulesync/hook'
7
+ require 'modulesync/puppet_module'
7
8
  require 'modulesync/renderer'
8
9
  require 'modulesync/settings'
9
10
  require 'modulesync/util'
11
+
10
12
  require 'monkey_patches'
11
13
 
12
14
  module ModuleSync # rubocop:disable Metrics/ModuleLength
15
+ class Error < StandardError; end
16
+
13
17
  include Constants
14
18
 
15
19
  def self.config_defaults
@@ -21,12 +25,12 @@ module ModuleSync # rubocop:disable Metrics/ModuleLength
21
25
  }
22
26
  end
23
27
 
24
- def self.local_file(config_path, file)
25
- File.join(config_path, MODULE_FILES_DIR, file)
28
+ def self.options
29
+ @options
26
30
  end
27
31
 
28
- def self.module_file(project_root, namespace, puppet_module, *parts)
29
- File.join(project_root, namespace, puppet_module, *parts)
32
+ def self.local_file(config_path, file)
33
+ File.join(config_path, MODULE_FILES_DIR, file)
30
34
  end
31
35
 
32
36
  # List all template files.
@@ -39,10 +43,10 @@ module ModuleSync # rubocop:disable Metrics/ModuleLength
39
43
  .collect { |p| p.chomp('.erb') }
40
44
  .to_a
41
45
  else
42
- $stdout.puts "#{local_template_dir} does not exist." \
46
+ $stderr.puts "#{local_template_dir} does not exist." \
43
47
  ' Check that you are working in your module configs directory or' \
44
48
  ' that you have passed in the correct directory with -c.'
45
- exit
49
+ exit 1
46
50
  end
47
51
  end
48
52
 
@@ -50,21 +54,20 @@ module ModuleSync # rubocop:disable Metrics/ModuleLength
50
54
  file_list.map { |file| file.sub(/#{path}/, '') }
51
55
  end
52
56
 
53
- def self.managed_modules(config_file, filter, negative_filter)
57
+ def self.managed_modules
58
+ config_file = config_path(options[:managed_modules_conf], options)
59
+ filter = options[:filter]
60
+ negative_filter = options[:negative_filter]
61
+
54
62
  managed_modules = Util.parse_config(config_file)
55
63
  if managed_modules.empty?
56
- $stdout.puts "No modules found in #{config_file}." \
64
+ $stderr.puts "No modules found in #{config_file}." \
57
65
  ' Check that you specified the right :configs directory and :managed_modules_conf file.'
58
- exit
66
+ exit 1
59
67
  end
60
68
  managed_modules.select! { |m| m =~ Regexp.new(filter) } unless filter.nil?
61
69
  managed_modules.reject! { |m| m =~ Regexp.new(negative_filter) } unless negative_filter.nil?
62
- managed_modules
63
- end
64
-
65
- def self.module_name(module_name, default_namespace)
66
- return [default_namespace, module_name] unless module_name.include?('/')
67
- ns, mod = module_name.split('/')
70
+ managed_modules.map { |given_name, options| PuppetModule.new(given_name, options) }
68
71
  end
69
72
 
70
73
  def self.hook(options)
@@ -78,11 +81,11 @@ module ModuleSync # rubocop:disable Metrics/ModuleLength
78
81
  end
79
82
  end
80
83
 
81
- def self.manage_file(filename, settings, options)
84
+ def self.manage_file(puppet_module, filename, settings, options)
82
85
  namespace = settings.additional_settings[:namespace]
83
86
  module_name = settings.additional_settings[:puppet_module]
84
87
  configs = settings.build_file_configs(filename)
85
- target_file = module_file(options[:project_root], namespace, module_name, filename)
88
+ target_file = puppet_module.path(filename)
86
89
  if configs['delete']
87
90
  Renderer.remove(target_file)
88
91
  else
@@ -92,50 +95,52 @@ module ModuleSync # rubocop:disable Metrics/ModuleLength
92
95
  # Meta data passed to the template as @metadata[:name]
93
96
  metadata = {
94
97
  :module_name => module_name,
95
- :workdir => module_file(options[:project_root], namespace, module_name),
98
+ :workdir => puppet_module.working_directory,
96
99
  :target_file => target_file,
97
100
  }
98
101
  template = Renderer.render(erb, configs, metadata)
99
102
  Renderer.sync(template, target_file)
100
- rescue # rubocop:disable Lint/RescueWithoutErrorClass
101
- $stderr.puts "Error while rendering #{filename}"
103
+ rescue StandardError => e
104
+ $stderr.puts "#{puppet_module.given_name}: Error while rendering file: '#{filename}'"
102
105
  raise
103
106
  end
104
107
  end
105
108
  end
106
109
 
107
- def self.manage_module(puppet_module, module_files, module_options, defaults, options)
108
- default_namespace = options[:namespace]
109
- if module_options.is_a?(Hash) && module_options.key?(:namespace)
110
- default_namespace = module_options[:namespace]
111
- end
112
- namespace, module_name = module_name(puppet_module, default_namespace)
113
- git_repo = File.join(namespace, module_name)
114
- unless options[:offline]
115
- Git.pull(options[:git_base], git_repo, options[:branch], options[:project_root], module_options || {})
116
- end
110
+ def self.manage_module(puppet_module, module_files, defaults)
111
+ puts "Syncing '#{puppet_module.given_name}'"
112
+ puppet_module.repository.prepare_workspace(options[:branch]) unless options[:offline]
117
113
 
118
- module_configs = Util.parse_config(module_file(options[:project_root], namespace, module_name, MODULE_CONF_FILE))
114
+ module_configs = Util.parse_config puppet_module.path(MODULE_CONF_FILE)
119
115
  settings = Settings.new(defaults[GLOBAL_DEFAULTS_KEY] || {},
120
116
  defaults,
121
117
  module_configs[GLOBAL_DEFAULTS_KEY] || {},
122
118
  module_configs,
123
- :puppet_module => module_name,
119
+ :puppet_module => puppet_module.repository_name,
124
120
  :git_base => options[:git_base],
125
- :namespace => namespace)
121
+ :namespace => puppet_module.repository_namespace)
122
+
126
123
  settings.unmanaged_files(module_files).each do |filename|
127
- $stdout.puts "Not managing #{filename} in #{module_name}"
124
+ $stdout.puts "Not managing '#{filename}' in '#{puppet_module.given_name}'"
128
125
  end
129
126
 
130
127
  files_to_manage = settings.managed_files(module_files)
131
- files_to_manage.each { |filename| manage_file(filename, settings, options) }
128
+ files_to_manage.each { |filename| manage_file(puppet_module, filename, settings, options) }
132
129
 
133
130
  if options[:noop]
134
- Git.update_noop(git_repo, options)
135
- options[:pr] && pr(module_options).manage(namespace, module_name, options)
131
+ puts "Using no-op. Files in '#{puppet_module.given_name}' may be changed but will not be committed."
132
+ puppet_module.repository.show_changes(options)
133
+ options[:pr] && \
134
+ pr(puppet_module).manage(puppet_module.repository_namespace, puppet_module.repository_name, options)
136
135
  elsif !options[:offline]
137
- pushed = Git.update(git_repo, files_to_manage, options)
138
- pushed && options[:pr] && pr(module_options).manage(namespace, module_name, options)
136
+ pushed = puppet_module.repository.submit_changes(files_to_manage, options)
137
+ # Only bump/tag if pushing didn't fail (i.e. there were changes)
138
+ if pushed && options[:bump]
139
+ new = puppet_module.bump(options[:message], options[:changelog])
140
+ puppet_module.repository.tag(new, options[:tag_pattern]) if options[:tag]
141
+ end
142
+ pushed && options[:pr] && \
143
+ pr(puppet_module).manage(puppet_module.repository_namespace, puppet_module.repository_name, options)
139
144
  end
140
145
  end
141
146
 
@@ -148,9 +153,10 @@ module ModuleSync # rubocop:disable Metrics/ModuleLength
148
153
  self.class.config_path(file, options)
149
154
  end
150
155
 
151
- def self.update(options)
152
- options = config_defaults.merge(options)
156
+ def self.update(cli_options)
157
+ @options = config_defaults.merge(cli_options)
153
158
  defaults = Util.parse_config(config_path(CONF_FILE, options))
159
+
154
160
  if options[:pr]
155
161
  unless options[:branch]
156
162
  $stderr.puts 'A branch must be specified with --branch to use --pr!'
@@ -164,28 +170,28 @@ module ModuleSync # rubocop:disable Metrics/ModuleLength
164
170
  local_files = find_template_files(local_template_dir)
165
171
  module_files = relative_names(local_files, local_template_dir)
166
172
 
167
- managed_modules = self.managed_modules(config_path(options[:managed_modules_conf], options),
168
- options[:filter],
169
- options[:negative_filter])
170
-
171
173
  errors = false
172
174
  # managed_modules is either an array or a hash
173
- managed_modules.each do |puppet_module, module_options|
175
+ managed_modules.each do |puppet_module|
174
176
  begin
175
- mod_options = module_options.nil? ? nil : Util.symbolize_keys(module_options)
176
- manage_module(puppet_module, module_files, mod_options, defaults, options)
177
- rescue # rubocop:disable Lint/RescueWithoutErrorClass
178
- $stderr.puts "Error while updating #{puppet_module}"
177
+ manage_module(puppet_module, module_files, defaults)
178
+ rescue ModuleSync::Error, Git::GitExecuteError => e
179
+ message = e.message || "Error during '#{options[:command]}'"
180
+ $stderr.puts "#{puppet_module.given_name}: #{message}"
181
+ exit 1 unless options[:skip_broken]
182
+ errors = true
183
+ $stdout.puts "Skipping '#{puppet_module.given_name}' as update process failed"
184
+ rescue StandardError => e
179
185
  raise unless options[:skip_broken]
180
186
  errors = true
181
- $stdout.puts "Skipping #{puppet_module} as update process failed"
187
+ $stdout.puts "Skipping '#{puppet_module.given_name}' as update process failed"
182
188
  end
183
189
  end
184
190
  exit 1 if errors && options[:fail_on_warnings]
185
191
  end
186
192
 
187
- def self.pr(module_options)
188
- module_options ||= {}
193
+ def self.pr(puppet_module)
194
+ module_options = puppet_module.options
189
195
  github_conf = module_options[:github]
190
196
  gitlab_conf = module_options[:gitlab]
191
197
 
@@ -1,10 +1,12 @@
1
1
  require 'thor'
2
+
2
3
  require 'modulesync'
4
+ require 'modulesync/cli/thor'
3
5
  require 'modulesync/constants'
4
6
  require 'modulesync/util'
5
7
 
6
8
  module ModuleSync
7
- class CLI
9
+ module CLI
8
10
  def self.defaults
9
11
  @defaults ||= Util.symbolize_keys(Util.parse_config(Constants::MODULESYNC_CONF_FILE))
10
12
  end
@@ -36,15 +38,14 @@ module ModuleSync
36
38
  class Base < Thor
37
39
  class_option :project_root,
38
40
  :aliases => '-c',
39
- :desc => 'Path used by git to clone modules into. Defaults to "modules"',
41
+ :desc => 'Path used by git to clone modules into.',
40
42
  :default => CLI.defaults[:project_root] || 'modules'
41
43
  class_option :git_base,
42
44
  :desc => 'Specify the base part of a git URL to pull from',
43
45
  :default => CLI.defaults[:git_base] || 'git@github.com:'
44
46
  class_option :namespace,
45
47
  :aliases => '-n',
46
- :desc => 'Remote github namespace (user or organization) to clone from and push to.' \
47
- ' Defaults to puppetlabs',
48
+ :desc => 'Remote github namespace (user or organization) to clone from and push to.',
48
49
  :default => CLI.defaults[:namespace] || 'puppetlabs'
49
50
  class_option :filter,
50
51
  :aliases => '-f',
@@ -0,0 +1,24 @@
1
+ require 'thor'
2
+ require 'modulesync/cli'
3
+
4
+ module ModuleSync
5
+ module CLI
6
+ # Workaround some, still unfixed, Thor behaviors
7
+ #
8
+ # This class extends ::Thor class to
9
+ # - exit with status code sets to `1` on Thor failure (e.g. missing required option)
10
+ # - exit with status code sets to `1` when user calls `msync` (or a subcommand) without required arguments
11
+ class Thor < ::Thor
12
+ desc '_invalid_command_call', 'Invalid command', hide: true
13
+ def _invalid_command_call
14
+ self.class.new.help
15
+ exit 1
16
+ end
17
+ default_task :_invalid_command_call
18
+
19
+ def self.exit_on_failure?
20
+ true
21
+ end
22
+ end
23
+ end
24
+ 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,162 @@
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
+ base_branch = default_branch
55
+ unless base_branch
56
+ puts "Couldn't detect default branch. Falling back to assuming 'master'"
57
+ base_branch = 'master'
58
+ end
59
+ puts "Creating new branch #{branch} from #{base_branch}"
60
+ repo.checkout("origin/#{base_branch}")
61
+ repo.branch(branch).checkout
62
+ end
63
+ end
64
+
65
+ def prepare_workspace(branch)
66
+ # Repo needs to be cloned in the cwd
67
+ if !Dir.exist?("#{@directory}/.git")
68
+ puts "Cloning repository fresh from '#{@remote}'"
69
+ @git = Git.clone(@remote, @directory)
70
+ switch_branch(branch)
71
+ # Repo already cloned, check out master and override local changes
72
+ else
73
+ # Some versions of git can't properly handle managing a repo from outside the repo directory
74
+ Dir.chdir(@directory) do
75
+ puts "Overriding any local changes to repository in '#{@directory}'"
76
+ @git = Git.open('.')
77
+ repo.fetch
78
+ repo.reset_hard
79
+ switch_branch(branch)
80
+ git.pull('origin', branch) if remote_branch_exists?(branch)
81
+ end
82
+ end
83
+ end
84
+
85
+ def tag(version, tag_pattern)
86
+ tag = tag_pattern % version
87
+ puts "Tagging with #{tag}"
88
+ repo.add_tag(tag)
89
+ repo.push('origin', tag)
90
+ end
91
+
92
+ def checkout_branch(branch)
93
+ selected_branch = branch || repo.current_branch || 'master'
94
+ repo.branch(selected_branch).checkout
95
+ selected_branch
96
+ end
97
+
98
+ # Git add/rm, git commit, git push
99
+ def submit_changes(files, options)
100
+ message = options[:message]
101
+ branch = checkout_branch(options[:branch])
102
+ files.each do |file|
103
+ if repo.status.deleted.include?(file)
104
+ repo.remove(file)
105
+ elsif File.exist?("#{@directory}/#{file}")
106
+ repo.add(file)
107
+ end
108
+ end
109
+ begin
110
+ opts_commit = {}
111
+ opts_push = {}
112
+ opts_commit = { :amend => true } if options[:amend]
113
+ opts_push = { :force => true } if options[:force]
114
+ if options[:pre_commit_script]
115
+ script = "#{File.dirname(File.dirname(__FILE__))}/../contrib/#{options[:pre_commit_script]}"
116
+ `#{script} #{@directory}`
117
+ end
118
+ repo.commit(message, opts_commit)
119
+ if options[:remote_branch]
120
+ if remote_branch_differ?(branch, options[:remote_branch])
121
+ repo.push('origin', "#{branch}:#{options[:remote_branch]}", opts_push)
122
+ end
123
+ else
124
+ repo.push('origin', branch, opts_push)
125
+ end
126
+ rescue Git::GitExecuteError => e
127
+ raise unless e.message.match?(/working (directory|tree) clean/)
128
+
129
+ puts "There were no changes in '#{@directory}'. Not committing."
130
+ return false
131
+ end
132
+
133
+ true
134
+ end
135
+
136
+ # Needed because of a bug in the git gem that lists ignored files as
137
+ # untracked under some circumstances
138
+ # https://github.com/schacon/ruby-git/issues/130
139
+ def untracked_unignored_files
140
+ ignore_path = "#{@directory}/.gitignore"
141
+ ignored = File.exist?(ignore_path) ? File.read(ignore_path).split : []
142
+ repo.status.untracked.keep_if { |f, _| ignored.none? { |i| File.fnmatch(i, f) } }
143
+ end
144
+
145
+ def show_changes(options)
146
+ checkout_branch(options[:branch])
147
+
148
+ puts 'Files changed:'
149
+ repo.diff('HEAD', '--').each do |diff|
150
+ puts diff.patch
151
+ end
152
+
153
+ puts 'Files added:'
154
+ untracked_unignored_files.each_key do |file|
155
+ puts file
156
+ end
157
+
158
+ puts "\n\n"
159
+ puts '--------------------------------'
160
+ end
161
+ end
162
+ end