git-duet 0.1.1

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,45 @@
1
+ require 'git/duet'
2
+ require 'git/duet/author_mapper'
3
+ require 'git/duet/command_methods'
4
+
5
+ class Git::Duet::SoloCommand
6
+ include Git::Duet::CommandMethods
7
+
8
+ def initialize(soloist, quiet = false, global = false)
9
+ @soloist = soloist
10
+ @quiet = !!quiet
11
+ @global = !!global
12
+ @author_mapper = Git::Duet::AuthorMapper.new
13
+ end
14
+
15
+ def execute!
16
+ set_soloist_as_git_config_user
17
+ unset_committer_vars
18
+ report_env_vars
19
+ write_env_vars
20
+ end
21
+
22
+ private
23
+ attr_accessor :soloist, :author_mapper
24
+
25
+ def set_soloist_as_git_config_user
26
+ exec_check("git config #{@global ? '--global ' : ''}user.name '#{soloist_info[:name]}'")
27
+ exec_check("git config #{@global ? '--global ' : ''}user.email '#{soloist_info[:email]}'")
28
+ end
29
+
30
+ def unset_committer_vars
31
+ exec_check("git config #{@global ? '--global ' : ''}--unset-all duet.env.git-committer-name", [0, 5])
32
+ exec_check("git config #{@global ? '--global ' : ''}--unset-all duet.env.git-committer-email", [0, 5])
33
+ end
34
+
35
+ def var_map
36
+ {
37
+ 'GIT_AUTHOR_NAME' => soloist_info[:name],
38
+ 'GIT_AUTHOR_EMAIL' => soloist_info[:email]
39
+ }
40
+ end
41
+
42
+ def soloist_info
43
+ @soloist_info ||= author_mapper.map(@soloist).fetch(@soloist)
44
+ end
45
+ end
@@ -0,0 +1,7 @@
1
+ unless defined?(Git::Duet::VERSION)
2
+ module Git
3
+ module Duet
4
+ VERSION = '0.1.1'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,304 @@
1
+ require 'tmpdir'
2
+
3
+ describe 'git-duet end to end', :integration => true do
4
+ EMAIL_LOOKUP_SCRIPT = <<-EOF.gsub(/^ /, '')
5
+ #!/usr/bin/env ruby
6
+ addr = {
7
+ 'jd' => 'jane_doe@lookie.me.local',
8
+ 'fb' => 'fb9000@dalek.info.local'
9
+ }[ARGV.first]
10
+ puts addr
11
+ EOF
12
+
13
+ def install_hook
14
+ Dir.chdir(@repo_dir)
15
+ `git duet-install-hook -q`
16
+ end
17
+
18
+ def uninstall_hook
19
+ FileUtils.rm_f('.git/hooks/pre-commit')
20
+ end
21
+
22
+ def make_an_edit
23
+ Dir.chdir(@repo_dir)
24
+ File.open('file.txt', 'w') { |f| f.puts "foo-#{rand(100000)}" }
25
+ `git add file.txt`
26
+ end
27
+
28
+ before :all do
29
+ @startdir = Dir.pwd
30
+ @tmpdir = Dir.mktmpdir('git-duet-specs')
31
+ @git_authors = File.join(@tmpdir, '.git-authors')
32
+ @email_lookup_path = File.join(@tmpdir, 'email-lookup')
33
+ File.open(@git_authors, 'w') do |f|
34
+ f.puts YAML.dump(
35
+ 'pairs' => {
36
+ 'jd' => 'Jane Doe',
37
+ 'fb' => 'Frances Bar',
38
+ 'zp' => 'Zubaz Pants'
39
+ },
40
+ 'email' => {
41
+ 'domain' => 'hamster.info.local'
42
+ },
43
+ 'email_addresses' => {
44
+ 'jd' => 'jane@hamsters.biz.local'
45
+ }
46
+ )
47
+ end
48
+ ENV['GIT_DUET_AUTHORS_FILE'] = @git_authors
49
+ ENV['PATH'] = "#{File.expand_path('../../../bin', __FILE__)}:#{ENV['PATH']}"
50
+ File.open(@email_lookup_path, 'w') { |f| f.puts EMAIL_LOOKUP_SCRIPT }
51
+ FileUtils.chmod(0755, @email_lookup_path)
52
+ @repo_dir = File.join(@tmpdir, 'foo')
53
+ Dir.chdir(@tmpdir)
54
+ `git init #{@repo_dir}`
55
+ end
56
+
57
+ after :all do
58
+ Dir.chdir(@startdir)
59
+ if ENV['RSPEC_NO_CLEANUP']
60
+ File.open('integration-end-to-end-test-dir.txt', 'w') { |f| f.puts @tmpdir }
61
+ else
62
+ FileUtils.rm_rf(@tmpdir)
63
+ end
64
+ end
65
+
66
+ context 'when installing the pre-commit hook' do
67
+ before(:each) { install_hook }
68
+ after(:each) { uninstall_hook }
69
+
70
+ it 'should write the hook to the `pre-commit` hook file' do
71
+ File.exist?('.git/hooks/pre-commit').should be_true
72
+ end
73
+
74
+ it 'should make the `pre-commit` hook file executable' do
75
+ File.executable?('.git/hooks/pre-commit').should be_true
76
+ end
77
+ end
78
+
79
+ context 'when setting the author via solo' do
80
+ before :each do
81
+ Dir.chdir(@repo_dir)
82
+ `git solo jd -q`
83
+ end
84
+
85
+ it 'should set the git user name' do
86
+ `git config user.name`.chomp.should == 'Jane Doe'
87
+ end
88
+
89
+ it 'should set the git user email' do
90
+ `git config user.email`.chomp.should == 'jane@hamsters.biz.local'
91
+ end
92
+
93
+ it 'should cache the git user name as author name' do
94
+ `git config duet.env.git-author-name`.chomp.should == 'Jane Doe'
95
+ end
96
+
97
+ it 'should cache the git user email as author email' do
98
+ `git config duet.env.git-author-email`.chomp.should == 'jane@hamsters.biz.local'
99
+ end
100
+ end
101
+
102
+
103
+ context 'when an external email lookup is provided' do
104
+ before :each do
105
+ @old_email_lookup = ENV.delete('GIT_DUET_EMAIL_LOOKUP_COMMAND')
106
+ ENV['GIT_DUET_EMAIL_LOOKUP_COMMAND'] = @email_lookup_path
107
+ end
108
+
109
+ after :each do
110
+ ENV['GIT_DUET_EMAIL_LOOKUP_COMMAND'] = @old_email_lookup
111
+ end
112
+
113
+ context 'when setting the author via solo' do
114
+ before :each do
115
+ Dir.chdir(@repo_dir)
116
+ `git solo jd -q`
117
+ end
118
+
119
+ it 'should set the author email address given by the external email lookup' do
120
+ `git config duet.env.git-author-email`.chomp.should == 'jane_doe@lookie.me.local'
121
+ end
122
+ end
123
+
124
+ context 'when setting author and committer via duet' do
125
+ before :each do
126
+ Dir.chdir(@repo_dir)
127
+ `git duet jd fb -q`
128
+ end
129
+
130
+ it 'should set the author email address given by the external email lookup' do
131
+ `git config duet.env.git-author-email`.chomp.should == 'jane_doe@lookie.me.local'
132
+ end
133
+
134
+ it 'should set the committer email address given by the external email lookup' do
135
+ `git config duet.env.git-committer-email`.chomp.should == 'fb9000@dalek.info.local'
136
+ end
137
+ end
138
+ end
139
+
140
+ context 'when a custom email template is provided' do
141
+ before :each do
142
+ authors_cfg = YAML.load_file(@git_authors)
143
+ @name_suffix = rand(9999)
144
+ authors_cfg['email_template'] =
145
+ %Q^<%= '' << author.split.first.downcase << ^ <<
146
+ %Q^author.split.last[0].chr.downcase << '#{@name_suffix}@mompopshop.local' %>^
147
+ File.open(@git_authors, 'w') do |f|
148
+ f.puts YAML.dump(authors_cfg)
149
+ end
150
+ end
151
+
152
+ after :each do
153
+ authors_cfg = YAML.load_file(@git_authors)
154
+ authors_cfg.delete('email_template')
155
+ File.open(@git_authors, 'w') do |f|
156
+ f.puts YAML.dump(authors_cfg)
157
+ end
158
+ end
159
+
160
+ context 'after running git-solo' do
161
+ before :each do
162
+ Dir.chdir(@repo_dir)
163
+ `git solo zp -q`
164
+ make_an_edit
165
+ end
166
+
167
+ it 'should use the email template to construct the author email' do
168
+ `git duet-commit -q -m 'Testing custom email template for author'`
169
+ `git log -1 --format='%an <%ae>'`.chomp.should == "Zubaz Pants <zubazp#{@name_suffix}@mompopshop.local>"
170
+ end
171
+
172
+ it 'should use the email template to construct the committer email' do
173
+ `git duet-commit -q -m 'Testing custom email template for committer'`
174
+ `git log -1 --format='%cn <%ce>'`.chomp.should == "Zubaz Pants <zubazp#{@name_suffix}@mompopshop.local>"
175
+ end
176
+ end
177
+
178
+ context 'after running git-duet' do
179
+ before :each do
180
+ Dir.chdir(@repo_dir)
181
+ `git duet zp fb -q`
182
+ make_an_edit
183
+ end
184
+
185
+ it 'should use the email template to construct the author email' do
186
+ `git duet-commit -q -m 'Testing custom email template for author'`
187
+ `git log -1 --format='%an <%ae>'`.chomp.should == "Zubaz Pants <zubazp#{@name_suffix}@mompopshop.local>"
188
+ end
189
+
190
+ it 'should use the email template to construct the committer email' do
191
+ `git duet-commit -q -m 'Testing custom email template for committer'`
192
+ `git log -1 --format='%cn <%ce>'`.chomp.should == "Frances Bar <francesb#{@name_suffix}@mompopshop.local>"
193
+ end
194
+ end
195
+ end
196
+
197
+ context 'when setting author and committer via duet' do
198
+ before :each do
199
+ Dir.chdir(@repo_dir)
200
+ `git duet jd fb -q`
201
+ end
202
+
203
+ it 'should set the git user name' do
204
+ `git config user.name`.chomp.should == 'Jane Doe'
205
+ end
206
+
207
+ it 'should set the git user email' do
208
+ `git config user.email`.chomp.should == 'jane@hamsters.biz.local'
209
+ end
210
+
211
+ it 'should cache the git committer name' do
212
+ `git config duet.env.git-committer-name`.chomp.should == 'Frances Bar'
213
+ end
214
+
215
+ it 'should cache the git committer email' do
216
+ `git config duet.env.git-committer-email`.chomp.should == 'f.bar@hamster.info.local'
217
+ end
218
+ end
219
+
220
+ context 'when committing via git-duet-commit' do
221
+ context 'after running git-duet' do
222
+ before :each do
223
+ Dir.chdir(@repo_dir)
224
+ `git duet jd fb -q`
225
+ make_an_edit
226
+ end
227
+
228
+ it 'should list the alpha of the duet as author in the log' do
229
+ `git duet-commit -q -m 'Testing set of alpha as author'`
230
+ `git log -1 --format='%an <%ae>'`.chomp.should == 'Jane Doe <jane@hamsters.biz.local>'
231
+ end
232
+
233
+ it 'should list the omega of the duet as committer in the log' do
234
+ `git duet-commit -q -m 'Testing set of omega as committer'`
235
+ `git log -1 --format='%cn <%ce>'`.chomp.should == 'Frances Bar <f.bar@hamster.info.local>'
236
+ end
237
+
238
+ context 'with the pre-commit hook in place' do
239
+ before :each do
240
+ `git commit -m 'Committing before installing the hook'`
241
+ @latest_sha1 = `git log -1 --format=%H`.chomp
242
+ make_an_edit
243
+ install_hook
244
+ `git config --unset-all duet.env.mtime`
245
+ ENV['GIT_DUET_QUIET'] = '1'
246
+ end
247
+
248
+ after :each do
249
+ uninstall_hook
250
+ ENV.delete('GIT_DUET_QUIET')
251
+ end
252
+
253
+ it 'should fire the hook and reject the commit' do
254
+ `git duet-commit -q -m 'Testing hook firing'`
255
+ `git log -1 --format=%H`.chomp.should == @latest_sha1
256
+ end
257
+ end
258
+ end
259
+
260
+ context 'after running git-solo' do
261
+ before :each do
262
+ Dir.chdir(@repo_dir)
263
+ `git solo jd -q`
264
+ make_an_edit
265
+ end
266
+
267
+ it 'should list the soloist as author in the log' do
268
+ `git duet-commit -m 'Testing set of soloist as author' 2>/dev/null`
269
+ `git log -1 --format='%an <%ae>'`.chomp.should == 'Jane Doe <jane@hamsters.biz.local>'
270
+ end
271
+
272
+ it 'should list the soloist as committer in the log' do
273
+ `git duet-commit -m 'Testing set of soloist as committer' 2>/dev/null`
274
+ `git log -1 --format='%cn <%ce>'`.chomp.should == 'Jane Doe <jane@hamsters.biz.local>'
275
+ end
276
+
277
+ it 'should not include "Signed-off-by" in the commit message' do
278
+ `git duet-commit -m 'Testing ommitting signoff when only one author' 2>/dev/null`
279
+ `grep 'Signed-off-by' .git/COMMIT_EDITMSG`.chomp.should == ''
280
+ end
281
+
282
+ context 'with the pre-commit hook in place' do
283
+ before :each do
284
+ `git commit -m 'Committing before installing the hook'`
285
+ @latest_sha1 = `git log -1 --format=%H`.chomp
286
+ make_an_edit
287
+ install_hook
288
+ `git config --unset-all duet.env.mtime`
289
+ ENV['GIT_DUET_QUIET'] = '1'
290
+ end
291
+
292
+ after :each do
293
+ uninstall_hook
294
+ ENV.delete('GIT_DUET_QUIET')
295
+ end
296
+
297
+ it 'should fire the hook and reject the commit' do
298
+ `git duet-commit -q -m 'Testing hook firing'`
299
+ `git log -1 --format=%H`.chomp.should == @latest_sha1
300
+ end
301
+ end
302
+ end
303
+ end
304
+ end
@@ -0,0 +1,170 @@
1
+ require 'git/duet/author_mapper'
2
+
3
+ describe Git::Duet::AuthorMapper do
4
+ before :each do
5
+ subject.instance_variable_set(:@cfg, {
6
+ 'authors' => {
7
+ 'jd' => 'Jane Doe; jdoe',
8
+ 'fb' => 'Frances Bar; frances',
9
+ 'qx' => 'Quincy Xavier; qx',
10
+ 'hb' => 'Hampton Bones'
11
+ },
12
+ 'email' => {
13
+ 'domain' => 'awesometown.me'
14
+ },
15
+ 'email_addresses' => {
16
+ 'jd' => 'jane@awesome.biz'
17
+ }
18
+ })
19
+ end
20
+
21
+ after :each do
22
+ ENV.delete('GIT_DUET_AUTHORS_FILE')
23
+ end
24
+
25
+ it 'should use an authors file given at initialization' do
26
+ instance = described_class.new('/blarggie/blarggie/new/friend/.git-authors')
27
+ instance.authors_file.should == '/blarggie/blarggie/new/friend/.git-authors'
28
+ end
29
+
30
+ it 'should use the `GIT_DUET_AUTHORS_FILE` if provided' do
31
+ ENV['GIT_DUET_AUTHORS_FILE'] = '/fizzle/bizzle/.git-authors'
32
+ instance = described_class.new
33
+ instance.authors_file.should == '/fizzle/bizzle/.git-authors'
34
+ end
35
+
36
+ it 'should fall back to using `~/.git-authors` for the author map' do
37
+ subject.authors_file.should == File.join(ENV['HOME'], '.git-authors')
38
+ end
39
+
40
+ it 'should map initials to name -> email pairs' do
41
+ subject.map('jd').fetch('jd').should == {
42
+ :name => 'Jane Doe',
43
+ :email => 'jane@awesome.biz'
44
+ }
45
+ end
46
+
47
+ it 'should construct default email addresses from first initial and last name plus domain' do
48
+ subject.map('hb').should == {
49
+ 'hb' => {
50
+ :name => 'Hampton Bones',
51
+ :email => 'h.bones@awesometown.me'
52
+ }
53
+ }
54
+ end
55
+
56
+ it 'should construct email addresses from optional username (if given) plus domain' do
57
+ subject.map('fb').should == {
58
+ 'fb' => {
59
+ :name => 'Frances Bar',
60
+ :email => 'frances@awesometown.me'
61
+ }
62
+ }
63
+ end
64
+
65
+ it 'should use an explicitly-configured email address if given' do
66
+ subject.map('jd').should == {
67
+ 'jd' => {
68
+ :name => 'Jane Doe',
69
+ :email => 'jane@awesome.biz'
70
+ }
71
+ }
72
+ end
73
+
74
+ it 'should map any number of initials to name -> email pairs' do
75
+ subject.map('jd', 'fb', 'qx', 'hb').should == {
76
+ 'jd' => {
77
+ :name => 'Jane Doe',
78
+ :email => 'jane@awesome.biz'
79
+ },
80
+ 'fb' => {
81
+ :name => 'Frances Bar',
82
+ :email => 'frances@awesometown.me'
83
+ },
84
+ 'qx' => {
85
+ :name => 'Quincy Xavier',
86
+ :email => 'qx@awesometown.me'
87
+ },
88
+ 'hb' => {
89
+ :name => 'Hampton Bones',
90
+ :email => 'h.bones@awesometown.me'
91
+ }
92
+ }
93
+ end
94
+
95
+ context 'when using a `~/.pairs` config' do
96
+ before :each do
97
+ subject.stub(:cfg => {
98
+ 'pairs' => {
99
+ 'jd' => 'Jane Doe; jdoe',
100
+ 'fb' => 'Frances Bar; frances',
101
+ 'qx' => 'Quincy Xavier; qx',
102
+ 'hb' => 'Hampton Bones'
103
+ },
104
+ 'email' => {
105
+ 'domain' => 'awesometown.me'
106
+ },
107
+ 'email_addresses' => {
108
+ 'jd' => 'jane@awesome.biz'
109
+ }
110
+ })
111
+ end
112
+
113
+ it 'should map initials to name -> email pairs' do
114
+ subject.map('jd').fetch('jd').should == {
115
+ :name => 'Jane Doe',
116
+ :email => 'jane@awesome.biz'
117
+ }
118
+ end
119
+
120
+ it 'should map any number of initials to name -> email pairs' do
121
+ subject.map('jd', 'fb', 'qx', 'hb').should == {
122
+ 'jd' => {
123
+ :name => 'Jane Doe',
124
+ :email => 'jane@awesome.biz'
125
+ },
126
+ 'fb' => {
127
+ :name => 'Frances Bar',
128
+ :email => 'frances@awesometown.me'
129
+ },
130
+ 'qx' => {
131
+ :name => 'Quincy Xavier',
132
+ :email => 'qx@awesometown.me'
133
+ },
134
+ 'hb' => {
135
+ :name => 'Hampton Bones',
136
+ :email => 'h.bones@awesometown.me'
137
+ }
138
+ }
139
+ end
140
+ end
141
+
142
+ context 'when the authors file does not exist' do
143
+ let :bad_path do
144
+ "/its/#{rand(999)}/hosed/#{rand(999)}/captain/#{rand(999)}/.git-authors"
145
+ end
146
+
147
+ subject do
148
+ described_class.new(bad_path)
149
+ end
150
+
151
+ before :each do
152
+ subject.instance_variable_set(:@cfg, nil)
153
+ IO.stub(:read).with(bad_path).and_raise(
154
+ Errno::ENOENT.new("No such file or directory - #{bad_path}")
155
+ )
156
+ end
157
+
158
+ it 'should warn about missing authors file' do
159
+ STDERR.should_receive(:puts).with(
160
+ /Missing or corrupt authors file.*#{bad_path}/i
161
+ )
162
+ expect { subject.map('zzz') }.to raise_error
163
+ end
164
+
165
+ it 'should raise a ScriptDieError' do
166
+ STDERR.stub(:puts)
167
+ expect { subject.map('zzz') }.to raise_error(Git::Duet::ScriptDieError)
168
+ end
169
+ end
170
+ end