summaryrefslogtreecommitdiffstats
path: root/src/main/distrib/data/groovy/sendmail-html.groovy
blob: 2692556818f8aad77644d4c21460aa92594749b1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
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
270
271
272
273
274
275
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
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
/*
 * Copyright 2012 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import com.gitblit.GitBlit
import com.gitblit.Keys
import com.gitblit.models.RepositoryModel
import com.gitblit.models.TeamModel
import com.gitblit.models.UserModel
import com.gitblit.utils.JGitUtils
import java.text.SimpleDateFormat

import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.IndexDiff;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.lib.Config
import org.eclipse.jgit.patch.FileHeader;
import org.eclipse.jgit.revwalk.RevCommit
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand
import org.eclipse.jgit.transport.ReceiveCommand.Result
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.util.io.DisabledOutputStream;
import org.slf4j.Logger
import groovy.xml.MarkupBuilder

import java.io.IOException;
import java.security.MessageDigest


/**
 * Sample Gitblit Post-Receive Hook: sendmail-html
 *
 * The Post-Receive hook is executed AFTER the pushed commits have been applied
 * to the Git repository.  This is the appropriate point to trigger an
 * integration build or to send a notification.
 * 
 * This script is only executed when pushing to *Gitblit*, not to other Git
 * tooling you may be using.
 * 
 * If this script is specified in *groovy.postReceiveScripts* of gitblit.properties
 * or web.xml then it will be executed by any repository when it receives a
 * push.  If you choose to share your script then you may have to consider
 * tailoring control-flow based on repository access restrictions.
 *
 * Scripts may also be specified per-repository in the repository settings page.
 * Shared scripts will be excluded from this list of available scripts.
 * 
 * This script is dynamically reloaded and it is executed within it's own
 * exception handler so it will not crash another script nor crash Gitblit.
 *
 * If you want this hook script to fail and abort all subsequent scripts in the
 * chain, "return false" at the appropriate failure points.
 * 
 * Bound Variables:
 *  gitblit         Gitblit Server               com.gitblit.GitBlit
 *  repository      Gitblit Repository           com.gitblit.models.RepositoryModel
 *  user            Gitblit User                 com.gitblit.models.UserModel
 *  commands        JGit commands                Collection<org.eclipse.jgit.transport.ReceiveCommand>
 *  url             Base url for Gitblit         java.lang.String
 *  logger          Logs messages to Gitblit     org.slf4j.Logger
 *  clientLogger    Logs messages to Git client  com.gitblit.utils.ClientLogger
 *
 * Accessing Gitblit Custom Fields:
 *   def myCustomField = repository.customFields.myCustomField
 *  
 */

com.gitblit.models.UserModel userModel = user

// Indicate we have started the script
logger.info("sendmail-html hook triggered by ${user.username} for ${repository.name}")

/*
 * Primitive email notification.
 * This requires the mail settings to be properly configured in Gitblit.
 */

Repository r = gitblit.getRepository(repository.name)

// reuse existing repository config settings, if available
Config config = r.getConfig()
def mailinglist = config.getString('hooks', null, 'mailinglist')
def emailprefix = config.getString('hooks', null, 'emailprefix')

// set default values
def toAddresses = []
if (emailprefix == null) {
    emailprefix = '[Gitblit]'
}

if (mailinglist != null) {
    def addrs = mailinglist.split(/(,|\s)/)
    toAddresses.addAll(addrs)
}

// add all mailing lists defined in gitblit.properties or web.xml
toAddresses.addAll(gitblit.getStrings(Keys.mail.mailingLists))

// add all team mailing lists
def teams = gitblit.getRepositoryTeams(repository)
for (team in teams) {
    TeamModel model = gitblit.getTeamModel(team)
    if (model.mailingLists) {
        toAddresses.addAll(model.mailingLists)
    }
}

// add all mailing lists for the repository
toAddresses.addAll(repository.mailingLists)

// define the summary and commit urls
def repo = repository.name
def summaryUrl = url + "/summary?r=$repo"
def baseCommitUrl = url + "/commit?r=$repo&h="
def baseBlobDiffUrl = url + "/blobdiff/?r=$repo&h="
def baseCommitDiffUrl = url + "/commitdiff/?r=$repo&h="
def forwardSlashChar = gitblit.getString(Keys.web.forwardSlashCharacter, '/')

if (gitblit.getBoolean(Keys.web.mountParameters, true)) {
    repo = repo.replace('/', forwardSlashChar).replace('/', '%2F')
    summaryUrl = url + "/summary/$repo"
    baseCommitUrl = url + "/commit/$repo/"
    baseBlobDiffUrl = url + "/blobdiff/$repo/"
    baseCommitDiffUrl = url + "/commitdiff/$repo/"
}

class HtmlMailWriter {
    Repository repository
    def url
    def baseCommitUrl
    def baseCommitDiffUrl
    def baseBlobDiffUrl
    def mountParameters
	def forwardSlashChar
	def includeGravatar
	def shortCommitIdLength
    def commitCount = 0
    def commands
    def writer = new StringWriter();
    def builder = new MarkupBuilder(writer)

    def writeStyle() {
        builder.style(type:"text/css", '''
    .table td {
        vertical-align: middle;
    }
    tr.noborder td {
        border: none;
        padding-top: 0px;
    }
    .gravatar-column {
        width: 5%; 
    }
    .author-column {
        width: 20%; 
    }
    .commit-column {
        width: 5%; 
    }
    .status-column {
        width: 10%;
    }
    .table-disable-hover.table tbody tr:hover td,
    .table-disable-hover.table tbody tr:hover th {
        background-color: inherit;
    }
    .table-disable-hover.table-striped tbody tr:nth-child(odd):hover td,
    .table-disable-hover.table-striped tbody tr:nth-child(odd):hover th {
      background-color: #f9f9f9;
    }
    ''')
    }

    def writeBranchTitle(type, name, action, number) {
        builder.div('class' : 'pageTitle') {
			builder.span('class':'project') {
				mkp.yield "$type "
				span('class': 'repository', name )
				if (number > 0) {
					mkp.yield " $action ($number commits)"
				} else {
					mkp.yield " $action"
				}
			}
        }
    }

    def writeBranchDeletedTitle(type, name) {
		builder.div('class' : 'pageTitle', 'style':'color:red') {
			builder.span('class':'project') {
				mkp.yield "$type "
				span('class': 'repository', name )
				mkp.yield " deleted"
			}
		}
    }

    def commitUrl(RevCommit commit) {
        "${baseCommitUrl}$commit.id.name"
    }

    def commitDiffUrl(RevCommit commit) {
        "${baseCommitDiffUrl}$commit.id.name"
    }

    def encoded(String path) {
        path.replace('/', forwardSlashChar).replace('/', '%2F')
    }

    def blobDiffUrl(objectId, path) {
        if (mountParameters) {
            // REST style
            "${baseBlobDiffUrl}${objectId.name()}/${encoded(path)}"
        } else {
            "${baseBlobDiffUrl}${objectId.name()}&f=${path}"
        }

    }

    def writeCommitTable(commits, includeChangedPaths=true) {
        // Write commits table
        builder.table('class':"table table-disable-hover") {
            thead {
                tr {
					th(colspan: includeGravatar ? 2 : 1, "Author")
                    th( "Commit" )
                    th( "Message" )
                }
            }
            tbody() {

                // Write all the commits
                for (commit in commits) {
                    writeCommit(commit)

					if (includeChangedPaths) {
						// Write detail on that particular commit
						tr('class' : 'noborder') {
							td (colspan: includeGravatar ? 3 : 2)
							td (colspan:2) { writeStatusTable(commit) }
						}
					}
                }
            }
        }
    }

    def writeCommit(commit) {
        def abbreviated = repository.newObjectReader().abbreviate(commit.id, shortCommitIdLength).name()
        def author = commit.authorIdent.name
        def email = commit.authorIdent.emailAddress
        def message = commit.shortMessage
        builder.tr {
			if (includeGravatar) {
				td('class':"gravatar-column") {
					img(src:gravatarUrl(email), 'class':"gravatar")
				}
			}
            td('class':"author-column", author)
            td('class':"commit-column") {
                a(href:commitUrl(commit)) {
                    span('class':"label label-info",  abbreviated )
                }
            }
            td {
                mkp.yield message
                a('class':'link', href:commitDiffUrl(commit), " [commitdiff]" )
            }
        }
    }

    def writeStatusLabel(style, tooltip) {
        builder.span('class' : style,  'title' : tooltip )
    }

    def writeAddStatusLine(ObjectId id, FileHeader header) {		
        builder.td('class':'changeType') {
            writeStatusLabel("addition", "addition")
        }
        builder.td {
            a(href:blobDiffUrl(id, header.newPath), header.newPath)
        }
    }

    def writeCopyStatusLine(ObjectId id, FileHeader header) {
        builder.td('class':'changeType') {
            writeStatusLabel("rename", "rename")
        }
        builder.td() {
            a(href:blobDiffUrl(id, header.newPath), header.oldPath + " copied to " + header.newPath)
        }
    }

    def writeDeleteStatusLine(ObjectId id, FileHeader header) {
        builder.td('class':'changeType') {
            writeStatusLabel("deletion", "deletion")
        }
        builder.td() {
            a(href:blobDiffUrl(id, header.oldPath), header.oldPath)
        }
    }

    def writeModifyStatusLine(ObjectId id, FileHeader header) {
        builder.td('class':'changeType') {
			writeStatusLabel("modification", "modification")
        }
        builder.td() {
            a(href:blobDiffUrl(id, header.oldPath), header.oldPath)
        }
    }

    def writeRenameStatusLine(ObjectId id, FileHeader header) {
        builder.td('class':'changeType') {
             writeStatusLabel("rename", "rename")
        }
        builder.td() {
            mkp.yield header.oldPath
			mkp.yieldUnescaped "<b> -&rt; </b>"
			a(href:blobDiffUrl(id, header.newPath),  header.newPath)
        }
    }

    def writeStatusLine(ObjectId id, FileHeader header) {
        builder.tr {
            switch (header.changeType) {
                case ChangeType.ADD:
                    writeAddStatusLine(id, header)
                    break;
                case ChangeType.COPY:
                    writeCopyStatusLine(id, header)
                    break;
                case ChangeType.DELETE:
                    writeDeleteStatusLine(id, header)
                    break;
                case ChangeType.MODIFY:
                    writeModifyStatusLine(id, header)
                    break;
                case ChangeType.RENAME:
                    writeRenameStatusLine(id, header)
                    break;
            }
        }
    }

    def writeStatusTable(RevCommit commit) {
        DiffFormatter formatter = new DiffFormatter(DisabledOutputStream.INSTANCE)
        formatter.setRepository(repository)
        formatter.setDetectRenames(true)
        formatter.setDiffComparator(RawTextComparator.DEFAULT);

        def diffs
		RevWalk rw = new RevWalk(repository)
        if (commit.parentCount > 0) {
			RevCommit parent = rw.parseCommit(commit.parents[0].id)
            diffs = formatter.scan(parent.tree, commit.tree)
        } else {
            diffs = formatter.scan(new EmptyTreeIterator(),
                                   new CanonicalTreeParser(null, rw.objectReader, commit.tree))
        }
		rw.dispose()
        // Write status table
        builder.table('class':"plain") {
            tbody() {
                for (DiffEntry entry in diffs) {
                    FileHeader header = formatter.toFileHeader(entry)
                    writeStatusLine(commit.id, header)
                }
            }
        }
    }


    def md5(text) {

        def digest = MessageDigest.getInstance("MD5")

        //Quick MD5 of text
        def hash = new BigInteger(1, digest.digest(text.getBytes()))
                         .toString(16)
                         .padLeft(32, "0")
        hash.toString()
    }

    def gravatarUrl(email) {
        def cleaned = email.trim().toLowerCase()
        "http://www.gravatar.com/avatar/${md5(cleaned)}?s=30"
    }

    def writeNavbar() {
        builder.div('class':"navbar navbar-fixed-top") {
            div('class':"navbar-inner") {
                div('class':"container") {
                    a('class':"brand", href:"${url}", title:"GitBlit") {
                        img(src:"${url}/gitblt_25_white.png",
                            width:"79",
                            height:"25",
                            'class':"logo")
                    }
                }
            }
        }
    }

    def write() {
        builder.html {
            head {
                link(rel:"stylesheet", href:"${url}/bootstrap/css/bootstrap.css")
                link(rel:"stylesheet", href:"${url}/gitblit.css")
				link(rel:"stylesheet", href:"${url}/bootstrap/css/bootstrap-responsive.css")
                writeStyle()
            }
            body {

                writeNavbar()

				div('class':"container") {

                for (command in commands) {
                    def ref = command.refName
                    def refType = 'Branch'
                    if (ref.startsWith('refs/heads/')) {
                        ref  = command.refName.substring('refs/heads/'.length())
                    } else if (ref.startsWith('refs/tags/')) {
                        ref  = command.refName.substring('refs/tags/'.length())
                        refType = 'Tag'
                    }

                    switch (command.type) {
                        case ReceiveCommand.Type.CREATE:
							def commits = JGitUtils.getRevLog(repository, command.oldId.name, command.newId.name).reverse()
							commitCount += commits.size()
							if (refType == 'Branch') {
								// new branch
								writeBranchTitle(refType, ref, "created", commits.size())
								writeCommitTable(commits, true)
							} else {
								// new tag
								writeBranchTitle(refType, ref, "created", 0)
								writeCommitTable(commits, false)
							}
                            break
                        case ReceiveCommand.Type.UPDATE:
                            def commits = JGitUtils.getRevLog(repository, command.oldId.name, command.newId.name).reverse()
                            commitCount += commits.size()
                            // fast-forward branch commits table
                            // Write header
                            writeBranchTitle(refType, ref, "updated", commits.size())
                            writeCommitTable(commits)
                            break
                        case ReceiveCommand.Type.UPDATE_NONFASTFORWARD:
                            def commits = JGitUtils.getRevLog(repository, command.oldId.name, command.newId.name).reverse()
                            commitCount += commits.size()
                            // non-fast-forward branch commits table
                            // Write header
                            writeBranchTitle(refType, ref, "updated [NON fast-forward]", commits.size())
                            writeCommitTable(commits)
                            break
                        case ReceiveCommand.Type.DELETE:
                            // deleted branch/tag
                            writeBranchDeletedTitle(refType, ref)
                            break
                        default:
                            break
                    }
                }
                }
            }
        }
        writer.toString()
    }

}

def mailWriter = new HtmlMailWriter()
mailWriter.repository = r
mailWriter.baseCommitUrl = baseCommitUrl
mailWriter.baseBlobDiffUrl = baseBlobDiffUrl
mailWriter.baseCommitDiffUrl = baseCommitDiffUrl
mailWriter.forwardSlashChar = forwardSlashChar
mailWriter.commands = commands
mailWriter.url = url
mailWriter.mountParameters = gitblit.getBoolean(Keys.web.mountParameters, true)
mailWriter.includeGravatar = gitblit.getBoolean(Keys.web.allowGravatar, true)
mailWriter.shortCommitIdLength = gitblit.getInteger(Keys.web.shortCommitIdLength, 8)

def content = mailWriter.write()

// close the repository reference
r.close()

// tell Gitblit to send the message (Gitblit filters duplicate addresses)
def repositoryName = repository.name.substring(0, repository.name.length() - 4)
gitblit.sendHtmlMail("${emailprefix} ${userModel.displayName} pushed ${mailWriter.commitCount} commits => $repositoryName",
                     content,
                     toAddresses)