--- /dev/null
+/*\r
+ * Copyright 2012 gitblit.com.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ * http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+import com.gitblit.GitBlit\r
+import com.gitblit.Keys\r
+import com.gitblit.models.RepositoryModel\r
+import com.gitblit.models.TeamModel\r
+import com.gitblit.models.UserModel\r
+import com.gitblit.utils.JGitUtils\r
+import java.text.SimpleDateFormat\r
+\r
+import org.eclipse.jgit.api.Status;\r
+import org.eclipse.jgit.api.errors.JGitInternalException;\r
+import org.eclipse.jgit.diff.DiffEntry;\r
+import org.eclipse.jgit.diff.DiffFormatter;\r
+import org.eclipse.jgit.diff.RawTextComparator;\r
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;\r
+import org.eclipse.jgit.lib.Constants;\r
+import org.eclipse.jgit.lib.IndexDiff;\r
+import org.eclipse.jgit.lib.ObjectId;\r
+import org.eclipse.jgit.lib.Repository\r
+import org.eclipse.jgit.lib.Config\r
+import org.eclipse.jgit.patch.FileHeader;\r
+import org.eclipse.jgit.revwalk.RevCommit\r
+import org.eclipse.jgit.revwalk.RevWalk;\r
+import org.eclipse.jgit.transport.ReceiveCommand\r
+import org.eclipse.jgit.transport.ReceiveCommand.Result\r
+import org.eclipse.jgit.treewalk.FileTreeIterator;\r
+import org.eclipse.jgit.treewalk.EmptyTreeIterator;\r
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;\r
+import org.eclipse.jgit.util.io.DisabledOutputStream;\r
+import org.slf4j.Logger\r
+import groovy.xml.MarkupBuilder\r
+\r
+import java.io.IOException;\r
+import java.security.MessageDigest\r
+\r
+\r
+/**\r
+ * Sample Gitblit Post-Receive Hook: sendmail-html\r
+ *\r
+ * The Post-Receive hook is executed AFTER the pushed commits have been applied\r
+ * to the Git repository. This is the appropriate point to trigger an\r
+ * integration build or to send a notification.\r
+ * \r
+ * This script is only executed when pushing to *Gitblit*, not to other Git\r
+ * tooling you may be using.\r
+ * \r
+ * If this script is specified in *groovy.postReceiveScripts* of gitblit.properties\r
+ * or web.xml then it will be executed by any repository when it receives a\r
+ * push. If you choose to share your script then you may have to consider\r
+ * tailoring control-flow based on repository access restrictions.\r
+ *\r
+ * Scripts may also be specified per-repository in the repository settings page.\r
+ * Shared scripts will be excluded from this list of available scripts.\r
+ * \r
+ * This script is dynamically reloaded and it is executed within it's own\r
+ * exception handler so it will not crash another script nor crash Gitblit.\r
+ *\r
+ * If you want this hook script to fail and abort all subsequent scripts in the\r
+ * chain, "return false" at the appropriate failure points.\r
+ * \r
+ * Bound Variables:\r
+ * gitblit Gitblit Server com.gitblit.GitBlit\r
+ * repository Gitblit Repository com.gitblit.models.RepositoryModel\r
+ * user Gitblit User com.gitblit.models.UserModel\r
+ * commands JGit commands Collection<org.eclipse.jgit.transport.ReceiveCommand>\r
+ * url Base url for Gitblit java.lang.String\r
+ * logger Logs messages to Gitblit org.slf4j.Logger\r
+ * clientLogger Logs messages to Git client com.gitblit.utils.ClientLogger\r
+ *\r
+ * Accessing Gitblit Custom Fields:\r
+ * def myCustomField = repository.customFields.myCustomField\r
+ * \r
+ */\r
+\r
+com.gitblit.models.UserModel userModel = user\r
+\r
+// Indicate we have started the script\r
+logger.info("sendmail hook triggered by ${user.username} for ${repository.name}")\r
+\r
+/*\r
+ * Primitive email notification.\r
+ * This requires the mail settings to be properly configured in Gitblit.\r
+ */\r
+\r
+Repository r = gitblit.getRepository(repository.name)\r
+\r
+// reuse existing repository config settings, if available\r
+Config config = r.getConfig()\r
+def mailinglist = config.getString('hooks', null, 'mailinglist')\r
+def emailprefix = config.getString('hooks', null, 'emailprefix')\r
+\r
+// set default values\r
+def toAddresses = []\r
+if (emailprefix == null) {\r
+ emailprefix = '[Gitblit]'\r
+}\r
+\r
+if (mailinglist != null) {\r
+ def addrs = mailinglist.split(/(,|\s)/)\r
+ toAddresses.addAll(addrs)\r
+}\r
+\r
+// add all mailing lists defined in gitblit.properties or web.xml\r
+toAddresses.addAll(gitblit.getStrings(Keys.mail.mailingLists))\r
+\r
+// add all team mailing lists\r
+def teams = gitblit.getRepositoryTeams(repository)\r
+for (team in teams) {\r
+ TeamModel model = gitblit.getTeamModel(team)\r
+ if (model.mailingLists) {\r
+ toAddresses.addAll(model.mailingLists)\r
+ }\r
+}\r
+\r
+// add all mailing lists for the repository\r
+toAddresses.addAll(repository.mailingLists)\r
+\r
+// define the summary and commit urls\r
+def repo = repository.name\r
+def summaryUrl = url + "/summary?r=$repo"\r
+def baseCommitUrl = url + "/commit?r=$repo&h="\r
+def baseBlobDiffUrl = url + "/blobdiff/?r=$repo&h="\r
+def baseCommitDiffUrl = url + "/commitdiff/?r=$repo&h="\r
+\r
+if (gitblit.getBoolean(Keys.web.mountParameters, true)) {\r
+ repo = repo.replace('/', gitblit.getString(Keys.web.forwardSlashCharacter, '/')).replace('/', '%2F')\r
+ summaryUrl = url + "/summary/$repo"\r
+ baseCommitUrl = url + "/commit/$repo/"\r
+ baseBlobDiffUrl = url + "/blobdiff/$repo/"\r
+ baseCommitDiffUrl = url + "/commitdiff/$repo/"\r
+}\r
+\r
+class HtmlMailWriter {\r
+ Repository repository\r
+ def url\r
+ def baseCommitUrl\r
+ def baseCommitDiffUrl\r
+ def baseBlobDiffUrl\r
+ def mountParameters\r
+ def commitCount = 0\r
+ def commands\r
+ def writer = new StringWriter();\r
+ def builder = new MarkupBuilder(writer)\r
+\r
+ def writeStyle() {\r
+ builder.style(type:"text/css", '''\r
+ th, td { \r
+ padding: 2px; \r
+ }\r
+ thead {\r
+ text-align: left;\r
+ font-weight: bold; \r
+ }\r
+ thead tr {\r
+ border-bottom: 1px dotted #000; \r
+ }\r
+ a {\r
+ text-decoration: none;\r
+ }\r
+ .commits-table {\r
+ border-collapse: collapse;\r
+ font-family: sans-serif; \r
+ width: 100%; \r
+ }\r
+ .label-commit {\r
+ border-radius:4px;\r
+ background-color: #3A87AD;\r
+ padding: 2px 4px;\r
+ color: white;\r
+ vertical-align: baseline; \r
+ font-weight: bold; \r
+ font-family: monospace; \r
+ }\r
+ .label-add {\r
+ border-radius:4px;\r
+ background-color: green;\r
+ padding: 2px 4px;\r
+ color: white;\r
+ vertical-align: baseline; \r
+ font-weight: bold; \r
+ font-family: monospace; \r
+ }\r
+ .label-delete {\r
+ border-radius:4px;\r
+ background-color: grey;\r
+ padding: 2px 4px;\r
+ color: white;\r
+ vertical-align: baseline; \r
+ font-weight: bold; \r
+ font-family: monospace; \r
+ }\r
+ .label-rename {\r
+ border-radius:4px;\r
+ background-color: blue;\r
+ padding: 2px 4px;\r
+ color: white;\r
+ vertical-align: baseline; \r
+ font-weight: bold; \r
+ font-family: monospace; \r
+ }\r
+ .label-modify {\r
+ border-radius:4px;\r
+ background-color: orange;\r
+ padding: 2px 4px;\r
+ color: white;\r
+ vertical-align: baseline; \r
+ font-weight: bold; \r
+ font-family: monospace; \r
+ }\r
+ .label-copy {\r
+ border-radius:4px;\r
+ background-color: teal;\r
+ padding: 2px 4px;\r
+ color: white;\r
+ vertical-align: baseline; \r
+ font-weight: bold; \r
+ font-family: monospace; \r
+ }\r
+ .gravatar-column {\r
+ width: 5%; \r
+ }\r
+ .author-column {\r
+ width: 10%; \r
+ }\r
+ .commit-column {\r
+ width: 5%; \r
+ }\r
+ .status-column {\r
+ width: 10%;\r
+ padding-bottom: 5px; \r
+ padding-top: 5px; \r
+ }\r
+ ''')\r
+ }\r
+\r
+ def writeBranchTitle(type, name, action, number) {\r
+ builder.h2 {\r
+ mkp.yield "$type "\r
+ span(style:"font-family: monospace;", name )\r
+ mkp.yield " $action ($number commits)"\r
+ }\r
+ }\r
+\r
+ def writeBranchDeletedTitle(type, name) {\r
+ builder.h2 {\r
+ mkp.yield "$type "\r
+ span(style:"font-family: monospace;", name )\r
+ mkp.yield " deleted"\r
+ }\r
+ }\r
+\r
+ def commitUrl(RevCommit commit) {\r
+ "${baseCommitUrl}$commit.id.name"\r
+ }\r
+\r
+ def commitDiffUrl(RevCommit commit) {\r
+ "${baseCommitDiffUrl}$commit.id.name"\r
+ }\r
+\r
+ def encoded(String path) {\r
+ path.replace('/', '!')\r
+ }\r
+\r
+ def blobDiffUrl(objectId, path) {\r
+ if (mountParameters) {\r
+ // REST style\r
+ "${baseBlobDiffUrl}${objectId.name()}/${encoded(path)}"\r
+ } else {\r
+ "${baseBlobDiffUrl}${objectId.name()}&f=${path}"\r
+ }\r
+\r
+ }\r
+\r
+ def writeCommitTable(commits) {\r
+ // Write commits table\r
+ builder.table('class':"commits-table") {\r
+ thead {\r
+ tr {\r
+ th(colspan:2, "Author")\r
+ th( "Commit" )\r
+ th( "Message" )\r
+ }\r
+ }\r
+ tbody() {\r
+\r
+ // Write all the commits\r
+ for (commit in commits) {\r
+ writeCommit(commit)\r
+\r
+ // Write detail on that particular commit\r
+ tr {\r
+ td (colspan:3)\r
+ td { writeStatusTable(commit) }\r
+ }\r
+ }\r
+ }\r
+ }\r
+ }\r
+\r
+ def writeCommit(commit) {\r
+ def abbreviated = repository.newObjectReader().abbreviate(commit.id, 6).name()\r
+ def author = commit.authorIdent.name\r
+ def email = commit.authorIdent.emailAddress\r
+ def message = commit.shortMessage\r
+ builder.tr {\r
+ td('class':"gravatar-column") {\r
+ img(src:gravatarUrl(email))\r
+ }\r
+ td('class':"author-column") { p(author) }\r
+ td('class':"commit-column") {\r
+ a(href:commitUrl(commit)) {\r
+ span('class':"label-commit", abbreviated )\r
+ }\r
+ }\r
+ td {\r
+ mkp.yield message\r
+ a(href:commitDiffUrl(commit), " [commitdiff]" )\r
+ }\r
+ }\r
+ }\r
+\r
+ def writeStatusLabel(style, label) {\r
+ builder.span('class' : style, label )\r
+ }\r
+\r
+ def writeAddStatusLine(ObjectId id, FileHeader header) {\r
+ builder.td('class':"status-column") {\r
+ a(href:blobDiffUrl(id, header.newPath)) { writeStatusLabel("label-add", "add") }\r
+ }\r
+ builder.td {\r
+ span(style:'font-family: monospace;', header.newPath)\r
+ }\r
+ }\r
+\r
+ def writeCopyStatusLine(ObjectId id, FileHeader header) {\r
+ builder.td('class':"status-column") {\r
+ a(href:blobDiffUrl(id, header.newPath)) { writeStatusLabel("label-copy", "copy") }\r
+ }\r
+ builder.td() {\r
+ span(style : "font-family: monospace; ", header.oldPath + " copied to " + header.newPath)\r
+ }\r
+ }\r
+\r
+ def writeDeleteStatusLine(ObjectId id, FileHeader header) {\r
+ builder.td('class':"status-column") {\r
+ a(href:blobDiffUrl(id, header.oldPath)) { writeStatusLabel("label-delete", "delete") }\r
+ }\r
+ builder.td() {\r
+ span(style : "font-family: monospace; ", header.oldPath)\r
+ }\r
+ }\r
+\r
+ def writeModifyStatusLine(ObjectId id, FileHeader header) {\r
+ builder.td('class':"status-column") {\r
+ a(href:blobDiffUrl(id, header.oldPath)) { writeStatusLabel("label-modify", "modify") }\r
+ }\r
+ builder.td() {\r
+ span(style : "font-family: monospace; ", header.oldPath)\r
+ }\r
+ }\r
+\r
+ def writeRenameStatusLine(ObjectId id, FileHeader header) {\r
+ builder.td('class':"status-column") {\r
+ a(href:blobDiffUrl(id, header.newPath)) { writeStatusLabel("label-rename", "rename") }\r
+ }\r
+ builder.td() {\r
+ span(style : "font-family: monospace; ", header.olPath + " -> " + header.newPath)\r
+ }\r
+ }\r
+\r
+ def writeStatusLine(ObjectId id, FileHeader header) {\r
+ builder.tr {\r
+ switch (header.changeType) {\r
+ case ChangeType.ADD:\r
+ writeAddStatusLine(id, header)\r
+ break;\r
+ case ChangeType.COPY:\r
+ writeCopyStatusLine(id, header)\r
+ break;\r
+ case ChangeType.DELETE:\r
+ writeDeleteStatusLine(id, header)\r
+ break;\r
+ case ChangeType.MODIFY:\r
+ writeModifyStatusLine(id, header)\r
+ break;\r
+ case ChangeType.RENAME:\r
+ writeRenameStatusLine(id, header)\r
+ break;\r
+ }\r
+ }\r
+ }\r
+\r
+ def writeStatusTable(RevCommit commit) {\r
+ DiffFormatter formatter = new DiffFormatter(DisabledOutputStream.INSTANCE)\r
+ formatter.setRepository(repository)\r
+ formatter.setDetectRenames(true)\r
+ formatter.setDiffComparator(RawTextComparator.DEFAULT);\r
+\r
+ def diffs\r
+ RevWalk rw = new RevWalk(repository);\r
+ if (commit.parentCount > 0) {\r
+ RevCommit parent = commit.parents[0]\r
+ diffs = formatter.scan(parent.tree, commit.tree)\r
+ } else {\r
+ diffs = formatter.scan(new EmptyTreeIterator(),\r
+ new CanonicalTreeParser(null, rw.objectReader, commit.tree))\r
+ }\r
+ // Write status table\r
+ builder.table('class':"commits-table") {\r
+ tbody() {\r
+ for (DiffEntry entry in diffs) {\r
+ FileHeader header = formatter.toFileHeader(entry)\r
+ writeStatusLine(commit.id, header)\r
+ }\r
+ }\r
+ }\r
+ }\r
+\r
+\r
+ def md5(text) {\r
+\r
+ def digest = MessageDigest.getInstance("MD5")\r
+\r
+ //Quick MD5 of text\r
+ def hash = new BigInteger(1, digest.digest(text.getBytes()))\r
+ .toString(16)\r
+ .padLeft(32, "0")\r
+ hash.toString()\r
+ }\r
+\r
+ def gravatarUrl(email) {\r
+ def cleaned = email.trim().toLowerCase()\r
+ "http://www.gravatar.com/avatar/${md5(cleaned)}?s=30"\r
+ }\r
+\r
+ def writeNavbar() {\r
+ builder.div('class':"navbar navbar-fixed-top") {\r
+ div('class':"navbar-inner") {\r
+ div('class':"container") {\r
+ a('class':"brand", href:"${url}", title:"GitBlit") {\r
+ img(src:"${url}/gitblt_25_white.png",\r
+ width:"79",\r
+ height:"25",\r
+ 'class':"logo")\r
+ }\r
+ }\r
+ }\r
+ }\r
+ }\r
+\r
+ def write() {\r
+ builder.html {\r
+ head {\r
+ link(rel:"stylesheet", href:"${url}/bootstrap/css/bootstrap.css")\r
+ link(rel:"stylesheet", href:"${url}/gitblit.css")\r
+ writeStyle()\r
+ }\r
+ body {\r
+\r
+ writeNavbar()\r
+\r
+ for (command in commands) {\r
+ def ref = command.refName\r
+ def refType = 'Branch'\r
+ if (ref.startsWith('refs/heads/')) {\r
+ ref = command.refName.substring('refs/heads/'.length())\r
+ } else if (ref.startsWith('refs/tags/')) {\r
+ ref = command.refName.substring('refs/tags/'.length())\r
+ refType = 'Tag'\r
+ }\r
+\r
+ switch (command.type) {\r
+ case ReceiveCommand.Type.CREATE:\r
+ def commits = JGitUtils.getRevLog(repository, command.oldId.name, command.newId.name).reverse()\r
+ commitCount += commits.size()\r
+ // new branch\r
+ // Write header\r
+ writeBranchTitle(refType, ref, "created", commits.size())\r
+ writeCommitTable(commits)\r
+ break\r
+ case ReceiveCommand.Type.UPDATE:\r
+ def commits = JGitUtils.getRevLog(repository, command.oldId.name, command.newId.name).reverse()\r
+ commitCount += commits.size()\r
+ // fast-forward branch commits table\r
+ // Write header\r
+ writeBranchTitle(refType, ref, "updated", commits.size())\r
+ writeCommitTable(commits)\r
+ break\r
+ case ReceiveCommand.Type.UPDATE_NONFASTFORWARD:\r
+ def commits = JGitUtils.getRevLog(repository, command.oldId.name, command.newId.name).reverse()\r
+ commitCount += commits.size()\r
+ // non-fast-forward branch commits table\r
+ // Write header\r
+ writeBranchTitle(refType, ref, "updated [NON fast-forward]", commits.size())\r
+ writeCommitTable(commits)\r
+ break\r
+ case ReceiveCommand.Type.DELETE:\r
+ // deleted branch/tag\r
+ writeBranchDeletedTitle(refType, ref)\r
+ break\r
+ default:\r
+ break\r
+ }\r
+ }\r
+ }\r
+ }\r
+ writer.toString()\r
+ }\r
+\r
+}\r
+\r
+def mailWriter = new HtmlMailWriter()\r
+mailWriter.repository = r\r
+mailWriter.baseCommitUrl = baseCommitUrl\r
+mailWriter.baseBlobDiffUrl = baseBlobDiffUrl\r
+mailWriter.baseCommitDiffUrl = baseCommitDiffUrl\r
+mailWriter.commands = commands\r
+mailWriter.url = url\r
+mailWriter.mountParameters = gitblit.getBoolean(Keys.web.mountParameters, true)\r
+\r
+def content = mailWriter.write()\r
+\r
+// close the repository reference\r
+r.close()\r
+\r
+// tell Gitblit to send the message (Gitblit filters duplicate addresses)\r
+def repositoryName = repository.name.substring(0, repository.name.length() - 4)\r
+gitblit.sendHtmlMail("${emailprefix}[$repositoryName] ${userModel.displayName} pushed ${mailWriter.commitCount} commits",\r
+ content,\r
+ toAddresses)\r