Obey core.filemode setting
[egit.git] / org.spearce.jgit / src / org / spearce / jgit / lib / GitIndex.java
blobfa31314e052b187b4475f73018bdd20e0f07f8b3
1 package org.spearce.jgit.lib;
3 import java.io.File;
4 import java.io.FileInputStream;
5 import java.io.FileNotFoundException;
6 import java.io.FileOutputStream;
7 import java.io.IOException;
8 import java.io.InputStream;
9 import java.io.RandomAccessFile;
10 import java.io.UnsupportedEncodingException;
11 import java.lang.reflect.InvocationTargetException;
12 import java.lang.reflect.Method;
13 import java.nio.ByteBuffer;
14 import java.nio.ByteOrder;
15 import java.nio.channels.FileChannel;
16 import java.security.MessageDigest;
17 import java.util.Comparator;
18 import java.util.Date;
19 import java.util.Iterator;
20 import java.util.Map;
21 import java.util.Stack;
22 import java.util.TreeMap;
24 import org.spearce.jgit.errors.CorruptObjectException;
26 public class GitIndex {
28 /** Stage 0 represents merged entries. */
29 public static final int STAGE_0 = 0;
31 private RandomAccessFile cache;
33 private File cacheFile;
35 // Index is modified
36 private boolean changed;
38 // Stat information updated
39 private boolean statDirty;
41 private Header header;
43 private long lastCacheTime;
45 private final Repository db;
47 private Map entries = new TreeMap(new Comparator() {
48 public int compare(Object arg0, Object arg1) {
49 byte[] a = (byte[]) arg0;
50 byte[] b = (byte[]) arg1;
51 for (int i = 0; i < a.length && i < b.length; ++i) {
52 int c = a[i] - b[i];
53 if (c != 0)
54 return c;
56 if (a.length < b.length)
57 return -1;
58 else if (a.length > b.length)
59 return 1;
60 return 0;
62 });
64 public GitIndex(Repository db) {
65 this.db = db;
66 this.cacheFile = new File(db.getDirectory(), "index");
69 public boolean isChanged() {
70 return changed || statDirty;
73 public void rereadIfNecessary() throws IOException {
74 if (cacheFile.exists() && cacheFile.lastModified() != lastCacheTime) {
75 read();
79 public void add(File wd, File f) throws IOException {
80 byte[] key = makeKey(wd, f);
81 Entry e = (Entry) entries.get(key);
82 if (e == null) {
83 e = new Entry(key, f, 0);
84 entries.put(key, e);
85 } else {
86 e.update(f);
90 public boolean remove(File wd, File f) {
91 byte[] key = makeKey(wd, f);
92 return entries.remove(key) != null;
95 public void read() throws IOException {
96 long t0 = System.currentTimeMillis();
97 changed = false;
98 statDirty = false;
99 cache = new RandomAccessFile(cacheFile, "r");
100 try {
101 FileChannel channel = cache.getChannel();
102 ByteBuffer buffer = ByteBuffer.allocateDirect((int) cacheFile.length());
103 buffer.order(ByteOrder.BIG_ENDIAN);
104 int j = channel.read(buffer);
105 if (j != buffer.capacity())
106 throw new IOException("Could not read index in one go, only "+j+" out of "+buffer.capacity()+" read");
107 buffer.flip();
108 header = new Header(buffer);
109 entries.clear();
110 for (int i = 0; i < header.entries; ++i) {
111 Entry entry = new Entry(buffer);
112 entries.put(entry.name, entry);
114 long t1 = System.currentTimeMillis();
115 lastCacheTime = cacheFile.lastModified();
116 System.out.println("Read index "+cacheFile+" in "+((t1-t0)/1000.0)+"s");
117 } finally {
118 cache.close();
122 public void write() throws IOException {
123 checkWriteOk();
124 File tmpIndex = new File(cacheFile.getAbsoluteFile() + ".tmp");
125 File lock = new File(cacheFile.getAbsoluteFile() + ".lock");
126 if (!lock.createNewFile())
127 throw new IOException("Index file is in use");
128 try {
129 FileOutputStream fileOutputStream = new FileOutputStream(tmpIndex);
130 FileChannel fc = fileOutputStream.getChannel();
131 ByteBuffer buf = ByteBuffer.allocate(4096);
132 MessageDigest newMessageDigest = Constants.newMessageDigest();
133 header = new Header(entries);
134 header.write(buf);
135 buf.flip();
136 newMessageDigest
137 .update(buf.array(), buf.arrayOffset(), buf.limit());
138 fc.write(buf);
139 buf.flip();
140 buf.clear();
141 for (Iterator i = entries.values().iterator(); i.hasNext();) {
142 Entry e = (Entry) i.next();
143 e.write(buf);
144 buf.flip();
145 newMessageDigest.update(buf.array(), buf.arrayOffset(), buf
146 .limit());
147 fc.write(buf);
148 buf.flip();
149 buf.clear();
151 buf.put(newMessageDigest.digest());
152 buf.flip();
153 fc.write(buf);
154 fc.close();
155 fileOutputStream.close();
156 if (cacheFile.exists())
157 if (!cacheFile.delete())
158 throw new IOException(
159 "Could not rename delete old index");
160 if (!tmpIndex.renameTo(cacheFile))
161 throw new IOException(
162 "Could not rename temporary index file to index");
163 changed = false;
164 statDirty = false;
165 } finally {
166 if (!lock.delete())
167 throw new IOException(
168 "Could not delete lock file. Should not happen");
169 if (tmpIndex.exists() && !tmpIndex.delete())
170 throw new IOException(
171 "Could not delete temporary index file. Should not happen");
175 private void checkWriteOk() throws IOException {
176 for (Iterator i = entries.values().iterator(); i.hasNext();) {
177 Entry e = (Entry) i.next();
178 if (e.stage != 0) {
179 throw new IOException("Cannot work with other stages than zero right now. Won't write corrupt index.");
184 static Method canExecute;
185 static Method setExecute;
186 static {
187 try {
188 canExecute = File.class.getMethod("canExecute", (Class[]) null);
189 setExecute = File.class.getMethod("setExecutable", new Class[] { Boolean.TYPE });
190 } catch (SecurityException e) {
191 e.printStackTrace();
192 } catch (NoSuchMethodException e) {
193 System.out.println("This vm cannot handle execute file permission. Feature disabled");
198 * JDK1.6 has file.canExecute
200 * if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
201 * return true;
203 boolean File_canExecute(File f) {
204 if (canExecute != null) {
205 try {
206 return ((Boolean) canExecute.invoke(f, (Object[]) null))
207 .booleanValue();
208 } catch (IllegalArgumentException e) {
209 throw new Error(e);
210 } catch (IllegalAccessException e) {
211 throw new Error(e);
212 } catch (InvocationTargetException e) {
213 throw new Error(e);
215 } else
216 return false;
220 * JDK1.6 has file.setExecute
222 boolean File_setExecute(File f,boolean value) {
223 if (setExecute != null) {
224 try {
225 return ((Boolean) setExecute.invoke(f,
226 new Object[] { new Boolean(value) })).booleanValue();
227 } catch (IllegalArgumentException e) {
228 throw new Error(e);
229 } catch (IllegalAccessException e) {
230 throw new Error(e);
231 } catch (InvocationTargetException e) {
232 throw new Error(e);
234 } else
235 return false;
238 static byte[] makeKey(File wd, File f) {
239 if (!f.getPath().startsWith(wd.getPath()))
240 throw new Error("Path is not in working dir");
241 String relName = f.getPath().substring(wd.getPath().length() + 1)
242 .replace(File.separatorChar, '/');
243 return relName.getBytes();
246 Boolean filemode;
247 private boolean config_filemode() {
248 // temporary til we can actually set parameters. We need to be able
249 // to change this for testing.
250 if (filemode != null)
251 return filemode.booleanValue();
252 RepositoryConfig config = db.getConfig();
253 return config.getBoolean("core", "filemode", true);
256 public class Entry {
257 private long ctime;
259 private long mtime;
261 private int dev;
263 private int ino;
265 private int mode;
267 private int uid;
269 private int gid;
271 private int size;
273 private ObjectId sha1;
275 private short flags;
277 private byte[] name;
279 private int stage;
281 public Entry(byte[] key, File f, int stage)
282 throws IOException {
283 ctime = f.lastModified() * 1000000L;
284 mtime = ctime; // we use same here
285 dev = -1;
286 ino = -1;
287 if (config_filemode() && File_canExecute(f))
288 mode = FileMode.EXECUTABLE_FILE.getBits();
289 else
290 mode = FileMode.REGULAR_FILE.getBits();
291 uid = -1;
292 gid = -1;
293 size = (int) f.length();
294 ObjectWriter writer = new ObjectWriter(db);
295 sha1 = writer.writeBlob(f);
296 name = key;
297 flags = (short) ((stage << 12) | name.length); // TODO: fix flags
300 public Entry(TreeEntry f, int stage)
301 throws UnsupportedEncodingException {
302 ctime = -1; // hmm
303 mtime = -1;
304 dev = -1;
305 ino = -1;
306 mode = f.getMode().getBits();
307 uid = -1;
308 gid = -1;
309 size = -1;
310 sha1 = f.getId();
311 name = f.getFullName().getBytes("UTF-8");
312 flags = (short) ((stage << 12) | name.length); // TODO: fix flags
315 Entry(ByteBuffer b) {
316 int startposition = b.position();
317 ctime = b.getInt() * 1000000000L + (b.getInt() % 1000000000L);
318 mtime = b.getInt() * 1000000000L + (b.getInt() % 1000000000L);
319 dev = b.getInt();
320 ino = b.getInt();
321 mode = b.getInt();
322 uid = b.getInt();
323 gid = b.getInt();
324 size = b.getInt();
325 byte[] sha1bytes = new byte[Constants.OBJECT_ID_LENGTH];
326 b.get(sha1bytes);
327 sha1 = new ObjectId(sha1bytes);
328 flags = b.getShort();
329 stage = (flags & 0x3000) >> 12;
330 name = new byte[flags & 0xFFF];
331 b.get(name);
333 .position(startposition
334 + ((8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 20 + 2
335 + name.length + 8) & ~7));
338 public boolean update(File f) throws IOException {
339 boolean modified = false;
340 long lm = f.lastModified() * 1000000L;
341 if (mtime != lm)
342 modified = true;
343 mtime = f.lastModified() * 1000000L;
344 if (size != f.length())
345 modified = true;
346 if (config_filemode()) {
347 if (File_canExecute(f) != FileMode.EXECUTABLE_FILE.equals(mode)) {
348 mode = FileMode.EXECUTABLE_FILE.getBits();
349 modified = true;
352 if (modified) {
353 size = (int) f.length();
354 ObjectWriter writer = new ObjectWriter(db);
355 ObjectId newsha1 = sha1 = writer.writeBlob(f);
356 if (!newsha1.equals(sha1))
357 modified = true;
358 sha1 = newsha1;
360 return modified;
363 public void write(ByteBuffer buf) {
364 int startposition = buf.position();
365 buf.putInt((int) (ctime / 1000000000L));
366 buf.putInt((int) (ctime % 1000000000L));
367 buf.putInt((int) (mtime / 1000000000L));
368 buf.putInt((int) (mtime % 1000000000L));
369 buf.putInt(dev);
370 buf.putInt(ino);
371 buf.putInt(mode);
372 buf.putInt(uid);
373 buf.putInt(gid);
374 buf.putInt(size);
375 buf.put(sha1.getBytes());
376 buf.putShort(flags);
377 buf.put(name);
378 int end = startposition
379 + ((8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 20 + 2 + name.length + 8) & ~7);
380 int remain = end - buf.position();
381 while (remain-- > 0)
382 buf.put((byte) 0);
386 * Check if an entry's content is different from the cache,
388 * File status information is used and status is same we
389 * consider the file identical to the state in the working
390 * directory. Native git uses more stat fields than we
391 * have accessible in Java.
393 * @param wd working directory to compare content with
394 * @return true if content is most likely different.
396 public boolean isModified(File wd) {
397 return isModified(wd, false);
401 * Check if an entry's content is different from the cache,
403 * File status information is used and status is same we
404 * consider the file identical to the state in the working
405 * directory. Native git uses more stat fields than we
406 * have accessible in Java.
408 * @param wd working directory to compare content with
409 * @param forceContentCheck True if the actual file content
410 * should be checked if modification time differs.
412 * @return true if content is most likely different.
414 public boolean isModified(File wd, boolean forceContentCheck) {
415 File file = getFile(wd);
416 if (!file.exists())
417 return true;
419 // JDK1.6 has file.canExecute
420 // if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
421 // return true;
422 final int exebits = FileMode.EXECUTABLE_FILE.getBits()
423 ^ FileMode.REGULAR_FILE.getBits();
425 if (config_filemode() && FileMode.EXECUTABLE_FILE.equals(mode)) {
426 if (!File_canExecute(file)&& canExecute != null)
427 return true;
428 } else {
429 if (FileMode.REGULAR_FILE.equals(mode&~exebits)) {
430 if (!file.isFile())
431 return true;
432 if (config_filemode() && File_canExecute(file) && canExecute != null)
433 return true;
434 } else {
435 if (FileMode.SYMLINK.equals(mode)) {
436 return true;
437 } else {
438 if (FileMode.TREE.equals(mode)) {
439 if (!file.isDirectory())
440 return true;
441 } else {
442 System.out.println("Does not handle mode "+mode+" ("+file+")");
443 return true;
449 long javamtime = mtime / 1000000L;
450 long lastm = file.lastModified();
451 if (file.length() != size)
452 return true;
453 if (lastm != javamtime) {
454 if (!forceContentCheck)
455 return true;
457 try {
458 InputStream is = new FileInputStream(file);
459 ObjectWriter objectWriter = new ObjectWriter(db);
460 try {
461 ObjectId newId = objectWriter.computeBlobSha1(file
462 .length(), is);
463 boolean ret = !newId.equals(sha1);
464 return ret;
465 } catch (IOException e) {
466 e.printStackTrace();
467 } finally {
468 try {
469 is.close();
470 } catch (IOException e) {
471 // can't happen, but if it does we ignore it
472 e.printStackTrace();
475 } catch (FileNotFoundException e) {
476 // should not happen because we already checked this
477 e.printStackTrace();
478 throw new Error(e);
481 return false;
484 private File getFile(File wd) {
485 return new File(wd, getName());
488 public String toString() {
489 return new String(name) + "/SHA-1(" + sha1 + ")/M:"
490 + new Date(ctime / 1000000L) + "/C:"
491 + new Date(mtime / 1000000L) + "/d" + dev + "/i" + ino
492 + "/m" + Integer.toString(mode, 8) + "/u" + uid + "/g"
493 + gid + "/s" + size + "/f" + flags + "/@" + stage;
496 public String getName() {
497 return new String(name);
500 public ObjectId getObjectId() {
501 return sha1;
504 public int getStage() {
505 return stage;
509 static class Header {
510 private int signature;
512 private int version;
514 int entries;
516 public Header(ByteBuffer map) throws CorruptObjectException {
517 read(map);
520 private void read(ByteBuffer buf) throws CorruptObjectException {
521 signature = buf.getInt();
522 version = buf.getInt();
523 entries = buf.getInt();
524 if (signature != 0x44495243)
525 throw new CorruptObjectException("Index signature is invalid: "
526 + signature);
527 if (version != 2)
528 throw new CorruptObjectException(
529 "Unknow index version (or corrupt index):" + version);
532 public void write(ByteBuffer buf) {
533 buf.order(ByteOrder.BIG_ENDIAN);
534 buf.putInt(signature);
535 buf.putInt(version);
536 buf.putInt(entries);
539 public Header(Map entryset) {
540 signature = 0x44495243;
541 version = 2;
542 entries = entryset.size();
546 public void readTree(Tree t) throws IOException {
547 readTree("", t);
550 public void readTree(String prefix, Tree t) throws IOException {
551 TreeEntry[] members = t.members();
552 for (int i = 0; i < members.length; ++i) {
553 TreeEntry te = members[i];
554 String name;
555 if (prefix.length() > 0)
556 name = prefix + "/" + te.getName();
557 else
558 name = te.getName();
559 if (te instanceof Tree) {
560 readTree(name, (Tree) te);
561 } else {
562 Entry e = new Entry(te, 0);
563 entries.put(name.getBytes("UTF-8"), e);
568 public void checkout(File wd) throws IOException {
569 for (Iterator i = entries.values().iterator(); i.hasNext();) {
570 Entry e = (Entry) i.next();
571 if (e.stage != 0)
572 continue;
573 ObjectLoader ol = db.openBlob(e.sha1);
574 byte[] bytes = ol.getBytes();
575 File file = new File(wd, e.getName());
576 file.delete();
577 file.getParentFile().mkdirs();
578 FileChannel channel = new FileOutputStream(file).getChannel();
579 ByteBuffer buffer = ByteBuffer.wrap(bytes);
580 int j = channel.write(buffer);
581 if (j != bytes.length)
582 throw new IOException("Could not write file " + file);
583 channel.close();
584 if (config_filemode() && canExecute != null) {
585 if (FileMode.EXECUTABLE_FILE.equals(e.mode)) {
586 if (!File_canExecute(file))
587 File_setExecute(file, true);
588 } else {
589 if (File_canExecute(file))
590 File_setExecute(file, false);
593 e.mtime = file.lastModified() * 1000000L;
594 e.ctime = e.mtime;
598 public ObjectId writeTree() throws IOException {
599 checkWriteOk();
600 ObjectWriter writer = new ObjectWriter(db);
601 Tree current = new Tree(db);
602 Stack trees = new Stack();
603 trees.push(current);
604 String[] prevName = new String[0];
605 for (Iterator i = entries.values().iterator(); i.hasNext();) {
606 Entry e = (Entry) i.next();
607 if (e.stage != 0)
608 continue;
609 String[] newName = splitDirPath(e.getName());
610 int c = longestCommonPath(prevName, newName);
611 while (c < trees.size() - 1) {
612 current.setId(writer.writeTree(current));
613 trees.pop();
614 current = trees.isEmpty() ? null : (Tree) trees.peek();
616 while (trees.size() < newName.length) {
617 if (!current.existsTree(newName[trees.size() - 1])) {
618 current = new Tree(current, newName[trees.size() - 1]
619 .getBytes());
620 current.getParent().addEntry(current);
621 trees.push(current);
622 } else {
623 current = (Tree) current.findTreeMember(newName[trees
624 .size() - 1]);
625 trees.push(current);
628 FileTreeEntry ne = new FileTreeEntry(current, e.sha1,
629 newName[newName.length - 1].getBytes(),
630 (e.mode & FileMode.EXECUTABLE_FILE.getBits()) == FileMode.EXECUTABLE_FILE.getBits());
631 current.addEntry(ne);
633 while (!trees.isEmpty()) {
634 current.setId(writer.writeTree(current));
635 trees.pop();
636 if (!trees.isEmpty())
637 current = (Tree) trees.peek();
639 return current.getTreeId();
642 String[] splitDirPath(String name) {
643 String[] tmp = new String[name.length() / 2 + 1];
644 int p0 = -1;
645 int p1;
646 int c = 0;
647 while ((p1 = name.indexOf('/', p0 + 1)) != -1) {
648 tmp[c++] = name.substring(p0 + 1, p1);
649 p0 = p1;
651 tmp[c++] = name.substring(p0 + 1);
652 String[] ret = new String[c];
653 for (int i = 0; i < c; ++i) {
654 ret[i] = tmp[i];
656 return ret;
659 int longestCommonPath(String[] a, String[] b) {
660 int i;
661 for (i = 0; i < a.length && i < b.length; ++i)
662 if (!a[i].equals(b[i]))
663 return i;
664 return i;
667 public Entry[] getMembers() {
668 return (Entry[]) entries.values().toArray(new Entry[entries.size()]);
671 public Entry getEntry(String path) throws UnsupportedEncodingException {
672 return (Entry) entries.get(Repository.gitInternalSlash(path.getBytes("ISO-8859-1")));
675 public Repository getRepository() {
676 return db;