Remember how a Ref was read in from disk and created
[egit/zawir.git] / org.spearce.jgit / src / org / spearce / jgit / lib / RefDatabase.java
blob9e3e0202aed6f7d36bd44a933f5a44c0b61cc8ff
1 /*
2 * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
3 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
5 * All rights reserved.
7 * Redistribution and use in source and binary forms, with or
8 * without modification, are permitted provided that the following
9 * conditions are met:
11 * - Redistributions of source code must retain the above copyright
12 * notice, this list of conditions and the following disclaimer.
14 * - Redistributions in binary form must reproduce the above
15 * copyright notice, this list of conditions and the following
16 * disclaimer in the documentation and/or other materials provided
17 * with the distribution.
19 * - Neither the name of the Git Development Community nor the
20 * names of its contributors may be used to endorse or promote
21 * products derived from this software without specific prior
22 * written permission.
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
25 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
26 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
27 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
28 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
29 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
30 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
31 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
32 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
33 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
34 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
35 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
36 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39 package org.spearce.jgit.lib;
41 import java.io.BufferedReader;
42 import java.io.EOFException;
43 import java.io.File;
44 import java.io.FileInputStream;
45 import java.io.FileNotFoundException;
46 import java.io.IOException;
47 import java.io.InputStreamReader;
48 import java.io.UnsupportedEncodingException;
49 import java.util.HashMap;
50 import java.util.Map;
52 import org.spearce.jgit.errors.ObjectWritingException;
53 import org.spearce.jgit.util.FS;
54 import org.spearce.jgit.util.NB;
56 class RefDatabase {
57 private static final String CHAR_ENC = Constants.CHARACTER_ENCODING;
59 private static final String REFS_SLASH = "refs/";
61 private static final String HEADS_SLASH = Constants.HEADS_PREFIX + "/";
63 private static final String TAGS_SLASH = Constants.TAGS_PREFIX + "/";
65 private static final String[] refSearchPaths = { "", REFS_SLASH,
66 TAGS_SLASH, HEADS_SLASH, Constants.REMOTES_PREFIX + "/" };
68 private final Repository db;
70 private final File gitDir;
72 private final File refsDir;
74 private Map<String, CachedRef> looseRefs;
76 private final File packedRefsFile;
78 private Map<String, Ref> packedRefs;
80 private long packedRefsLastModified;
82 private long packedRefsLength;
84 RefDatabase(final Repository r) {
85 db = r;
86 gitDir = db.getDirectory();
87 refsDir = FS.resolve(gitDir, "refs");
88 packedRefsFile = FS.resolve(gitDir, "packed-refs");
89 clearCache();
92 void clearCache() {
93 looseRefs = new HashMap<String, CachedRef>();
94 packedRefs = new HashMap<String, Ref>();
95 packedRefsLastModified = 0;
96 packedRefsLength = 0;
99 Repository getRepository() {
100 return db;
103 void create() {
104 refsDir.mkdir();
105 new File(refsDir, "heads").mkdir();
106 new File(refsDir, "tags").mkdir();
109 ObjectId idOf(final String name) throws IOException {
110 final Ref r = readRefBasic(name, 0);
111 return r != null ? r.getObjectId() : null;
115 * Create a command to update (or create) a ref in this repository.
117 * @param name
118 * name of the ref the caller wants to modify.
119 * @return an update command. The caller must finish populating this command
120 * and then invoke one of the update methods to actually make a
121 * change.
122 * @throws IOException
123 * a symbolic ref was passed in and could not be resolved back
124 * to the base ref, as the symbolic ref could not be read.
126 RefUpdate newUpdate(final String name) throws IOException {
127 Ref r = readRefBasic(name, 0);
128 if (r == null)
129 r = new Ref(Ref.Storage.NEW, name, null);
130 return new RefUpdate(this, r, fileForRef(r.getName()));
133 void stored(final String name, final ObjectId id, final long time) {
134 looseRefs.put(name, new CachedRef(Ref.Storage.LOOSE, name, id, time));
138 * Writes a symref (e.g. HEAD) to disk
140 * @param name
141 * symref name
142 * @param target
143 * pointed to ref
144 * @throws IOException
146 void link(final String name, final String target) throws IOException {
147 final byte[] content = ("ref: " + target + "\n").getBytes(CHAR_ENC);
148 final LockFile lck = new LockFile(fileForRef(name));
149 if (!lck.lock())
150 throw new ObjectWritingException("Unable to lock " + name);
151 try {
152 lck.write(content);
153 } catch (IOException ioe) {
154 throw new ObjectWritingException("Unable to write " + name, ioe);
156 if (!lck.commit())
157 throw new ObjectWritingException("Unable to write " + name);
160 Ref readRef(final String partialName) throws IOException {
161 refreshPackedRefs();
162 for (int k = 0; k < refSearchPaths.length; k++) {
163 final Ref r = readRefBasic(refSearchPaths[k] + partialName, 0);
164 if (r != null && r.getObjectId() != null)
165 return r;
167 return null;
171 * @return all known refs (heads, tags, remotes).
173 Map<String, Ref> getAllRefs() {
174 return readRefs();
178 * @return all tags; key is short tag name ("v1.0") and value of the entry
179 * contains the ref with the full tag name ("refs/tags/v1.0").
181 Map<String, Ref> getTags() {
182 final Map<String, Ref> tags = new HashMap<String, Ref>();
183 for (final Ref r : readRefs().values()) {
184 if (r.getName().startsWith(TAGS_SLASH))
185 tags.put(r.getName().substring(TAGS_SLASH.length()), r);
187 return tags;
190 private Map<String, Ref> readRefs() {
191 final HashMap<String, Ref> avail = new HashMap<String, Ref>();
192 readPackedRefs(avail);
193 readLooseRefs(avail, REFS_SLASH, refsDir);
194 readOneLooseRef(avail, Constants.HEAD, new File(gitDir, Constants.HEAD));
195 return avail;
198 private void readPackedRefs(final Map<String, Ref> avail) {
199 refreshPackedRefs();
200 avail.putAll(packedRefs);
203 private void readLooseRefs(final Map<String, Ref> avail,
204 final String prefix, final File dir) {
205 final File[] entries = dir.listFiles();
206 if (entries == null)
207 return;
209 for (final File ent : entries) {
210 final String entName = ent.getName();
211 if (".".equals(entName) || "..".equals(entName))
212 continue;
213 readOneLooseRef(avail, prefix + entName, ent);
217 private void readOneLooseRef(final Map<String, Ref> avail,
218 final String refName, final File ent) {
219 // Unchanged and cached? Don't read it again.
221 CachedRef ref = looseRefs.get(refName);
222 if (ref != null) {
223 if (ref.lastModified == ent.lastModified()) {
224 avail.put(ref.getName(), ref);
225 return;
227 looseRefs.remove(refName);
230 // Recurse into the directory.
232 if (ent.isDirectory()) {
233 readLooseRefs(avail, refName + "/", ent);
234 return;
237 // Assume its a valid loose reference we need to cache.
239 try {
240 final FileInputStream in = new FileInputStream(ent);
241 try {
242 final ObjectId id;
243 try {
244 final byte[] str = new byte[Constants.OBJECT_ID_LENGTH * 2];
245 NB.readFully(in, str, 0, str.length);
246 id = ObjectId.fromString(str, 0);
247 } catch (EOFException tooShortToBeRef) {
248 // Its below the minimum length needed. It could
249 // be a symbolic reference.
251 return;
252 } catch (IllegalArgumentException notRef) {
253 // It is not a well-formed ObjectId. It may be
254 // a symbolic reference ("ref: ").
256 return;
259 ref = new CachedRef(Ref.Storage.LOOSE, refName, id, ent
260 .lastModified());
261 looseRefs.put(ref.getName(), ref);
262 avail.put(ref.getName(), ref);
263 } finally {
264 in.close();
266 } catch (FileNotFoundException noFile) {
267 // Deleted while we were reading? Its gone now!
269 } catch (IOException err) {
270 // Whoops.
272 throw new RuntimeException("Cannot read ref " + ent, err);
276 private File fileForRef(final String name) {
277 if (name.startsWith(REFS_SLASH))
278 return new File(refsDir, name.substring(REFS_SLASH.length()));
279 return new File(gitDir, name);
282 private Ref readRefBasic(final String name, final int depth)
283 throws IOException {
284 // Prefer loose ref to packed ref as the loose
285 // file can be more up-to-date than a packed one.
287 CachedRef ref = looseRefs.get(name);
288 final File loose = fileForRef(name);
289 final long mtime = loose.lastModified();
290 if (ref != null) {
291 if (ref.lastModified == mtime)
292 return ref;
293 looseRefs.remove(name);
296 if (mtime == 0) {
297 // If last modified is 0 the file does not exist.
298 // Try packed cache.
300 return packedRefs.get(name);
303 final String line;
304 try {
305 line = readLine(loose);
306 } catch (FileNotFoundException notLoose) {
307 return packedRefs.get(name);
310 if (line == null || line.length() == 0)
311 return new Ref(Ref.Storage.LOOSE, name, null);
313 if (line.startsWith("ref: ")) {
314 if (depth >= 5) {
315 throw new IOException("Exceeded maximum ref depth of " + depth
316 + " at " + name + ". Circular reference?");
319 final String target = line.substring("ref: ".length());
320 final Ref r = readRefBasic(target, depth + 1);
321 return r != null ? r : new Ref(Ref.Storage.LOOSE, target, null);
324 final ObjectId id;
325 try {
326 id = ObjectId.fromString(line);
327 } catch (IllegalArgumentException notRef) {
328 throw new IOException("Not a ref: " + name + ": " + line);
331 ref = new CachedRef(Ref.Storage.LOOSE, name, id, mtime);
332 looseRefs.put(name, ref);
333 return ref;
336 private void refreshPackedRefs() {
337 final long currTime = packedRefsFile.lastModified();
338 final long currLen = currTime == 0 ? 0 : packedRefsFile.length();
339 if (currTime == packedRefsLastModified && currLen == packedRefsLength)
340 return;
341 if (currTime == 0) {
342 packedRefsLastModified = 0;
343 packedRefsLength = 0;
344 packedRefs = new HashMap<String, Ref>();
345 return;
348 final Map<String, Ref> newPackedRefs = new HashMap<String, Ref>();
349 try {
350 final BufferedReader b = openReader(packedRefsFile);
351 try {
352 String p;
353 Ref last = null;
354 while ((p = b.readLine()) != null) {
355 if (p.charAt(0) == '#')
356 continue;
358 if (p.charAt(0) == '^') {
359 if (last == null)
360 throw new IOException("Peeled line before ref.");
362 final ObjectId id = ObjectId.fromString(p.substring(1));
363 last = new Ref(Ref.Storage.PACKED, last.getName(), last
364 .getObjectId(), id);
365 newPackedRefs.put(last.getName(), last);
366 continue;
369 final int sp = p.indexOf(' ');
370 final ObjectId id = ObjectId.fromString(p.substring(0, sp));
371 final String name = new String(p.substring(sp + 1));
372 last = new Ref(Ref.Storage.PACKED, name, id);
373 newPackedRefs.put(last.getName(), last);
375 } finally {
376 b.close();
378 packedRefsLastModified = currTime;
379 packedRefsLength = currLen;
380 packedRefs = newPackedRefs;
381 } catch (FileNotFoundException noPackedRefs) {
382 // Ignore it and leave the new map empty.
384 packedRefsLastModified = 0;
385 packedRefsLength = 0;
386 packedRefs = newPackedRefs;
387 } catch (IOException e) {
388 throw new RuntimeException("Cannot read packed refs", e);
392 private static String readLine(final File file)
393 throws FileNotFoundException, IOException {
394 final BufferedReader br = openReader(file);
395 try {
396 return br.readLine();
397 } finally {
398 br.close();
402 private static BufferedReader openReader(final File fileLocation)
403 throws UnsupportedEncodingException, FileNotFoundException {
404 return new BufferedReader(new InputStreamReader(new FileInputStream(
405 fileLocation), CHAR_ENC));
408 private static class CachedRef extends Ref {
409 final long lastModified;
411 CachedRef(final Storage st, final String refName, final ObjectId id,
412 final long mtime) {
413 super(st, refName, id);
414 lastModified = mtime;