5656 */
5757public abstract class GitRatchet <Project > implements AutoCloseable {
5858
59+ private final Map <Repository , DirCache > dirCaches = new HashMap <>();
60+
5961 public boolean isClean (Project project , ObjectId treeSha , File file ) throws IOException {
60- Repository repo = repositoryFor (project );
61- String relativePath = FileSignature .pathNativeToUnix (repo .getWorkTree ().toPath ().relativize (file .toPath ()).toString ());
62- return isClean (project , treeSha , relativePath );
62+ return isClean (
63+ project ,
64+ treeSha ,
65+ FileSignature .pathNativeToUnix (repositoryFor (project ).getWorkTree ().toPath ().relativize (file .toPath ()).toString ()));
6366 }
6467
65- private final Map <Repository , DirCache > dirCaches = new HashMap <>();
66-
6768 /**
6869 * This is the highest-level method, which all the others serve. Given the sha
6970 * of a git tree (not a commit!), and the file in question, this method returns
@@ -73,16 +74,22 @@ public boolean isClean(Project project, ObjectId treeSha, File file) throws IOEx
7374 public boolean isClean (Project project , ObjectId treeSha , String relativePathUnix ) throws IOException {
7475 Repository repo = repositoryFor (project );
7576
76- DirCacheIterator dirCacheIteratorInit ;
77+ DirCacheIterator dirCacheIterator ;
7778 synchronized (this ) {
7879 // each DirCache is thread-safe, and we compute them one-to-one based on `repositoryFor`
7980 DirCache dirCache = dirCaches .computeIfAbsent (repo , Errors .rethrow ().wrap (Repository ::readDirCache ));
80- dirCacheIteratorInit = new DirCacheIterator (dirCache );
81+ dirCacheIterator = new DirCacheIterator (dirCache );
8182 }
83+
84+ return checkFileCleanliness (treeSha , relativePathUnix , repo , dirCacheIterator );
85+ }
86+
87+ private static boolean checkFileCleanliness (final ObjectId treeSha , final String relativePathUnix ,
88+ final Repository repo , final DirCacheIterator dirCacheIterator ) throws IOException {
8289 try (TreeWalk treeWalk = new TreeWalk (repo )) {
8390 treeWalk .setRecursive (true );
8491 treeWalk .addTree (treeSha );
85- treeWalk .addTree (dirCacheIteratorInit );
92+ treeWalk .addTree (dirCacheIterator );
8693 treeWalk .addTree (new FileTreeIterator (repo ));
8794 treeWalk .setFilter (AndTreeFilter .create (
8895 PathFilter .create (relativePathUnix ),
@@ -93,31 +100,17 @@ public boolean isClean(Project project, ObjectId treeSha, String relativePathUni
93100 return true ;
94101 } else {
95102 AbstractTreeIterator treeIterator = treeWalk .getTree (TREE , AbstractTreeIterator .class );
96- DirCacheIterator dirCacheIterator = treeWalk .getTree (INDEX , DirCacheIterator .class );
97- WorkingTreeIterator workingTreeIterator = treeWalk .getTree (WORKDIR , WorkingTreeIterator .class );
98-
99- boolean hasTree = treeIterator != null ;
100- boolean hasDirCache = dirCacheIterator != null ;
101-
102- if (!hasTree ) {
103+ if (treeIterator == null ) {
103104 // it's not in the tree, so it was added
104105 return false ;
105106 } else {
106- if (hasDirCache ) {
107- boolean treeEqualsIndex = treeIterator .idEqual (dirCacheIterator ) && treeIterator .getEntryRawMode () == dirCacheIterator .getEntryRawMode ();
108- boolean indexEqualsWC = !workingTreeIterator .isModified (dirCacheIterator .getDirCacheEntry (), true , treeWalk .getObjectReader ());
109- if (treeEqualsIndex != indexEqualsWC ) {
110- // if one is equal and the other isn't, then it has definitely changed
111- return false ;
112- } else if (treeEqualsIndex ) {
113- // this means they are all equal to each other, which should never happen
114- // the IndexDiffFilter should keep those out of the TreeWalk entirely
115- throw new IllegalStateException ("Index status for " + relativePathUnix + " against treeSha " + treeSha + " is invalid." );
116- } else {
117- // they are all unique
118- // we have to check manually
119- return worktreeIsCleanCheckout (treeWalk );
120- }
107+ DirCacheIterator indexIterator = treeWalk .getTree (INDEX , DirCacheIterator .class );
108+ if (indexIterator != null ) {
109+ boolean treeEqualsIndex = treeIterator .idEqual (indexIterator ) &&
110+ treeIterator .getEntryRawMode () == indexIterator .getEntryRawMode ();
111+ WorkingTreeIterator workingTreeIterator = treeWalk .getTree (WORKDIR , WorkingTreeIterator .class );
112+ return compareTreeIndexAndWorktreeStates (treeSha , relativePathUnix , treeEqualsIndex ,
113+ workingTreeIterator , indexIterator , treeWalk );
121114 } else {
122115 // no dirCache, so we will compare the tree to the workdir manually
123116 return worktreeIsCleanCheckout (treeWalk );
@@ -127,6 +120,27 @@ public boolean isClean(Project project, ObjectId treeSha, String relativePathUni
127120 }
128121 }
129122
123+ /**
124+ * Compares the three states (tree, index, worktree) to determine file cleanliness.
125+ * Handles gitignored files by treating them as not clean.
126+ */
127+ private static boolean compareTreeIndexAndWorktreeStates (final ObjectId treeSha , final String relativePathUnix ,
128+ final boolean treeEqualsIndex , final WorkingTreeIterator workingTreeIterator ,
129+ final DirCacheIterator indexIterator , final TreeWalk treeWalk ) throws IOException {
130+ if (treeEqualsIndex == workingTreeIterator .isModified (indexIterator .getDirCacheEntry (), true , treeWalk .getObjectReader ())) {
131+ // if one is equal and the other isn't, then it has definitely changed
132+ return false ;
133+ } else if (treeEqualsIndex ) {
134+ // this means they are all equal to each other, which should never happen
135+ // the IndexDiffFilter should keep those out of the TreeWalk entirely
136+ throw new IllegalStateException ("Index status for " + relativePathUnix + " against treeSha " + treeSha + " is invalid." );
137+ } else {
138+ // they are all unique
139+ // we have to check manually
140+ return worktreeIsCleanCheckout (treeWalk );
141+ }
142+ }
143+
130144 /** Returns true if the worktree file is a clean checkout of head (possibly smudged). */
131145 private static boolean worktreeIsCleanCheckout (TreeWalk treeWalk ) {
132146 return treeWalk .idEqual (TREE , WORKDIR );
@@ -178,16 +192,7 @@ public synchronized ObjectId rootTreeShaOf(Project project, String reference) {
178192 if (commitSha == null ) {
179193 throw new IllegalArgumentException ("No such reference '" + reference + "'" );
180194 }
181-
182- RevCommit ratchetFrom = revWalk .parseCommit (commitSha );
183- RevCommit head = revWalk .parseCommit (repo .resolve (Constants .HEAD ));
184-
185- revWalk .setRevFilter (RevFilter .MERGE_BASE );
186- revWalk .markStart (ratchetFrom );
187- revWalk .markStart (head );
188-
189- RevCommit mergeBase = revWalk .next ();
190- treeSha = Optional .ofNullable (mergeBase ).orElse (ratchetFrom ).getTree ();
195+ treeSha = getMergeBaseTreeSha (revWalk , repo , revWalk .parseCommit (commitSha ));
191196 }
192197 rootTreeShaCache .put (repo , reference , treeSha .copy ());
193198 }
@@ -197,6 +202,13 @@ public synchronized ObjectId rootTreeShaOf(Project project, String reference) {
197202 }
198203 }
199204
205+ private static ObjectId getMergeBaseTreeSha (final RevWalk revWalk , final Repository repo , final RevCommit ratchetFrom ) throws IOException {
206+ revWalk .setRevFilter (RevFilter .MERGE_BASE );
207+ revWalk .markStart (ratchetFrom );
208+ revWalk .markStart (revWalk .parseCommit (repo .resolve (Constants .HEAD )));
209+ return Optional .ofNullable (revWalk .next ()).orElse (ratchetFrom ).getTree ();
210+ }
211+
200212 /**
201213 * Returns the sha of the git subtree which represents the root of the given project, or {@link ObjectId#zeroId()}
202214 * if there is no git subtree at the project root.
@@ -224,7 +236,8 @@ public synchronized ObjectId subtreeShaOf(Project project, ObjectId rootTreeSha)
224236
225237 @ Override
226238 public void close () {
227- gitRoots .values ().stream ()
239+ gitRoots .values ()
240+ .stream ()
228241 .distinct ()
229242 .forEach (Repository ::close );
230243 }
0 commit comments