Switch jgit library to the EDL (3-clause BSD)
[jgit.git] / org.spearce.jgit / src / org / spearce / jgit / lib / RefDatabase.java
blob1857982f24305ac177e68c2fa45bd38e082d8140
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(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(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(refName, id, ent.lastModified());
260 looseRefs.put(ref.getName(), ref);
261 avail.put(ref.getName(), ref);
262 } finally {
263 in.close();
265 } catch (FileNotFoundException noFile) {
266 // Deleted while we were reading? Its gone now!
268 } catch (IOException err) {
269 // Whoops.
271 throw new RuntimeException("Cannot read ref " + ent, err);
275 private File fileForRef(final String name) {
276 if (name.startsWith(REFS_SLASH))
277 return new File(refsDir, name.substring(REFS_SLASH.length()));
278 return new File(gitDir, name);
281 private Ref readRefBasic(final String name, final int depth)
282 throws IOException {
283 // Prefer loose ref to packed ref as the loose
284 // file can be more up-to-date than a packed one.
286 CachedRef ref = looseRefs.get(name);
287 final File loose = fileForRef(name);
288 final long mtime = loose.lastModified();
289 if (ref != null) {
290 if (ref.lastModified == mtime)
291 return ref;
292 looseRefs.remove(name);
295 if (mtime == 0) {
296 // If last modified is 0 the file does not exist.
297 // Try packed cache.
299 return packedRefs.get(name);
302 final String line;
303 try {
304 line = readLine(loose);
305 } catch (FileNotFoundException notLoose) {
306 return packedRefs.get(name);
309 if (line == null || line.length() == 0)
310 return new Ref(name, null);
312 if (line.startsWith("ref: ")) {
313 if (depth >= 5) {
314 throw new IOException("Exceeded maximum ref depth of " + depth
315 + " at " + name + ". Circular reference?");
318 final String target = line.substring("ref: ".length());
319 final Ref r = readRefBasic(target, depth + 1);
320 return r != null ? r : new Ref(target, null);
323 final ObjectId id;
324 try {
325 id = ObjectId.fromString(line);
326 } catch (IllegalArgumentException notRef) {
327 throw new IOException("Not a ref: " + name + ": " + line);
330 ref = new CachedRef(name, id, mtime);
331 looseRefs.put(name, ref);
332 return ref;
335 private void refreshPackedRefs() {
336 final long currTime = packedRefsFile.lastModified();
337 final long currLen = currTime == 0 ? 0 : packedRefsFile.length();
338 if (currTime == packedRefsLastModified && currLen == packedRefsLength)
339 return;
340 if (currTime == 0) {
341 packedRefsLastModified = 0;
342 packedRefsLength = 0;
343 packedRefs = new HashMap<String, Ref>();
344 return;
347 final Map<String, Ref> newPackedRefs = new HashMap<String, Ref>();
348 try {
349 final BufferedReader b = openReader(packedRefsFile);
350 try {
351 String p;
352 Ref last = null;
353 while ((p = b.readLine()) != null) {
354 if (p.charAt(0) == '#')
355 continue;
357 if (p.charAt(0) == '^') {
358 if (last == null)
359 throw new IOException("Peeled line before ref.");
361 final ObjectId id = ObjectId.fromString(p.substring(1));
362 last = new Ref(last.getName(), last.getObjectId(), id);
363 newPackedRefs.put(last.getName(), last);
364 continue;
367 final int sp = p.indexOf(' ');
368 final ObjectId id = ObjectId.fromString(p.substring(0, sp));
369 final String name = new String(p.substring(sp + 1));
370 last = new Ref(name, id);
371 newPackedRefs.put(last.getName(), last);
373 } finally {
374 b.close();
376 packedRefsLastModified = currTime;
377 packedRefsLength = currLen;
378 packedRefs = newPackedRefs;
379 } catch (FileNotFoundException noPackedRefs) {
380 // Ignore it and leave the new map empty.
382 packedRefsLastModified = 0;
383 packedRefsLength = 0;
384 packedRefs = newPackedRefs;
385 } catch (IOException e) {
386 throw new RuntimeException("Cannot read packed refs", e);
390 private static String readLine(final File file)
391 throws FileNotFoundException, IOException {
392 final BufferedReader br = openReader(file);
393 try {
394 return br.readLine();
395 } finally {
396 br.close();
400 private static BufferedReader openReader(final File fileLocation)
401 throws UnsupportedEncodingException, FileNotFoundException {
402 return new BufferedReader(new InputStreamReader(new FileInputStream(
403 fileLocation), CHAR_ENC));
406 private static class CachedRef extends Ref {
407 final long lastModified;
409 CachedRef(final String refName, final ObjectId id, final long mtime) {
410 super(refName, id);
411 lastModified = mtime;