Doesn't look at execute bit for difference if we can't actually check it
[egit/egit-new.git] / org.spearce.jgit / src / org / spearce / jgit / lib / GitIndex.java
blobb6312e75b15e0683e07f9df2fd49ed6e103769ff
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.MappedByteBuffer;
16 import java.nio.channels.FileChannel;
17 import java.nio.channels.FileChannel.MapMode;
18 import java.security.MessageDigest;
19 import java.util.Comparator;
20 import java.util.Date;
21 import java.util.Iterator;
22 import java.util.Map;
23 import java.util.Stack;
24 import java.util.TreeMap;
26 import org.spearce.jgit.errors.CorruptObjectException;
28 public class GitIndex {
30 /** Stage 0 represents merged entries. */
31 public static final int STAGE_0 = 0;
33 private RandomAccessFile cache;
35 private File cacheFile;
37 // Index is modified
38 private boolean changed;
40 // Stat information updated
41 private boolean statDirty;
43 private Header header;
45 private long lastCacheTime;
47 private final Repository db;
49 private Map entries = new TreeMap(new Comparator() {
50 public int compare(Object arg0, Object arg1) {
51 byte[] a = (byte[]) arg0;
52 byte[] b = (byte[]) arg1;
53 for (int i = 0; i < a.length && i < b.length; ++i) {
54 int c = a[i] - b[i];
55 if (c != 0)
56 return c;
58 if (a.length < b.length)
59 return -1;
60 else if (a.length > b.length)
61 return 1;
62 return 0;
64 });
66 public GitIndex(Repository db) {
67 this.db = db;
68 this.cacheFile = new File(db.getDirectory(), "index");
71 public boolean isChanged() {
72 return changed || statDirty;
75 public void rereadIfNecessary() throws IOException {
76 if (cacheFile.exists() && cacheFile.lastModified() != lastCacheTime) {
77 read();
81 public void add(File wd, File f) throws IOException {
82 byte[] key = Entry.makeKey(wd, f);
83 Entry e = (Entry) entries.get(key);
84 if (e == null) {
85 e = new Entry(key, f, 0, this);
86 entries.put(key, e);
87 } else {
88 e.update(f, db);
92 public void remove(File wd, File f) {
93 byte[] key = Entry.makeKey(wd, f);
94 entries.remove(key);
97 public void read() throws IOException {
98 long t0 = System.currentTimeMillis();
99 changed = false;
100 statDirty = false;
101 cache = new RandomAccessFile(cacheFile, "r");
102 try {
103 MappedByteBuffer map = cache.getChannel().map(MapMode.READ_ONLY, 0,
104 cacheFile.length());
105 map.order(ByteOrder.BIG_ENDIAN);
106 header = new Header(map);
107 entries.clear();
108 for (int i = 0; i < header.entries; ++i) {
109 Entry entry = new Entry(this, map);
110 entries.put(entry.name, entry);
112 long t1 = System.currentTimeMillis();
113 lastCacheTime = cacheFile.lastModified();
114 System.out.println("Read index "+cacheFile+" in "+((t1-t0)/1000.0)+"s");
115 } finally {
116 cache.close();
120 public void write() throws IOException {
121 checkWriteOk();
122 File tmpIndex = new File(cacheFile.getAbsoluteFile() + ".tmp");
123 File lock = new File(cacheFile.getAbsoluteFile() + ".lock");
124 if (!lock.createNewFile())
125 throw new IOException("Index file is in use");
126 try {
127 FileOutputStream fileOutputStream = new FileOutputStream(tmpIndex);
128 FileChannel fc = fileOutputStream.getChannel();
129 ByteBuffer buf = ByteBuffer.allocate(4096);
130 MessageDigest newMessageDigest = Constants.newMessageDigest();
131 header = new Header(entries);
132 header.write(buf);
133 buf.flip();
134 newMessageDigest
135 .update(buf.array(), buf.arrayOffset(), buf.limit());
136 fc.write(buf);
137 buf.flip();
138 buf.clear();
139 for (Iterator i = entries.values().iterator(); i.hasNext();) {
140 Entry e = (Entry) i.next();
141 e.write(buf);
142 buf.flip();
143 newMessageDigest.update(buf.array(), buf.arrayOffset(), buf
144 .limit());
145 fc.write(buf);
146 buf.flip();
147 buf.clear();
149 buf.put(newMessageDigest.digest());
150 buf.flip();
151 fc.write(buf);
152 fc.close();
153 fileOutputStream.close();
154 if (!tmpIndex.renameTo(cacheFile))
155 throw new IOException(
156 "Could not rename temporary index file to index");
157 changed = false;
158 statDirty = false;
159 } finally {
160 if (!lock.delete())
161 throw new IOException(
162 "Could not delete lock file. Should not happen");
163 if (tmpIndex.exists() && !tmpIndex.delete())
164 throw new IOException(
165 "Could not delete temporary index file. Should not happen");
169 private void checkWriteOk() throws IOException {
170 for (Iterator i = entries.values().iterator(); i.hasNext();) {
171 Entry e = (Entry) i.next();
172 if (e.stage != 0) {
173 throw new IOException("Cannot work with other stages than zero right now. Won't write corrupt index.");
178 public static class Entry {
179 private long ctime;
181 private long mtime;
183 private int dev;
185 private int ino;
187 private int mode;
189 private int uid;
191 private int gid;
193 private int size;
195 private ObjectId sha1;
197 private short flags;
199 private byte[] name;
201 private int stage;
203 private GitIndex theIndex;
205 static byte[] makeKey(File wd, File f) {
206 if (!f.getPath().startsWith(wd.getPath()))
207 throw new Error("Path is not in working dir");
208 String relName = f.getPath().substring(wd.getPath().length() + 1)
209 .replace(File.separatorChar, '/');
210 return relName.getBytes();
213 public Entry(byte[] key, File f, int stage, GitIndex index)
214 throws IOException {
215 theIndex = index;
216 ctime = f.lastModified() * 1000000L;
217 mtime = ctime; // we use same here
218 dev = -1;
219 ino = -1;
220 mode = FileMode.REGULAR_FILE.getBits();
221 uid = -1;
222 gid = -1;
223 size = (int) f.length();
224 ObjectWriter writer = new ObjectWriter(theIndex.db);
225 sha1 = writer.writeBlob(f);
226 name = key;
227 flags = (short) ((stage << 12) | name.length); // TODO: fix flags
230 public Entry(TreeEntry f, int stage, GitIndex index)
231 throws UnsupportedEncodingException {
232 theIndex = index;
233 ctime = -1; // hmm
234 mtime = -1;
235 dev = -1;
236 ino = -1;
237 mode = f.getMode().getBits();
238 uid = -1;
239 gid = -1;
240 size = -1;
241 sha1 = f.getId();
242 name = f.getFullName().getBytes("UTF-8");
243 flags = (short) ((stage << 12) | name.length); // TODO: fix flags
246 Entry(GitIndex index, ByteBuffer b) {
247 theIndex = index;
248 int startposition = b.position();
249 ctime = b.getInt() * 1000000000L + (b.getInt() % 1000000000L);
250 mtime = b.getInt() * 1000000000L + (b.getInt() % 1000000000L);
251 dev = b.getInt();
252 ino = b.getInt();
253 mode = b.getInt();
254 uid = b.getInt();
255 gid = b.getInt();
256 size = b.getInt();
257 byte[] sha1bytes = new byte[Constants.OBJECT_ID_LENGTH];
258 b.get(sha1bytes);
259 sha1 = new ObjectId(sha1bytes);
260 flags = b.getShort();
261 stage = (flags & 0x3000) >> 12;
262 name = new byte[flags & 0xFFF];
263 b.get(name);
265 .position(startposition
266 + ((8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 20 + 2
267 + name.length + 8) & ~7));
270 public boolean update(File f, Repository db) throws IOException {
271 boolean modified = false;
272 long lm = f.lastModified() * 1000000L;
273 if (mtime != lm)
274 modified = true;
275 mtime = f.lastModified() * 1000000L;
276 if (size != f.length())
277 modified = true;
278 if (File_canExecute(f) != FileMode.EXECUTABLE_FILE.equals(mode)) {
279 mode = FileMode.EXECUTABLE_FILE.getBits();
280 modified = true;
282 if (modified) {
283 size = (int) f.length();
284 ObjectWriter writer = new ObjectWriter(db);
285 ObjectId newsha1 = sha1 = writer.writeBlob(f);
286 if (!newsha1.equals(sha1))
287 modified = true;
288 sha1 = newsha1;
290 return modified;
293 public void write(ByteBuffer buf) {
294 int startposition = buf.position();
295 buf.putInt((int) (ctime / 1000000000L));
296 buf.putInt((int) (ctime % 1000000000L));
297 buf.putInt((int) (mtime / 1000000000L));
298 buf.putInt((int) (mtime % 1000000000L));
299 buf.putInt(dev);
300 buf.putInt(ino);
301 buf.putInt(mode);
302 buf.putInt(uid);
303 buf.putInt(gid);
304 buf.putInt(size);
305 buf.put(sha1.getBytes());
306 buf.putShort(flags);
307 buf.put(name);
308 int end = startposition
309 + ((8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 20 + 2 + name.length + 8) & ~7);
310 int remain = end - buf.position();
311 while (remain-- > 0)
312 buf.put((byte) 0);
315 static Method canExecute;
316 static {
317 try {
318 canExecute = File.class.getMethod("canExecute", (Class[]) null);
319 } catch (SecurityException e) {
320 // TODO Auto-generated catch block
321 e.printStackTrace();
322 } catch (NoSuchMethodException e) {
323 // TODO Auto-generated catch block
324 e.printStackTrace();
329 * JDK1.6 has file.canExecute
331 * if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
332 * return true;
334 boolean File_canExecute(File f) {
335 if (canExecute != null) {
336 try {
337 return ((Boolean) canExecute.invoke(f, (Object[]) null))
338 .booleanValue();
339 } catch (IllegalArgumentException e) {
340 // TODO Auto-generated catch block
341 e.printStackTrace();
342 return false;
343 } catch (IllegalAccessException e) {
344 // TODO Auto-generated catch block
345 e.printStackTrace();
346 return false;
347 } catch (InvocationTargetException e) {
348 // TODO Auto-generated catch block
349 e.printStackTrace();
350 return false;
352 } else
353 return false;
357 * Check if an entry's content is different from the cache,
359 * File status information is used and status is same we
360 * consider the file identical to the state in the working
361 * directory. Native git uses more stat fields than we
362 * have accessible in Java.
364 * @param wd working directory to compare content with
365 * @return true if content is most likely different.
367 public boolean isModified(File wd) {
368 return isModified(wd, false);
372 * Check if an entry's content is different from the cache,
374 * File status information is used and status is same we
375 * consider the file identical to the state in the working
376 * directory. Native git uses more stat fields than we
377 * have accessible in Java.
379 * @param wd working directory to compare content with
380 * @param forceContentCheck True if the actual file content
381 * should be checked if modification time differs.
383 * @return true if content is most likely different.
385 public boolean isModified(File wd, boolean forceContentCheck) {
386 File file = getFile(wd);
387 if (!file.exists())
388 return true;
390 // JDK1.6 has file.canExecute
391 // if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
392 // return true;
393 if (FileMode.EXECUTABLE_FILE.equals(mode)) {
394 if (!File_canExecute(file)&& canExecute != null)
395 return true;
396 } else {
397 if (FileMode.REGULAR_FILE.equals(mode)) {
398 if (!file.isFile())
399 return true;
400 if (File_canExecute(file) && canExecute != null)
401 return true;
402 } else {
403 if (FileMode.SYMLINK.equals(mode)) {
404 return true;
405 } else {
406 if (FileMode.TREE.equals(mode)) {
407 if (!file.isDirectory())
408 return true;
409 } else {
410 System.out.println("Does not handle mode "+mode+" ("+file+")");
411 return true;
417 long javamtime = mtime / 1000000L;
418 long lastm = file.lastModified();
419 if (file.length() != size)
420 return true;
421 if (lastm != javamtime) {
422 if (!forceContentCheck)
423 return true;
425 try {
426 InputStream is = new FileInputStream(file);
427 ObjectWriter objectWriter = new ObjectWriter(theIndex.db);
428 try {
429 ObjectId newId = objectWriter.computeBlobSha1(file
430 .length(), is);
431 boolean ret = !newId.equals(sha1);
432 theIndex.statDirty = true;
433 return ret;
434 } catch (IOException e) {
435 e.printStackTrace();
436 } finally {
437 try {
438 is.close();
439 } catch (IOException e) {
440 // can't happen, but if it does we ignore it
441 e.printStackTrace();
444 } catch (FileNotFoundException e) {
445 // should not happen because we already checked this
446 e.printStackTrace();
447 throw new Error(e);
450 return false;
453 private File getFile(File wd) {
454 return new File(wd, getName());
457 public String toString() {
458 return new String(name) + "/SHA-1(" + sha1 + ")/M:"
459 + new Date(ctime / 1000000L) + "/C:"
460 + new Date(mtime / 1000000L) + "/d" + dev + "/i" + ino
461 + "/m" + Integer.toString(mode, 8) + "/u" + uid + "/g"
462 + gid + "/s" + size + "/f" + flags + "/@" + stage;
465 public String getName() {
466 return new String(name);
469 public ObjectId getObjectId() {
470 return sha1;
473 public int getStage() {
474 return stage;
478 static class Header {
479 private int signature;
481 private int version;
483 int entries;
485 public Header(ByteBuffer map) throws CorruptObjectException {
486 read(map);
489 private void read(ByteBuffer buf) throws CorruptObjectException {
490 signature = buf.getInt();
491 version = buf.getInt();
492 entries = buf.getInt();
493 if (signature != 0x44495243)
494 throw new CorruptObjectException("Index signature is invalid: "
495 + signature);
496 if (version != 2)
497 throw new CorruptObjectException(
498 "Unknow index version (or corrupt index):" + version);
501 public void write(ByteBuffer buf) {
502 buf.order(ByteOrder.BIG_ENDIAN);
503 buf.putInt(signature);
504 buf.putInt(version);
505 buf.putInt(entries);
508 public Header(Map entryset) {
509 signature = 0x44495243;
510 version = 2;
511 entries = entryset.size();
515 public void readTree(Tree t) throws IOException {
516 readTree("", t);
519 public void readTree(String prefix, Tree t) throws IOException {
520 TreeEntry[] members = t.members();
521 for (int i = 0; i < members.length; ++i) {
522 TreeEntry te = members[i];
523 String name;
524 if (prefix.length() > 0)
525 name = prefix + "/" + te.getName();
526 else
527 name = te.getName();
528 if (te instanceof Tree) {
529 readTree(name, (Tree) te);
530 } else {
531 Entry e = new Entry(te, 0, this);
532 entries.put(name.getBytes("UTF-8"), e);
537 public void checkout(File wd) throws IOException {
538 for (Iterator i = entries.values().iterator(); i.hasNext();) {
539 Entry e = (Entry) i.next();
540 if (e.stage != 0)
541 continue;
542 ObjectLoader ol = db.openBlob(e.sha1);
543 byte[] bytes = ol.getBytes();
544 File file = new File(wd, e.getName());
545 file.delete();
546 file.getParentFile().mkdirs();
547 FileChannel channel = new FileOutputStream(file).getChannel();
548 ByteBuffer buffer = ByteBuffer.wrap(bytes);
549 int j = channel.write(buffer);
550 if (j != bytes.length)
551 throw new IOException("Could not write file " + file);
552 channel.close();
556 public ObjectId writeTree() throws IOException {
557 checkWriteOk();
558 ObjectWriter writer = new ObjectWriter(db);
559 Tree current = new Tree(db);
560 Stack trees = new Stack();
561 trees.push(current);
562 String[] prevName = new String[0];
563 for (Iterator i = entries.values().iterator(); i.hasNext();) {
564 Entry e = (Entry) i.next();
565 if (e.stage != 0)
566 continue;
567 String[] newName = splitDirPath(e.getName());
568 int c = longestCommonPath(prevName, newName);
569 while (c < trees.size() - 1) {
570 current.setId(writer.writeTree(current));
571 trees.pop();
572 current = trees.isEmpty() ? null : (Tree) trees.peek();
574 while (trees.size() < newName.length) {
575 if (!current.existsTree(newName[trees.size() - 1])) {
576 current = new Tree(current, newName[trees.size() - 1]
577 .getBytes());
578 current.getParent().addEntry(current);
579 trees.push(current);
580 } else {
581 current = (Tree) current.findTreeMember(newName[trees
582 .size() - 1]);
583 trees.push(current);
586 FileTreeEntry ne = new FileTreeEntry(current, e.sha1,
587 newName[newName.length - 1].getBytes(),
588 (e.mode & FileMode.EXECUTABLE_FILE.getBits()) == FileMode.EXECUTABLE_FILE.getBits());
589 current.addEntry(ne);
591 while (!trees.isEmpty()) {
592 current.setId(writer.writeTree(current));
593 trees.pop();
594 if (!trees.isEmpty())
595 current = (Tree) trees.peek();
597 return current.getTreeId();
600 String[] splitDirPath(String name) {
601 String[] tmp = new String[name.length() / 2 + 1];
602 int p0 = -1;
603 int p1;
604 int c = 0;
605 while ((p1 = name.indexOf('/', p0 + 1)) != -1) {
606 tmp[c++] = name.substring(p0 + 1, p1);
607 p0 = p1;
609 tmp[c++] = name.substring(p0 + 1);
610 String[] ret = new String[c];
611 for (int i = 0; i < c; ++i) {
612 ret[i] = tmp[i];
614 return ret;
617 int longestCommonPath(String[] a, String[] b) {
618 int i;
619 for (i = 0; i < a.length && i < b.length; ++i)
620 if (!a[i].equals(b[i]))
621 return i;
622 return i;
625 public Entry[] getMembers() {
626 return (Entry[]) entries.values().toArray(new Entry[entries.size()]);
629 public Entry getEntry(String path) throws UnsupportedEncodingException {
630 return (Entry) entries.get(path.getBytes("ISO-8859-1"));
633 public Repository getRepository() {
634 return db;