docs/01_features.mkd
@@ -19,6 +19,7 @@ - Repository Owners may edit repositories through the web UI - Gravatar integration - Git-notes display support - gh-pages display support (Jekyll is not supported) - Branch metrics (uses Google Charts) - HEAD and Branch RSS feeds - Blame annotations view docs/04_releases.mkd
@@ -33,6 +33,8 @@ **New:** *web.allowFlashCopyToClipboard = true* - JavaScript-based 3-step (click, ctrl+c, enter) *copy to clipboard* of the primary repository url in the event that you do not want to use Flash on your installation - Empty repositories now link to an *empty repository* page which gives some direction to the user for the next step in using Gitblit. This page displays the primary push/clone url of the repository and gives sample syntax for the git command-line client. (issue 31) - automatic *gh-pages* branch serving (Jekyll is not supported) Gitblit does not checkout your gh-pages branch to a temporary filesystem, all page and resource requests are live through the repository - Gitblit Express bundle to get started running Gitblit on RedHat's OpenShift cloud <span class="label warning">BETA</span> #### changes src/WEB-INF/web.xml
@@ -84,6 +84,21 @@ </servlet-mapping> <!-- Pages Servlet <url-pattern> MUST match: * PagesFilter * com.gitblit.Constants.PAGES_PATH * Wicket Filter ignorePaths parameter --> <servlet> <servlet-name>PagesServlet</servlet-name> <servlet-class>com.gitblit.PagesServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>PagesServlet</servlet-name> <url-pattern>/pages/*</url-pattern> </servlet-mapping> <!-- Git Access Restriction Filter <url-pattern> MUST match: * GitServlet @@ -144,6 +159,21 @@ </filter-mapping> <!-- Pges Restriction Filter <url-pattern> MUST match: * PagesServlet * com.gitblit.Constants.PAGES_PATH * Wicket Filter ignorePaths parameter --> <filter> <filter-name>PagesFilter</filter-name> <filter-class>com.gitblit.PagesFilter</filter-class> </filter> <filter-mapping> <filter-name>PagesFilter</filter-name> <url-pattern>/pages/*</url-pattern> </filter-mapping> <!-- Wicket Filter --> <filter> <filter-name>wicketFilter</filter-name> @@ -168,8 +198,11 @@ * com.gitblit.Constants.ZIP_PATH * FederationServlet <url-pattern> * RpcFilter <url-pattern> * RpcServlet <url-pattern> --> <param-value>git/,feed/,zip/,federation/,rpc/</param-value> * RpcServlet <url-pattern> * PagesFilter <url-pattern> * PagesServlet <url-pattern> * com.gitblit.Constants.PAGES_PATH --> <param-value>git/,feed/,zip/,federation/,rpc/,pages/</param-value> </init-param> </filter> <filter-mapping> src/com/gitblit/GitBlit.java
@@ -653,22 +653,39 @@ * @return repository or null */ public Repository getRepository(String repositoryName) { return getRepository(repositoryName, true); } /** * Returns the JGit repository for the specified name. * * @param repositoryName * @param logError * @return repository or null */ public Repository getRepository(String repositoryName, boolean logError) { Repository r = null; try { r = repositoryResolver.open(null, repositoryName); } catch (RepositoryNotFoundException e) { r = null; if (logError) { logger.error("GitBlit.getRepository(String) failed to find " + new File(repositoriesFolder, repositoryName).getAbsolutePath()); } } catch (ServiceNotAuthorizedException e) { r = null; if (logError) { logger.error("GitBlit.getRepository(String) failed to find " + new File(repositoriesFolder, repositoryName).getAbsolutePath(), e); } } catch (ServiceNotEnabledException e) { r = null; if (logError) { logger.error("GitBlit.getRepository(String) failed to find " + new File(repositoriesFolder, repositoryName).getAbsolutePath(), e); } } return r; } src/com/gitblit/PagesFilter.java
New file @@ -0,0 +1,103 @@ /* * Copyright 2012 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; import org.eclipse.jgit.lib.Repository; import com.gitblit.Constants.AccessRestrictionType; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; /** * The PagesFilter is an AccessRestrictionFilter which ensures the gh-pages * requests for a view-restricted repository are authenticated and authorized. * * @author James Moger * */ public class PagesFilter extends AccessRestrictionFilter { /** * Extract the repository name from the url. * * @param url * @return repository name */ @Override protected String extractRepositoryName(String url) { // get the repository name from the url by finding a known url suffix String repository = ""; Repository r = null; int offset = 0; while (r == null) { int slash = url.indexOf('/', offset); if (slash == -1) { repository = url; } else { repository = url.substring(0, slash); } r = GitBlit.self().getRepository(repository, false); if (r == null) { // try again offset = slash + 1; } else { // close the repo r.close(); } if (repository.equals(url)) { // either only repository in url or no repository found break; } } return repository; } /** * Analyze the url and returns the action of the request. * * @param url * @return action of the request */ @Override protected String getUrlRequestAction(String suffix) { return "VIEW"; } /** * Determine if the repository requires authentication. * * @param repository * @return true if authentication required */ @Override protected boolean requiresAuthentication(RepositoryModel repository) { return repository.accessRestriction.atLeast(AccessRestrictionType.VIEW); } /** * Determine if the user can access the repository and perform the specified * action. * * @param repository * @param user * @param action * @return true if user may execute the action on the repository */ @Override protected boolean canAccess(RepositoryModel repository, UserModel user, String action) { return user.canAccessRepository(repository); } } src/com/gitblit/PagesServlet.java
New file @@ -0,0 +1,227 @@ /* * Copyright 2012 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; import java.io.IOException; import java.text.MessageFormat; import java.text.ParseException; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTree; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gitblit.models.RefModel; import com.gitblit.utils.ArrayUtils; import com.gitblit.utils.JGitUtils; import com.gitblit.utils.MarkdownUtils; import com.gitblit.utils.StringUtils; /** * Serves the content of a gh-pages branch. * * @author James Moger * */ public class PagesServlet extends HttpServlet { private static final long serialVersionUID = 1L; private transient Logger logger = LoggerFactory.getLogger(PagesServlet.class); public PagesServlet() { super(); } /** * Returns an url to this servlet for the specified parameters. * * @param baseURL * @param repository * @param path * @return an url */ public static String asLink(String baseURL, String repository, String path) { if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') { baseURL = baseURL.substring(0, baseURL.length() - 1); } return baseURL + Constants.PAGES + repository + "/" + (path == null ? "" : ("/" + path)); } /** * Retrieves the specified resource from the gh-pages branch of the * repository. * * @param request * @param response * @throws javax.servlet.ServletException * @throws java.io.IOException */ private void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String path = request.getPathInfo(); if (path.toLowerCase().endsWith(".git")) { // forward to url with trailing / // this is important for relative pages links response.sendRedirect(request.getServletPath() + path + "/"); return; } if (path.charAt(0) == '/') { // strip leading / path = path.substring(1); } // determine repository and resource from url String repository = ""; String resource = ""; Repository r = null; int offset = 0; while (r == null) { int slash = path.indexOf('/', offset); if (slash == -1) { repository = path; } else { repository = path.substring(0, slash); } r = GitBlit.self().getRepository(repository, false); offset = slash + 1; if (offset > 0) { resource = path.substring(offset); } if (repository.equals(path)) { // either only repository in url or no repository found break; } } ServletContext context = request.getSession().getServletContext(); try { if (r == null) { // repository not found! String mkd = MessageFormat.format( "# Error\nSorry, no valid **repository** specified in this url: {0}!", repository); error(response, mkd); return; } // retrieve the content from the repository RefModel pages = JGitUtils.getPagesBranch(r); RevCommit commit = JGitUtils.getCommit(r, pages.getObjectId().getName()); if (commit == null) { // branch not found! String mkd = MessageFormat.format( "# Error\nSorry, the repository {0} does not have a **gh-pages** branch!", repository); error(response, mkd); r.close(); return; } response.setDateHeader("Last-Modified", JGitUtils.getCommitDate(commit).getTime()); RevTree tree = commit.getTree(); byte[] content = null; if (StringUtils.isEmpty(resource)) { // find resource String[] files = { "index.html", "index.htm", "index.mkd" }; for (String file : files) { content = JGitUtils.getStringContent(r, tree, file) .getBytes(Constants.ENCODING); if (content != null) { resource = file; // assume text/html unless the servlet container // overrides response.setContentType("text/html; charset=" + Constants.ENCODING); break; } } } else { // specific resource String contentType = context.getMimeType(resource); if (contentType.startsWith("text")) { content = JGitUtils.getStringContent(r, tree, resource).getBytes( Constants.ENCODING); } else { content = JGitUtils.getByteContent(r, tree, resource); } response.setContentType(contentType); } // no content, try custom 404 page if (ArrayUtils.isEmpty(content)) { content = JGitUtils.getStringContent(r, tree, "404.html").getBytes( Constants.ENCODING); // still no content if (ArrayUtils.isEmpty(content)) { content = (MessageFormat.format( "# Error\nSorry, the requested resource **{0}** was not found.", resource)).getBytes(Constants.ENCODING); resource = "404.mkd"; } } // check to see if we should transform markdown files for (String ext : GitBlit.getStrings(Keys.web.markdownExtensions)) { if (resource.endsWith(ext)) { String mkd = new String(content, Constants.ENCODING); content = MarkdownUtils.transformMarkdown(mkd).getBytes(Constants.ENCODING); break; } } try { // output the content response.getOutputStream().write(content); response.flushBuffer(); } catch (Throwable t) { logger.error("Failed to write page to client", t); } // close the repository r.close(); } catch (Throwable t) { logger.error("Failed to write page to client", t); } } private void error(HttpServletResponse response, String mkd) throws ServletException, IOException, ParseException { String content = MarkdownUtils.transformMarkdown(mkd); response.setContentType("text/html; charset=" + Constants.ENCODING); response.getWriter().write(content); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } } src/com/gitblit/utils/ArrayUtils.java
@@ -26,6 +26,10 @@ */ public class ArrayUtils { public static boolean isEmpty(byte [] array) { return array == null || array.length == 0; } public static boolean isEmpty(Object [] array) { return array == null || array.length == 0; } src/com/gitblit/utils/JGitUtils.java
@@ -1284,6 +1284,39 @@ } /** * Returns a RefModel for the gh-pages branch in the repository. If the * branch can not be found, null is returned. * * @param repository * @return a refmodel for the gh-pages branch or null */ public static RefModel getPagesBranch(Repository repository) { RefModel ghPages = null; try { // search for gh-pages branch in local heads for (RefModel ref : JGitUtils.getLocalBranches(repository, false, -1)) { if (ref.displayName.endsWith("gh-pages")) { ghPages = ref; break; } } // search for gh-pages branch in remote heads if (ghPages == null) { for (RefModel ref : JGitUtils.getRemoteBranches(repository, false, -1)) { if (ref.displayName.endsWith("gh-pages")) { ghPages = ref; break; } } } } catch (Throwable t) { LOGGER.error("Failed to find gh-pages branch!", t); } return ghPages; } /** * Returns the list of notes entered about the commit from the refs/notes * namespace. If the repository does not exist or is empty, an empty list is * returned. src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -209,3 +209,4 @@ gb.federationRepositoryDescription = share this repository with other Gitblit servers gb.hookScriptsDescription = run Groovy scripts on pushes to this Gitblit server gb.reset = reset gb.pages = pages src/com/gitblit/wicket/PageRegistration.java
@@ -49,6 +49,24 @@ } /** * Represents a page link to a non-Wicket page. Might be external. * * @author James Moger * */ public static class OtherPageLink extends PageRegistration { private static final long serialVersionUID = 1L; public final String url; public OtherPageLink(String translationKey, String url) { super(translationKey, null); this.url = url; } } /** * Represents a DropDownMenu for the topbar * * @author James Moger src/com/gitblit/wicket/pages/RepositoryPage.java
@@ -41,6 +41,7 @@ import com.gitblit.Constants; import com.gitblit.GitBlit; import com.gitblit.Keys; import com.gitblit.PagesServlet; import com.gitblit.SyndicationServlet; import com.gitblit.models.RepositoryModel; import com.gitblit.utils.JGitUtils; @@ -48,6 +49,7 @@ import com.gitblit.utils.TicgitUtils; import com.gitblit.wicket.GitBlitWebSession; import com.gitblit.wicket.PageRegistration; import com.gitblit.wicket.PageRegistration.OtherPageLink; import com.gitblit.wicket.WicketUtils; import com.gitblit.wicket.panels.LinkPanel; import com.gitblit.wicket.panels.NavigationPanel; @@ -123,6 +125,12 @@ if (model.useDocs) { pages.put("docs", new PageRegistration("gb.docs", DocsPage.class, params)); } if (JGitUtils.getPagesBranch(r) != null) { OtherPageLink pagesLink = new OtherPageLink("gb.pages", PagesServlet.asLink( getRequest().getRelativePathPrefixToContextRoot(), repositoryName, null)); pages.put("pages", pagesLink); } // Conditionally add edit link final boolean showAdmin; if (GitBlit.getBoolean(Keys.web.authenticateAdminPages, true)) { @@ -142,8 +150,8 @@ @Override protected void setupPage(String repositoryName, String pageName) { add(new LinkPanel("repositoryName", null, StringUtils.stripDotGit(repositoryName), SummaryPage.class, WicketUtils.newRepositoryParameter(repositoryName))); add(new LinkPanel("repositoryName", null, StringUtils.stripDotGit(repositoryName), SummaryPage.class, WicketUtils.newRepositoryParameter(repositoryName))); add(new Label("pageName", pageName)); super.setupPage(repositoryName, pageName); @@ -245,7 +253,8 @@ } } protected void setPersonSearchTooltip(Component component, String value, Constants.SearchType searchType) { protected void setPersonSearchTooltip(Component component, String value, Constants.SearchType searchType) { if (searchType.equals(Constants.SearchType.AUTHOR)) { WicketUtils.setHtmlTooltip(component, getString("gb.searchForAuthor") + " " + value); } else if (searchType.equals(Constants.SearchType.COMMITTER)) { @@ -302,13 +311,14 @@ private final IModel<String> searchBoxModel = new Model<String>(""); private final IModel<Constants.SearchType> searchTypeModel = new Model<Constants.SearchType>(Constants.SearchType.COMMIT); private final IModel<Constants.SearchType> searchTypeModel = new Model<Constants.SearchType>( Constants.SearchType.COMMIT); public SearchForm(String id, String repositoryName) { super(id); this.repositoryName = repositoryName; DropDownChoice<Constants.SearchType> searchType = new DropDownChoice<Constants.SearchType>("searchType", Arrays.asList(Constants.SearchType.values())); DropDownChoice<Constants.SearchType> searchType = new DropDownChoice<Constants.SearchType>( "searchType", Arrays.asList(Constants.SearchType.values())); searchType.setModel(searchTypeModel); add(searchType.setVisible(GitBlit.getBoolean(Keys.web.showSearchTypeSelection, false))); TextField<String> searchBox = new TextField<String>("searchBox", searchBoxModel); src/com/gitblit/wicket/panels/LinkPanel.java
@@ -20,6 +20,7 @@ import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.link.BookmarkablePageLink; import org.apache.wicket.markup.html.link.ExternalLink; import org.apache.wicket.markup.html.link.Link; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.IModel; @@ -71,4 +72,23 @@ add(link); } public LinkPanel(String wicketId, String linkCssClass, String label, String href) { this(wicketId, linkCssClass, label, href, false); } public LinkPanel(String wicketId, String linkCssClass, String label, String href, boolean newWindow) { super(wicketId); this.labelModel = new Model<String>(label); ExternalLink link = new ExternalLink("link", href); if (newWindow) { link.add(new SimpleAttributeModifier("target", "_blank")); } if (linkCssClass != null) { link.add(new SimpleAttributeModifier("class", linkCssClass)); } link.add(new Label("label", labelModel)); add(link); } } src/com/gitblit/wicket/panels/NavigationPanel.java
@@ -25,6 +25,7 @@ import com.gitblit.wicket.PageRegistration; import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration; import com.gitblit.wicket.PageRegistration.OtherPageLink; import com.gitblit.wicket.WicketUtils; import com.gitblit.wicket.pages.BasePage; @@ -43,7 +44,12 @@ public void populateItem(final Item<PageRegistration> item) { PageRegistration entry = item.getModelObject(); if (entry instanceof DropDownMenuRegistration) { if (entry instanceof OtherPageLink) { // other link OtherPageLink link = (OtherPageLink) entry; Component c = new LinkPanel("link", null, getString(entry.translationKey), link.url); item.add(c); } else if (entry instanceof DropDownMenuRegistration) { // drop down menu DropDownMenuRegistration reg = (DropDownMenuRegistration) entry; Component c = new DropDownMenu("link", getString(entry.translationKey), reg); tests/com/gitblit/tests/GitBlitSuite.java
@@ -82,6 +82,14 @@ return new FileRepository(new File(REPOSITORIES, "test/bluez-gnome.git")); } public static Repository getAmbitionRepository() throws Exception { return new FileRepository(new File(REPOSITORIES, "test/ambition.git")); } public static Repository getTheoreticalPhysicsRepository() throws Exception { return new FileRepository(new File(REPOSITORIES, "test/theoretical-physics.git")); } public static boolean startGitblit() throws Exception { if (started.get()) { // already started @@ -123,6 +131,8 @@ "https://git.kernel.org/pub/scm/bluetooth/bluez-gnome.git"); cloneOrFetch("test/jgit.git", "https://github.com/eclipse/jgit.git"); cloneOrFetch("test/helloworld.git", "https://github.com/git/hello-world.git"); cloneOrFetch("test/ambition.git", "https://github.com/defunkt/ambition.git"); cloneOrFetch("test/theoretical-physics.git", "https://github.com/certik/theoretical-physics.git"); enableTickets("ticgit.git"); enableDocs("ticgit.git");