1 package org
.spearce
.jgit
.lib
;
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
;
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
;
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
) {
56 if (a
.length
< b
.length
)
58 else if (a
.length
> b
.length
)
64 public GitIndex(Repository 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
) {
79 public void add(File wd
, File f
) throws IOException
{
80 byte[] key
= makeKey(wd
, f
);
81 Entry e
= (Entry
) entries
.get(key
);
83 e
= new Entry(key
, f
, 0);
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();
99 cache
= new RandomAccessFile(cacheFile
, "r");
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");
108 header
= new Header(buffer
);
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");
122 public void write() throws IOException
{
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");
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
);
137 .update(buf
.array(), buf
.arrayOffset(), buf
.limit());
141 for (Iterator i
= entries
.values().iterator(); i
.hasNext();) {
142 Entry e
= (Entry
) i
.next();
145 newMessageDigest
.update(buf
.array(), buf
.arrayOffset(), buf
151 buf
.put(newMessageDigest
.digest());
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");
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();
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
;
188 canExecute
= File
.class.getMethod("canExecute", (Class
[]) null);
189 setExecute
= File
.class.getMethod("setExecutable", new Class
[] { Boolean
.TYPE
});
190 } catch (SecurityException e
) {
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))
203 boolean File_canExecute(File f
) {
204 if (canExecute
!= null) {
206 return ((Boolean
) canExecute
.invoke(f
, (Object
[]) null))
208 } catch (IllegalArgumentException e
) {
210 } catch (IllegalAccessException e
) {
212 } catch (InvocationTargetException e
) {
220 * JDK1.6 has file.setExecute
222 boolean File_setExecute(File f
,boolean value
) {
223 if (setExecute
!= null) {
225 return ((Boolean
) setExecute
.invoke(f
,
226 new Object
[] { new Boolean(value
) })).booleanValue();
227 } catch (IllegalArgumentException e
) {
229 } catch (IllegalAccessException e
) {
231 } catch (InvocationTargetException e
) {
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();
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);
273 private ObjectId sha1
;
281 public Entry(byte[] key
, File f
, int stage
)
283 ctime
= f
.lastModified() * 1000000L;
284 mtime
= ctime
; // we use same here
287 if (config_filemode() && File_canExecute(f
))
288 mode
= FileMode
.EXECUTABLE_FILE
.getBits();
290 mode
= FileMode
.REGULAR_FILE
.getBits();
293 size
= (int) f
.length();
294 ObjectWriter writer
= new ObjectWriter(db
);
295 sha1
= writer
.writeBlob(f
);
297 flags
= (short) ((stage
<< 12) | name
.length
); // TODO: fix flags
300 public Entry(TreeEntry f
, int stage
)
301 throws UnsupportedEncodingException
{
306 mode
= f
.getMode().getBits();
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);
325 byte[] sha1bytes
= new byte[Constants
.OBJECT_ID_LENGTH
];
327 sha1
= new ObjectId(sha1bytes
);
328 flags
= b
.getShort();
329 stage
= (flags
& 0x3000) >> 12;
330 name
= new byte[flags
& 0xFFF];
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;
343 mtime
= f
.lastModified() * 1000000L;
344 if (size
!= f
.length())
346 if (config_filemode()) {
347 if (File_canExecute(f
) != FileMode
.EXECUTABLE_FILE
.equals(mode
)) {
348 mode
= FileMode
.EXECUTABLE_FILE
.getBits();
353 size
= (int) f
.length();
354 ObjectWriter writer
= new ObjectWriter(db
);
355 ObjectId newsha1
= sha1
= writer
.writeBlob(f
);
356 if (!newsha1
.equals(sha1
))
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));
375 buf
.put(sha1
.getBytes());
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();
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
);
419 // JDK1.6 has file.canExecute
420 // if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
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)
429 if (FileMode
.REGULAR_FILE
.equals(mode
&~exebits
)) {
432 if (config_filemode() && File_canExecute(file
) && canExecute
!= null)
435 if (FileMode
.SYMLINK
.equals(mode
)) {
438 if (FileMode
.TREE
.equals(mode
)) {
439 if (!file
.isDirectory())
442 System
.out
.println("Does not handle mode "+mode
+" ("+file
+")");
449 long javamtime
= mtime
/ 1000000L;
450 long lastm
= file
.lastModified();
451 if (file
.length() != size
)
453 if (lastm
!= javamtime
) {
454 if (!forceContentCheck
)
458 InputStream is
= new FileInputStream(file
);
459 ObjectWriter objectWriter
= new ObjectWriter(db
);
461 ObjectId newId
= objectWriter
.computeBlobSha1(file
463 boolean ret
= !newId
.equals(sha1
);
465 } catch (IOException e
) {
470 } catch (IOException e
) {
471 // can't happen, but if it does we ignore it
475 } catch (FileNotFoundException e
) {
476 // should not happen because we already checked this
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() {
504 public int getStage() {
509 static class Header
{
510 private int signature
;
516 public Header(ByteBuffer map
) throws CorruptObjectException
{
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: "
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
);
539 public Header(Map entryset
) {
540 signature
= 0x44495243;
542 entries
= entryset
.size();
546 public void readTree(Tree t
) throws IOException
{
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
];
555 if (prefix
.length() > 0)
556 name
= prefix
+ "/" + te
.getName();
559 if (te
instanceof Tree
) {
560 readTree(name
, (Tree
) te
);
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();
573 ObjectLoader ol
= db
.openBlob(e
.sha1
);
574 byte[] bytes
= ol
.getBytes();
575 File file
= new File(wd
, e
.getName());
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
);
584 if (config_filemode() && canExecute
!= null) {
585 if (FileMode
.EXECUTABLE_FILE
.equals(e
.mode
)) {
586 if (!File_canExecute(file
))
587 File_setExecute(file
, true);
589 if (File_canExecute(file
))
590 File_setExecute(file
, false);
593 e
.mtime
= file
.lastModified() * 1000000L;
598 public ObjectId
writeTree() throws IOException
{
600 ObjectWriter writer
= new ObjectWriter(db
);
601 Tree current
= new Tree(db
);
602 Stack trees
= new Stack();
604 String
[] prevName
= new String
[0];
605 for (Iterator i
= entries
.values().iterator(); i
.hasNext();) {
606 Entry e
= (Entry
) i
.next();
609 String
[] newName
= splitDirPath(e
.getName());
610 int c
= longestCommonPath(prevName
, newName
);
611 while (c
< trees
.size() - 1) {
612 current
.setId(writer
.writeTree(current
));
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]
620 current
.getParent().addEntry(current
);
623 current
= (Tree
) current
.findTreeMember(newName
[trees
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
));
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];
647 while ((p1
= name
.indexOf('/', p0
+ 1)) != -1) {
648 tmp
[c
++] = name
.substring(p0
+ 1, p1
);
651 tmp
[c
++] = name
.substring(p0
+ 1);
652 String
[] ret
= new String
[c
];
653 for (int i
= 0; i
< c
; ++i
) {
659 int longestCommonPath(String
[] a
, String
[] b
) {
661 for (i
= 0; i
< a
.length
&& i
< b
.length
; ++i
)
662 if (!a
[i
].equals(b
[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() {