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
.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
;
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
;
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
) {
58 if (a
.length
< b
.length
)
60 else if (a
.length
> b
.length
)
66 public GitIndex(Repository 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
) {
81 public void add(File wd
, File f
) throws IOException
{
82 byte[] key
= Entry
.makeKey(wd
, f
);
83 Entry e
= (Entry
) entries
.get(key
);
85 e
= new Entry(key
, f
, 0, this);
92 public void remove(File wd
, File f
) {
93 byte[] key
= Entry
.makeKey(wd
, f
);
97 public void read() throws IOException
{
98 long t0
= System
.currentTimeMillis();
101 cache
= new RandomAccessFile(cacheFile
, "r");
103 MappedByteBuffer map
= cache
.getChannel().map(MapMode
.READ_ONLY
, 0,
105 map
.order(ByteOrder
.BIG_ENDIAN
);
106 header
= new Header(map
);
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");
120 public void write() throws IOException
{
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");
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
);
135 .update(buf
.array(), buf
.arrayOffset(), buf
.limit());
139 for (Iterator i
= entries
.values().iterator(); i
.hasNext();) {
140 Entry e
= (Entry
) i
.next();
143 newMessageDigest
.update(buf
.array(), buf
.arrayOffset(), buf
149 buf
.put(newMessageDigest
.digest());
153 fileOutputStream
.close();
154 if (!tmpIndex
.renameTo(cacheFile
))
155 throw new IOException(
156 "Could not rename temporary index file to index");
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();
173 throw new IOException("Cannot work with other stages than zero right now. Won't write corrupt index.");
178 public static class Entry
{
195 private ObjectId sha1
;
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
)
216 ctime
= f
.lastModified() * 1000000L;
217 mtime
= ctime
; // we use same here
220 mode
= FileMode
.REGULAR_FILE
.getBits();
223 size
= (int) f
.length();
224 ObjectWriter writer
= new ObjectWriter(theIndex
.db
);
225 sha1
= writer
.writeBlob(f
);
227 flags
= (short) ((stage
<< 12) | name
.length
); // TODO: fix flags
230 public Entry(TreeEntry f
, int stage
, GitIndex index
)
231 throws UnsupportedEncodingException
{
237 mode
= f
.getMode().getBits();
242 name
= f
.getFullName().getBytes("UTF-8");
243 flags
= (short) ((stage
<< 12) | name
.length
); // TODO: fix flags
246 Entry(GitIndex index
, ByteBuffer b
) {
248 int startposition
= b
.position();
249 ctime
= b
.getInt() * 1000000000L + (b
.getInt() % 1000000000L);
250 mtime
= b
.getInt() * 1000000000L + (b
.getInt() % 1000000000L);
257 byte[] sha1bytes
= new byte[Constants
.OBJECT_ID_LENGTH
];
259 sha1
= new ObjectId(sha1bytes
);
260 flags
= b
.getShort();
261 stage
= (flags
& 0x3000) >> 12;
262 name
= new byte[flags
& 0xFFF];
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;
275 mtime
= f
.lastModified() * 1000000L;
276 if (size
!= f
.length())
278 if (File_canExecute(f
) != FileMode
.EXECUTABLE_FILE
.equals(mode
)) {
279 mode
= FileMode
.EXECUTABLE_FILE
.getBits();
283 size
= (int) f
.length();
284 ObjectWriter writer
= new ObjectWriter(db
);
285 ObjectId newsha1
= sha1
= writer
.writeBlob(f
);
286 if (!newsha1
.equals(sha1
))
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));
305 buf
.put(sha1
.getBytes());
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();
315 static Method canExecute
;
318 canExecute
= File
.class.getMethod("canExecute", (Class
[]) null);
319 } catch (SecurityException e
) {
320 // TODO Auto-generated catch block
322 } catch (NoSuchMethodException e
) {
323 // TODO Auto-generated catch block
329 * JDK1.6 has file.canExecute
331 * if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
334 boolean File_canExecute(File f
) {
335 if (canExecute
!= null) {
337 return ((Boolean
) canExecute
.invoke(f
, (Object
[]) null))
339 } catch (IllegalArgumentException e
) {
340 // TODO Auto-generated catch block
343 } catch (IllegalAccessException e
) {
344 // TODO Auto-generated catch block
347 } catch (InvocationTargetException e
) {
348 // TODO Auto-generated catch block
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
);
390 // JDK1.6 has file.canExecute
391 // if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
393 if (FileMode
.EXECUTABLE_FILE
.equals(mode
)) {
394 if (!File_canExecute(file
)&& canExecute
!= null)
397 if (FileMode
.REGULAR_FILE
.equals(mode
)) {
400 if (File_canExecute(file
) && canExecute
!= null)
403 if (FileMode
.SYMLINK
.equals(mode
)) {
406 if (FileMode
.TREE
.equals(mode
)) {
407 if (!file
.isDirectory())
410 System
.out
.println("Does not handle mode "+mode
+" ("+file
+")");
417 long javamtime
= mtime
/ 1000000L;
418 long lastm
= file
.lastModified();
419 if (file
.length() != size
)
421 if (lastm
!= javamtime
) {
422 if (!forceContentCheck
)
426 InputStream is
= new FileInputStream(file
);
427 ObjectWriter objectWriter
= new ObjectWriter(theIndex
.db
);
429 ObjectId newId
= objectWriter
.computeBlobSha1(file
431 boolean ret
= !newId
.equals(sha1
);
432 theIndex
.statDirty
= true;
434 } catch (IOException e
) {
439 } catch (IOException e
) {
440 // can't happen, but if it does we ignore it
444 } catch (FileNotFoundException e
) {
445 // should not happen because we already checked this
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() {
473 public int getStage() {
478 static class Header
{
479 private int signature
;
485 public Header(ByteBuffer map
) throws CorruptObjectException
{
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: "
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
);
508 public Header(Map entryset
) {
509 signature
= 0x44495243;
511 entries
= entryset
.size();
515 public void readTree(Tree t
) throws IOException
{
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
];
524 if (prefix
.length() > 0)
525 name
= prefix
+ "/" + te
.getName();
528 if (te
instanceof Tree
) {
529 readTree(name
, (Tree
) te
);
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();
542 ObjectLoader ol
= db
.openBlob(e
.sha1
);
543 byte[] bytes
= ol
.getBytes();
544 File file
= new File(wd
, e
.getName());
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
);
556 public ObjectId
writeTree() throws IOException
{
558 ObjectWriter writer
= new ObjectWriter(db
);
559 Tree current
= new Tree(db
);
560 Stack trees
= new Stack();
562 String
[] prevName
= new String
[0];
563 for (Iterator i
= entries
.values().iterator(); i
.hasNext();) {
564 Entry e
= (Entry
) i
.next();
567 String
[] newName
= splitDirPath(e
.getName());
568 int c
= longestCommonPath(prevName
, newName
);
569 while (c
< trees
.size() - 1) {
570 current
.setId(writer
.writeTree(current
));
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]
578 current
.getParent().addEntry(current
);
581 current
= (Tree
) current
.findTreeMember(newName
[trees
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
));
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];
605 while ((p1
= name
.indexOf('/', p0
+ 1)) != -1) {
606 tmp
[c
++] = name
.substring(p0
+ 1, p1
);
609 tmp
[c
++] = name
.substring(p0
+ 1);
610 String
[] ret
= new String
[c
];
611 for (int i
= 0; i
< c
; ++i
) {
617 int longestCommonPath(String
[] a
, String
[] b
) {
619 for (i
= 0; i
< a
.length
&& i
< b
.length
; ++i
)
620 if (!a
[i
].equals(b
[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() {