git-duet 0.3.0 → 0.4.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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +6 -0
- data/.ruby-version +1 -1
- data/Gemfile +3 -1
- data/README.md +3 -2
- data/bin/git-dci +4 -0
- data/git-duet.gemspec +1 -0
- data/lib/git/duet/author_mapper.rb +67 -61
- data/lib/git/duet/cli.rb +100 -94
- data/lib/git/duet/command_methods.rb +112 -106
- data/lib/git/duet/commit_command.rb +61 -57
- data/lib/git/duet/duet_command.rb +52 -48
- data/lib/git/duet/install_hook_command.rb +29 -25
- data/lib/git/duet/pre_commit_command.rb +43 -38
- data/lib/git/duet/solo_command.rb +52 -48
- data/lib/git/duet/version.rb +1 -1
- data/spec/integration/end_to_end_spec.rb +77 -59
- data/spec/lib/git/duet/author_mapper_spec.rb +15 -13
- data/spec/lib/git/duet/command_methods_spec.rb +2 -2
- data/spec/lib/git/duet/solo_command_spec.rb +2 -2
- metadata +40 -39
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b0e935ea43b312f7b8f22a51c37ff101c8bcfa92
|
4
|
+
data.tar.gz: 5a01b500d7ed09135fd5f5acbd7500084ac72fe9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 92aa13d5ad13ea7540e47ba48aec5b9ae2e70aed2457dca8a0746d20fe4c1df2ee8cb3468a099e9026f1a858af9d5459faac31dffa0d3135cc4d9d802db4211f
|
7
|
+
data.tar.gz: 6d2e39230cf987832d19f259140cacb4b8b5663e4b0db208a0cac4a16001906190207541fa5313ef3615955836224d9351663fe74acadada7b62af85a17102c1
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.1.
|
1
|
+
2.1.1
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -64,11 +64,12 @@ Set the author and committer via `git duet`:
|
|
64
64
|
git duet jd fb
|
65
65
|
~~~~~
|
66
66
|
|
67
|
-
When you're ready to commit, use `git duet-commit`
|
68
|
-
a normal person. Something like `dci = duet-commit` should work.)
|
67
|
+
When you're ready to commit, use `git duet-commit` or `git dci`
|
69
68
|
|
70
69
|
~~~~~ bash
|
71
70
|
git duet-commit -v [any other git options]
|
71
|
+
# or...
|
72
|
+
git dci -v [any other git options]
|
72
73
|
~~~~~
|
73
74
|
|
74
75
|
When you're done pairing, set the author back to yourself with `git solo`:
|
data/bin/git-dci
ADDED
data/git-duet.gemspec
CHANGED
@@ -33,6 +33,7 @@ Gem::Specification.new do |gem|
|
|
33
33
|
gem.add_development_dependency 'rspec'
|
34
34
|
gem.add_development_dependency 'rubocop'
|
35
35
|
|
36
|
+
gem.add_development_dependency 'posix-spawn' unless RUBY_PLATFORM == 'java'
|
36
37
|
gem.add_development_dependency 'pry' unless RUBY_PLATFORM == 'java'
|
37
38
|
gem.add_development_dependency 'simplecov' unless RUBY_PLATFORM == 'java'
|
38
39
|
end
|
@@ -3,79 +3,85 @@ require 'yaml'
|
|
3
3
|
require 'erb'
|
4
4
|
require 'git/duet'
|
5
5
|
|
6
|
-
|
7
|
-
|
6
|
+
module Git
|
7
|
+
module Duet
|
8
|
+
class AuthorMapper
|
9
|
+
attr_accessor :authors_file
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
11
|
+
def initialize(authors_file = nil, email_lookup = nil)
|
12
|
+
@authors_file = authors_file ||
|
13
|
+
ENV['GIT_DUET_AUTHORS_FILE'] ||
|
14
|
+
File.join(ENV['HOME'], '.git-authors')
|
15
|
+
@email_lookup = email_lookup ||
|
16
|
+
ENV['GIT_DUET_EMAIL_LOOKUP_COMMAND']
|
17
|
+
end
|
16
18
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
19
|
+
def map(*initials_list)
|
20
|
+
author_map = {}
|
21
|
+
initials_list.each do |initials|
|
22
|
+
author_map[initials] = author_info(initials)
|
23
|
+
end
|
24
|
+
author_map
|
25
|
+
end
|
24
26
|
|
25
|
-
|
27
|
+
private
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
29
|
+
def author_info(initials)
|
30
|
+
author, username = author_map.fetch(initials).split(/;/).map(&:strip)
|
31
|
+
{
|
32
|
+
name: author,
|
33
|
+
email: lookup_author_email(initials, author, username)
|
34
|
+
}
|
35
|
+
end
|
34
36
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
37
|
+
def lookup_author_email(initials, author, username)
|
38
|
+
author_email = email_from_lookup(initials, author, username)
|
39
|
+
return author_email unless author_email.empty?
|
40
|
+
return email_addresses[initials] if email_addresses[initials]
|
41
|
+
if email_template
|
42
|
+
return email_from_template(initials, author, username)
|
43
|
+
end
|
44
|
+
return "#{username}@#{email_domain}" if username
|
41
45
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
+
author_name_parts = author.split
|
47
|
+
"#{author_name_parts.first[0, 1].downcase}." \
|
48
|
+
"#{author_name_parts.last.downcase}@#{email_domain}"
|
49
|
+
end
|
46
50
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
+
def email_from_lookup(initials, author, username)
|
52
|
+
return '' unless @email_lookup
|
53
|
+
`#{@email_lookup} '#{initials}' '#{author}' '#{username}'`.strip
|
54
|
+
end
|
51
55
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
56
|
+
def email_from_template(initials, author, username)
|
57
|
+
return ERB.new(email_template).result(binding)
|
58
|
+
rescue StandardError => e
|
59
|
+
$stderr.puts("git-duet: email template rendering error: #{e.message}")
|
60
|
+
raise Git::Duet::ScriptDieError, 8
|
61
|
+
end
|
58
62
|
|
59
|
-
|
60
|
-
|
61
|
-
|
63
|
+
def author_map
|
64
|
+
@author_map ||= (cfg['authors'] || cfg['pairs'])
|
65
|
+
end
|
62
66
|
|
63
|
-
|
64
|
-
|
65
|
-
|
67
|
+
def email_addresses
|
68
|
+
@email_addresses ||= (cfg['email_addresses'] || {})
|
69
|
+
end
|
66
70
|
|
67
|
-
|
68
|
-
|
69
|
-
|
71
|
+
def email_domain
|
72
|
+
@email_domain ||= cfg.fetch('email').fetch('domain')
|
73
|
+
end
|
70
74
|
|
71
|
-
|
72
|
-
|
73
|
-
|
75
|
+
def email_template
|
76
|
+
@email_template || cfg['email_template']
|
77
|
+
end
|
74
78
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
79
|
+
def cfg
|
80
|
+
@cfg ||= YAML.load(IO.read(authors_file))
|
81
|
+
rescue StandardError => e
|
82
|
+
$stderr.puts("git-duet: Missing or corrupt authors file: #{e.message}")
|
83
|
+
raise Git::Duet::ScriptDieError, 3
|
84
|
+
end
|
85
|
+
end
|
80
86
|
end
|
81
87
|
end
|
data/lib/git/duet/cli.rb
CHANGED
@@ -3,115 +3,121 @@ require 'optparse'
|
|
3
3
|
require 'git/duet'
|
4
4
|
require 'git/duet/script_die_error'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
6
|
+
module Git
|
7
|
+
module Duet
|
8
|
+
class Cli
|
9
|
+
class << self
|
10
|
+
def run(prog, argv)
|
11
|
+
method_name = File.basename(prog)
|
12
|
+
.sub(/^git-duet-/, '').sub(/^git-/, '').tr('-', '_')
|
13
|
+
send(method_name, parse_options(method_name, argv.clone))
|
14
|
+
0
|
15
|
+
rescue NoMethodError
|
16
|
+
raise ScriptError, 'How did you get here???'
|
17
|
+
rescue Git::Duet::ScriptDieError => e
|
18
|
+
e.exit_code
|
19
|
+
end
|
18
20
|
|
19
|
-
|
21
|
+
private
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
23
|
+
def parse_options(method_name, argv)
|
24
|
+
case method_name
|
25
|
+
when 'pre_commit', 'install_hook'
|
26
|
+
parse_generic_options(argv)
|
27
|
+
else
|
28
|
+
send("parse_#{method_name}_options", argv)
|
29
|
+
end
|
30
|
+
end
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
32
|
+
def with_common_opts(argv, banner)
|
33
|
+
options = {}
|
34
|
+
leftover_argv = OptionParser.new do |opts|
|
35
|
+
opts.banner = banner.gsub(/__PROG__/, opts.program_name)
|
36
|
+
opts.on('-q', 'Silence output') do |q|
|
37
|
+
options[:quiet] = true
|
38
|
+
end
|
39
|
+
yield opts, options if block_given?
|
40
|
+
end.parse!(argv)
|
41
|
+
[leftover_argv, options]
|
36
42
|
end
|
37
|
-
yield opts, options if block_given?
|
38
|
-
end.parse!(argv)
|
39
|
-
[leftover_argv, options]
|
40
|
-
end
|
41
43
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
44
|
+
def parse_solo_options(argv)
|
45
|
+
parse_options_with_positional_args(
|
46
|
+
argv, '<soloist-initials>') do |leftover_argv, options|
|
47
|
+
options[:soloist] = leftover_argv.first
|
48
|
+
end
|
49
|
+
end
|
48
50
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
51
|
+
def parse_duet_options(argv)
|
52
|
+
parse_options_with_positional_args(
|
53
|
+
argv,
|
54
|
+
'<alpha-initials> <omega-initials>'
|
55
|
+
) do |leftover_argv, options|
|
56
|
+
options[:alpha], options[:omega] = leftover_argv[0..1]
|
57
|
+
end
|
58
|
+
end
|
55
59
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
60
|
+
def parse_options_with_positional_args(argv, usage)
|
61
|
+
leftover_argv, options = with_common_opts(
|
62
|
+
argv, 'Usage: __PROG__ [options] ' << usage
|
63
|
+
) do |opts, options_hash|
|
64
|
+
opts.on('-g', '--global', 'Change global git config') do |g|
|
65
|
+
options_hash[:global] = true
|
66
|
+
end
|
67
|
+
end
|
68
|
+
yield leftover_argv, options
|
69
|
+
options
|
62
70
|
end
|
63
|
-
end
|
64
|
-
yield leftover_argv, options
|
65
|
-
options
|
66
|
-
end
|
67
71
|
|
68
|
-
|
69
|
-
|
70
|
-
|
72
|
+
def parse_generic_options(argv)
|
73
|
+
with_common_opts(argv, 'Usage: __PROG__').last
|
74
|
+
end
|
71
75
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
76
|
+
def parse_commit_options(argv)
|
77
|
+
opts_argv = []
|
78
|
+
opts_argv << '-q' if argv.delete('-q')
|
79
|
+
options = with_common_opts(opts_argv, 'Usage: __PROG__').last
|
80
|
+
options[:passthrough_args] = argv
|
81
|
+
options
|
82
|
+
end
|
79
83
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
84
|
+
def solo(options)
|
85
|
+
require 'git/duet/solo_command'
|
86
|
+
Git::Duet::SoloCommand.new(
|
87
|
+
options.fetch(:soloist),
|
88
|
+
options[:quiet],
|
89
|
+
options[:global]
|
90
|
+
).execute!
|
91
|
+
end
|
88
92
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
93
|
+
def duet(options)
|
94
|
+
require 'git/duet/duet_command'
|
95
|
+
Git::Duet::DuetCommand.new(
|
96
|
+
options.fetch(:alpha),
|
97
|
+
options.fetch(:omega),
|
98
|
+
options[:quiet],
|
99
|
+
options[:global]
|
100
|
+
).execute!
|
101
|
+
end
|
98
102
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
+
def pre_commit(options)
|
104
|
+
require 'git/duet/pre_commit_command'
|
105
|
+
Git::Duet::PreCommitCommand.new(options[:quiet]).execute!
|
106
|
+
end
|
103
107
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
+
def install_hook(options)
|
109
|
+
require 'git/duet/install_hook_command'
|
110
|
+
Git::Duet::InstallHookCommand.new(options[:quiet]).execute!
|
111
|
+
end
|
108
112
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
113
|
+
def commit(options)
|
114
|
+
require 'git/duet/commit_command'
|
115
|
+
Git::Duet::CommitCommand.new(
|
116
|
+
options[:passthrough_args],
|
117
|
+
options[:quiet]
|
118
|
+
).execute!
|
119
|
+
end
|
120
|
+
end
|
115
121
|
end
|
116
122
|
end
|
117
123
|
end
|
@@ -3,132 +3,138 @@ require 'English'
|
|
3
3
|
require 'git/duet'
|
4
4
|
require 'git/duet/script_die_error'
|
5
5
|
|
6
|
-
module Git
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
def write_env_vars
|
16
|
-
in_repo_root do
|
17
|
-
var_map.each do |key, value|
|
18
|
-
exec_check(
|
19
|
-
"#{git_config} #{Git::Duet::Config.namespace}." <<
|
20
|
-
"#{key.downcase.gsub(/_/, '-')} '#{value}'"
|
21
|
-
)
|
6
|
+
module Git
|
7
|
+
module Duet
|
8
|
+
module CommandMethods
|
9
|
+
private
|
10
|
+
|
11
|
+
def report_env_vars
|
12
|
+
var_map.each do |key, value|
|
13
|
+
info("#{key}='#{value}'")
|
14
|
+
end
|
22
15
|
end
|
23
|
-
|
16
|
+
|
17
|
+
def write_env_vars
|
18
|
+
in_repo_root do
|
19
|
+
var_map.each do |key, value|
|
20
|
+
exec_check(
|
21
|
+
"#{git_config} #{Git::Duet::Config.namespace}." \
|
22
|
+
"#{key.downcase.gsub(/_/, '-')} '#{value}'"
|
23
|
+
)
|
24
|
+
end
|
25
|
+
exec_check("#{git_config} #{Git::Duet::Config
|
24
26
|
.namespace}.mtime #{Time.now.to_i}")
|
25
|
-
|
26
|
-
|
27
|
+
end
|
28
|
+
end
|
27
29
|
|
28
|
-
|
29
|
-
|
30
|
-
|
30
|
+
def git_config
|
31
|
+
"git config#{@global ? ' --global' : ''}"
|
32
|
+
end
|
31
33
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
34
|
+
def author_env_vars_set?
|
35
|
+
%x(#{author_name_command} && #{author_email_command})
|
36
|
+
$CHILD_STATUS == 0
|
37
|
+
end
|
36
38
|
|
37
|
-
|
38
|
-
|
39
|
-
|
39
|
+
def author_name_command
|
40
|
+
"git config --get #{Git::Duet::Config.namespace}.git-author-name"
|
41
|
+
end
|
40
42
|
|
41
|
-
|
42
|
-
|
43
|
-
|
43
|
+
def author_email_command
|
44
|
+
"git config --get #{Git::Duet::Config.namespace}.git-author-email"
|
45
|
+
end
|
44
46
|
|
45
|
-
|
46
|
-
|
47
|
-
|
47
|
+
def current_config_command
|
48
|
+
"git config --get-regexp #{Git::Duet::Config.namespace}"
|
49
|
+
end
|
48
50
|
|
49
|
-
|
50
|
-
|
51
|
-
|
51
|
+
def show_current_config
|
52
|
+
info(exec_check(current_config_command))
|
53
|
+
end
|
52
54
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
55
|
+
def dump_env_vars
|
56
|
+
extract_env_vars_from_git_config.each do |k, v|
|
57
|
+
puts "#{k}='#{v}'"
|
58
|
+
end
|
59
|
+
end
|
58
60
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
61
|
+
def extract_env_vars_from_git_config
|
62
|
+
dest = {}
|
63
|
+
env_vars.each do |env_var, config_key|
|
64
|
+
begin
|
65
|
+
value = check_env_var_config_key(config_key)
|
66
|
+
dest[env_var] = value unless value.empty?
|
67
|
+
rescue StandardError => e
|
68
|
+
error("#{e.message}")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
dest
|
67
72
|
end
|
68
|
-
end
|
69
|
-
dest
|
70
|
-
end
|
71
73
|
|
72
|
-
|
73
|
-
|
74
|
-
|
74
|
+
def check_env_var_config_key(config_key)
|
75
|
+
exec_check(
|
76
|
+
"git config #{Git::Duet::Config.namespace}.#{config_key}"
|
77
|
+
).chomp
|
78
|
+
end
|
75
79
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
80
|
+
def exec_git_commit
|
81
|
+
if author_env_vars_set?
|
82
|
+
exec 'git commit ' << signoff_arg << quoted_passthrough_args
|
83
|
+
else
|
84
|
+
fail Git::Duet::ScriptDieError, 17
|
85
|
+
end
|
86
|
+
end
|
83
87
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
88
|
+
def in_repo_root
|
89
|
+
Dir.chdir(exec_check('git rev-parse --show-toplevel').chomp) do
|
90
|
+
yield
|
91
|
+
end
|
92
|
+
end
|
89
93
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
94
|
+
def exec_check(command, okay_statuses = [0].freeze)
|
95
|
+
output = `#{command}`
|
96
|
+
unless okay_statuses.include?($CHILD_STATUS.exitstatus)
|
97
|
+
error("Command #{command.inspect} exited with #{$CHILD_STATUS.to_i}")
|
98
|
+
fail Git::Duet::ScriptDieError, 1
|
99
|
+
end
|
100
|
+
output
|
101
|
+
end
|
98
102
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
103
|
+
def with_output_unquieted(&block)
|
104
|
+
@old_quiet = @quiet
|
105
|
+
@quiet = false
|
106
|
+
block.call
|
107
|
+
@quiet = @old_quiet
|
108
|
+
rescue StandardError => e
|
109
|
+
@quiet = @old_quiet
|
110
|
+
raise e
|
111
|
+
end
|
108
112
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
113
|
+
def with_output_quieted(&block)
|
114
|
+
@old_quiet = @quiet
|
115
|
+
@quiet = true
|
116
|
+
block.call
|
117
|
+
rescue StandardError => e
|
118
|
+
raise e
|
119
|
+
ensure
|
120
|
+
@quiet = @old_quiet
|
121
|
+
end
|
118
122
|
|
119
|
-
|
120
|
-
|
121
|
-
|
123
|
+
def info(msg)
|
124
|
+
$stdout.puts(msg) unless quiet?
|
125
|
+
end
|
122
126
|
|
123
|
-
|
124
|
-
|
125
|
-
|
127
|
+
def error(msg)
|
128
|
+
$stderr.puts(msg) unless quiet?
|
129
|
+
end
|
126
130
|
|
127
|
-
|
128
|
-
|
129
|
-
|
131
|
+
def prompt
|
132
|
+
$stdout.print '> '
|
133
|
+
end
|
130
134
|
|
131
|
-
|
132
|
-
|
135
|
+
def quiet?
|
136
|
+
ENV['GIT_DUET_QUIET'] == '1' || @quiet
|
137
|
+
end
|
138
|
+
end
|
133
139
|
end
|
134
140
|
end
|