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
;
25 import org
.spearce
.jgit
.errors
.NotSupportedException
;
27 public class GitIndex
{
29 /** Stage 0 represents merged entries. */
30 public static final int STAGE_0
= 0;
32 private RandomAccessFile cache
;
34 private File cacheFile
;
37 private boolean changed
;
39 // Stat information updated
40 private boolean statDirty
;
42 private Header header
;
44 private long lastCacheTime
;
46 private final Repository db
;
48 private Map entries
= new TreeMap(new Comparator() {
49 public int compare(Object arg0
, Object arg1
) {
50 byte[] a
= (byte[]) arg0
;
51 byte[] b
= (byte[]) arg1
;
52 for (int i
= 0; i
< a
.length
&& i
< b
.length
; ++i
) {
57 if (a
.length
< b
.length
)
59 else if (a
.length
> b
.length
)
65 public GitIndex(Repository db
) {
67 this.cacheFile
= new File(db
.getDirectory(), "index");
70 public boolean isChanged() {
71 return changed
|| statDirty
;
74 public void rereadIfNecessary() throws IOException
{
75 if (cacheFile
.exists() && cacheFile
.lastModified() != lastCacheTime
) {
80 public void add(File wd
, File f
) throws IOException
{
81 byte[] key
= makeKey(wd
, f
);
82 Entry e
= (Entry
) entries
.get(key
);
84 e
= new Entry(key
, f
, 0);
91 public boolean remove(File wd
, File f
) {
92 byte[] key
= makeKey(wd
, f
);
93 return entries
.remove(key
) != null;
96 public void read() throws IOException
{
97 long t0
= System
.currentTimeMillis();
100 cache
= new RandomAccessFile(cacheFile
, "r");
102 FileChannel channel
= cache
.getChannel();
103 ByteBuffer buffer
= ByteBuffer
.allocateDirect((int) cacheFile
.length());
104 buffer
.order(ByteOrder
.BIG_ENDIAN
);
105 int j
= channel
.read(buffer
);
106 if (j
!= buffer
.capacity())
107 throw new IOException("Could not read index in one go, only "+j
+" out of "+buffer
.capacity()+" read");
109 header
= new Header(buffer
);
111 for (int i
= 0; i
< header
.entries
; ++i
) {
112 Entry entry
= new Entry(buffer
);
113 entries
.put(entry
.name
, entry
);
115 long t1
= System
.currentTimeMillis();
116 lastCacheTime
= cacheFile
.lastModified();
117 System
.out
.println("Read index "+cacheFile
+" in "+((t1
-t0
)/1000.0)+"s");
123 public void write() throws IOException
{
125 File tmpIndex
= new File(cacheFile
.getAbsoluteFile() + ".tmp");
126 File lock
= new File(cacheFile
.getAbsoluteFile() + ".lock");
127 if (!lock
.createNewFile())
128 throw new IOException("Index file is in use");
130 FileOutputStream fileOutputStream
= new FileOutputStream(tmpIndex
);
131 FileChannel fc
= fileOutputStream
.getChannel();
132 ByteBuffer buf
= ByteBuffer
.allocate(4096);
133 MessageDigest newMessageDigest
= Constants
.newMessageDigest();
134 header
= new Header(entries
);
138 .update(buf
.array(), buf
.arrayOffset(), buf
.limit());
142 for (Iterator i
= entries
.values().iterator(); i
.hasNext();) {
143 Entry e
= (Entry
) i
.next();
146 newMessageDigest
.update(buf
.array(), buf
.arrayOffset(), buf
152 buf
.put(newMessageDigest
.digest());
156 fileOutputStream
.close();
157 if (cacheFile
.exists())
158 if (!cacheFile
.delete())
159 throw new IOException(
160 "Could not rename delete old index");
161 if (!tmpIndex
.renameTo(cacheFile
))
162 throw new IOException(
163 "Could not rename temporary index file to index");
168 throw new IOException(
169 "Could not delete lock file. Should not happen");
170 if (tmpIndex
.exists() && !tmpIndex
.delete())
171 throw new IOException(
172 "Could not delete temporary index file. Should not happen");
176 private void checkWriteOk() throws IOException
{
177 for (Iterator i
= entries
.values().iterator(); i
.hasNext();) {
178 Entry e
= (Entry
) i
.next();
180 throw new NotSupportedException("Cannot work with other stages than zero right now. Won't write corrupt index.");
185 static Method canExecute
;
186 static Method setExecute
;
189 canExecute
= File
.class.getMethod("canExecute", (Class
[]) null);
190 setExecute
= File
.class.getMethod("setExecutable", new Class
[] { Boolean
.TYPE
});
191 } catch (SecurityException e
) {
193 } catch (NoSuchMethodException e
) {
194 System
.out
.println("This vm cannot handle execute file permission. Feature disabled");
199 * JDK1.6 has file.canExecute
201 * if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
204 boolean File_canExecute(File f
) {
205 if (canExecute
!= null) {
207 return ((Boolean
) canExecute
.invoke(f
, (Object
[]) null))
209 } catch (IllegalArgumentException e
) {
211 } catch (IllegalAccessException e
) {
213 } catch (InvocationTargetException e
) {
221 * JDK1.6 has file.setExecute
223 boolean File_setExecute(File f
,boolean value
) {
224 if (setExecute
!= null) {
226 return ((Boolean
) setExecute
.invoke(f
,
227 new Object
[] { new Boolean(value
) })).booleanValue();
228 } catch (IllegalArgumentException e
) {
230 } catch (IllegalAccessException e
) {
232 } catch (InvocationTargetException e
) {
239 static byte[] makeKey(File wd
, File f
) {
240 if (!f
.getPath().startsWith(wd
.getPath()))
241 throw new Error("Path is not in working dir");
242 String relName
= f
.getPath().substring(wd
.getPath().length() + 1)
243 .replace(File
.separatorChar
, '/');
244 return relName
.getBytes();
248 private boolean config_filemode() {
249 // temporary til we can actually set parameters. We need to be able
250 // to change this for testing.
251 if (filemode
!= null)
252 return filemode
.booleanValue();
253 RepositoryConfig config
= db
.getConfig();
254 return config
.getBoolean("core", "filemode", true);
274 private ObjectId sha1
;
282 public Entry(byte[] key
, File f
, int stage
)
284 ctime
= f
.lastModified() * 1000000L;
285 mtime
= ctime
; // we use same here
288 if (config_filemode() && File_canExecute(f
))
289 mode
= FileMode
.EXECUTABLE_FILE
.getBits();
291 mode
= FileMode
.REGULAR_FILE
.getBits();
294 size
= (int) f
.length();
295 ObjectWriter writer
= new ObjectWriter(db
);
296 sha1
= writer
.writeBlob(f
);
298 flags
= (short) ((stage
<< 12) | name
.length
); // TODO: fix flags
301 public Entry(TreeEntry f
, int stage
)
302 throws UnsupportedEncodingException
{
307 mode
= f
.getMode().getBits();
312 name
= f
.getFullName().getBytes("UTF-8");
313 flags
= (short) ((stage
<< 12) | name
.length
); // TODO: fix flags
316 Entry(ByteBuffer b
) {
317 int startposition
= b
.position();
318 ctime
= b
.getInt() * 1000000000L + (b
.getInt() % 1000000000L);
319 mtime
= b
.getInt() * 1000000000L + (b
.getInt() % 1000000000L);
326 byte[] sha1bytes
= new byte[Constants
.OBJECT_ID_LENGTH
];
328 sha1
= new ObjectId(sha1bytes
);
329 flags
= b
.getShort();
330 stage
= (flags
& 0x3000) >> 12;
331 name
= new byte[flags
& 0xFFF];
334 .position(startposition
335 + ((8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 20 + 2
336 + name
.length
+ 8) & ~
7));
339 public boolean update(File f
) throws IOException
{
340 boolean modified
= false;
341 long lm
= f
.lastModified() * 1000000L;
344 mtime
= f
.lastModified() * 1000000L;
345 if (size
!= f
.length())
347 if (config_filemode()) {
348 if (File_canExecute(f
) != FileMode
.EXECUTABLE_FILE
.equals(mode
)) {
349 mode
= FileMode
.EXECUTABLE_FILE
.getBits();
354 size
= (int) f
.length();
355 ObjectWriter writer
= new ObjectWriter(db
);
356 ObjectId newsha1
= sha1
= writer
.writeBlob(f
);
357 if (!newsha1
.equals(sha1
))
364 public void write(ByteBuffer buf
) {
365 int startposition
= buf
.position();
366 buf
.putInt((int) (ctime
/ 1000000000L));
367 buf
.putInt((int) (ctime
% 1000000000L));
368 buf
.putInt((int) (mtime
/ 1000000000L));
369 buf
.putInt((int) (mtime
% 1000000000L));
376 buf
.put(sha1
.getBytes());
379 int end
= startposition
380 + ((8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 20 + 2 + name
.length
+ 8) & ~
7);
381 int remain
= end
- buf
.position();
387 * Check if an entry's content is different from the cache,
389 * File status information is used and status is same we
390 * consider the file identical to the state in the working
391 * directory. Native git uses more stat fields than we
392 * have accessible in Java.
394 * @param wd working directory to compare content with
395 * @return true if content is most likely different.
397 public boolean isModified(File wd
) {
398 return isModified(wd
, false);
402 * Check if an entry's content is different from the cache,
404 * File status information is used and status is same we
405 * consider the file identical to the state in the working
406 * directory. Native git uses more stat fields than we
407 * have accessible in Java.
409 * @param wd working directory to compare content with
410 * @param forceContentCheck True if the actual file content
411 * should be checked if modification time differs.
413 * @return true if content is most likely different.
415 public boolean isModified(File wd
, boolean forceContentCheck
) {
416 File file
= getFile(wd
);
420 // JDK1.6 has file.canExecute
421 // if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
423 final int exebits
= FileMode
.EXECUTABLE_FILE
.getBits()
424 ^ FileMode
.REGULAR_FILE
.getBits();
426 if (config_filemode() && FileMode
.EXECUTABLE_FILE
.equals(mode
)) {
427 if (!File_canExecute(file
)&& canExecute
!= null)
430 if (FileMode
.REGULAR_FILE
.equals(mode
&~exebits
)) {
433 if (config_filemode() && File_canExecute(file
) && canExecute
!= null)
436 if (FileMode
.SYMLINK
.equals(mode
)) {
439 if (FileMode
.TREE
.equals(mode
)) {
440 if (!file
.isDirectory())
443 System
.out
.println("Does not handle mode "+mode
+" ("+file
+")");
450 if (file
.length() != size
)
453 // Git under windows only stores seconds so we round the timestmap
454 // Java gives us if it looks like the timestamp in index is seconds
455 // only. Otherwise we compare the timestamp at millisecond prevision.
456 long javamtime
= mtime
/ 1000000L;
457 long lastm
= file
.lastModified();
458 if (javamtime
% 1000 == 0)
459 lastm
= lastm
- lastm
% 1000;
460 if (lastm
!= javamtime
) {
461 if (!forceContentCheck
)
465 InputStream is
= new FileInputStream(file
);
466 ObjectWriter objectWriter
= new ObjectWriter(db
);
468 ObjectId newId
= objectWriter
.computeBlobSha1(file
470 boolean ret
= !newId
.equals(sha1
);
472 } catch (IOException e
) {
477 } catch (IOException e
) {
478 // can't happen, but if it does we ignore it
482 } catch (FileNotFoundException e
) {
483 // should not happen because we already checked this
491 private File
getFile(File wd
) {
492 return new File(wd
, getName());
495 public String
toString() {
496 return new String(name
) + "/SHA-1(" + sha1
+ ")/M:"
497 + new Date(ctime
/ 1000000L) + "/C:"
498 + new Date(mtime
/ 1000000L) + "/d" + dev
+ "/i" + ino
499 + "/m" + Integer
.toString(mode
, 8) + "/u" + uid
+ "/g"
500 + gid
+ "/s" + size
+ "/f" + flags
+ "/@" + stage
;
503 public String
getName() {
504 return new String(name
);
507 public ObjectId
getObjectId() {
511 public int getStage() {
516 static class Header
{
517 private int signature
;
523 public Header(ByteBuffer map
) throws CorruptObjectException
{
527 private void read(ByteBuffer buf
) throws CorruptObjectException
{
528 signature
= buf
.getInt();
529 version
= buf
.getInt();
530 entries
= buf
.getInt();
531 if (signature
!= 0x44495243)
532 throw new CorruptObjectException("Index signature is invalid: "
535 throw new CorruptObjectException(
536 "Unknow index version (or corrupt index):" + version
);
539 public void write(ByteBuffer buf
) {
540 buf
.order(ByteOrder
.BIG_ENDIAN
);
541 buf
.putInt(signature
);
546 public Header(Map entryset
) {
547 signature
= 0x44495243;
549 entries
= entryset
.size();
553 public void readTree(Tree t
) throws IOException
{
557 public void readTree(String prefix
, Tree t
) throws IOException
{
558 TreeEntry
[] members
= t
.members();
559 for (int i
= 0; i
< members
.length
; ++i
) {
560 TreeEntry te
= members
[i
];
562 if (prefix
.length() > 0)
563 name
= prefix
+ "/" + te
.getName();
566 if (te
instanceof Tree
) {
567 readTree(name
, (Tree
) te
);
569 Entry e
= new Entry(te
, 0);
570 entries
.put(name
.getBytes("UTF-8"), e
);
575 public void checkout(File wd
) throws IOException
{
576 for (Iterator i
= entries
.values().iterator(); i
.hasNext();) {
577 Entry e
= (Entry
) i
.next();
580 ObjectLoader ol
= db
.openBlob(e
.sha1
);
581 byte[] bytes
= ol
.getBytes();
582 File file
= new File(wd
, e
.getName());
584 file
.getParentFile().mkdirs();
585 FileChannel channel
= new FileOutputStream(file
).getChannel();
586 ByteBuffer buffer
= ByteBuffer
.wrap(bytes
);
587 int j
= channel
.write(buffer
);
588 if (j
!= bytes
.length
)
589 throw new IOException("Could not write file " + file
);
591 if (config_filemode() && canExecute
!= null) {
592 if (FileMode
.EXECUTABLE_FILE
.equals(e
.mode
)) {
593 if (!File_canExecute(file
))
594 File_setExecute(file
, true);
596 if (File_canExecute(file
))
597 File_setExecute(file
, false);
600 e
.mtime
= file
.lastModified() * 1000000L;
605 public ObjectId
writeTree() throws IOException
{
607 ObjectWriter writer
= new ObjectWriter(db
);
608 Tree current
= new Tree(db
);
609 Stack trees
= new Stack();
611 String
[] prevName
= new String
[0];
612 for (Iterator i
= entries
.values().iterator(); i
.hasNext();) {
613 Entry e
= (Entry
) i
.next();
616 String
[] newName
= splitDirPath(e
.getName());
617 int c
= longestCommonPath(prevName
, newName
);
618 while (c
< trees
.size() - 1) {
619 current
.setId(writer
.writeTree(current
));
621 current
= trees
.isEmpty() ?
null : (Tree
) trees
.peek();
623 while (trees
.size() < newName
.length
) {
624 if (!current
.existsTree(newName
[trees
.size() - 1])) {
625 current
= new Tree(current
, newName
[trees
.size() - 1]
627 current
.getParent().addEntry(current
);
630 current
= (Tree
) current
.findTreeMember(newName
[trees
635 FileTreeEntry ne
= new FileTreeEntry(current
, e
.sha1
,
636 newName
[newName
.length
- 1].getBytes(),
637 (e
.mode
& FileMode
.EXECUTABLE_FILE
.getBits()) == FileMode
.EXECUTABLE_FILE
.getBits());
638 current
.addEntry(ne
);
640 while (!trees
.isEmpty()) {
641 current
.setId(writer
.writeTree(current
));
643 if (!trees
.isEmpty())
644 current
= (Tree
) trees
.peek();
646 return current
.getTreeId();
649 String
[] splitDirPath(String name
) {
650 String
[] tmp
= new String
[name
.length() / 2 + 1];
654 while ((p1
= name
.indexOf('/', p0
+ 1)) != -1) {
655 tmp
[c
++] = name
.substring(p0
+ 1, p1
);
658 tmp
[c
++] = name
.substring(p0
+ 1);
659 String
[] ret
= new String
[c
];
660 for (int i
= 0; i
< c
; ++i
) {
666 int longestCommonPath(String
[] a
, String
[] b
) {
668 for (i
= 0; i
< a
.length
&& i
< b
.length
; ++i
)
669 if (!a
[i
].equals(b
[i
]))
674 public Entry
[] getMembers() {
675 return (Entry
[]) entries
.values().toArray(new Entry
[entries
.size()]);
678 public Entry
getEntry(String path
) throws UnsupportedEncodingException
{
679 return (Entry
) entries
.get(Repository
.gitInternalSlash(path
.getBytes("ISO-8859-1")));
682 public Repository
getRepository() {