Refactor Repository's ref reading/writing for aggressive caching
[egit/zawir.git] / org.spearce.jgit / src / org / spearce / jgit / lib / RefDatabase.java
blobdc9885eb8a1ab2fb0132c495d04cd3dcf7af2d44
1 /*
2 * Copyright (C) 2008 Shawn Pearce <spearce@spearce.org>
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public
6 * License, version 2, as published by the Free Software Foundation.
8 * This library is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * General Public License for more details.
13 * You should have received a copy of the GNU General Public
14 * License along with this library; if not, write to the Free Software
15 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
17 package org.spearce.jgit.lib;
19 import java.io.BufferedReader;
20 import java.io.EOFException;
21 import java.io.File;
22 import java.io.FileInputStream;
23 import java.io.FileNotFoundException;
24 import java.io.IOException;
25 import java.io.InputStreamReader;
26 import java.io.UnsupportedEncodingException;
27 import java.util.HashMap;
28 import java.util.Map;
30 import org.spearce.jgit.errors.ObjectWritingException;
31 import org.spearce.jgit.util.FS;
32 import org.spearce.jgit.util.NB;
34 class RefDatabase {
35 private static final String CHAR_ENC = Constants.CHARACTER_ENCODING;
37 private static final String REFS_SLASH = "refs/";
39 private static final String HEADS_SLASH = Constants.HEADS_PREFIX + "/";
41 private static final String TAGS_SLASH = Constants.TAGS_PREFIX + "/";
43 private static final String[] refSearchPaths = { "", REFS_SLASH,
44 TAGS_SLASH, HEADS_SLASH, Constants.REMOTES_PREFIX + "/" };
46 private final Repository db;
48 private final File gitDir;
50 private final File refsDir;
52 private Map<String, CachedRef> looseRefs;
54 private final File packedRefsFile;
56 private Map<String, Ref> packedRefs;
58 private long packedRefsLastModified;
60 private long packedRefsLength;
62 RefDatabase(final Repository r) {
63 db = r;
64 gitDir = db.getDirectory();
65 refsDir = FS.resolve(gitDir, "refs");
66 packedRefsFile = FS.resolve(gitDir, "packed-refs");
67 clearCache();
70 void clearCache() {
71 looseRefs = new HashMap<String, CachedRef>();
72 packedRefs = new HashMap<String, Ref>();
73 packedRefsLastModified = 0;
74 packedRefsLength = 0;
77 Repository getRepository() {
78 return db;
81 void create() {
82 refsDir.mkdir();
83 new File(refsDir, "heads").mkdir();
84 new File(refsDir, "tags").mkdir();
87 ObjectId idOf(final String name) throws IOException {
88 final Ref r = readRefBasic(name, 0);
89 return r != null ? r.getObjectId() : null;
92 /**
93 * Create a command to update (or create) a ref in this repository.
95 * @param name
96 * name of the ref the caller wants to modify.
97 * @return an update command. The caller must finish populating this command
98 * and then invoke one of the update methods to actually make a
99 * change.
100 * @throws IOException
101 * a symbolic ref was passed in and could not be resolved back
102 * to the base ref, as the symbolic ref could not be read.
104 RefUpdate newUpdate(final String name) throws IOException {
105 Ref r = readRefBasic(name, 0);
106 if (r == null)
107 r = new Ref(name, null);
108 return new RefUpdate(this, r, fileForRef(r.getName()));
111 void stored(final String name, final ObjectId id, final long time) {
112 looseRefs.put(name, new CachedRef(name, id, time));
116 * Writes a symref (e.g. HEAD) to disk
118 * @param name
119 * symref name
120 * @param target
121 * pointed to ref
122 * @throws IOException
124 void link(final String name, final String target) throws IOException {
125 final byte[] content = ("ref: " + target + "\n").getBytes(CHAR_ENC);
126 final LockFile lck = new LockFile(fileForRef(name));
127 if (!lck.lock())
128 throw new ObjectWritingException("Unable to lock " + name);
129 try {
130 lck.write(content);
131 } catch (IOException ioe) {
132 throw new ObjectWritingException("Unable to write " + name, ioe);
134 if (!lck.commit())
135 throw new ObjectWritingException("Unable to write " + name);
138 Ref readRef(final String partialName) throws IOException {
139 refreshPackedRefs();
140 for (int k = 0; k < refSearchPaths.length; k++) {
141 final Ref r = readRefBasic(refSearchPaths[k] + partialName, 0);
142 if (r != null && r.getObjectId() != null)
143 return r;
145 return null;
149 * @return all known refs (heads, tags, remotes).
151 Map<String, Ref> getAllRefs() {
152 return readRefs();
156 * @return all tags; key is short tag name ("v1.0") and value of the entry
157 * contains the ref with the full tag name ("refs/tags/v1.0").
159 Map<String, Ref> getTags() {
160 final Map<String, Ref> tags = new HashMap<String, Ref>();
161 for (final Ref r : readRefs().values()) {
162 if (r.getName().startsWith(TAGS_SLASH))
163 tags.put(r.getName().substring(TAGS_SLASH.length()), r);
165 return tags;
168 private Map<String, Ref> readRefs() {
169 final HashMap<String, Ref> avail = new HashMap<String, Ref>();
170 readPackedRefs(avail);
171 readLooseRefs(avail, REFS_SLASH, refsDir);
172 readOneLooseRef(avail, Constants.HEAD, new File(gitDir, Constants.HEAD));
173 return avail;
176 private void readPackedRefs(final Map<String, Ref> avail) {
177 refreshPackedRefs();
178 avail.putAll(packedRefs);
181 private void readLooseRefs(final Map<String, Ref> avail,
182 final String prefix, final File dir) {
183 final File[] entries = dir.listFiles();
184 if (entries == null)
185 return;
187 for (final File ent : entries) {
188 final String entName = ent.getName();
189 if (".".equals(entName) || "..".equals(entName))
190 continue;
191 readOneLooseRef(avail, prefix + entName, ent);
195 private void readOneLooseRef(final Map<String, Ref> avail,
196 final String refName, final File ent) {
197 // Unchanged and cached? Don't read it again.
199 CachedRef ref = looseRefs.get(refName);
200 if (ref != null) {
201 if (ref.lastModified == ent.lastModified()) {
202 avail.put(ref.getName(), ref);
203 return;
205 looseRefs.remove(refName);
208 // Recurse into the directory.
210 if (ent.isDirectory()) {
211 readLooseRefs(avail, refName + "/", ent);
212 return;
215 // Assume its a valid loose reference we need to cache.
217 try {
218 final FileInputStream in = new FileInputStream(ent);
219 try {
220 final ObjectId id;
221 try {
222 final byte[] str = new byte[Constants.OBJECT_ID_LENGTH * 2];
223 NB.readFully(in, str, 0, str.length);
224 id = ObjectId.fromString(str, 0);
225 } catch (EOFException tooShortToBeRef) {
226 // Its below the minimum length needed. It could
227 // be a symbolic reference.
229 return;
230 } catch (IllegalArgumentException notRef) {
231 // It is not a well-formed ObjectId. It may be
232 // a symbolic reference ("ref: ").
234 return;
237 ref = new CachedRef(refName, id, ent.lastModified());
238 looseRefs.put(ref.getName(), ref);
239 avail.put(ref.getName(), ref);
240 } finally {
241 in.close();
243 } catch (FileNotFoundException noFile) {
244 // Deleted while we were reading? Its gone now!
246 } catch (IOException err) {
247 // Whoops.
249 throw new RuntimeException("Cannot read ref " + ent, err);
253 private File fileForRef(final String name) {
254 if (name.startsWith(REFS_SLASH))
255 return new File(refsDir, name.substring(REFS_SLASH.length()));
256 return new File(gitDir, name);
259 private Ref readRefBasic(final String name, final int depth)
260 throws IOException {
261 // Prefer loose ref to packed ref as the loose
262 // file can be more up-to-date than a packed one.
264 CachedRef ref = looseRefs.get(name);
265 final File loose = fileForRef(name);
266 final long mtime = loose.lastModified();
267 if (ref != null) {
268 if (ref.lastModified == mtime)
269 return ref;
270 looseRefs.remove(name);
273 if (mtime == 0) {
274 // If last modified is 0 the file does not exist.
275 // Try packed cache.
277 return packedRefs.get(name);
280 final String line;
281 try {
282 line = readLine(loose);
283 } catch (FileNotFoundException notLoose) {
284 return packedRefs.get(name);
287 if (line == null || line.length() == 0)
288 return new Ref(name, null);
290 if (line.startsWith("ref: ")) {
291 if (depth >= 5) {
292 throw new IOException("Exceeded maximum ref depth of " + depth
293 + " at " + name + ". Circular reference?");
296 final String target = line.substring("ref: ".length());
297 final Ref r = readRefBasic(target, depth + 1);
298 return r != null ? r : new Ref(target, null);
301 final ObjectId id;
302 try {
303 id = ObjectId.fromString(line);
304 } catch (IllegalArgumentException notRef) {
305 throw new IOException("Not a ref: " + name + ": " + line);
308 ref = new CachedRef(name, id, mtime);
309 looseRefs.put(name, ref);
310 return ref;
313 private void refreshPackedRefs() {
314 final long currTime = packedRefsFile.lastModified();
315 final long currLen = currTime == 0 ? 0 : packedRefsFile.length();
316 if (currTime == packedRefsLastModified && currLen == packedRefsLength)
317 return;
318 if (currTime == 0) {
319 packedRefsLastModified = 0;
320 packedRefsLength = 0;
321 packedRefs = new HashMap<String, Ref>();
322 return;
325 final Map<String, Ref> newPackedRefs = new HashMap<String, Ref>();
326 try {
327 final BufferedReader b = openReader(packedRefsFile);
328 try {
329 String p;
330 Ref last = null;
331 while ((p = b.readLine()) != null) {
332 if (p.charAt(0) == '#')
333 continue;
335 if (p.charAt(0) == '^') {
336 if (last == null)
337 throw new IOException("Peeled line before ref.");
339 final ObjectId id = ObjectId.fromString(p.substring(1));
340 last = new Ref(last.getName(), last.getObjectId(), id);
341 newPackedRefs.put(last.getName(), last);
342 continue;
345 final int sp = p.indexOf(' ');
346 final ObjectId id = ObjectId.fromString(p.substring(0, sp));
347 final String name = new String(p.substring(sp + 1));
348 last = new Ref(name, id);
349 newPackedRefs.put(last.getName(), last);
351 } finally {
352 b.close();
354 packedRefsLastModified = currTime;
355 packedRefsLength = currLen;
356 packedRefs = newPackedRefs;
357 } catch (FileNotFoundException noPackedRefs) {
358 // Ignore it and leave the new map empty.
360 packedRefsLastModified = 0;
361 packedRefsLength = 0;
362 packedRefs = newPackedRefs;
363 } catch (IOException e) {
364 throw new RuntimeException("Cannot read packed refs", e);
368 private static String readLine(final File file)
369 throws FileNotFoundException, IOException {
370 final BufferedReader br = openReader(file);
371 try {
372 return br.readLine();
373 } finally {
374 br.close();
378 private static BufferedReader openReader(final File fileLocation)
379 throws UnsupportedEncodingException, FileNotFoundException {
380 return new BufferedReader(new InputStreamReader(new FileInputStream(
381 fileLocation), CHAR_ENC));
384 private static class CachedRef extends Ref {
385 final long lastModified;
387 CachedRef(final String refName, final ObjectId id, final long mtime) {
388 super(refName, id);
389 lastModified = mtime;