James Moger
2012-11-30 d7f4a1baf51f3cb869518d133a882c99dddf021b
commit | author | age
d60a42 1 /*
GS 2  * Copyright 2012 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 import com.gitblit.GitBlit
17 import com.gitblit.Keys
18 import com.gitblit.models.RepositoryModel
19 import com.gitblit.models.TeamModel
20 import com.gitblit.models.UserModel
21 import com.gitblit.utils.JGitUtils
22 import java.text.SimpleDateFormat
23
24 import org.eclipse.jgit.api.Status;
25 import org.eclipse.jgit.api.errors.JGitInternalException;
26 import org.eclipse.jgit.diff.DiffEntry;
27 import org.eclipse.jgit.diff.DiffFormatter;
28 import org.eclipse.jgit.diff.RawTextComparator;
29 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
30 import org.eclipse.jgit.lib.Constants;
31 import org.eclipse.jgit.lib.IndexDiff;
32 import org.eclipse.jgit.lib.ObjectId;
33 import org.eclipse.jgit.lib.Repository
34 import org.eclipse.jgit.lib.Config
35 import org.eclipse.jgit.patch.FileHeader;
36 import org.eclipse.jgit.revwalk.RevCommit
37 import org.eclipse.jgit.revwalk.RevWalk;
38 import org.eclipse.jgit.transport.ReceiveCommand
39 import org.eclipse.jgit.transport.ReceiveCommand.Result
40 import org.eclipse.jgit.treewalk.FileTreeIterator;
41 import org.eclipse.jgit.treewalk.EmptyTreeIterator;
42 import org.eclipse.jgit.treewalk.CanonicalTreeParser;
43 import org.eclipse.jgit.util.io.DisabledOutputStream;
44 import org.slf4j.Logger
45 import groovy.xml.MarkupBuilder
46
47 import java.io.IOException;
48 import java.security.MessageDigest
49
50
51 /**
52  * Sample Gitblit Post-Receive Hook: sendmail-html
53  *
54  * The Post-Receive hook is executed AFTER the pushed commits have been applied
55  * to the Git repository.  This is the appropriate point to trigger an
56  * integration build or to send a notification.
57  * 
58  * This script is only executed when pushing to *Gitblit*, not to other Git
59  * tooling you may be using.
60  * 
61  * If this script is specified in *groovy.postReceiveScripts* of gitblit.properties
62  * or web.xml then it will be executed by any repository when it receives a
63  * push.  If you choose to share your script then you may have to consider
64  * tailoring control-flow based on repository access restrictions.
65  *
66  * Scripts may also be specified per-repository in the repository settings page.
67  * Shared scripts will be excluded from this list of available scripts.
68  * 
69  * This script is dynamically reloaded and it is executed within it's own
70  * exception handler so it will not crash another script nor crash Gitblit.
71  *
72  * If you want this hook script to fail and abort all subsequent scripts in the
73  * chain, "return false" at the appropriate failure points.
74  * 
75  * Bound Variables:
76  *  gitblit         Gitblit Server               com.gitblit.GitBlit
77  *  repository      Gitblit Repository           com.gitblit.models.RepositoryModel
78  *  user            Gitblit User                 com.gitblit.models.UserModel
79  *  commands        JGit commands                Collection<org.eclipse.jgit.transport.ReceiveCommand>
80  *  url             Base url for Gitblit         java.lang.String
81  *  logger          Logs messages to Gitblit     org.slf4j.Logger
82  *  clientLogger    Logs messages to Git client  com.gitblit.utils.ClientLogger
83  *
84  * Accessing Gitblit Custom Fields:
85  *   def myCustomField = repository.customFields.myCustomField
86  *  
87  */
88
89 com.gitblit.models.UserModel userModel = user
90
91 // Indicate we have started the script
efa4df 92 logger.info("sendmail-html hook triggered by ${user.username} for ${repository.name}")
d60a42 93
GS 94 /*
95  * Primitive email notification.
96  * This requires the mail settings to be properly configured in Gitblit.
97  */
98
99 Repository r = gitblit.getRepository(repository.name)
100
101 // reuse existing repository config settings, if available
102 Config config = r.getConfig()
103 def mailinglist = config.getString('hooks', null, 'mailinglist')
104 def emailprefix = config.getString('hooks', null, 'emailprefix')
105
106 // set default values
107 def toAddresses = []
108 if (emailprefix == null) {
109     emailprefix = '[Gitblit]'
110 }
111
112 if (mailinglist != null) {
113     def addrs = mailinglist.split(/(,|\s)/)
114     toAddresses.addAll(addrs)
115 }
116
117 // add all mailing lists defined in gitblit.properties or web.xml
4789d1 118 toAddresses.addAll(GitBlit.getStrings(Keys.mail.mailingLists))
d60a42 119
GS 120 // add all team mailing lists
121 def teams = gitblit.getRepositoryTeams(repository)
122 for (team in teams) {
123     TeamModel model = gitblit.getTeamModel(team)
124     if (model.mailingLists) {
125         toAddresses.addAll(model.mailingLists)
126     }
127 }
128
129 // add all mailing lists for the repository
130 toAddresses.addAll(repository.mailingLists)
131
132 // define the summary and commit urls
133 def repo = repository.name
134 def summaryUrl = url + "/summary?r=$repo"
135 def baseCommitUrl = url + "/commit?r=$repo&h="
136 def baseBlobDiffUrl = url + "/blobdiff/?r=$repo&h="
137 def baseCommitDiffUrl = url + "/commitdiff/?r=$repo&h="
814f70 138 def forwardSlashChar = gitblit.getString(Keys.web.forwardSlashCharacter, '/')
d60a42 139
GS 140 if (gitblit.getBoolean(Keys.web.mountParameters, true)) {
814f70 141     repo = repo.replace('/', forwardSlashChar).replace('/', '%2F')
d60a42 142     summaryUrl = url + "/summary/$repo"
GS 143     baseCommitUrl = url + "/commit/$repo/"
144     baseBlobDiffUrl = url + "/blobdiff/$repo/"
145     baseCommitDiffUrl = url + "/commitdiff/$repo/"
146 }
147
148 class HtmlMailWriter {
149     Repository repository
150     def url
151     def baseCommitUrl
152     def baseCommitDiffUrl
153     def baseBlobDiffUrl
154     def mountParameters
814f70 155     def forwardSlashChar
JM 156     def includeGravatar
798581 157     def shortCommitIdLength
d60a42 158     def commitCount = 0
GS 159     def commands
160     def writer = new StringWriter();
161     def builder = new MarkupBuilder(writer)
162
163     def writeStyle() {
164         builder.style(type:"text/css", '''
ddec28 165     .table td {
GS 166         vertical-align: middle;
d60a42 167     }
4789d1 168     tr.noborder td {
JM 169         border: none;
170         padding-top: 0px;
171     }
d60a42 172     .gravatar-column {
GS 173         width: 5%; 
174     }
175     .author-column {
ddec28 176         width: 20%; 
d60a42 177     }
GS 178     .commit-column {
179         width: 5%; 
180     }
181     .status-column {
182         width: 10%;
183     }
4789d1 184     .table-disable-hover.table tbody tr:hover td,
JM 185     .table-disable-hover.table tbody tr:hover th {
186         background-color: inherit;
187     }
188     .table-disable-hover.table-striped tbody tr:nth-child(odd):hover td,
189     .table-disable-hover.table-striped tbody tr:nth-child(odd):hover th {
190       background-color: #f9f9f9;
191     }
d60a42 192     ''')
GS 193     }
194
195     def writeBranchTitle(type, name, action, number) {
814f70 196         builder.div('class' : 'pageTitle') {
JM 197             builder.span('class':'project') {
198                 mkp.yield "$type "
199                 span('class': 'repository', name )
b0d303 200                 if (number > 0) {
JM 201                     mkp.yield " $action ($number commits)"
202                 } else {
203                     mkp.yield " $action"
204                 }
814f70 205             }
d60a42 206         }
GS 207     }
208
209     def writeBranchDeletedTitle(type, name) {
814f70 210         builder.div('class' : 'pageTitle', 'style':'color:red') {
JM 211             builder.span('class':'project') {
212                 mkp.yield "$type "
213                 span('class': 'repository', name )
214                 mkp.yield " deleted"
215             }
216         }
d60a42 217     }
GS 218
219     def commitUrl(RevCommit commit) {
220         "${baseCommitUrl}$commit.id.name"
221     }
222
223     def commitDiffUrl(RevCommit commit) {
224         "${baseCommitDiffUrl}$commit.id.name"
225     }
226
227     def encoded(String path) {
814f70 228         path.replace('/', forwardSlashChar).replace('/', '%2F')
d60a42 229     }
GS 230
231     def blobDiffUrl(objectId, path) {
232         if (mountParameters) {
233             // REST style
234             "${baseBlobDiffUrl}${objectId.name()}/${encoded(path)}"
235         } else {
236             "${baseBlobDiffUrl}${objectId.name()}&f=${path}"
237         }
238
239     }
240
b0d303 241     def writeCommitTable(commits, includeChangedPaths=true) {
d60a42 242         // Write commits table
ddec28 243         builder.table('class':"table table-disable-hover") {
d60a42 244             thead {
GS 245                 tr {
814f70 246                     th(colspan: includeGravatar ? 2 : 1, "Author")
d60a42 247                     th( "Commit" )
GS 248                     th( "Message" )
249                 }
250             }
251             tbody() {
252
253                 // Write all the commits
254                 for (commit in commits) {
255                     writeCommit(commit)
256
b0d303 257                     if (includeChangedPaths) {
JM 258                         // Write detail on that particular commit
259                         tr('class' : 'noborder') {
260                             td (colspan: includeGravatar ? 3 : 2)
261                             td (colspan:2) { writeStatusTable(commit) }
262                         }
263                     }
d60a42 264                 }
GS 265             }
266         }
267     }
268
269     def writeCommit(commit) {
798581 270         def abbreviated = repository.newObjectReader().abbreviate(commit.id, shortCommitIdLength).name()
d60a42 271         def author = commit.authorIdent.name
GS 272         def email = commit.authorIdent.emailAddress
273         def message = commit.shortMessage
274         builder.tr {
814f70 275             if (includeGravatar) {
JM 276                 td('class':"gravatar-column") {
277                     img(src:gravatarUrl(email), 'class':"gravatar")
278                 }
279             }
ddec28 280             td('class':"author-column", author)
d60a42 281             td('class':"commit-column") {
GS 282                 a(href:commitUrl(commit)) {
ddec28 283                     span('class':"label label-info",  abbreviated )
d60a42 284                 }
GS 285             }
286             td {
287                 mkp.yield message
814f70 288                 a('class':'link', href:commitDiffUrl(commit), " [commitdiff]" )
d60a42 289             }
GS 290         }
291     }
292
814f70 293     def writeStatusLabel(style, tooltip) {
JM 294         builder.span('class' : style,  'title' : tooltip )
d60a42 295     }
GS 296
814f70 297     def writeAddStatusLine(ObjectId id, FileHeader header) {        
JM 298         builder.td('class':'changeType') {
299             writeStatusLabel("addition", "addition")
d60a42 300         }
GS 301         builder.td {
4789d1 302             a(href:blobDiffUrl(id, header.newPath), header.newPath)
d60a42 303         }
GS 304     }
305
306     def writeCopyStatusLine(ObjectId id, FileHeader header) {
814f70 307         builder.td('class':'changeType') {
JM 308             writeStatusLabel("rename", "rename")
d60a42 309         }
GS 310         builder.td() {
4789d1 311             a(href:blobDiffUrl(id, header.newPath), header.oldPath + " copied to " + header.newPath)
d60a42 312         }
GS 313     }
314
315     def writeDeleteStatusLine(ObjectId id, FileHeader header) {
814f70 316         builder.td('class':'changeType') {
JM 317             writeStatusLabel("deletion", "deletion")
d60a42 318         }
GS 319         builder.td() {
4789d1 320             a(href:blobDiffUrl(id, header.oldPath), header.oldPath)
d60a42 321         }
GS 322     }
323
324     def writeModifyStatusLine(ObjectId id, FileHeader header) {
814f70 325         builder.td('class':'changeType') {
JM 326             writeStatusLabel("modification", "modification")
d60a42 327         }
GS 328         builder.td() {
4789d1 329             a(href:blobDiffUrl(id, header.oldPath), header.oldPath)
d60a42 330         }
GS 331     }
332
333     def writeRenameStatusLine(ObjectId id, FileHeader header) {
814f70 334         builder.td('class':'changeType') {
JM 335              writeStatusLabel("rename", "rename")
d60a42 336         }
GS 337         builder.td() {
adf0e0 338             mkp.yield header.oldPath
JM 339             mkp.yieldUnescaped "<b> -&rt; </b>"
340             a(href:blobDiffUrl(id, header.newPath),  header.newPath)
d60a42 341         }
GS 342     }
343
344     def writeStatusLine(ObjectId id, FileHeader header) {
345         builder.tr {
346             switch (header.changeType) {
347                 case ChangeType.ADD:
348                     writeAddStatusLine(id, header)
349                     break;
350                 case ChangeType.COPY:
351                     writeCopyStatusLine(id, header)
352                     break;
353                 case ChangeType.DELETE:
354                     writeDeleteStatusLine(id, header)
355                     break;
356                 case ChangeType.MODIFY:
357                     writeModifyStatusLine(id, header)
358                     break;
359                 case ChangeType.RENAME:
360                     writeRenameStatusLine(id, header)
361                     break;
362             }
363         }
364     }
365
366     def writeStatusTable(RevCommit commit) {
367         DiffFormatter formatter = new DiffFormatter(DisabledOutputStream.INSTANCE)
368         formatter.setRepository(repository)
369         formatter.setDetectRenames(true)
370         formatter.setDiffComparator(RawTextComparator.DEFAULT);
371
372         def diffs
373         RevWalk rw = new RevWalk(repository);
374         if (commit.parentCount > 0) {
375             RevCommit parent = commit.parents[0]
376             diffs = formatter.scan(parent.tree, commit.tree)
377         } else {
378             diffs = formatter.scan(new EmptyTreeIterator(),
379                                    new CanonicalTreeParser(null, rw.objectReader, commit.tree))
380         }
381         // Write status table
814f70 382         builder.table('class':"plain") {
d60a42 383             tbody() {
GS 384                 for (DiffEntry entry in diffs) {
385                     FileHeader header = formatter.toFileHeader(entry)
386                     writeStatusLine(commit.id, header)
387                 }
388             }
389         }
390     }
391
392
393     def md5(text) {
394
395         def digest = MessageDigest.getInstance("MD5")
396
397         //Quick MD5 of text
398         def hash = new BigInteger(1, digest.digest(text.getBytes()))
399                          .toString(16)
400                          .padLeft(32, "0")
401         hash.toString()
402     }
403
404     def gravatarUrl(email) {
405         def cleaned = email.trim().toLowerCase()
406         "http://www.gravatar.com/avatar/${md5(cleaned)}?s=30"
407     }
408
409     def writeNavbar() {
410         builder.div('class':"navbar navbar-fixed-top") {
411             div('class':"navbar-inner") {
412                 div('class':"container") {
413                     a('class':"brand", href:"${url}", title:"GitBlit") {
414                         img(src:"${url}/gitblt_25_white.png",
415                             width:"79",
416                             height:"25",
417                             'class':"logo")
418                     }
419                 }
420             }
421         }
422     }
423
424     def write() {
425         builder.html {
426             head {
427                 link(rel:"stylesheet", href:"${url}/bootstrap/css/bootstrap.css")
428                 link(rel:"stylesheet", href:"${url}/gitblit.css")
4789d1 429                 link(rel:"stylesheet", href:"${url}/bootstrap/css/bootstrap-responsive.css")
d60a42 430                 writeStyle()
GS 431             }
432             body {
433
434                 writeNavbar()
435
ddec28 436                 div('class':"container") {
GS 437
d60a42 438                 for (command in commands) {
GS 439                     def ref = command.refName
440                     def refType = 'Branch'
441                     if (ref.startsWith('refs/heads/')) {
442                         ref  = command.refName.substring('refs/heads/'.length())
443                     } else if (ref.startsWith('refs/tags/')) {
444                         ref  = command.refName.substring('refs/tags/'.length())
445                         refType = 'Tag'
446                     }
447
448                     switch (command.type) {
449                         case ReceiveCommand.Type.CREATE:
b0d303 450                             def commits = JGitUtils.getRevLog(repository, command.oldId.name, command.newId.name).reverse()
JM 451                             commitCount += commits.size()
452                             if (refType == 'Branch') {
453                                 // new branch
454                                 writeBranchTitle(refType, ref, "created", commits.size())
455                                 writeCommitTable(commits, true)
456                             } else {
457                                 // new tag
458                                 writeBranchTitle(refType, ref, "created", 0)
459                                 writeCommitTable(commits, false)
460                             }
d60a42 461                             break
GS 462                         case ReceiveCommand.Type.UPDATE:
463                             def commits = JGitUtils.getRevLog(repository, command.oldId.name, command.newId.name).reverse()
464                             commitCount += commits.size()
465                             // fast-forward branch commits table
466                             // Write header
467                             writeBranchTitle(refType, ref, "updated", commits.size())
468                             writeCommitTable(commits)
469                             break
470                         case ReceiveCommand.Type.UPDATE_NONFASTFORWARD:
471                             def commits = JGitUtils.getRevLog(repository, command.oldId.name, command.newId.name).reverse()
472                             commitCount += commits.size()
473                             // non-fast-forward branch commits table
474                             // Write header
475                             writeBranchTitle(refType, ref, "updated [NON fast-forward]", commits.size())
476                             writeCommitTable(commits)
477                             break
478                         case ReceiveCommand.Type.DELETE:
479                             // deleted branch/tag
480                             writeBranchDeletedTitle(refType, ref)
481                             break
482                         default:
483                             break
484                     }
485                 }
ddec28 486                 }
d60a42 487             }
GS 488         }
489         writer.toString()
490     }
491
492 }
493
494 def mailWriter = new HtmlMailWriter()
495 mailWriter.repository = r
496 mailWriter.baseCommitUrl = baseCommitUrl
497 mailWriter.baseBlobDiffUrl = baseBlobDiffUrl
498 mailWriter.baseCommitDiffUrl = baseCommitDiffUrl
814f70 499 mailWriter.forwardSlashChar = forwardSlashChar
d60a42 500 mailWriter.commands = commands
GS 501 mailWriter.url = url
4789d1 502 mailWriter.mountParameters = GitBlit.getBoolean(Keys.web.mountParameters, true)
JM 503 mailWriter.includeGravatar = GitBlit.getBoolean(Keys.web.allowGravatar, true)
798581 504 mailWriter.shortCommitIdLength = GitBlit.getInteger(Keys.web.shortCommitIdLength, 8)
d60a42 505
GS 506 def content = mailWriter.write()
507
508 // close the repository reference
509 r.close()
510
511 // tell Gitblit to send the message (Gitblit filters duplicate addresses)
512 def repositoryName = repository.name.substring(0, repository.name.length() - 4)
814f70 513 gitblit.sendHtmlMail("${emailprefix} ${userModel.displayName} pushed ${mailWriter.commitCount} commits => $repositoryName",
d60a42 514                      content,
GS 515                      toAddresses)