/* | |
* 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. | |
*/ | |
package com.gitblit.git; | |
import static org.eclipse.jgit.transport.BasePackPushConnection.CAPABILITY_SIDE_BAND_64K; | |
import groovy.lang.Binding; | |
import groovy.util.GroovyScriptEngine; | |
import java.io.File; | |
import java.io.IOException; | |
import java.text.MessageFormat; | |
import java.util.ArrayList; | |
import java.util.Collection; | |
import java.util.LinkedHashMap; | |
import java.util.LinkedHashSet; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Set; | |
import java.util.SortedMap; | |
import java.util.TreeMap; | |
import java.util.concurrent.TimeUnit; | |
import org.eclipse.jgit.lib.AnyObjectId; | |
import org.eclipse.jgit.lib.BatchRefUpdate; | |
import org.eclipse.jgit.lib.NullProgressMonitor; | |
import org.eclipse.jgit.lib.ObjectId; | |
import org.eclipse.jgit.lib.PersonIdent; | |
import org.eclipse.jgit.lib.ProgressMonitor; | |
import org.eclipse.jgit.lib.Ref; | |
import org.eclipse.jgit.lib.RefUpdate; | |
import org.eclipse.jgit.lib.Repository; | |
import org.eclipse.jgit.revwalk.RevCommit; | |
import org.eclipse.jgit.revwalk.RevWalk; | |
import org.eclipse.jgit.transport.PostReceiveHook; | |
import org.eclipse.jgit.transport.PreReceiveHook; | |
import org.eclipse.jgit.transport.ReceiveCommand; | |
import org.eclipse.jgit.transport.ReceiveCommand.Result; | |
import org.eclipse.jgit.transport.ReceivePack; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import com.gitblit.Constants; | |
import com.gitblit.Constants.AccessRestrictionType; | |
import com.gitblit.IStoredSettings; | |
import com.gitblit.Keys; | |
import com.gitblit.client.Translation; | |
import com.gitblit.extensions.ReceiveHook; | |
import com.gitblit.manager.IGitblit; | |
import com.gitblit.models.RepositoryModel; | |
import com.gitblit.models.TicketModel; | |
import com.gitblit.models.UserModel; | |
import com.gitblit.models.TicketModel.Change; | |
import com.gitblit.models.TicketModel.Field; | |
import com.gitblit.models.TicketModel.Patchset; | |
import com.gitblit.models.TicketModel.Status; | |
import com.gitblit.models.TicketModel.TicketAction; | |
import com.gitblit.models.TicketModel.TicketLink; | |
import com.gitblit.tickets.BranchTicketService; | |
import com.gitblit.tickets.ITicketService; | |
import com.gitblit.tickets.TicketNotifier; | |
import com.gitblit.utils.ArrayUtils; | |
import com.gitblit.utils.ClientLogger; | |
import com.gitblit.utils.CommitCache; | |
import com.gitblit.utils.JGitUtils; | |
import com.gitblit.utils.RefLogUtils; | |
import com.gitblit.utils.StringUtils; | |
import com.google.common.collect.Lists; | |
/** | |
* GitblitReceivePack processes receive commands. It also executes Groovy pre- | |
* and post- receive hooks. | |
* | |
* The general execution flow is: | |
* <ol> | |
* <li>onPreReceive()</li> | |
* <li>executeCommands()</li> | |
* <li>onPostReceive()</li> | |
* </ol> | |
* | |
* @author Android Open Source Project | |
* @author James Moger | |
* | |
*/ | |
public class GitblitReceivePack extends ReceivePack implements PreReceiveHook, PostReceiveHook { | |
private static final Logger LOGGER = LoggerFactory.getLogger(GitblitReceivePack.class); | |
protected final RepositoryModel repository; | |
protected final UserModel user; | |
protected final File groovyDir; | |
protected String gitblitUrl; | |
protected GroovyScriptEngine gse; | |
protected final IStoredSettings settings; | |
protected final IGitblit gitblit; | |
protected final ITicketService ticketService; | |
protected final TicketNotifier ticketNotifier; | |
public GitblitReceivePack( | |
IGitblit gitblit, | |
Repository db, | |
RepositoryModel repository, | |
UserModel user) { | |
super(db); | |
this.settings = gitblit.getSettings(); | |
this.gitblit = gitblit; | |
this.repository = repository; | |
this.user = user; | |
this.groovyDir = gitblit.getHooksFolder(); | |
try { | |
// set Grape root | |
File grapeRoot = gitblit.getGrapesFolder(); | |
grapeRoot.mkdirs(); | |
System.setProperty("grape.root", grapeRoot.getAbsolutePath()); | |
this.gse = new GroovyScriptEngine(groovyDir.getAbsolutePath()); | |
} catch (IOException e) { | |
} | |
if (gitblit.getTicketService().isAcceptingTicketUpdates(repository)) { | |
this.ticketService = gitblit.getTicketService(); | |
this.ticketNotifier = this.ticketService.createNotifier(); | |
} else { | |
this.ticketService = null; | |
this.ticketNotifier = null; | |
} | |
// set advanced ref permissions | |
setAllowCreates(user.canCreateRef(repository)); | |
setAllowDeletes(user.canDeleteRef(repository)); | |
setAllowNonFastForwards(user.canRewindRef(repository)); | |
int maxObjectSz = settings.getInteger(Keys.git.maxObjectSizeLimit, -1); | |
if (maxObjectSz >= 0) { | |
setMaxObjectSizeLimit(maxObjectSz); | |
} | |
int maxPackSz = settings.getInteger(Keys.git.maxPackSizeLimit, -1); | |
if (maxPackSz >= 0) { | |
setMaxPackSizeLimit(maxPackSz); | |
} | |
setCheckReceivedObjects(settings.getBoolean(Keys.git.checkReceivedObjects, true)); | |
setCheckReferencedObjectsAreReachable(settings.getBoolean(Keys.git.checkReferencedObjectsAreReachable, true)); | |
// setup pre and post receive hook | |
setPreReceiveHook(this); | |
setPostReceiveHook(this); | |
} | |
/** | |
* Returns true if the user is permitted to apply the receive commands to | |
* the repository. | |
* | |
* @param commands | |
* @return true if the user may push these commands | |
*/ | |
protected boolean canPush(Collection<ReceiveCommand> commands) { | |
// TODO Consider supporting branch permissions here (issue-36) | |
// Not sure if that should be Gerrit-style, refs/meta/config, or | |
// gitolite-style, permissions in users.conf | |
// | |
// How could commands be empty? | |
// | |
// Because a subclass, like PatchsetReceivePack, filters receive | |
// commands before this method is called. This makes it possible for | |
// this method to test an empty list. In this case, we assume that the | |
// subclass receive pack properly enforces push restrictions. for the | |
// ref. | |
// | |
// The empty test is not explicitly required, it's written here to | |
// clarify special-case behavior. | |
return commands.isEmpty() ? true : user.canPush(repository); | |
} | |
/** | |
* Instrumentation point where the incoming push event has been parsed, | |
* validated, objects created BUT refs have not been updated. You might | |
* use this to enforce a branch-write permissions model. | |
*/ | |
@Override | |
public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) { | |
if (commands.size() == 0) { | |
// no receive commands to process | |
// this can happen if receive pack subclasses intercept and filter | |
// the commands | |
LOGGER.debug("skipping pre-receive processing, no refs created, updated, or removed"); | |
return; | |
} | |
if (repository.isMirror) { | |
// repository is a mirror | |
for (ReceiveCommand cmd : commands) { | |
sendRejection(cmd, "Gitblit does not allow pushes to \"{0}\" because it is a mirror!", repository.name); | |
} | |
return; | |
} | |
if (repository.isFrozen) { | |
// repository is frozen/readonly | |
for (ReceiveCommand cmd : commands) { | |
sendRejection(cmd, "Gitblit does not allow pushes to \"{0}\" because it is frozen!", repository.name); | |
} | |
return; | |
} | |
if (!repository.isBare) { | |
// repository has a working copy | |
for (ReceiveCommand cmd : commands) { | |
sendRejection(cmd, "Gitblit does not allow pushes to \"{0}\" because it has a working copy!", repository.name); | |
} | |
return; | |
} | |
if (!canPush(commands)) { | |
// user does not have push permissions | |
for (ReceiveCommand cmd : commands) { | |
sendRejection(cmd, "User \"{0}\" does not have push permissions for \"{1}\"!", user.username, repository.name); | |
} | |
return; | |
} | |
if (repository.accessRestriction.atLeast(AccessRestrictionType.PUSH) && repository.verifyCommitter) { | |
// enforce committer verification | |
if (StringUtils.isEmpty(user.emailAddress)) { | |
// reject the push because the pushing account does not have an email address | |
for (ReceiveCommand cmd : commands) { | |
sendRejection(cmd, "Sorry, the account \"{0}\" does not have an email address set for committer verification!", user.username); | |
} | |
return; | |
} | |
// Optionally enforce that the committer of first parent chain | |
// match the account being used to push the commits. | |
// | |
// This requires all merge commits are executed with the "--no-ff" | |
// option to force a merge commit even if fast-forward is possible. | |
// This ensures that the chain first parents has the commit | |
// identity of the merging user. | |
boolean allRejected = false; | |
for (ReceiveCommand cmd : commands) { | |
String firstParent = null; | |
try { | |
List<RevCommit> commits = JGitUtils.getRevLog(rp.getRepository(), cmd.getOldId().name(), cmd.getNewId().name()); | |
for (RevCommit commit : commits) { | |
if (firstParent != null) { | |
if (!commit.getName().equals(firstParent)) { | |
// ignore: commit is right-descendant of a merge | |
continue; | |
} | |
} | |
// update expected next commit id | |
if (commit.getParentCount() == 0) { | |
firstParent = null; | |
} else { | |
firstParent = commit.getParents()[0].getId().getName(); | |
} | |
PersonIdent committer = commit.getCommitterIdent(); | |
if (!user.is(committer.getName(), committer.getEmailAddress())) { | |
// verification failed | |
String reason = MessageFormat.format("{0} by {1} <{2}> was not committed by {3} ({4}) <{5}>", | |
commit.getId().name(), committer.getName(), StringUtils.isEmpty(committer.getEmailAddress()) ? "?":committer.getEmailAddress(), user.getDisplayName(), user.username, user.emailAddress); | |
LOGGER.warn(reason); | |
cmd.setResult(Result.REJECTED_OTHER_REASON, reason); | |
allRejected &= true; | |
break; | |
} else { | |
allRejected = false; | |
} | |
} | |
} catch (Exception e) { | |
LOGGER.error("Failed to verify commits were made by pushing user", e); | |
} | |
} | |
if (allRejected) { | |
// all ref updates rejected, abort | |
return; | |
} | |
} | |
for (ReceiveCommand cmd : commands) { | |
String ref = cmd.getRefName(); | |
if (ref.startsWith(Constants.R_HEADS)) { | |
switch (cmd.getType()) { | |
case UPDATE_NONFASTFORWARD: | |
case DELETE: | |
// reset branch commit cache on REWIND and DELETE | |
CommitCache.instance().clear(repository.name, ref); | |
break; | |
default: | |
break; | |
} | |
} else if (ref.equals(BranchTicketService.BRANCH)) { | |
// ensure pushing user is an administrator OR an owner | |
// i.e. prevent ticket tampering | |
boolean permitted = user.canAdmin() || repository.isOwner(user.username); | |
if (!permitted) { | |
sendRejection(cmd, "{0} is not permitted to push to {1}", user.username, ref); | |
} | |
} else if (ref.startsWith(Constants.R_FOR)) { | |
// prevent accidental push to refs/for | |
sendRejection(cmd, "{0} is not configured to receive patchsets", repository.name); | |
} | |
} | |
// call pre-receive plugins | |
for (ReceiveHook hook : gitblit.getExtensions(ReceiveHook.class)) { | |
try { | |
hook.onPreReceive(this, commands); | |
} catch (Exception e) { | |
LOGGER.error("Failed to execute extension", e); | |
} | |
} | |
Set<String> scripts = new LinkedHashSet<String>(); | |
scripts.addAll(gitblit.getPreReceiveScriptsInherited(repository)); | |
if (!ArrayUtils.isEmpty(repository.preReceiveScripts)) { | |
scripts.addAll(repository.preReceiveScripts); | |
} | |
runGroovy(commands, scripts); | |
for (ReceiveCommand cmd : commands) { | |
if (!Result.NOT_ATTEMPTED.equals(cmd.getResult())) { | |
LOGGER.warn(MessageFormat.format("{0} {1} because \"{2}\"", cmd.getNewId() | |
.getName(), cmd.getResult(), cmd.getMessage())); | |
} | |
} | |
} | |
/** | |
* Instrumentation point where the incoming push has been applied to the | |
* repository. This is the point where we would trigger a Jenkins build | |
* or send an email. | |
*/ | |
@Override | |
public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) { | |
if (commands.size() == 0) { | |
LOGGER.debug("skipping post-receive processing, no refs created, updated, or removed"); | |
return; | |
} | |
logRefChange(commands); | |
updateIncrementalPushTags(commands); | |
updateGitblitRefLog(commands); | |
// check for updates pushed to the BranchTicketService branch | |
// if the BranchTicketService is active it will reindex, as appropriate | |
for (ReceiveCommand cmd : commands) { | |
if (Result.OK.equals(cmd.getResult()) | |
&& BranchTicketService.BRANCH.equals(cmd.getRefName())) { | |
rp.getRepository().fireEvent(new ReceiveCommandEvent(repository, cmd)); | |
} | |
} | |
// call post-receive plugins | |
for (ReceiveHook hook : gitblit.getExtensions(ReceiveHook.class)) { | |
try { | |
hook.onPostReceive(this, commands); | |
} catch (Exception e) { | |
LOGGER.error("Failed to execute extension", e); | |
} | |
} | |
// run Groovy hook scripts | |
Set<String> scripts = new LinkedHashSet<String>(); | |
scripts.addAll(gitblit.getPostReceiveScriptsInherited(repository)); | |
if (!ArrayUtils.isEmpty(repository.postReceiveScripts)) { | |
scripts.addAll(repository.postReceiveScripts); | |
} | |
runGroovy(commands, scripts); | |
} | |
/** | |
* Log the ref changes in the container log. | |
* | |
* @param commands | |
*/ | |
protected void logRefChange(Collection<ReceiveCommand> commands) { | |
boolean isRefCreationOrDeletion = false; | |
// log ref changes | |
for (ReceiveCommand cmd : commands) { | |
if (Result.OK.equals(cmd.getResult())) { | |
// add some logging for important ref changes | |
switch (cmd.getType()) { | |
case DELETE: | |
LOGGER.info(MessageFormat.format("{0} DELETED {1} in {2} ({3})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name())); | |
isRefCreationOrDeletion = true; | |
break; | |
case CREATE: | |
LOGGER.info(MessageFormat.format("{0} CREATED {1} in {2}", user.username, cmd.getRefName(), repository.name)); | |
isRefCreationOrDeletion = true; | |
break; | |
case UPDATE: | |
LOGGER.info(MessageFormat.format("{0} UPDATED {1} in {2} (from {3} to {4})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name(), cmd.getNewId().name())); | |
break; | |
case UPDATE_NONFASTFORWARD: | |
LOGGER.info(MessageFormat.format("{0} UPDATED NON-FAST-FORWARD {1} in {2} (from {3} to {4})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name(), cmd.getNewId().name())); | |
break; | |
default: | |
break; | |
} | |
} | |
} | |
if (isRefCreationOrDeletion) { | |
gitblit.resetRepositoryCache(repository.name); | |
} | |
} | |
/** | |
* Optionally update the incremental push tags. | |
* | |
* @param commands | |
*/ | |
protected void updateIncrementalPushTags(Collection<ReceiveCommand> commands) { | |
if (!repository.useIncrementalPushTags) { | |
return; | |
} | |
// tag each pushed branch tip | |
String emailAddress = user.emailAddress == null ? getRefLogIdent().getEmailAddress() : user.emailAddress; | |
PersonIdent userIdent = new PersonIdent(user.getDisplayName(), emailAddress); | |
for (ReceiveCommand cmd : commands) { | |
if (!cmd.getRefName().startsWith(Constants.R_HEADS)) { | |
// only tag branch ref changes | |
continue; | |
} | |
if (!ReceiveCommand.Type.DELETE.equals(cmd.getType()) | |
&& ReceiveCommand.Result.OK.equals(cmd.getResult())) { | |
String objectId = cmd.getNewId().getName(); | |
String branch = cmd.getRefName().substring(Constants.R_HEADS.length()); | |
// get translation based on the server's locale setting | |
String template = Translation.get("gb.incrementalPushTagMessage"); | |
String msg = MessageFormat.format(template, branch); | |
String prefix; | |
if (StringUtils.isEmpty(repository.incrementalPushTagPrefix)) { | |
prefix = settings.getString(Keys.git.defaultIncrementalPushTagPrefix, "r"); | |
} else { | |
prefix = repository.incrementalPushTagPrefix; | |
} | |
JGitUtils.createIncrementalRevisionTag( | |
getRepository(), | |
objectId, | |
userIdent, | |
prefix, | |
"0", | |
msg); | |
} | |
} | |
} | |
/** | |
* Update Gitblit's internal reflog. | |
* | |
* @param commands | |
*/ | |
protected void updateGitblitRefLog(Collection<ReceiveCommand> commands) { | |
try { | |
RefLogUtils.updateRefLog(user, getRepository(), commands); | |
LOGGER.debug(MessageFormat.format("{0} reflog updated", repository.name)); | |
} catch (Exception e) { | |
LOGGER.error(MessageFormat.format("Failed to update {0} reflog", repository.name), e); | |
} | |
} | |
/** Execute commands to update references. */ | |
@Override | |
protected void executeCommands() { | |
List<ReceiveCommand> toApply = filterCommands(Result.NOT_ATTEMPTED); | |
if (toApply.isEmpty()) { | |
return; | |
} | |
ProgressMonitor updating = NullProgressMonitor.INSTANCE; | |
boolean sideBand = isCapabilityEnabled(CAPABILITY_SIDE_BAND_64K); | |
if (sideBand) { | |
SideBandProgressMonitor pm = new SideBandProgressMonitor(msgOut); | |
pm.setDelayStart(250, TimeUnit.MILLISECONDS); | |
updating = pm; | |
} | |
BatchRefUpdate batch = getRepository().getRefDatabase().newBatchUpdate(); | |
batch.setAllowNonFastForwards(isAllowNonFastForwards()); | |
batch.setRefLogIdent(getRefLogIdent()); | |
batch.setRefLogMessage("push", true); | |
for (ReceiveCommand cmd : toApply) { | |
if (Result.NOT_ATTEMPTED != cmd.getResult()) { | |
// Already rejected by the core receive process. | |
continue; | |
} | |
batch.addCommand(cmd); | |
} | |
if (!batch.getCommands().isEmpty()) { | |
try { | |
batch.execute(getRevWalk(), updating); | |
} catch (IOException err) { | |
for (ReceiveCommand cmd : toApply) { | |
if (cmd.getResult() == Result.NOT_ATTEMPTED) { | |
sendRejection(cmd, "lock error: {0}", err.getMessage()); | |
} | |
} | |
} | |
} | |
// | |
// if there are ref update receive commands that were | |
// successfully processed and there is an active ticket service for the repository | |
// then process any referenced tickets | |
// | |
if (ticketService != null) { | |
List<ReceiveCommand> allUpdates = ReceiveCommand.filter(batch.getCommands(), Result.OK); | |
if (!allUpdates.isEmpty()) { | |
int ticketsProcessed = 0; | |
for (ReceiveCommand cmd : allUpdates) { | |
switch (cmd.getType()) { | |
case CREATE: | |
case UPDATE: | |
if (cmd.getRefName().startsWith(Constants.R_HEADS)) { | |
Collection<TicketModel> tickets = processReferencedTickets(cmd); | |
ticketsProcessed += tickets.size(); | |
for (TicketModel ticket : tickets) { | |
ticketNotifier.queueMailing(ticket); | |
} | |
} | |
break; | |
case UPDATE_NONFASTFORWARD: | |
if (cmd.getRefName().startsWith(Constants.R_HEADS)) { | |
String base = JGitUtils.getMergeBase(getRepository(), cmd.getOldId(), cmd.getNewId()); | |
List<TicketLink> deletedRefs = JGitUtils.identifyTicketsBetweenCommits(getRepository(), settings, base, cmd.getOldId().name()); | |
for (TicketLink link : deletedRefs) { | |
link.isDelete = true; | |
} | |
Change deletion = new Change(user.username); | |
deletion.pendingLinks = deletedRefs; | |
ticketService.updateTicket(repository, 0, deletion); | |
Collection<TicketModel> tickets = processReferencedTickets(cmd); | |
ticketsProcessed += tickets.size(); | |
for (TicketModel ticket : tickets) { | |
ticketNotifier.queueMailing(ticket); | |
} | |
} | |
break; | |
case DELETE: | |
//Identify if the branch has been merged | |
SortedMap<Integer, String> bases = new TreeMap<Integer, String>(); | |
try { | |
ObjectId dObj = cmd.getOldId(); | |
Collection<Ref> tips = getRepository().getRefDatabase().getRefs(Constants.R_HEADS).values(); | |
for (Ref ref : tips) { | |
ObjectId iObj = ref.getObjectId(); | |
String mergeBase = JGitUtils.getMergeBase(getRepository(), dObj, iObj); | |
if (mergeBase != null) { | |
int d = JGitUtils.countCommits(getRepository(), getRevWalk(), mergeBase, dObj.name()); | |
bases.put(d, mergeBase); | |
//All commits have been merged into some other branch | |
if (d == 0) { | |
break; | |
} | |
} | |
} | |
if (bases.isEmpty()) { | |
//TODO: Handle orphan branch case | |
} else { | |
if (bases.firstKey() > 0) { | |
//Delete references from the remaining commits that haven't been merged | |
String mergeBase = bases.get(bases.firstKey()); | |
List<TicketLink> deletedRefs = JGitUtils.identifyTicketsBetweenCommits(getRepository(), | |
settings, mergeBase, dObj.name()); | |
for (TicketLink link : deletedRefs) { | |
link.isDelete = true; | |
} | |
Change deletion = new Change(user.username); | |
deletion.pendingLinks = deletedRefs; | |
ticketService.updateTicket(repository, 0, deletion); | |
} | |
} | |
} catch (IOException e) { | |
LOGGER.error(null, e); | |
} | |
break; | |
default: | |
break; | |
} | |
} | |
if (ticketsProcessed == 1) { | |
sendInfo("1 ticket updated"); | |
} else if (ticketsProcessed > 1) { | |
sendInfo("{0} tickets updated", ticketsProcessed); | |
} | |
} | |
// reset the ticket caches for the repository | |
ticketService.resetCaches(repository); | |
} | |
} | |
protected void setGitblitUrl(String url) { | |
this.gitblitUrl = url; | |
} | |
public void sendRejection(final ReceiveCommand cmd, final String why, Object... objects) { | |
String text; | |
if (ArrayUtils.isEmpty(objects)) { | |
text = why; | |
} else { | |
text = MessageFormat.format(why, objects); | |
} | |
cmd.setResult(Result.REJECTED_OTHER_REASON, text); | |
LOGGER.error(text + " (" + user.username + ")"); | |
} | |
public void sendHeader(String msg, Object... objects) { | |
sendInfo("--> ", msg, objects); | |
} | |
public void sendInfo(String msg, Object... objects) { | |
sendInfo(" ", msg, objects); | |
} | |
private void sendInfo(String prefix, String msg, Object... objects) { | |
String text; | |
if (ArrayUtils.isEmpty(objects)) { | |
text = msg; | |
super.sendMessage(prefix + msg); | |
} else { | |
text = MessageFormat.format(msg, objects); | |
super.sendMessage(prefix + text); | |
} | |
if (!StringUtils.isEmpty(msg)) { | |
LOGGER.info(text + " (" + user.username + ")"); | |
} | |
} | |
public void sendError(String msg, Object... objects) { | |
String text; | |
if (ArrayUtils.isEmpty(objects)) { | |
text = msg; | |
super.sendError(msg); | |
} else { | |
text = MessageFormat.format(msg, objects); | |
super.sendError(text); | |
} | |
if (!StringUtils.isEmpty(msg)) { | |
LOGGER.error(text + " (" + user.username + ")"); | |
} | |
} | |
/** | |
* Runs the specified Groovy hook scripts. | |
* | |
* @param repository | |
* @param user | |
* @param commands | |
* @param scripts | |
*/ | |
private void runGroovy(Collection<ReceiveCommand> commands, Set<String> scripts) { | |
if (scripts == null || scripts.size() == 0) { | |
// no Groovy scripts to execute | |
return; | |
} | |
Binding binding = new Binding(); | |
binding.setVariable("gitblit", gitblit); | |
binding.setVariable("repository", repository); | |
binding.setVariable("receivePack", this); | |
binding.setVariable("user", user); | |
binding.setVariable("commands", commands); | |
binding.setVariable("url", gitblitUrl); | |
binding.setVariable("logger", LOGGER); | |
binding.setVariable("clientLogger", new ClientLogger(this)); | |
for (String script : scripts) { | |
if (StringUtils.isEmpty(script)) { | |
continue; | |
} | |
// allow script to be specified without .groovy extension | |
// this is easier to read in the settings | |
File file = new File(groovyDir, script); | |
if (!file.exists() && !script.toLowerCase().endsWith(".groovy")) { | |
file = new File(groovyDir, script + ".groovy"); | |
if (file.exists()) { | |
script = file.getName(); | |
} | |
} | |
try { | |
Object result = gse.run(script, binding); | |
if (result instanceof Boolean) { | |
if (!((Boolean) result)) { | |
LOGGER.error(MessageFormat.format( | |
"Groovy script {0} has failed! Hook scripts aborted.", script)); | |
break; | |
} | |
} | |
} catch (Exception e) { | |
LOGGER.error( | |
MessageFormat.format("Failed to execute Groovy script {0}", script), e); | |
} | |
} | |
} | |
public IGitblit getGitblit() { | |
return gitblit; | |
} | |
public RepositoryModel getRepositoryModel() { | |
return repository; | |
} | |
public UserModel getUserModel() { | |
return user; | |
} | |
/** | |
* Automatically closes open tickets and adds references to tickets if made in the commit message. | |
* | |
* @param cmd | |
*/ | |
private Collection<TicketModel> processReferencedTickets(ReceiveCommand cmd) { | |
Map<Long, TicketModel> changedTickets = new LinkedHashMap<Long, TicketModel>(); | |
final RevWalk rw = getRevWalk(); | |
try { | |
rw.reset(); | |
rw.markStart(rw.parseCommit(cmd.getNewId())); | |
if (!ObjectId.zeroId().equals(cmd.getOldId())) { | |
rw.markUninteresting(rw.parseCommit(cmd.getOldId())); | |
} | |
RevCommit c; | |
while ((c = rw.next()) != null) { | |
rw.parseBody(c); | |
List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, c); | |
if (ticketLinks == null) { | |
continue; | |
} | |
for (TicketLink link : ticketLinks) { | |
TicketModel ticket = ticketService.getTicket(repository, link.targetTicketId); | |
if (ticket == null) { | |
continue; | |
} | |
Change change = null; | |
String commitSha = c.getName(); | |
String branchName = Repository.shortenRefName(cmd.getRefName()); | |
switch (link.action) { | |
case Commit: { | |
//A commit can reference a ticket in any branch even if the ticket is closed. | |
//This allows developers to identify and communicate related issues | |
change = new Change(user.username); | |
change.referenceCommit(commitSha); | |
} break; | |
case Close: { | |
// As this isn't a patchset theres no merging taking place when closing a ticket | |
if (ticket.isClosed()) { | |
continue; | |
} | |
change = new Change(user.username); | |
change.setField(Field.status, Status.Fixed); | |
if (StringUtils.isEmpty(ticket.responsible)) { | |
// unassigned tickets are assigned to the closer | |
change.setField(Field.responsible, user.username); | |
} | |
} | |
default: { | |
//No action | |
} break; | |
} | |
if (change != null) { | |
ticket = ticketService.updateTicket(repository, ticket.number, change); | |
} | |
if (ticket != null) { | |
sendInfo(""); | |
sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG)); | |
switch (link.action) { | |
case Commit: { | |
sendInfo("referenced by push of {0} to {1}", commitSha, branchName); | |
changedTickets.put(ticket.number, ticket); | |
} break; | |
case Close: { | |
sendInfo("closed by push of {0} to {1}", commitSha, branchName); | |
changedTickets.put(ticket.number, ticket); | |
} break; | |
default: { } | |
} | |
sendInfo(ticketService.getTicketUrl(ticket)); | |
sendInfo(""); | |
} else { | |
switch (link.action) { | |
case Commit: { | |
sendError("FAILED to reference ticket {0} by push of {1}", link.targetTicketId, commitSha); | |
} break; | |
case Close: { | |
sendError("FAILED to close ticket {0} by push of {1}", link.targetTicketId, commitSha); | |
} break; | |
default: { } | |
} | |
} | |
} | |
} | |
} catch (IOException e) { | |
LOGGER.error("Can't scan for changes to reference or close", e); | |
} finally { | |
rw.reset(); | |
} | |
return changedTickets.values(); | |
} | |
} |