blob: 5fda0df949312b566cf92b1e6f38abd441dfebf4 [file] [log] [blame]
Dave Borowitz9de65952012-08-13 16:09:45 -07001// Copyright 2012 Google Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.gitiles;
16
17import static com.google.common.base.Preconditions.checkNotNull;
18
Dave Borowitzfa0fef02013-01-07 13:17:53 -080019import com.google.common.base.CharMatcher;
Shawn Pearcec709c4c2015-08-28 15:30:42 -070020import com.google.common.base.Strings;
Dave Borowitz9de65952012-08-13 16:09:45 -070021import com.google.common.collect.Lists;
22import com.google.common.collect.Maps;
23import com.google.common.collect.Queues;
24
25import org.eclipse.jgit.errors.ConfigInvalidException;
26import org.eclipse.jgit.errors.RepositoryNotFoundException;
27import org.eclipse.jgit.http.server.ServletUtils;
Dave Borowitzded109a2014-03-03 15:25:39 -050028import org.eclipse.jgit.lib.Config;
Dave Borowitz9de65952012-08-13 16:09:45 -070029import org.eclipse.jgit.lib.Ref;
30import org.eclipse.jgit.lib.Repository;
31import org.eclipse.jgit.lib.StoredConfig;
32import org.eclipse.jgit.transport.resolver.FileResolver;
33import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
34import org.eclipse.jgit.util.IO;
35
36import java.io.File;
37import java.io.IOException;
Dave Borowitz9e95e632013-11-27 16:16:24 -050038import java.text.Collator;
Dave Borowitz9de65952012-08-13 16:09:45 -070039import java.util.Collection;
40import java.util.Collections;
41import java.util.List;
Dave Borowitz9e95e632013-11-27 16:16:24 -050042import java.util.Locale;
Dave Borowitz9de65952012-08-13 16:09:45 -070043import java.util.Map;
44import java.util.Queue;
45import java.util.Set;
46
47import javax.servlet.http.HttpServletRequest;
48
49/**
50 * Default implementation of {@link GitilesAccess} with local repositories.
51 * <p>
52 * Repositories are scanned on-demand under the given path, configured by
53 * default from {@code gitiles.basePath}. There is no access control beyond what
54 * user the JVM is running under.
55 */
56public class DefaultAccess implements GitilesAccess {
57 private static final String ANONYMOUS_USER_KEY = "anonymous user";
58
Dave Borowitzfa0fef02013-01-07 13:17:53 -080059 private static final String DEFAULT_DESCRIPTION =
60 "Unnamed repository; edit this file 'description' to name the repository.";
61
Dave Borowitz9e95e632013-11-27 16:16:24 -050062 private static final Collator US_COLLATOR = Collator.getInstance(Locale.US);
63
Dave Borowitz9de65952012-08-13 16:09:45 -070064 public static class Factory implements GitilesAccess.Factory {
65 private final File basePath;
66 private final String canonicalBasePath;
67 private final String baseGitUrl;
Dave Borowitzded109a2014-03-03 15:25:39 -050068 private final Config baseConfig;
Dave Borowitz9de65952012-08-13 16:09:45 -070069 private final FileResolver<HttpServletRequest> resolver;
70
Dave Borowitzded109a2014-03-03 15:25:39 -050071 Factory(File basePath, String baseGitUrl, Config baseConfig,
72 FileResolver<HttpServletRequest> resolver) throws IOException {
Dave Borowitz9de65952012-08-13 16:09:45 -070073 this.basePath = checkNotNull(basePath, "basePath");
74 this.baseGitUrl = checkNotNull(baseGitUrl, "baseGitUrl");
Dave Borowitzded109a2014-03-03 15:25:39 -050075 this.baseConfig = checkNotNull(baseConfig, "baseConfig");
Dave Borowitz9de65952012-08-13 16:09:45 -070076 this.resolver = checkNotNull(resolver, "resolver");
77 this.canonicalBasePath = basePath.getCanonicalPath();
78 }
79
80 @Override
81 public GitilesAccess forRequest(HttpServletRequest req) {
Dave Borowitz9de65952012-08-13 16:09:45 -070082 return newAccess(basePath, canonicalBasePath, baseGitUrl, resolver, req);
83 }
84
85 protected DefaultAccess newAccess(File basePath, String canonicalBasePath, String baseGitUrl,
86 FileResolver<HttpServletRequest> resolver, HttpServletRequest req) {
Dave Borowitzded109a2014-03-03 15:25:39 -050087 return new DefaultAccess(basePath, canonicalBasePath, baseGitUrl, baseConfig, resolver, req);
Dave Borowitz9de65952012-08-13 16:09:45 -070088 }
89 }
90
91 protected final File basePath;
92 protected final String canonicalBasePath;
93 protected final String baseGitUrl;
Dave Borowitzded109a2014-03-03 15:25:39 -050094 protected final Config baseConfig;
Dave Borowitz9de65952012-08-13 16:09:45 -070095 protected final FileResolver<HttpServletRequest> resolver;
96 protected final HttpServletRequest req;
97
98 protected DefaultAccess(File basePath, String canonicalBasePath, String baseGitUrl,
Dave Borowitzded109a2014-03-03 15:25:39 -050099 Config baseConfig, FileResolver<HttpServletRequest> resolver, HttpServletRequest req) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700100 this.basePath = checkNotNull(basePath, "basePath");
101 this.canonicalBasePath = checkNotNull(canonicalBasePath, "canonicalBasePath");
102 this.baseGitUrl = checkNotNull(baseGitUrl, "baseGitUrl");
Dave Borowitzded109a2014-03-03 15:25:39 -0500103 this.baseConfig = checkNotNull(baseConfig, "baseConfig");
Dave Borowitz9de65952012-08-13 16:09:45 -0700104 this.resolver = checkNotNull(resolver, "resolver");
105 this.req = checkNotNull(req, "req");
106 }
107
108 @Override
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700109 public Map<String, RepositoryDescription> listRepositories(String prefix,
110 Set<String> branches) throws IOException {
Dave Borowitz9e95e632013-11-27 16:16:24 -0500111 Map<String, RepositoryDescription> repos = Maps.newTreeMap(US_COLLATOR);
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700112 for (Repository repo : scanRepositories(basePath, prefix, req)) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700113 repos.put(getRepositoryName(repo), buildDescription(repo, branches));
114 repo.close();
115 }
116 return repos;
117 }
118
119 @Override
120 public Object getUserKey() {
121 // Always return the same anonymous user key (effectively running with the
122 // same user permissions as the JVM). Subclasses may override this behavior.
123 return ANONYMOUS_USER_KEY;
124 }
125
126 @Override
127 public String getRepositoryName() {
128 return getRepositoryName(ServletUtils.getRepository(req));
129 }
130
131 @Override
132 public RepositoryDescription getRepositoryDescription() throws IOException {
133 return buildDescription(ServletUtils.getRepository(req), Collections.<String> emptySet());
134 }
135
Dave Borowitzded109a2014-03-03 15:25:39 -0500136 @Override
137 public Config getConfig() {
138 return baseConfig;
139 }
140
Dave Borowitz9de65952012-08-13 16:09:45 -0700141 private String getRepositoryName(Repository repo) {
142 String path = getRelativePath(repo);
143 if (repo.isBare() && path.endsWith(".git")) {
144 path = path.substring(0, path.length() - 4);
145 }
146 return path;
147 }
148
149 private String getRelativePath(Repository repo) {
150 String path = repo.isBare() ? repo.getDirectory().getPath() : repo.getDirectory().getParent();
151 if (repo.isBare()) {
152 path = repo.getDirectory().getPath();
153 if (path.endsWith(".git")) {
154 path = path.substring(0, path.length() - 4);
155 }
156 } else {
157 path = repo.getDirectory().getParent();
158 }
159 return getRelativePath(path);
160 }
161
162 private String getRelativePath(String path) {
163 String base = basePath.getPath();
164 if (path.startsWith(base)) {
165 return path.substring(base.length() + 1);
166 }
167 if (path.startsWith(canonicalBasePath)) {
168 return path.substring(canonicalBasePath.length() + 1);
169 }
170 throw new IllegalStateException(String.format(
171 "Repository path %s is outside base path %s", path, base));
172 }
173
174 private String loadDescriptionText(Repository repo) throws IOException {
175 String desc = null;
176 StoredConfig config = repo.getConfig();
177 IOException configError = null;
178 try {
179 config.load();
180 desc = config.getString("gitweb", null, "description");
181 } catch (ConfigInvalidException e) {
182 configError = new IOException(e);
183 }
184 if (desc == null) {
185 File descFile = new File(repo.getDirectory(), "description");
186 if (descFile.exists()) {
187 desc = new String(IO.readFully(descFile));
Dave Borowitzfa0fef02013-01-07 13:17:53 -0800188 if (DEFAULT_DESCRIPTION.equals(CharMatcher.WHITESPACE.trimFrom(desc))) {
189 desc = null;
190 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700191 } else if (configError != null) {
192 throw configError;
193 }
194 }
195 return desc;
196 }
197
198 private RepositoryDescription buildDescription(Repository repo, Set<String> branches)
199 throws IOException {
200 RepositoryDescription desc = new RepositoryDescription();
201 desc.name = getRepositoryName(repo);
202 desc.cloneUrl = baseGitUrl + getRelativePath(repo);
203 desc.description = loadDescriptionText(repo);
204 if (!branches.isEmpty()) {
205 desc.branches = Maps.newLinkedHashMap();
206 for (String name : branches) {
207 Ref ref = repo.getRef(normalizeRefName(name));
208 if ((ref != null) && (ref.getObjectId() != null)) {
209 desc.branches.put(name, ref.getObjectId().name());
210 }
211 }
212 }
213 return desc;
214 }
215
216 private static String normalizeRefName(String name) {
217 if (name.startsWith("refs/")) {
218 return name;
219 }
220 return "refs/heads/" + name;
221 }
222
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700223 private Collection<Repository> scanRepositories(File basePath, String prefix,
224 HttpServletRequest req) throws IOException {
Dave Borowitz9de65952012-08-13 16:09:45 -0700225 List<Repository> repos = Lists.newArrayList();
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700226 Queue<File> todo = initScan(basePath, prefix);
Dave Borowitz9de65952012-08-13 16:09:45 -0700227 while (!todo.isEmpty()) {
228 File file = todo.remove();
229 try {
230 repos.add(resolver.open(req, getRelativePath(file.getPath())));
231 } catch (RepositoryNotFoundException e) {
232 File[] children = file.listFiles();
233 if (children != null) {
Dave Borowitz27058932014-12-03 15:44:46 -0800234 Collections.addAll(todo, children);
Dave Borowitz9de65952012-08-13 16:09:45 -0700235 }
236 } catch (ServiceNotEnabledException e) {
237 throw new IOException(e);
238 }
239 }
240 return repos;
241 }
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700242
243 private Queue<File> initScan(File basePath, String prefix)
244 throws IOException {
245 Queue<File> todo = Queues.newArrayDeque();
246 File[] entries;
247 if (isValidPrefix(prefix)) {
248 entries = new File(basePath, CharMatcher.is('/').trimFrom(prefix)).listFiles();
249 } else {
250 entries = basePath.listFiles();
251 }
252 if (entries != null) {
253 Collections.addAll(todo, entries);
254 } else if (!basePath.isDirectory()) {
255 throw new IOException("base path is not a directory: " + basePath.getPath());
256 }
257 return todo;
258 }
259
260 private static boolean isValidPrefix(String prefix) {
261 return !Strings.isNullOrEmpty(prefix)
262 && !prefix.equals(".") && !prefix.equals("..")
263 && !prefix.contains("../")
264 && !prefix.endsWith("/..");
265 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700266}