From 121e2f0dd548176c6a7346da13868117500695d6 Mon Sep 17 00:00:00 2001 From: "Simeon H.K. Fitch" Date: Fri, 3 Jun 2016 20:40:37 -0400 Subject: [PATCH] Initial implementation of a JetBrains YouTrack hook for GitBlit. --- .../distrib/data/groovy/youtrack-readme.md | 30 +++ src/main/distrib/data/groovy/youtrack.groovy | 224 ++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 src/main/distrib/data/groovy/youtrack-readme.md create mode 100644 src/main/distrib/data/groovy/youtrack.groovy diff --git a/src/main/distrib/data/groovy/youtrack-readme.md b/src/main/distrib/data/groovy/youtrack-readme.md new file mode 100644 index 00000000..1a6f9136 --- /dev/null +++ b/src/main/distrib/data/groovy/youtrack-readme.md @@ -0,0 +1,30 @@ +# GitBlit YouTrack Receive Hook + +GitBlit receive hook for updating referenced YouTrack issues. + +This script has only been tested with the cloud hosted YouTrack instance. + +## Usage + +Due to limited authentication options when using the YouTrack REST API, you have to store a username and password for an account with appropriate permissions for adding comments to any issue. Hopefully in the future YouTrack will support API keys or similar. + +1. Update your `gitblit.properties` file with the following entries: + * `groovy.customFields = "youtrackProjectID=YouTrack Project ID" ` *(or append to existing setting)* + * `youtrack.host = example.myjetbrains.com` + * `youtrack.user = ytUser` + * `youtrack.pass = insecurep@sswordsRus` + + (But using your own host and credential info). + +2. Copy the `youtrack.groovy` script to the `/groovy` scripts directory. +3. In GitBlit, go to a repository, click the *edit* button, then click the *receive* link. In the *post0receive scripts* section you should see `youtrack` as an option. Move it over to the *Selected* column. +4. At the bottom of this same screen should should be a *custom fields* section with a **YouTrack Project ID** field. Enter the YouTrack Project ID associated with the repository. +5. When you commit changes, reference YouTrack issues with `#{projectID}-{issueID}` where `{projectID}` is the YouTrack Project ID, and `{issueID}` is the issue number. For example, to references issue `34` in project `fizz`: + + git commit -m'Changed bazinator to fix issue #fizz-34.' + + Multiple issues may be referenced in the same commit message. + +## Attribution + +Much of this script was cobbled together from the example receive hooks in the official [GitBlit](https://github.com/gitblit/gitblit) distribution. diff --git a/src/main/distrib/data/groovy/youtrack.groovy b/src/main/distrib/data/groovy/youtrack.groovy new file mode 100644 index 00000000..c94d8fd1 --- /dev/null +++ b/src/main/distrib/data/groovy/youtrack.groovy @@ -0,0 +1,224 @@ +/* + * Copyright 2013 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.lib.Repository +import org.eclipse.jgit.lib.Config +import org.eclipse.jgit.transport.ReceiveCommand +import org.eclipse.jgit.transport.ReceiveCommand.Result +import org.slf4j.Logger + +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.lib.Constants; +import org.eclipse.jgit.lib.IndexDiff; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.patch.FileHeader; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.revwalk.RevCommit; +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 java.util.Set; +import java.util.HashSet; + +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.AuthCache; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.protocol.*; +import org.apache.http.client.protocol.*; +import org.apache.http.client.methods.*; +import org.apache.http.impl.client.*; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.util.EntityUtils; + + +/** + * GitBlit Post-Receive Hook for YouTrack + * + * The purpose of this script is to invoke the YouTrack API and update a case when + * push is received based. + * + * 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. + * + * 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 + * receivePack JGit Receive Pack org.eclipse.jgit.transport.ReceivePack + * user Gitblit User com.gitblit.models.UserModel + * commands JGit commands Collection + * url Base url for Gitblit String + * logger Logs messages to Gitblit org.slf4j.Logger + * clientLogger Logs messages to Git client com.gitblit.utils.ClientLogger + * + * + * Custom Fileds Used by This script + * youtrackProjectID - Project ID in YouTrack + * + * Make sure to add the following to your gitblit.properties file: + * groovy.customFields = "youtrackProjectID=YouTrack Project ID" + * youtrack.host = example.myjetbrains.com + * youtrack.user = ytUser + * youtrack.pass = insecurep@sswordsRus + */ + +// Indicate we have started the script +logger.info("youtrack hook triggered in ${url} by ${user.username} for ${repository.name}") + +Repository r = gitblit.getRepository(repository.name) + +// pull custom fields from repository specific values +def youtrackProjectID = repository.customFields.youtrackProjectID + +if(youtrackProjectID == null || youtrackProjectID.length() == 0) return true; + +def youtrackHost = gitblit.getString('youtrack.host', 'nohost') +def bugIdRegex = gitblit.getString('youtrack.commitMessageRegex', "#${youtrackProjectID}-([0-9]+)") +def youtrackUser = gitblit.getString('youtrack.user', 'nouser') +def youtrackPass = gitblit.getString('youtrack.pass', 'nopassword') + +HttpHost target = new HttpHost(youtrackHost, 80, "http"); +CredentialsProvider credsProvider = new BasicCredentialsProvider(); +credsProvider.setCredentials( + new AuthScope(target.getHostName(), target.getPort()), + new UsernamePasswordCredentials(youtrackUser, youtrackPass)); +def httpclient = new DefaultHttpClient(); + +httpclient.setCredentialsProvider(credsProvider); + +try { + + AuthCache authCache = new BasicAuthCache(); + BasicScheme basicAuth = new BasicScheme(); + authCache.put(target, basicAuth); + BasicHttpContext localcontext = new BasicHttpContext(); + localcontext.setAttribute(ClientContext.AUTH_CACHE, authCache); + + + for (command in commands) { + for( commit in JGitUtils.getRevLog(r, command.oldId.name, command.newId.name).reverse() ) { + def bugIds = new java.util.HashSet() + def longMsg = commit.getFullMessage() + // Grab the second match group and then filter out each numeric ID and add it to array + (longMsg =~ bugIdRegex).each{ (it[1] =~ "\\d+").each { bugIds.add(it)} } + + if(bugIds.size() > 0) { + def comment = createIssueComment(command, commit) + + logger.debug("Submitting youtrack comment:\n" + comment) + + def encoded = URLEncoder.encode(comment) + for(bugId in bugIds ) { + def baseURL = "http://${youtrackHost}/youtrack/rest/issue/${youtrackProjectID}-${bugId}/execute?command=&comment=" + encoded + def post = new HttpPost(baseURL); + + clientLogger.info("Executing request " + post.getRequestLine() + " to target " + target); + def response = httpclient.execute(target, post, localcontext); + logger.debug(response.getStatusLine().toString()); + EntityUtils.consume(response.getEntity()); + } + } + } + } +} +finally { + r.close() +} + +def createIssueComment(command, commit) { + def commits = [commit] // Borrowed code expects a collection. + Repository r = gitblit.getRepository(repository.name) + // define the summary and commit urls + def repo = repository.name + def summaryUrl + def commitUrl + if (gitblit.getBoolean(Keys.web.mountParameters, true)) { + repo = repo.replace('/', gitblit.getString(Keys.web.forwardSlashCharacter, '/')).replace('/', '%2F') + summaryUrl = url + "/summary/$repo" + commitUrl = url + "/commit/$repo/" + } else { + summaryUrl = url + "/summary?r=$repo" + commitUrl = url + "/commit?r=$repo&h=" + } + + // construct a simple text summary of the changes contained in the push + def commitBreak = '\n' + def commitCount = 0 + def changes = '' + + SimpleDateFormat df = new SimpleDateFormat(gitblit.getString(Keys.web.datetimestampLongFormat, 'EEEE, MMMM d, yyyy h:mm a z')) + + def table = { + def shortSha = it.id.name.substring(0, 8) + "* [$commitUrl$it.id.name ${shortSha}] by *${it.authorIdent.name}* on ${df.format(JGitUtils.getCommitDate(it))}\n" + + " {cut $it.shortMessage}\n{noformat}$it.fullMessage{noformat}{cut}" + } + + 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: + // new branch + changes += "''new $refType $ref created''\n" + changes += commits.collect(table).join(commitBreak) + changes += '\n' + break + case ReceiveCommand.Type.UPDATE: + // fast-forward branch commits table + changes += "''$ref $refType updated''\n" + changes += commits.collect(table).join(commitBreak) + changes += '\n' + break + case ReceiveCommand.Type.UPDATE_NONFASTFORWARD: + // non-fast-forward branch commits table + changes += "''$ref $refType updated [NON fast-forward]''" + changes += commits.collect(table).join(commitBreak) + changes += '\n' + break + case ReceiveCommand.Type.DELETE: + // deleted branch/tag + changes += "''$ref $refType deleted''" + break + default: + break + } + + return "$user.username pushed commits to [$summaryUrl $repository.name]\n$changes" +} -- 2.39.5