James Moger
2012-09-10 fabe060d3a435f116128851f828e35c2af5fde67
commit | author | age
892570 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  */
896c53 16 package com.gitblit;
JM 17
fa54be 18 import groovy.lang.Binding;
JM 19 import groovy.util.GroovyScriptEngine;
20
21 import java.io.BufferedReader;
22 import java.io.BufferedWriter;
23 import java.io.File;
24 import java.io.IOException;
25 import java.io.InputStreamReader;
26 import java.io.OutputStreamWriter;
27 import java.text.MessageFormat;
28 import java.util.Collection;
367505 29 import java.util.Enumeration;
6cc1d4 30 import java.util.LinkedHashSet;
JM 31 import java.util.Set;
fa54be 32
JM 33 import javax.servlet.ServletConfig;
367505 34 import javax.servlet.ServletContext;
fa54be 35 import javax.servlet.ServletException;
JM 36 import javax.servlet.http.HttpServletRequest;
37
38 import org.eclipse.jgit.http.server.resolver.DefaultReceivePackFactory;
39 import org.eclipse.jgit.lib.PersonIdent;
40 import org.eclipse.jgit.lib.Repository;
41 import org.eclipse.jgit.transport.PostReceiveHook;
42 import org.eclipse.jgit.transport.PreReceiveHook;
43 import org.eclipse.jgit.transport.ReceiveCommand;
44 import org.eclipse.jgit.transport.ReceiveCommand.Result;
45 import org.eclipse.jgit.transport.ReceivePack;
46 import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
47 import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 import com.gitblit.models.RepositoryModel;
52 import com.gitblit.models.UserModel;
a612e6 53 import com.gitblit.utils.ClientLogger;
fa54be 54 import com.gitblit.utils.HttpUtils;
JM 55 import com.gitblit.utils.StringUtils;
56
892570 57 /**
JM 58  * The GitServlet exists to force configuration of the JGit GitServlet based on
59  * the Gitblit settings from either gitblit.properties or from context
60  * parameters in the web.xml file.
fa54be 61  * 
JM 62  * It also implements and registers the Groovy hook mechanism.
892570 63  * 
JM 64  * Access to this servlet is protected by the GitFilter.
65  * 
66  * @author James Moger
67  * 
68  */
896c53 69 public class GitServlet extends org.eclipse.jgit.http.server.GitServlet {
JM 70
71     private static final long serialVersionUID = 1L;
fa54be 72
JM 73     private GroovyScriptEngine gse;
896c53 74
6cc1d4 75     private File groovyDir;
JM 76
fa54be 77     @Override
JM 78     public void init(ServletConfig config) throws ServletException {
367505 79         groovyDir = GitBlit.getGroovyScriptsFolder();
fa54be 80         try {
67d4f8 81             // set Grape root
JM 82             File grapeRoot = new File(GitBlit.getString(Keys.groovy.grapeFolder, "groovy/grape")).getAbsoluteFile();
83             grapeRoot.mkdirs();
84             System.setProperty("grape.root", grapeRoot.getAbsolutePath());
85             
86             gse = new GroovyScriptEngine(groovyDir.getAbsolutePath());            
fa54be 87         } catch (IOException e) {
JM 88             throw new ServletException("Failed to instantiate Groovy Script Engine!", e);
89         }
90
91         // set the Gitblit receive hook
92         setReceivePackFactory(new DefaultReceivePackFactory() {
93             @Override
94             public ReceivePack create(HttpServletRequest req, Repository db)
95                     throws ServiceNotEnabledException, ServiceNotAuthorizedException {
756117 96                 
JM 97                 // determine repository name from request
98                 String repositoryName = req.getPathInfo().substring(1);
99                 repositoryName = GitFilter.getRepositoryName(repositoryName);
100                 
fa54be 101                 GitblitReceiveHook hook = new GitblitReceiveHook();
756117 102                 hook.repositoryName = repositoryName;
fa54be 103                 hook.gitblitUrl = HttpUtils.getGitblitURL(req);
756117 104
JM 105                 ReceivePack rp = super.create(req, db);
fa54be 106                 rp.setPreReceiveHook(hook);
JM 107                 rp.setPostReceiveHook(hook);
108                 return rp;
109             }
110         });
367505 111         super.init(new GitblitServletConfig(config));
JM 112     }
113
114     /**
d7905a 115      * Transitional wrapper class to configure the JGit 1.2 GitFilter. This
JM 116      * GitServlet will probably be replaced by a GitFilter so that Gitblit can
117      * serve Git repositories on the root URL and not a /git sub-url.
367505 118      * 
JM 119      * @author James Moger
120      * 
121      */
122     private class GitblitServletConfig implements ServletConfig {
123         final ServletConfig config;
124
125         GitblitServletConfig(ServletConfig config) {
126             this.config = config;
127         }
128
129         @Override
130         public String getServletName() {
131             return config.getServletName();
132         }
133
134         @Override
135         public ServletContext getServletContext() {
136             return config.getServletContext();
137         }
138
139         @Override
140         public String getInitParameter(String name) {
141             if (name.equals("base-path")) {
142                 return GitBlit.getRepositoriesFolder().getAbsolutePath();
143             } else if (name.equals("export-all")) {
144                 return "1";
145             }
146             return config.getInitParameter(name);
147         }
148
149         @Override
150         public Enumeration<String> getInitParameterNames() {
151             return config.getInitParameterNames();
152         }
fa54be 153     }
JM 154
155     /**
156      * The Gitblit receive hook allows for special processing on push events.
157      * That might include rejecting writes to specific branches or executing a
158      * script.
159      * 
160      * @author James Moger
161      * 
162      */
163     private class GitblitReceiveHook implements PreReceiveHook, PostReceiveHook {
164
165         protected final Logger logger = LoggerFactory.getLogger(GitblitReceiveHook.class);
166
756117 167         protected String repositoryName;
JM 168         
fa54be 169         protected String gitblitUrl;
JM 170
171         /**
172          * Instrumentation point where the incoming push event has been parsed,
173          * validated, objects created BUT refs have not been updated. You might
174          * use this to enforce a branch-write permissions model.
175          */
176         @Override
177         public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
756117 178             RepositoryModel repository = GitBlit.self().getRepositoryModel(repositoryName);
d7905a 179             Set<String> scripts = new LinkedHashSet<String>();
JM 180             scripts.addAll(GitBlit.self().getPreReceiveScriptsInherited(repository));
fa54be 181             scripts.addAll(repository.preReceiveScripts);
JM 182             UserModel user = getUserModel(rp);
a612e6 183             runGroovy(repository, user, commands, rp, scripts);
fa54be 184             for (ReceiveCommand cmd : commands) {
JM 185                 if (!Result.NOT_ATTEMPTED.equals(cmd.getResult())) {
186                     logger.warn(MessageFormat.format("{0} {1} because \"{2}\"", cmd.getNewId()
187                             .getName(), cmd.getResult(), cmd.getMessage()));
188                 }
189             }
190
191             // Experimental
192             // runNativeScript(rp, "hooks/pre-receive", commands);
193         }
194
195         /**
196          * Instrumentation point where the incoming push has been applied to the
197          * repository. This is the point where we would trigger a Jenkins build
198          * or send an email.
199          */
200         @Override
201         public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
202             if (commands.size() == 0) {
203                 logger.info("skipping post-receive hooks, no refs created, updated, or removed");
204                 return;
205             }
756117 206             RepositoryModel repository = GitBlit.self().getRepositoryModel(repositoryName);
d7905a 207             Set<String> scripts = new LinkedHashSet<String>();
JM 208             scripts.addAll(GitBlit.self().getPostReceiveScriptsInherited(repository));
fa54be 209             scripts.addAll(repository.postReceiveScripts);
JM 210             UserModel user = getUserModel(rp);
a612e6 211             runGroovy(repository, user, commands, rp, scripts);
fa54be 212
JM 213             // Experimental
214             // runNativeScript(rp, "hooks/post-receive", commands);
215         }
216
217         /**
218          * Returns the UserModel for the user pushing the changes.
219          * 
220          * @param rp
221          * @return a UserModel
222          */
223         protected UserModel getUserModel(ReceivePack rp) {
224             PersonIdent person = rp.getRefLogIdent();
225             UserModel user = GitBlit.self().getUserModel(person.getName());
226             if (user == null) {
227                 // anonymous push, create a temporary usermodel
228                 user = new UserModel(person.getName());
6adf56 229                 user.isAuthenticated = false;
fa54be 230             }
JM 231             return user;
232         }
233
234         /**
235          * Runs the specified Groovy hook scripts.
236          * 
237          * @param repository
238          * @param user
239          * @param commands
240          * @param scripts
241          */
242         protected void runGroovy(RepositoryModel repository, UserModel user,
a612e6 243                 Collection<ReceiveCommand> commands, ReceivePack rp, Set<String> scripts) {
fa54be 244             if (scripts == null || scripts.size() == 0) {
JM 245                 // no Groovy scripts to execute
246                 return;
247             }
248
249             Binding binding = new Binding();
250             binding.setVariable("gitblit", GitBlit.self());
251             binding.setVariable("repository", repository);
252             binding.setVariable("user", user);
253             binding.setVariable("commands", commands);
254             binding.setVariable("url", gitblitUrl);
255             binding.setVariable("logger", logger);
a612e6 256             binding.setVariable("clientLogger", new ClientLogger(rp));
fa54be 257             for (String script : scripts) {
JM 258                 if (StringUtils.isEmpty(script)) {
259                     continue;
260                 }
6cc1d4 261                 // allow script to be specified without .groovy extension
JM 262                 // this is easier to read in the settings
263                 File file = new File(groovyDir, script);
264                 if (!file.exists() && !script.toLowerCase().endsWith(".groovy")) {
265                     file = new File(groovyDir, script + ".groovy");
266                     if (file.exists()) {
267                         script = file.getName();
268                     }
269                 }
fa54be 270                 try {
JM 271                     Object result = gse.run(script, binding);
272                     if (result instanceof Boolean) {
273                         if (!((Boolean) result)) {
274                             logger.error(MessageFormat.format(
275                                     "Groovy script {0} has failed!  Hook scripts aborted.", script));
276                             break;
277                         }
278                     }
279                 } catch (Exception e) {
280                     logger.error(
281                             MessageFormat.format("Failed to execute Groovy script {0}", script), e);
282                 }
283             }
284         }
285
286         /**
287          * Runs the native push hook script.
288          * 
289          * http://book.git-scm.com/5_git_hooks.html
290          * http://longair.net/blog/2011/04/09/missing-git-hooks-documentation/
291          * 
292          * @param rp
293          * @param script
294          * @param commands
295          */
296         @SuppressWarnings("unused")
297         protected void runNativeScript(ReceivePack rp, String script,
298                 Collection<ReceiveCommand> commands) {
299
300             Repository repository = rp.getRepository();
301             File scriptFile = new File(repository.getDirectory(), script);
302
303             int resultCode = 0;
304             if (scriptFile.exists()) {
305                 try {
306                     logger.debug("executing " + scriptFile);
307                     Process process = Runtime.getRuntime().exec(scriptFile.getAbsolutePath(), null,
308                             repository.getDirectory());
309                     BufferedReader reader = new BufferedReader(new InputStreamReader(
310                             process.getInputStream()));
311                     BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
312                             process.getOutputStream()));
313                     for (ReceiveCommand command : commands) {
314                         switch (command.getType()) {
315                         case UPDATE:
316                             // updating a ref
317                             writer.append(MessageFormat.format("{0} {1} {2}\n", command.getOldId()
318                                     .getName(), command.getNewId().getName(), command.getRefName()));
319                             break;
320                         case CREATE:
321                             // new ref
322                             // oldrev hard-coded to 40? weird.
323                             writer.append(MessageFormat.format("40 {0} {1}\n", command.getNewId()
324                                     .getName(), command.getRefName()));
325                             break;
326                         }
327                     }
328                     resultCode = process.waitFor();
329
330                     // read and buffer stdin
331                     // this is supposed to be piped back to the git client.
332                     // not sure how to do that right now.
333                     StringBuilder sb = new StringBuilder();
334                     String line = null;
335                     while ((line = reader.readLine()) != null) {
336                         sb.append(line).append('\n');
337                     }
338                     logger.debug(sb.toString());
339                 } catch (Throwable e) {
340                     resultCode = -1;
341                     logger.error(
342                             MessageFormat.format("Failed to execute {0}",
343                                     scriptFile.getAbsolutePath()), e);
344                 }
345             }
346
347             // reject push
348             if (resultCode != 0) {
349                 for (ReceiveCommand command : commands) {
350                     command.setResult(Result.REJECTED_OTHER_REASON, MessageFormat.format(
351                             "Native script {0} rejected push or failed",
352                             scriptFile.getAbsolutePath()));
353                 }
354             }
355         }
356     }
896c53 357 }