git-commit-mailer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,563 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2009 Ryo Onodera <onodera@clear-code.com>
4
+ # Copyright (C) 2012-2014 Kouhei Sutou <kou@clear-code.com>
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ class GitCommitMailer
20
+ class HTMLMailBodyFormatter < MailBodyFormatter
21
+ include ERB::Util
22
+
23
+ def format
24
+ @indent_level = 0
25
+ super
26
+ end
27
+
28
+ private
29
+ def template
30
+ <<-EOT
31
+ <!DOCTYPE html>
32
+ <html>
33
+ <head>
34
+ </head>
35
+ <body>
36
+ <%= dl_start %>
37
+ <%= dt("Author") %>
38
+ <%= dd(h("\#{@info.author_name} <\#{@info.author_email}>")) %>
39
+ <%= dt("Date") %>
40
+ <%= dd(h(@mailer.format_time(@info.date))) %>
41
+ <%= dt("New Revision") %>
42
+ <%= dd(format_revision) %>
43
+ <% unless @info.merge_status.empty? %>
44
+ <%= dt("Merge") %>
45
+ <%= dd_start %>
46
+ <ul>
47
+ <% @info.merge_status.each do |status| %>
48
+ <li><%= h(status) %></li>
49
+ <% end %>
50
+ </ul>
51
+ </dd>
52
+ <% end %>
53
+ <%= dt("Message") %>
54
+ <%= dd(pre(h(@info.summary.strip))) %>
55
+ <%= format_files("Added", @info.added_files) %>
56
+ <%= format_files("Copied", @info.copied_files) %>
57
+ <%= format_files("Removed", @info.deleted_files) %>
58
+ <%= format_files("Modified", @info.updated_files) %>
59
+ <%= format_files("Renamed", @info.renamed_files) %>
60
+ <%= format_files("Type Changed", @info.type_changed_files) %>
61
+ </dl>
62
+
63
+ <%= format_diffs %>
64
+ </body>
65
+ </html>
66
+ EOT
67
+ end
68
+
69
+ def format_revision
70
+ revision = @info.revision
71
+ url = commit_url
72
+ if url
73
+ formatted_revision = "<a href=\"#{h(url)}\">#{h(revision)}</a>"
74
+ else
75
+ formatted_revision = h(revision)
76
+ end
77
+ formatted_revision
78
+ end
79
+
80
+ def format_files(title, items)
81
+ return "" if items.empty?
82
+
83
+ formatted_files = ""
84
+ formatted_files << " #{dt(h(title) + ' files')}\n"
85
+ formatted_files << " #{dd_start}\n"
86
+ formatted_files << " <ul>\n"
87
+ items.each do |item_name, new_item_name|
88
+ if new_item_name.nil?
89
+ formatted_files << " <li>#{format_file(item_name)}</li>\n"
90
+ else
91
+ formatted_files << " <li>\n"
92
+ formatted_files << " #{format_file(new_item_name)}<br>\n"
93
+ formatted_files << " (from #{item_name})\n"
94
+ formatted_files << " </li>\n"
95
+ end
96
+ end
97
+ formatted_files << " </ul>\n"
98
+ formatted_files << " </dd>\n"
99
+ formatted_files
100
+ end
101
+
102
+ def format_file(file)
103
+ content = h(file)
104
+ url = commit_file_url(file)
105
+ if url
106
+ content = tag("a", {"href" => url}, content)
107
+ end
108
+ content
109
+ end
110
+
111
+ def format_diffs
112
+ return "" if @info.diffs.empty?
113
+
114
+ formatted_diff = ""
115
+ formatted_diff << " #{div_diff_section_start}\n"
116
+ @indent_level = 3
117
+ @info.diffs.each do |diff|
118
+ formatted_diff << "#{format_diff(diff)}\n"
119
+ end
120
+ formatted_diff << " </div>\n"
121
+ formatted_diff
122
+ end
123
+
124
+ def format_diff(diff)
125
+ header_column = format_header_column(diff)
126
+ from_line_column, to_line_column, content_column =
127
+ format_body_columns(diff)
128
+
129
+ table_diff do
130
+ head = tag("thead") do
131
+ tr_diff_header do
132
+ tag("td", {"colspan" => "3"}) do
133
+ pre_column(header_column)
134
+ end
135
+ end
136
+ end
137
+
138
+ body = tag("tbody") do
139
+ tag("tr") do
140
+ [
141
+ th_diff_line_number {pre_column(from_line_column)},
142
+ th_diff_line_number {pre_column(to_line_column)},
143
+ td_diff_content {pre_column(content_column)},
144
+ ]
145
+ end
146
+ end
147
+
148
+ [head, body]
149
+ end
150
+ end
151
+
152
+ def format_header_column(diff)
153
+ header_column = ""
154
+ diff.format_header.each_line do |line|
155
+ line = line.chomp
156
+ case line
157
+ when /^=/
158
+ header_column << span_diff_header_mark(h(line))
159
+ else
160
+ header_column << span_diff_header(h(line))
161
+ end
162
+ header_column << "\n"
163
+ end
164
+ header_column
165
+ end
166
+
167
+ def format_body_columns(diff)
168
+ from_line_column = ""
169
+ to_line_column = ""
170
+ content_column = ""
171
+ file_path = diff.file_path
172
+ diff.changes.each do |type, line_number, line|
173
+ case type
174
+ when :hunk_header
175
+ from_line_number, to_line_number = line_number
176
+ from_line_column << span_line_number_hunk_header(file_path, :from,
177
+ from_line_number)
178
+ to_line_column << span_line_number_hunk_header(file_path, :to,
179
+ to_line_number)
180
+ case line
181
+ when /\A(@@[\s0-9\-+,]+@@\s*)(.+)(\s*)\z/
182
+ hunk_info = $1
183
+ context = $2
184
+ formatted_line = h(hunk_info) + span_diff_context(h(context))
185
+ else
186
+ formatted_line = h(line)
187
+ end
188
+ content_column << span_diff_hunk_header(formatted_line)
189
+ when :added
190
+ from_line_column << span_line_number_nothing
191
+ to_line_column << span_line_number_added(file_path, line_number)
192
+ content_column << span_diff_added(h(line))
193
+ when :deleted
194
+ from_line_column << span_line_number_deleted(file_path, line_number)
195
+ to_line_column << span_line_number_nothing
196
+ content_column << span_diff_deleted(h(line))
197
+ when :not_changed
198
+ from_line_number, to_line_number = line_number
199
+ from_line_column << span_line_number_not_changed(file_path, :from,
200
+ from_line_number)
201
+ to_line_column << span_line_number_not_changed(file_path, :to,
202
+ to_line_number)
203
+ content_column << span_diff_not_changed(h(line))
204
+ end
205
+ from_line_column << "\n"
206
+ to_line_column << "\n"
207
+ content_column << "\n"
208
+ end
209
+ [from_line_column, to_line_column, content_column]
210
+ end
211
+
212
+ def tag_start(name, attributes)
213
+ start_tag = "<#{name}"
214
+ unless attributes.empty?
215
+ sorted_attributes = attributes.sort_by do |key, value|
216
+ key
217
+ end
218
+ formatted_attributes = sorted_attributes.collect do |key, value|
219
+ if value.is_a?(Hash)
220
+ sorted_value = value.sort_by do |value_key, value_value|
221
+ value_key
222
+ end
223
+ value = sorted_value.collect do |value_key, value_value|
224
+ "#{value_key}: #{value_value}"
225
+ end
226
+ end
227
+ if value.is_a?(Array)
228
+ value = value.sort.join("; ")
229
+ end
230
+ "#{h(key)}=\"#{h(value)}\""
231
+ end
232
+ formatted_attributes = formatted_attributes.join(" ")
233
+ start_tag << " #{formatted_attributes}"
234
+ end
235
+ start_tag << ">"
236
+ start_tag
237
+ end
238
+
239
+ def tag(name, attributes={}, content=nil, &block)
240
+ block_used = false
241
+ if content.nil? and block_given?
242
+ @indent_level += 1
243
+ if block.arity == 1
244
+ content = []
245
+ yield(content)
246
+ else
247
+ content = yield
248
+ end
249
+ @indent_level -= 1
250
+ block_used = true
251
+ end
252
+ content ||= ""
253
+ if content.is_a?(Array)
254
+ if block_used
255
+ separator = "\n"
256
+ else
257
+ separator = ""
258
+ end
259
+ content = content.join(separator)
260
+ end
261
+
262
+ formatted_tag = ""
263
+ formatted_tag << " " * @indent_level if block_used
264
+ formatted_tag << tag_start(name, attributes)
265
+ formatted_tag << "\n" if block_used
266
+ formatted_tag << content
267
+ formatted_tag << "\n" + (" " * @indent_level) if block_used
268
+ formatted_tag << "</#{name}>"
269
+ formatted_tag
270
+ end
271
+
272
+ def dl_start
273
+ tag_start("dl",
274
+ "style" => {
275
+ "margin-left" => "2em",
276
+ "line-height" => "1.5",
277
+ })
278
+ end
279
+
280
+ def dt_margin
281
+ 8
282
+ end
283
+
284
+ def dt(content)
285
+ tag("dt",
286
+ {
287
+ "style" => {
288
+ "clear" => "both",
289
+ "float" => "left",
290
+ "width" => "#{dt_margin}em",
291
+ "font-weight" => "bold",
292
+ },
293
+ },
294
+ content)
295
+ end
296
+
297
+ def dd_start
298
+ tag_start("dd",
299
+ "style" => {
300
+ "margin-left" => "#{dt_margin + 0.5}em",
301
+ })
302
+ end
303
+
304
+ def dd(content)
305
+ "#{dd_start}#{content}</dd>"
306
+ end
307
+
308
+ def border_styles
309
+ {
310
+ "border" => "1px solid #aaa",
311
+ }
312
+ end
313
+
314
+ def pre(content, styles={})
315
+ font_families = [
316
+ "Consolas", "Menlo", "\"Liberation Mono\"",
317
+ "Courier", "monospace"
318
+ ]
319
+ pre_styles = {
320
+ "font-family" => font_families.join(", "),
321
+ "line-height" => "1.2",
322
+ "padding" => "0.5em",
323
+ "width" => "auto",
324
+ }
325
+ pre_styles = pre_styles.merge(border_styles)
326
+ tag("pre", {"style" => pre_styles.merge(styles)}, content)
327
+ end
328
+
329
+ def div_diff_section_start
330
+ tag_start("div",
331
+ "class" => "diff-section",
332
+ "style" => {
333
+ "clear" => "both",
334
+ })
335
+ end
336
+
337
+ def div_diff_start
338
+ tag_start("div",
339
+ "class" => "diff",
340
+ "style" => {
341
+ "margin-left" => "1em",
342
+ "margin-right" => "1em",
343
+ })
344
+ end
345
+
346
+ def table_diff(&block)
347
+ styles = {
348
+ "border-collapse" => "collapse",
349
+ }
350
+ tag("table",
351
+ {
352
+ "style" => border_styles.merge(styles),
353
+ },
354
+ &block)
355
+ end
356
+
357
+ def tr_diff_header(&block)
358
+ tag("tr",
359
+ {
360
+ "class" => "diff-header",
361
+ "style" => border_styles,
362
+ },
363
+ &block)
364
+ end
365
+
366
+ def th_diff_line_number(&block)
367
+ tag("th",
368
+ {
369
+ "class" => "diff-line-number",
370
+ "style" => border_styles,
371
+ },
372
+ &block)
373
+ end
374
+
375
+ def td_diff_content(&block)
376
+ tag("td",
377
+ {
378
+ "class" => "diff-content",
379
+ "style" => border_styles,
380
+ },
381
+ &block)
382
+ end
383
+
384
+ def pre_column(column)
385
+ pre(column,
386
+ "white-space" => "normal",
387
+ "margin" => "0",
388
+ "border" => "0")
389
+ end
390
+
391
+ def span_common_styles
392
+ {
393
+ "white-space" => "pre",
394
+ "display" => "block",
395
+ }
396
+ end
397
+
398
+ def span_context_styles
399
+ {
400
+ "background-color" => "#ffffaa",
401
+ "color" => "#000000",
402
+ }
403
+ end
404
+
405
+ def span_deleted_styles
406
+ {
407
+ "background-color" => "#ffaaaa",
408
+ "color" => "#000000",
409
+ }
410
+ end
411
+
412
+ def span_added_styles
413
+ {
414
+ "background-color" => "#aaffaa",
415
+ "color" => "#000000",
416
+ }
417
+ end
418
+
419
+ def span_line_number_styles
420
+ span_common_styles
421
+ end
422
+
423
+ def span_line_number_nothing
424
+ tag("span",
425
+ {
426
+ "class" => "diff-line-number-nothing",
427
+ "style" => span_line_number_styles,
428
+ },
429
+ "&nbsp;")
430
+ end
431
+
432
+ def span_line_number_hunk_header(file_path, direction, offset)
433
+ content = "..."
434
+ url = commit_file_line_number_url(file_path, direction, offset - 1)
435
+ if url
436
+ content = tag("a", {"href" => url}, content)
437
+ end
438
+ tag("span",
439
+ {
440
+ "class" => "diff-line-number-hunk-header",
441
+ "style" => span_line_number_styles,
442
+ },
443
+ content)
444
+ end
445
+
446
+ def span_line_number_deleted(file_path, line_number)
447
+ content = h(line_number.to_s)
448
+ url = commit_file_line_number_url(file_path, :from, line_number)
449
+ if url
450
+ content = tag("a", {"href" => url}, content)
451
+ end
452
+ tag("span",
453
+ {
454
+ "class" => "diff-line-number-deleted",
455
+ "style" => span_line_number_styles.merge(span_deleted_styles),
456
+ },
457
+ content)
458
+ end
459
+
460
+ def span_line_number_added(file_path, line_number)
461
+ content = h(line_number.to_s)
462
+ url = commit_file_line_number_url(file_path, :to, line_number)
463
+ if url
464
+ content = tag("a", {"href" => url}, content)
465
+ end
466
+ tag("span",
467
+ {
468
+ "class" => "diff-line-number-added",
469
+ "style" => span_line_number_styles.merge(span_added_styles),
470
+ },
471
+ content)
472
+ end
473
+
474
+ def span_line_number_not_changed(file_path, direction, line_number)
475
+ content = h(line_number.to_s)
476
+ url = commit_file_line_number_url(file_path, direction, line_number)
477
+ if url
478
+ content = tag("a", {"href" => url}, content)
479
+ end
480
+ tag("span",
481
+ {
482
+ "class" => "diff-line-number-not-changed",
483
+ "style" => span_line_number_styles,
484
+ },
485
+ content)
486
+ end
487
+
488
+ def span_diff_styles
489
+ span_common_styles
490
+ end
491
+
492
+ def span_diff_metadata_styles
493
+ styles = {
494
+ "background-color" => "#eaf2f5",
495
+ "color" => "#999999",
496
+ }
497
+ span_diff_styles.merge(styles)
498
+ end
499
+
500
+ def span_diff_header(content)
501
+ tag("span",
502
+ {
503
+ "class" => "diff-header",
504
+ "style" => span_diff_metadata_styles,
505
+ },
506
+ content)
507
+ end
508
+
509
+ def span_diff_header_mark(content)
510
+ tag("span",
511
+ {
512
+ "class" => "diff-header-mark",
513
+ "style" => span_diff_metadata_styles,
514
+ },
515
+ content)
516
+ end
517
+
518
+ def span_diff_hunk_header(content)
519
+ tag("span",
520
+ {
521
+ "class" => "diff-hunk-header",
522
+ "style" => span_diff_metadata_styles,
523
+ },
524
+ content)
525
+ end
526
+
527
+ def span_diff_context(content)
528
+ tag("span",
529
+ {
530
+ "class" => "diff-context",
531
+ "style" => span_context_styles,
532
+ },
533
+ content)
534
+ end
535
+
536
+ def span_diff_deleted(content)
537
+ tag("span",
538
+ {
539
+ "class" => "diff-deleted",
540
+ "style" => span_diff_styles.merge(span_deleted_styles),
541
+ },
542
+ content)
543
+ end
544
+
545
+ def span_diff_added(content)
546
+ tag("span",
547
+ {
548
+ "class" => "diff-added",
549
+ "style" => span_diff_styles.merge(span_added_styles),
550
+ },
551
+ content)
552
+ end
553
+
554
+ def span_diff_not_changed(content)
555
+ tag("span",
556
+ {
557
+ "class" => "diff-not-changed",
558
+ "style" => span_diff_styles,
559
+ },
560
+ content)
561
+ end
562
+ end
563
+ end