James Moger
2013-07-02 2e4b03f7fe33ed5b84ec98ce689f3e1cabf97bff
commit | author | age
75bca8 1 /*
JM 2  * Copyright 2011 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 package com.gitblit.git;
17
18 import groovy.lang.Binding;
19 import groovy.util.GroovyScriptEngine;
20
21 import java.io.File;
22 import java.io.IOException;
23 import java.text.MessageFormat;
24 import java.util.Collection;
25 import java.util.LinkedHashSet;
26 import java.util.List;
27 import java.util.Set;
28
29 import org.eclipse.jgit.lib.PersonIdent;
30 import org.eclipse.jgit.revwalk.RevCommit;
31 import org.eclipse.jgit.transport.PostReceiveHook;
32 import org.eclipse.jgit.transport.PreReceiveHook;
33 import org.eclipse.jgit.transport.ReceiveCommand;
34 import org.eclipse.jgit.transport.ReceiveCommand.Result;
35 import org.eclipse.jgit.transport.ReceivePack;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 import com.gitblit.Constants.AccessRestrictionType;
40 import com.gitblit.GitBlit;
41 import com.gitblit.Keys;
42 import com.gitblit.client.Translation;
43 import com.gitblit.models.RepositoryModel;
44 import com.gitblit.models.UserModel;
978820 45 import com.gitblit.utils.ArrayUtils;
75bca8 46 import com.gitblit.utils.ClientLogger;
JM 47 import com.gitblit.utils.JGitUtils;
ff7d3c 48 import com.gitblit.utils.RefLogUtils;
75bca8 49 import com.gitblit.utils.StringUtils;
JM 50
51 /**
52  * The Gitblit receive hook allows for special processing on push events.
53  * That might include rejecting writes to specific branches or executing a
54  * script.
55  * 
56  * @author James Moger
57  * 
58  */
59 public class ReceiveHook implements PreReceiveHook, PostReceiveHook {
60
61     protected final Logger logger = LoggerFactory.getLogger(ReceiveHook.class);
62
63     protected UserModel user;
64     
65     protected RepositoryModel repository;
66
67     protected String gitblitUrl;
68
69     private GroovyScriptEngine gse;
70
71     private File groovyDir;
72
73     public ReceiveHook() {
74         groovyDir = GitBlit.getGroovyScriptsFolder();
75         try {
76             // set Grape root
77             File grapeRoot = GitBlit.getFileOrFolder(Keys.groovy.grapeFolder, "${baseFolder}/groovy/grape").getAbsoluteFile();
78             grapeRoot.mkdirs();
79             System.setProperty("grape.root", grapeRoot.getAbsolutePath());
80
81             gse = new GroovyScriptEngine(groovyDir.getAbsolutePath());            
82         } catch (IOException e) {
83             //throw new ServletException("Failed to instantiate Groovy Script Engine!", e);
84         }
85     }
86
87     /**
88      * Instrumentation point where the incoming push event has been parsed,
89      * validated, objects created BUT refs have not been updated. You might
90      * use this to enforce a branch-write permissions model.
91      */
92     @Override
93     public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
94         if (repository.isFrozen) {
95             // repository is frozen/readonly
96             String reason = MessageFormat.format("Gitblit does not allow pushes to \"{0}\" because it is frozen!", repository.name);
97             logger.warn(reason);
98             for (ReceiveCommand cmd : commands) {
99                 cmd.setResult(Result.REJECTED_OTHER_REASON, reason);
100             }
101             return;
102         }
103         
104         if (!repository.isBare) {
105             // repository has a working copy
106             String reason = MessageFormat.format("Gitblit does not allow pushes to \"{0}\" because it has a working copy!", repository.name);
107             logger.warn(reason);
108             for (ReceiveCommand cmd : commands) {
109                 cmd.setResult(Result.REJECTED_OTHER_REASON, reason);
110             }
111             return;
112         }
113
114         if (!user.canPush(repository)) {
115             // user does not have push permissions
116             String reason = MessageFormat.format("User \"{0}\" does not have push permissions for \"{1}\"!", user.username, repository.name);
117             logger.warn(reason);
118             for (ReceiveCommand cmd : commands) {
119                 cmd.setResult(Result.REJECTED_OTHER_REASON, reason);
120             }
121             return;
122         }
123
124         if (repository.accessRestriction.atLeast(AccessRestrictionType.PUSH) && repository.verifyCommitter) {
125             // enforce committer verification
126             if (StringUtils.isEmpty(user.emailAddress)) {
127                 // emit warning if user does not have an email address 
128                 logger.warn(MessageFormat.format("Consider setting an email address for {0} ({1}) to improve committer verification.", user.getDisplayName(), user.username));
129             }
130
131             // Optionally enforce that the committer of the left parent chain
132             // match the account being used to push the commits.
133             // 
134             // This requires all merge commits are executed with the "--no-ff"
135             // option to force a merge commit even if fast-forward is possible.
136             // This ensures that the chain of left parents has the commit
137             // identity of the merging user.
138             boolean allRejected = false;
139             for (ReceiveCommand cmd : commands) {
140                 try {
141                     List<RevCommit> commits = JGitUtils.getRevLog(rp.getRepository(), cmd.getOldId().name(), cmd.getNewId().name());
142                     for (RevCommit commit : commits) {
143                         PersonIdent committer = commit.getCommitterIdent();
144                         if (!user.is(committer.getName(), committer.getEmailAddress())) {
145                             String reason;
146                             if (StringUtils.isEmpty(user.emailAddress)) {
147                                 // account does not have en email address
148                                 reason = MessageFormat.format("{0} by {1} <{2}> was not committed by {3} ({4})", commit.getId().name(), committer.getName(), StringUtils.isEmpty(committer.getEmailAddress()) ? "?":committer.getEmailAddress(), user.getDisplayName(), user.username);
149                             } else {
150                                 // account has an email address
151                                 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);
152                             }
153                             logger.warn(reason);
154                             cmd.setResult(Result.REJECTED_OTHER_REASON, reason);
155                             allRejected &= true;
156                             break;
157                         } else {
158                             allRejected = false;
159                         }
160                     }
161                 } catch (Exception e) {
162                     logger.error("Failed to verify commits were made by pushing user", e);
163                 }
164             }
165
166             if (allRejected) {
167                 // all ref updates rejected, abort
168                 return;
169             }
170         }
171
172         Set<String> scripts = new LinkedHashSet<String>();
173         scripts.addAll(GitBlit.self().getPreReceiveScriptsInherited(repository));
978820 174         if (!ArrayUtils.isEmpty(repository.preReceiveScripts)) {
JM 175             scripts.addAll(repository.preReceiveScripts);
176         }
75bca8 177         runGroovy(repository, user, commands, rp, scripts);
JM 178         for (ReceiveCommand cmd : commands) {
179             if (!Result.NOT_ATTEMPTED.equals(cmd.getResult())) {
180                 logger.warn(MessageFormat.format("{0} {1} because \"{2}\"", cmd.getNewId()
181                         .getName(), cmd.getResult(), cmd.getMessage()));
182             }
183         }
184     }
185
186     /**
187      * Instrumentation point where the incoming push has been applied to the
188      * repository. This is the point where we would trigger a Jenkins build
189      * or send an email.
190      */
191     @Override
192     public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
193         if (commands.size() == 0) {
194             logger.debug("skipping post-receive hooks, no refs created, updated, or removed");
195             return;
196         }
197
198         // log ref changes
199         for (ReceiveCommand cmd : commands) {
200             if (Result.OK.equals(cmd.getResult())) {
201                 // add some logging for important ref changes
202                 switch (cmd.getType()) {
203                 case DELETE:
204                     logger.info(MessageFormat.format("{0} DELETED {1} in {2} ({3})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name()));
205                     break;
206                 case CREATE:
207                     logger.info(MessageFormat.format("{0} CREATED {1} in {2}", user.username, cmd.getRefName(), repository.name));
208                     break;
209                 case UPDATE:
210                     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()));
211                     break;
212                 case UPDATE_NONFASTFORWARD:
213                     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()));
214                     break;
215                 default:
216                     break;
217                 }
218             }
219         }
220
221         if (repository.useIncrementalPushTags) {
222             // tag each pushed branch tip
223             String emailAddress = user.emailAddress == null ? rp.getRefLogIdent().getEmailAddress() : user.emailAddress;
224             PersonIdent userIdent = new PersonIdent(user.getDisplayName(), emailAddress);
225
226             for (ReceiveCommand cmd : commands) {
227                 if (!cmd.getRefName().startsWith("refs/heads/")) {
228                     // only tag branch ref changes
229                     continue;
230                 }
231
232                 if (!ReceiveCommand.Type.DELETE.equals(cmd.getType())
233                         && ReceiveCommand.Result.OK.equals(cmd.getResult())) {
234                     String objectId = cmd.getNewId().getName();
235                     String branch = cmd.getRefName().substring("refs/heads/".length());
236                     // get translation based on the server's locale setting
237                     String template = Translation.get("gb.incrementalPushTagMessage");
238                     String msg = MessageFormat.format(template, branch);
239                     String prefix;
240                     if (StringUtils.isEmpty(repository.incrementalPushTagPrefix)) {
241                         prefix = GitBlit.getString(Keys.git.defaultIncrementalPushTagPrefix, "r");
242                     } else {
243                         prefix = repository.incrementalPushTagPrefix;
244                     }
245
246                     JGitUtils.createIncrementalRevisionTag(
247                             rp.getRepository(),
248                             objectId,
249                             userIdent,
250                             prefix,
251                             "0",
252                             msg);
253                 }
254             }                
255         }
256
257         // update push log
258         try {
ff7d3c 259             RefLogUtils.updateRefLog(user, rp.getRepository(), commands);
75bca8 260             logger.debug(MessageFormat.format("{0} push log updated", repository.name));
JM 261         } catch (Exception e) {
262             logger.error(MessageFormat.format("Failed to update {0} pushlog", repository.name), e);
263         }
264
265         // run Groovy hook scripts 
266         Set<String> scripts = new LinkedHashSet<String>();
267         scripts.addAll(GitBlit.self().getPostReceiveScriptsInherited(repository));
978820 268         if (!ArrayUtils.isEmpty(repository.postReceiveScripts)) {
JM 269             scripts.addAll(repository.postReceiveScripts);
270         }
75bca8 271         runGroovy(repository, user, commands, rp, scripts);
JM 272     }
273
274     /**
275      * Runs the specified Groovy hook scripts.
276      * 
277      * @param repository
278      * @param user
279      * @param commands
280      * @param scripts
281      */
282     protected void runGroovy(RepositoryModel repository, UserModel user,
283             Collection<ReceiveCommand> commands, ReceivePack rp, Set<String> scripts) {
284         if (scripts == null || scripts.size() == 0) {
285             // no Groovy scripts to execute
286             return;
287         }
288
289         Binding binding = new Binding();
290         binding.setVariable("gitblit", GitBlit.self());
291         binding.setVariable("repository", repository);
292         binding.setVariable("receivePack", rp);
293         binding.setVariable("user", user);
294         binding.setVariable("commands", commands);
295         binding.setVariable("url", gitblitUrl);
296         binding.setVariable("logger", logger);
297         binding.setVariable("clientLogger", new ClientLogger(rp));
298         for (String script : scripts) {
299             if (StringUtils.isEmpty(script)) {
300                 continue;
301             }
302             // allow script to be specified without .groovy extension
303             // this is easier to read in the settings
304             File file = new File(groovyDir, script);
305             if (!file.exists() && !script.toLowerCase().endsWith(".groovy")) {
306                 file = new File(groovyDir, script + ".groovy");
307                 if (file.exists()) {
308                     script = file.getName();
309                 }
310             }
311             try {
312                 Object result = gse.run(script, binding);
313                 if (result instanceof Boolean) {
314                     if (!((Boolean) result)) {
315                         logger.error(MessageFormat.format(
316                                 "Groovy script {0} has failed!  Hook scripts aborted.", script));
317                         break;
318                     }
319                 }
320             } catch (Exception e) {
321                 logger.error(
322                         MessageFormat.format("Failed to execute Groovy script {0}", script), e);
323             }
324         }
325     }
326 }