Class: Dco::CLI

Inherits:
Thor
  • Object
show all
Defined in:
lib/dco/cli.rb

Overview

Since:

  • 1.0.0

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.exit_on_failure?Boolean

Because this isn't the default and exit statuses are what the cool kids do.

Returns:

  • (Boolean)

Since:

  • 1.0.0



26
27
28
# File 'lib/dco/cli.rb', line 26

def self.exit_on_failure?
  true
end

Instance Method Details

#check(branch = nil) ⇒ Object

Since:

  • 1.0.0



276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/dco/cli.rb', line 276

def check(branch=nil)
  branch ||= current_branch
  log = (options[:all] || branch == options[:base]) ? repo.log :  repo.log.between(options[:base], branch)
  bad_commits = []
  log.each do |commit|
    sign_off = has_sign_off?(commit)
    if !sign_off
      # No sign-off at all, tsk tsk.
      bad_commits << [commit, :no_sign_off]
    elsif !options[:allow_author_mismatch] && sign_off != "#{commit.author.name} <#{commit.author.email}>"
      # The signer-off and commit author don't match.
      bad_commits << [commit, :author_mismatch]
    end
  end

  if bad_commits.empty?
    # Yay!
    say("All commits are signed off", :green) unless options[:quiet]
  else
    # Something bad happened.
    unless options[:quiet]
      say("N: No Sign-off   M: Author mismatch", :red)
      bad_commits.each do |commit, reason|
        reason_string = {no_sign_off: 'N', author_mismatch: 'M'}[reason]
        say("#{reason_string} #{format_commit(commit)}", :red)
      end
    end
    exit 1
  end
end

#disableObject

Since:

  • 1.0.0



174
175
176
177
178
179
180
181
182
183
# File 'lib/dco/cli.rb', line 174

def disable
  assert_repo!
  unless our_hook?
    raise Thor::Error.new('commit-msg hook is external, not removing')
  end
  if File.exist?(HOOK_PATH)
    File.unlink(HOOK_PATH)
  end
  say('DCO auto-sign-off disabled', :green)
end

#enableObject

Since:

  • 1.0.0



158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/dco/cli.rb', line 158

def enable
  assert_repo!
  unless our_hook?
    raise Thor::Error.new('commit-msg hook already exists, not overwriting')
  end
  say("#{DCO_TEXT}\n\n", :yellow)
  unless confirm?("Do you, #{git_identity}, certify that all future commits to this repository will be under the terms of the Developer Certificate of Origin? [yes/no]")
    raise Thor::Error.new('Not enabling auto-sign-off without approval')
  end
  IO.write(HOOK_PATH, HOOK_SCRIPT)
  # 755 is what the defaults from `git init` use so probably good enough.
  File.chmod(00755, HOOK_PATH)
  say('DCO auto-sign-off enabled', :green)
end

#process_commit_message(tmp_path = nil) ⇒ Object

Since:

  • 1.0.0



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/dco/cli.rb', line 75

def process_commit_message(tmp_path=nil)
  # Set the repo path if passed.
  self.repo_path = options[:repo] if options[:repo]
  # If a path is passed use it as a tmpfile, otherwise assume filter mode.
  commit_msg = tmp_path ? IO.read(tmp_path) : STDIN.read
  unless has_sign_off?(commit_msg)
    # If we're in filter mode and not on-behalf-of, do a final check of the author.
    if !tmp_path && !options[:behalf] && ENV['GIT_AUTHOR_EMAIL'] != repo_config['user.email']
      # Something went wrong, refuse to rewrite.
      STDOUT.write(commit_msg)
      raise Thor::Error.new("Author mismatch on commit #{ENV['GIT_COMMIT']}: #{ENV['GIT_AUTHOR_EMAIL']} vs #{repo_config['user.email']}")
    end
    commit_msg << "\n" unless commit_msg.end_with?("\n")
    commit_msg << "\nSigned-off-by: #{ENV['GIT_AUTHOR_NAME']} <#{ENV['GIT_AUTHOR_EMAIL']}>\n"
    if options[:behalf]
      # This requires loading the actual repo config, which is slower.
      commit_msg << "Sign-off-executed-by: #{git_identity}\n"
      commit_msg << "Approved-at: #{options[:behalf]}\n"
    end
    IO.write(tmp_path, commit_msg) if tmp_path
  end
  # Always display the replacement commit message if we're in filter mode.
  STDOUT.write(commit_msg) unless tmp_path
end

#sign(branch = nil) ⇒ Object

Since:

  • 1.0.0



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/dco/cli.rb', line 189

def sign(branch=nil)
  # What two branches are we using?
  base_branch = options[:base]
  branch ||= current_branch
  if base_branch == branch
    # This should also catch people trying to sign-off on master.
    raise Thor::Error.new("Cannot use #{branch} for both the base and target branch")
  end

  # First check for a stored ref under refs/original/.
  begin
    repo.show("refs/original/refs/heads/#{branch}")
    # If this doesn't error, a backup ref is present.
    unless confirm?("An existing backup of branch #{branch} is present from a previous filter-branch. Do you want to remove this backup and continue? [yes/no]")
      raise Thor::Error.new('Backup ref present, not continuing')
    end
    # Clear the backup.
    File.unlink(".git/refs/original/refs/heads/#{branch}")
  rescue Git::GitExecuteError
    # This means there was no backup, keep going.
  end

  # Next examine all the commits we will be touching.
  commits = repo.log.between(base_branch, branch).to_a.select {|commit| !has_sign_off?(commit) }
  if commits.empty?
    raise Thor::Error.new("Branch #{branch} has no commits which require sign-off")
  end
  if !options[:behalf] && commits.any? {|commit| commit.author.email != repo_config['user.email'] }
    raise Thor::Error.new("Branch #{branch} contains commits not authored by you. Please use the --behalf flag when signing off for another contributor")
  end

  # Display the DCO text.
  say("#{DCO_TEXT}\n\n", :yellow) unless options[:behalf]

  # Display the list of commits.
  say("Going to sign-off the following commits:")
  commits.each do |commit|
    say("* #{format_commit(commit)}")
  end

  # Get confirmation.
  confirm_msg = if options[:behalf]
    "Do you, #{git_identity}, certify that these commits are contributed under the terms of the Developer Certificate of Origin as evidenced by #{options[:behalf]}? [yes/no]"
  else
    "Do you, #{git_identity}, certify that these commits are contributed under the terms of the Developer Certificate of Origin? [yes/no]"
  end
  unless confirm?(confirm_msg)
    raise Thor::Error.new('Not signing off on commits without approval')
  end

  # Stash if needed.
  did_stash = false
  status = repo.status
  unless status.changed.empty? && status.added.empty? && status.deleted.empty?
    say("Stashing uncommited changes before continuing")
    repo.lib.send(:command, 'stash', ['save', 'dco sign temp stash'])
    did_stash = true
  end

  # Run the filter branch. Here be dragons. Yes, I'm calling a private method. I'm sorry.
  filter_cmd = [Thor::Util.ruby_command, File.expand_path('../../../bin/dco', __FILE__), 'process_commit_message', '--repo', repo.dir.path]
  if options[:behalf]
    filter_cmd << '--behalf'
    filter_cmd << options[:behalf]
  end
  begin
    output = repo.lib.send(:command, 'filter-branch', ['--msg-filter', Shellwords.join(filter_cmd), "#{base_branch}..#{branch}"])
    say(output)
   ensure
    if did_stash
      # If we had a stash, make sure to replay it.
      say("Unstashing previous changes")
      # For whatever reason, the git gem doesn't expose this.
      repo.lib.send(:command, 'stash', ['pop'])
    end
  end

  # Hopefully that worked.
  say("Sign-off complete", :green)
  say("Don't forget to use --force when pushing this branch to your git server (eg. git push --force origin #{branch})", :green) # TODO I could detect the actual remote for this branch, if any.
end