diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 3d8c641e..b50ab4ad 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -29,6 +29,7 @@
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b5d06733..0b1ca05b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -45,11 +45,14 @@ android {
debug {
isMinifyEnabled = false
-
+
}
}
+ kotlinOptions {
+ jvmTarget = "11"
+ }
packagingOptions {
- resources.excludes.addAll(listOf("META-INF/**","xsd/*","license/*"))
+ resources.excludes.addAll(listOf("META-INF/**", "xsd/*", "license/*"))
resources.pickFirsts.add("kotlin/**")
}
compileOptions {
@@ -72,6 +75,7 @@ dependencies {
implementation(project(":core-api"))
implementation(project(":functional"))
+ implementation(project(":snapshots"))
implementation(project(":file-temp"))
/*
diff --git a/subprojects/snapshots/.gitignore b/subprojects/snapshots/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/subprojects/snapshots/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/subprojects/snapshots/build.gradle.kts b/subprojects/snapshots/build.gradle.kts
new file mode 100644
index 00000000..376bcc67
--- /dev/null
+++ b/subprojects/snapshots/build.gradle.kts
@@ -0,0 +1,23 @@
+plugins {
+ id("java-library")
+}
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+}
+
+dependencies {
+ api(project(":files"))
+ api(project(":hashing"))
+
+ implementation(project(":base-annotations"))
+
+
+ implementation("com.google.guava:guava:30.1.1-jre")
+ implementation("com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava")
+ implementation("com.google.code.findbugs:jsr305:3.0.2")
+
+
+ implementation("org.slf4j:slf4j-api:1.7.36")
+}
\ No newline at end of file
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/RelativePathSupplier.java b/subprojects/snapshots/src/main/java/org/gradle/internal/RelativePathSupplier.java
new file mode 100644
index 00000000..1ddb98af
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/RelativePathSupplier.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal;
+
+import java.util.Collection;
+
+public interface RelativePathSupplier {
+ boolean isRoot();
+
+ Collection getSegments();
+
+ String toRelativePath();
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/CurrentFileCollectionFingerprint.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/CurrentFileCollectionFingerprint.java
new file mode 100644
index 00000000..0a72e228
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/CurrentFileCollectionFingerprint.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint;
+
+import org.gradle.internal.hash.HashCode;
+import org.gradle.internal.snapshot.FileSystemSnapshot;
+
+/**
+ * A file collection fingerprint taken during this build.
+ */
+public interface CurrentFileCollectionFingerprint extends FileCollectionFingerprint {
+ /**
+ * Returns the combined hash of the contents of this {@link CurrentFileCollectionFingerprint}.
+ */
+ HashCode getHash();
+
+ String getStrategyIdentifier();
+
+ /**
+ * Returns the snapshot used to capture these fingerprints.
+ */
+ FileSystemSnapshot getSnapshot();
+
+ boolean isEmpty();
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/DirectorySensitivity.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/DirectorySensitivity.java
new file mode 100644
index 00000000..e5383add
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/DirectorySensitivity.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint;
+
+import org.gradle.internal.file.FileType;
+import org.gradle.internal.snapshot.FileSystemLocationSnapshot;
+
+import java.util.function.Predicate;
+
+/**
+ * Specifies how a fingerprinter should handle directories that are found in a filecollection.
+ */
+public enum DirectorySensitivity {
+ /**
+ * Whatever the default behavior is for the given fingerprinter. For some fingerprinters, the
+ * default behavior is to fingerprint directories, for others, they ignore directories by default.
+ */
+ DEFAULT(snapshot -> true),
+ /**
+ * Ignore directories
+ */
+ IGNORE_DIRECTORIES(snapshot -> snapshot.getType() != FileType.Directory);
+
+ private final Predicate fingerprintCheck;
+
+ DirectorySensitivity(Predicate fingerprintCheck) {
+ this.fingerprintCheck = fingerprintCheck;
+ }
+
+ public boolean shouldFingerprint(FileSystemLocationSnapshot snapshot) {
+ return fingerprintCheck.test(snapshot);
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/FileCollectionFingerprint.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/FileCollectionFingerprint.java
new file mode 100644
index 00000000..3fc4c666
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/FileCollectionFingerprint.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint;
+
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedMap;
+import org.gradle.internal.hash.HashCode;
+import org.gradle.internal.hash.Hashing;
+
+import java.util.Map;
+
+/**
+ * An immutable snapshot of some aspects of the contents and meta-data of a collection of files or directories.
+ */
+public interface FileCollectionFingerprint {
+
+ /**
+ * The underlying fingerprints.
+ */
+ Map getFingerprints();
+
+ /**
+ * The Merkle hashes of the roots which make up this file collection fingerprint.
+ */
+ ImmutableMultimap getRootHashes();
+
+ /**
+ * The absolute paths for the roots of this file collection fingerprint.
+ */
+ default ImmutableSet getRootPaths() {
+ return getRootHashes().keySet();
+ }
+
+ FileCollectionFingerprint EMPTY = new FileCollectionFingerprint() {
+ private final HashCode strategyConfigurationHash = Hashing.signature(getClass());
+
+ @Override
+ public Map getFingerprints() {
+ return ImmutableSortedMap.of();
+ }
+
+ @Override
+ public ImmutableMultimap getRootHashes() {
+ return ImmutableMultimap.of();
+ }
+
+ @Override
+ public ImmutableSet getRootPaths() {
+ return ImmutableSet.of();
+ }
+
+ @Override
+ public HashCode getStrategyConfigurationHash() {
+ return strategyConfigurationHash;
+ }
+
+ @Override
+ public String toString() {
+ return "EMPTY";
+ }
+ };
+
+ HashCode getStrategyConfigurationHash();
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/FileSystemLocationFingerprint.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/FileSystemLocationFingerprint.java
new file mode 100644
index 00000000..6439f776
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/FileSystemLocationFingerprint.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint;
+
+import org.gradle.internal.file.FileType;
+import org.gradle.internal.hash.HashCode;
+import org.gradle.internal.hash.Hashable;
+import org.gradle.internal.hash.Hashing;
+
+/**
+ * An immutable fingerprint of some aspects of a file's metadata and content.
+ *
+ * Should implement {@link #equals(Object)} and {@link #hashCode()} to compare these aspects.
+ * Comparisons are very frequent, so these methods need to be fast.
+ *
+ * File fingerprints are cached between builds, so their memory footprint should be kept to a minimum.
+ */
+public interface FileSystemLocationFingerprint extends Comparable, Hashable {
+ HashCode DIR_SIGNATURE = Hashing.signature("DIR");
+ HashCode MISSING_FILE_SIGNATURE = Hashing.signature("MISSING");
+
+ String getNormalizedPath();
+ HashCode getNormalizedContentHash();
+ FileType getType();
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/FingerprintHashingStrategy.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/FingerprintHashingStrategy.java
new file mode 100644
index 00000000..03e3ec1b
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/FingerprintHashingStrategy.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint;
+
+import com.google.common.collect.ImmutableList;
+import org.gradle.internal.hash.Hasher;
+
+import java.util.Collection;
+
+/**
+ * Strategy for appending a collection of fingerprints to a hasher.
+ */
+public enum FingerprintHashingStrategy {
+ SORT {
+ @Override
+ public void appendToHasher(Hasher hasher, Collection fingerprints) {
+ ImmutableList sortedFingerprints = ImmutableList.sortedCopyOf(fingerprints);
+ appendCollectionToHasherKeepingOrder(hasher, sortedFingerprints);
+ }
+ },
+ KEEP_ORDER {
+ @Override
+ public void appendToHasher(Hasher hasher, Collection fingerprints) {
+ appendCollectionToHasherKeepingOrder(hasher, fingerprints);
+ }
+ };
+
+ public abstract void appendToHasher(Hasher hasher, Collection fingerprints);
+
+ protected void appendCollectionToHasherKeepingOrder(Hasher hasher, Collection fingerprints) {
+ for (FileSystemLocationFingerprint fingerprint : fingerprints) {
+ fingerprint.appendToHasher(hasher);
+ }
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/FingerprintingStrategy.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/FingerprintingStrategy.java
new file mode 100644
index 00000000..a4eb9f35
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/FingerprintingStrategy.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint;
+
+import org.gradle.internal.hash.HashCode;
+import org.gradle.internal.snapshot.FileSystemLocationSnapshot;
+import org.gradle.internal.snapshot.FileSystemSnapshot;
+
+import java.util.Map;
+
+/**
+ * Strategy for converting a sequence of {@link FileSystemLocationSnapshot}s into a {@link FileCollectionFingerprint}.
+ */
+public interface FingerprintingStrategy {
+
+ // TODO wolfs: Move these identifiers to the actual strategy classes when they live in :snapshots
+ String CLASSPATH_IDENTIFIER = "CLASSPATH";
+ String COMPILE_CLASSPATH_IDENTIFIER = "COMPILE_CLASSPATH";
+
+ /**
+ * Converts the roots into the {@link FileSystemLocationFingerprint}s used by the {@link FileCollectionFingerprint}.
+ */
+ Map collectFingerprints(FileSystemSnapshot roots);
+
+ /**
+ * Used by the {@link FileCollectionFingerprint} to hash a map of fingerprints generated by {@link #collectFingerprints(FileSystemSnapshot)}
+ */
+ FingerprintHashingStrategy getHashingStrategy();
+
+ /**
+ * UsedByScansPlugin
+ * Names are expected as part of org.gradle.api.internal.tasks.SnapshotTaskInputsBuildOperationType.Result.VisitState.getPropertyNormalizationStrategyName().
+ */
+ String getIdentifier();
+
+ CurrentFileCollectionFingerprint getEmptyFingerprint();
+
+ String normalizePath(FileSystemLocationSnapshot snapshot);
+
+ HashCode getConfigurationHash();
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/LineEndingSensitivity.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/LineEndingSensitivity.java
new file mode 100644
index 00000000..557c223d
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/LineEndingSensitivity.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint;
+
+public enum LineEndingSensitivity {
+ DEFAULT,
+ NORMALIZE_LINE_ENDINGS;
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/ConfigurableNormalizer.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/ConfigurableNormalizer.java
new file mode 100644
index 00000000..3c01ce95
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/ConfigurableNormalizer.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint.hashing;
+
+import org.gradle.internal.hash.Hasher;
+
+/**
+ * A resource normalizer which is configurable.
+ *
+ * Allows tracking changes to its configuration.
+ */
+public interface ConfigurableNormalizer {
+ void appendConfigurationToHasher(Hasher hasher);
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/FileSystemLocationSnapshotHasher.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/FileSystemLocationSnapshotHasher.java
new file mode 100644
index 00000000..cb4c5381
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/FileSystemLocationSnapshotHasher.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint.hashing;
+
+import org.gradle.internal.hash.HashCode;
+import org.gradle.internal.hash.Hasher;
+import org.gradle.internal.snapshot.FileSystemLocationSnapshot;
+
+import javax.annotation.Nullable;
+import java.io.IOException;
+
+public interface FileSystemLocationSnapshotHasher extends ConfigurableNormalizer {
+ /**
+ * Returns {@code null} if the file should be ignored.
+ */
+ @Nullable
+ HashCode hash(FileSystemLocationSnapshot snapshot) throws IOException;
+
+ FileSystemLocationSnapshotHasher DEFAULT = new FileSystemLocationSnapshotHasher() {
+ @Nullable
+ @Override
+ public HashCode hash(FileSystemLocationSnapshot snapshot) {
+ return snapshot.getHash();
+ }
+
+ @Override
+ public void appendConfigurationToHasher(Hasher hasher) {
+ hasher.putString(getClass().getName());
+ }
+ };
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/RegularFileSnapshotContext.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/RegularFileSnapshotContext.java
new file mode 100644
index 00000000..0336eb96
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/RegularFileSnapshotContext.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint.hashing;
+
+import org.gradle.internal.snapshot.RegularFileSnapshot;
+
+import java.util.function.Supplier;
+
+public interface RegularFileSnapshotContext {
+ Supplier getRelativePathSegments();
+
+ RegularFileSnapshot getSnapshot();
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/RegularFileSnapshotContextHasher.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/RegularFileSnapshotContextHasher.java
new file mode 100644
index 00000000..d4766967
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/RegularFileSnapshotContextHasher.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint.hashing;
+
+import org.gradle.internal.hash.HashCode;
+
+import javax.annotation.Nullable;
+import java.io.IOException;
+
+public interface RegularFileSnapshotContextHasher {
+
+ /**
+ * Returns {@code null} if the file should be ignored.
+ */
+ @Nullable
+ HashCode hash(RegularFileSnapshotContext snapshotContext) throws IOException;
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/ResourceHasher.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/ResourceHasher.java
new file mode 100644
index 00000000..c995a9c5
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/ResourceHasher.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint.hashing;
+
+/**
+ * Hashes resources (e.g., a class file in a jar or a class file in a directory)
+ */
+public interface ResourceHasher extends ConfigurableNormalizer, RegularFileSnapshotContextHasher, ZipEntryContextHasher {
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/ZipEntryContext.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/ZipEntryContext.java
new file mode 100644
index 00000000..fb8d4c8c
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/ZipEntryContext.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint.hashing;
+
+import org.gradle.api.internal.file.archive.ZipEntry;
+
+import java.util.function.Supplier;
+
+public interface ZipEntryContext {
+ ZipEntry getEntry();
+
+ String getFullName();
+
+ String getRootParentName();
+
+ Supplier getRelativePathSegments();
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/ZipEntryContextHasher.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/ZipEntryContextHasher.java
new file mode 100644
index 00000000..16f8074c
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/ZipEntryContextHasher.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint.hashing;
+
+import org.gradle.internal.hash.HashCode;
+
+import javax.annotation.Nullable;
+import java.io.IOException;
+
+/**
+ * Hashes a zip entry (e.g. a class file in a jar, a manifest file, a properties file)
+ */
+public interface ZipEntryContextHasher {
+ /**
+ * Returns {@code null} if the zip entry should be ignored.
+ */
+ @Nullable
+ HashCode hash(ZipEntryContext zipEntryContext) throws IOException;
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/package-info.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/package-info.java
new file mode 100644
index 00000000..69a144f1
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/hashing/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@NonNullApi
+package org.gradle.internal.fingerprint.hashing;
+
+import org.gradle.api.NonNullApi;
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/AbsolutePathFingerprintingStrategy.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/AbsolutePathFingerprintingStrategy.java
new file mode 100644
index 00000000..d2ecf2ea
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/AbsolutePathFingerprintingStrategy.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint.impl;
+
+import com.google.common.collect.ImmutableMap;
+import org.gradle.internal.fingerprint.DirectorySensitivity;
+import org.gradle.internal.fingerprint.FileSystemLocationFingerprint;
+import org.gradle.internal.fingerprint.FingerprintHashingStrategy;
+import org.gradle.internal.fingerprint.FingerprintingStrategy;
+import org.gradle.internal.fingerprint.hashing.FileSystemLocationSnapshotHasher;
+import org.gradle.internal.hash.HashCode;
+import org.gradle.internal.snapshot.FileSystemLocationSnapshot;
+import org.gradle.internal.snapshot.FileSystemSnapshot;
+import org.gradle.internal.snapshot.RootTrackingFileSystemSnapshotHierarchyVisitor;
+import org.gradle.internal.snapshot.SnapshotVisitResult;
+
+import java.util.HashSet;
+import java.util.Map;
+
+/**
+ * Fingerprint files without path or content normalization.
+ */
+public class AbsolutePathFingerprintingStrategy extends AbstractFingerprintingStrategy {
+ public static final FingerprintingStrategy DEFAULT = new AbsolutePathFingerprintingStrategy(DirectorySensitivity.DEFAULT);
+ public static final FingerprintingStrategy IGNORE_DIRECTORIES = new AbsolutePathFingerprintingStrategy(DirectorySensitivity.IGNORE_DIRECTORIES);
+
+ public static final String IDENTIFIER = "ABSOLUTE_PATH";
+
+ private final FileSystemLocationSnapshotHasher normalizedContentHasher;
+
+ public AbsolutePathFingerprintingStrategy(DirectorySensitivity directorySensitivity, FileSystemLocationSnapshotHasher normalizedContentHasher) {
+ super(IDENTIFIER, directorySensitivity, normalizedContentHasher);
+ this.normalizedContentHasher = normalizedContentHasher;
+ }
+
+ private AbsolutePathFingerprintingStrategy(DirectorySensitivity directorySensitivity) {
+ this(directorySensitivity, FileSystemLocationSnapshotHasher.DEFAULT);
+ }
+
+ @Override
+ public String normalizePath(FileSystemLocationSnapshot snapshot) {
+ return snapshot.getAbsolutePath();
+ }
+
+ @Override
+ public Map collectFingerprints(FileSystemSnapshot roots) {
+ ImmutableMap.Builder builder = ImmutableMap.builder();
+ HashSet processedEntries = new HashSet<>();
+ roots.accept(new RootTrackingFileSystemSnapshotHierarchyVisitor() {
+ @Override
+ public SnapshotVisitResult visitEntry(FileSystemLocationSnapshot snapshot, boolean isRoot) {
+ String absolutePath = snapshot.getAbsolutePath();
+ if (processedEntries.add(absolutePath) && getDirectorySensitivity().shouldFingerprint(snapshot)) {
+ HashCode normalizedContentHash = getNormalizedContentHash(snapshot, normalizedContentHasher);
+ if (normalizedContentHash != null) {
+ builder.put(absolutePath, new DefaultFileSystemLocationFingerprint(snapshot.getAbsolutePath(), snapshot.getType(), normalizedContentHash));
+ }
+ }
+ return SnapshotVisitResult.CONTINUE;
+ }
+ });
+ return builder.build();
+ }
+
+ @Override
+ public FingerprintHashingStrategy getHashingStrategy() {
+ return FingerprintHashingStrategy.SORT;
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/AbstractFingerprintingStrategy.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/AbstractFingerprintingStrategy.java
new file mode 100644
index 00000000..6d448c30
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/AbstractFingerprintingStrategy.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint.impl;
+
+import org.gradle.internal.fingerprint.CurrentFileCollectionFingerprint;
+import org.gradle.internal.fingerprint.DirectorySensitivity;
+import org.gradle.internal.fingerprint.FingerprintingStrategy;
+import org.gradle.internal.fingerprint.hashing.ConfigurableNormalizer;
+import org.gradle.internal.fingerprint.hashing.FileSystemLocationSnapshotHasher;
+import org.gradle.internal.hash.HashCode;
+import org.gradle.internal.hash.Hasher;
+import org.gradle.internal.hash.Hashing;
+import org.gradle.internal.snapshot.FileSystemLocationSnapshot;
+
+import javax.annotation.Nullable;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+
+public abstract class AbstractFingerprintingStrategy implements FingerprintingStrategy {
+ private final String identifier;
+ private final CurrentFileCollectionFingerprint emptyFingerprint;
+ private final DirectorySensitivity directorySensitivity;
+ private final HashCode configurationHash;
+
+ public AbstractFingerprintingStrategy(
+ String identifier,
+ DirectorySensitivity directorySensitivity,
+ ConfigurableNormalizer contentNormalizer
+ ) {
+ this.identifier = identifier;
+ this.emptyFingerprint = new EmptyCurrentFileCollectionFingerprint(identifier);
+ this.directorySensitivity = directorySensitivity;
+ Hasher hasher = Hashing.newHasher();
+ hasher.putString(getClass().getName());
+ contentNormalizer.appendConfigurationToHasher(hasher);
+ this.configurationHash = hasher.hash();
+ }
+
+ @Override
+ public String getIdentifier() {
+ return identifier;
+ }
+
+ @Override
+ public CurrentFileCollectionFingerprint getEmptyFingerprint() {
+ return emptyFingerprint;
+ }
+
+ @Nullable
+ protected HashCode getNormalizedContentHash(FileSystemLocationSnapshot snapshot, FileSystemLocationSnapshotHasher normalizedContentHasher) {
+ try {
+ return normalizedContentHasher.hash(snapshot);
+ } catch (IOException e) {
+ throw new UncheckedIOException(failedToNormalize(snapshot), e);
+ } catch (UncheckedIOException e) {
+ throw new UncheckedIOException(failedToNormalize(snapshot), e.getCause());
+ }
+ }
+
+ private static String failedToNormalize(FileSystemLocationSnapshot snapshot) {
+ return String.format("Failed to normalize content of '%s'.", snapshot.getAbsolutePath());
+ }
+
+ public DirectorySensitivity getDirectorySensitivity() {
+ return directorySensitivity;
+ }
+
+ @Override
+ public HashCode getConfigurationHash() {
+ return configurationHash;
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/DefaultCurrentFileCollectionFingerprint.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/DefaultCurrentFileCollectionFingerprint.java
new file mode 100644
index 00000000..07ce00fd
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/DefaultCurrentFileCollectionFingerprint.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint.impl;
+
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Iterables;
+import org.gradle.internal.fingerprint.CurrentFileCollectionFingerprint;
+import org.gradle.internal.fingerprint.FileCollectionFingerprint;
+import org.gradle.internal.fingerprint.FileSystemLocationFingerprint;
+import org.gradle.internal.fingerprint.FingerprintHashingStrategy;
+import org.gradle.internal.fingerprint.FingerprintingStrategy;
+import org.gradle.internal.hash.HashCode;
+import org.gradle.internal.hash.Hasher;
+import org.gradle.internal.hash.Hashing;
+import org.gradle.internal.snapshot.FileSystemSnapshot;
+import org.gradle.internal.snapshot.SnapshotUtil;
+
+import javax.annotation.Nullable;
+import java.util.Map;
+
+public class DefaultCurrentFileCollectionFingerprint implements CurrentFileCollectionFingerprint {
+
+ private final Map fingerprints;
+ private final FingerprintHashingStrategy hashingStrategy;
+ private final String identifier;
+ private final FileSystemSnapshot roots;
+ private final ImmutableMultimap rootHashes;
+ private final HashCode strategyConfigurationHash;
+ private HashCode hash;
+
+ public static CurrentFileCollectionFingerprint from(FileSystemSnapshot roots, FingerprintingStrategy strategy, @Nullable FileCollectionFingerprint candidate) {
+ if (roots == FileSystemSnapshot.EMPTY) {
+ return strategy.getEmptyFingerprint();
+ }
+
+ ImmutableMultimap rootHashes = SnapshotUtil.getRootHashes(roots);
+ Map fingerprints;
+ if (candidate != null
+ && candidate.getStrategyConfigurationHash().equals(strategy.getConfigurationHash())
+ && equalRootHashes(candidate.getRootHashes(), rootHashes)
+ ) {
+ fingerprints = candidate.getFingerprints();
+ } else {
+ fingerprints = strategy.collectFingerprints(roots);
+ }
+ if (fingerprints.isEmpty()) {
+ return strategy.getEmptyFingerprint();
+ }
+ return new DefaultCurrentFileCollectionFingerprint(fingerprints, roots, rootHashes, strategy);
+ }
+
+ private static boolean equalRootHashes(ImmutableMultimap first, ImmutableMultimap second) {
+ // We cannot use `first.equals(second)`, since the order of the root hashes matters
+ return Iterables.elementsEqual(first.entries(), second.entries());
+ }
+
+ private DefaultCurrentFileCollectionFingerprint(
+ Map fingerprints,
+ FileSystemSnapshot roots,
+ ImmutableMultimap rootHashes,
+ FingerprintingStrategy strategy
+ ) {
+ this.fingerprints = fingerprints;
+ this.identifier = strategy.getIdentifier();
+ this.hashingStrategy = strategy.getHashingStrategy();
+ this.strategyConfigurationHash = strategy.getConfigurationHash();
+ this.roots = roots;
+ this.rootHashes = rootHashes;
+ }
+
+ @Override
+ public HashCode getHash() {
+ if (hash == null) {
+ Hasher hasher = Hashing.newHasher();
+ hashingStrategy.appendToHasher(hasher, fingerprints.values());
+ hash = hasher.hash();
+ }
+ return hash;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ // We'd have created an EmptyCurrentFileCollectionFingerprint if there were no file fingerprints
+ return false;
+ }
+
+ @Override
+ public Map getFingerprints() {
+ return fingerprints;
+ }
+
+ @Override
+ public ImmutableMultimap getRootHashes() {
+ return rootHashes;
+ }
+
+ @Override
+ public String getStrategyIdentifier() {
+ return identifier;
+ }
+
+ @Override
+ public FileSystemSnapshot getSnapshot() {
+ return roots;
+ }
+
+ @Override
+ public HashCode getStrategyConfigurationHash() {
+ return strategyConfigurationHash;
+ }
+
+ @Override
+ public String toString() {
+ return identifier + fingerprints;
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/DefaultFileSystemLocationFingerprint.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/DefaultFileSystemLocationFingerprint.java
new file mode 100644
index 00000000..27934c2f
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/DefaultFileSystemLocationFingerprint.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint.impl;
+
+import org.gradle.internal.file.FileType;
+import org.gradle.internal.fingerprint.FileSystemLocationFingerprint;
+import org.gradle.internal.hash.HashCode;
+import org.gradle.internal.hash.Hasher;
+import org.gradle.internal.snapshot.FileSystemLocationSnapshot;
+
+public class DefaultFileSystemLocationFingerprint implements FileSystemLocationFingerprint {
+ private final HashCode normalizedContentHash;
+ private final String normalizedPath;
+
+ public DefaultFileSystemLocationFingerprint(String normalizedPath, FileType type, HashCode contentHash) {
+ this.normalizedContentHash = hashForType(type, contentHash);
+ this.normalizedPath = normalizedPath;
+ }
+
+ public DefaultFileSystemLocationFingerprint(String normalizedPath, FileSystemLocationSnapshot snapshot) {
+ this(normalizedPath, snapshot.getType(), snapshot.getHash());
+ }
+
+ private static HashCode hashForType(FileType fileType, HashCode hash) {
+ switch (fileType) {
+ case Directory:
+ return DIR_SIGNATURE;
+ case Missing:
+ return MISSING_FILE_SIGNATURE;
+ case RegularFile:
+ return hash;
+ default:
+ throw new IllegalStateException("Unknown file type: " + fileType);
+ }
+ }
+
+ @Override
+ public final void appendToHasher(Hasher hasher) {
+ hasher.putString(getNormalizedPath());
+ hasher.putHash(getNormalizedContentHash());
+ }
+
+ @Override
+ public FileType getType() {
+ if (normalizedContentHash == DIR_SIGNATURE) {
+ return FileType.Directory;
+ } else if (normalizedContentHash == MISSING_FILE_SIGNATURE) {
+ return FileType.Missing;
+ } else {
+ return FileType.RegularFile;
+ }
+ }
+
+ @Override
+ public String getNormalizedPath() {
+ return normalizedPath;
+ }
+
+ @Override
+ public HashCode getNormalizedContentHash() {
+ return normalizedContentHash;
+ }
+
+ @Override
+ public final int compareTo(FileSystemLocationFingerprint o) {
+ int result = getNormalizedPath().compareTo(o.getNormalizedPath());
+ if (result == 0) {
+ result = getNormalizedContentHash().compareTo(o.getNormalizedContentHash());
+ }
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ DefaultFileSystemLocationFingerprint that = (DefaultFileSystemLocationFingerprint) o;
+
+ if (!normalizedContentHash.equals(that.normalizedContentHash)) {
+ return false;
+ }
+ return normalizedPath.equals(that.normalizedPath);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = normalizedContentHash.hashCode();
+ result = 31 * result + normalizedPath.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("'%s' / %s",
+ getNormalizedPath(),
+ getHashOrTypeToDisplay()
+ );
+ }
+
+ private Object getHashOrTypeToDisplay() {
+ switch (getType()) {
+ case Directory:
+ return "DIR";
+ case Missing:
+ return "MISSING";
+ default:
+ return normalizedContentHash;
+ }
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/EmptyCurrentFileCollectionFingerprint.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/EmptyCurrentFileCollectionFingerprint.java
new file mode 100644
index 00000000..fc31b0eb
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/EmptyCurrentFileCollectionFingerprint.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint.impl;
+
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import org.gradle.internal.fingerprint.CurrentFileCollectionFingerprint;
+import org.gradle.internal.fingerprint.FileSystemLocationFingerprint;
+import org.gradle.internal.hash.HashCode;
+import org.gradle.internal.hash.Hashing;
+import org.gradle.internal.snapshot.FileSystemSnapshot;
+
+import java.util.Collections;
+import java.util.Map;
+
+public class EmptyCurrentFileCollectionFingerprint implements CurrentFileCollectionFingerprint {
+
+ private static final HashCode SIGNATURE = Hashing.signature(EmptyCurrentFileCollectionFingerprint.class);
+
+ private final String identifier;
+
+ public EmptyCurrentFileCollectionFingerprint(String identifier) {
+ this.identifier = identifier;
+ }
+
+ @Override
+ public HashCode getHash() {
+ return SIGNATURE;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return true;
+ }
+
+ @Override
+ public Map getFingerprints() {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public FileSystemSnapshot getSnapshot() {
+ return FileSystemSnapshot.EMPTY;
+ }
+
+ @Override
+ public ImmutableMultimap getRootHashes() {
+ return ImmutableMultimap.of();
+ }
+
+ @Override
+ public ImmutableSet getRootPaths() {
+ return ImmutableSet.of();
+ }
+
+ @Override
+ public HashCode getStrategyConfigurationHash() {
+ return SIGNATURE;
+ }
+
+ @Override
+ public String getStrategyIdentifier() {
+ return identifier;
+ }
+
+ @Override
+ public String toString() {
+ return identifier + "{EMPTY}";
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/IgnoredPathFileSystemLocationFingerprint.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/IgnoredPathFileSystemLocationFingerprint.java
new file mode 100644
index 00000000..2e9f35ab
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/IgnoredPathFileSystemLocationFingerprint.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint.impl;
+
+import org.gradle.internal.file.FileType;
+import org.gradle.internal.fingerprint.FileSystemLocationFingerprint;
+import org.gradle.internal.hash.HashCode;
+import org.gradle.internal.hash.Hasher;
+
+public class IgnoredPathFileSystemLocationFingerprint implements FileSystemLocationFingerprint {
+
+ public static final IgnoredPathFileSystemLocationFingerprint DIRECTORY = new IgnoredPathFileSystemLocationFingerprint(FileType.Directory, DIR_SIGNATURE);
+ private static final IgnoredPathFileSystemLocationFingerprint MISSING_FILE = new IgnoredPathFileSystemLocationFingerprint(FileType.Missing, MISSING_FILE_SIGNATURE);
+
+ private final FileType type;
+ private final HashCode normalizedContentHash;
+
+ public static IgnoredPathFileSystemLocationFingerprint create(FileType type, HashCode contentHash) {
+ switch (type) {
+ case Directory:
+ return DIRECTORY;
+ case Missing:
+ return MISSING_FILE;
+ case RegularFile:
+ return new IgnoredPathFileSystemLocationFingerprint(FileType.RegularFile, contentHash);
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ private IgnoredPathFileSystemLocationFingerprint(FileType type, HashCode normalizedContentHash) {
+ this.type = type;
+ this.normalizedContentHash = normalizedContentHash;
+ }
+
+ @Override
+ public String getNormalizedPath() {
+ return "";
+ }
+
+ @Override
+ public HashCode getNormalizedContentHash() {
+ return normalizedContentHash;
+ }
+
+ @Override
+ public FileType getType() {
+ return type;
+ }
+
+ @Override
+ public int compareTo(FileSystemLocationFingerprint o) {
+ if (!(o instanceof IgnoredPathFileSystemLocationFingerprint)) {
+ return -1;
+ }
+ return normalizedContentHash.compareTo(o.getNormalizedContentHash());
+ }
+
+ @Override
+ public void appendToHasher(Hasher hasher) {
+ hasher.putHash(normalizedContentHash);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ IgnoredPathFileSystemLocationFingerprint that = (IgnoredPathFileSystemLocationFingerprint) o;
+ return normalizedContentHash.equals(that.normalizedContentHash);
+ }
+
+ @Override
+ public int hashCode() {
+ return normalizedContentHash.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("IGNORED / %s", getType() == FileType.Directory ? "DIR" : getType() == FileType.Missing ? "MISSING" : normalizedContentHash);
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/IgnoredPathFingerprintingStrategy.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/IgnoredPathFingerprintingStrategy.java
new file mode 100644
index 00000000..b01feac1
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/IgnoredPathFingerprintingStrategy.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint.impl;
+
+import com.google.common.collect.ImmutableMap;
+import org.gradle.internal.fingerprint.DirectorySensitivity;
+import org.gradle.internal.fingerprint.FileSystemLocationFingerprint;
+import org.gradle.internal.fingerprint.FingerprintHashingStrategy;
+import org.gradle.internal.fingerprint.hashing.FileSystemLocationSnapshotHasher;
+import org.gradle.internal.hash.HashCode;
+import org.gradle.internal.snapshot.FileSystemLocationSnapshot;
+import org.gradle.internal.snapshot.FileSystemLocationSnapshot.FileSystemLocationSnapshotVisitor;
+import org.gradle.internal.snapshot.FileSystemSnapshot;
+import org.gradle.internal.snapshot.MissingFileSnapshot;
+import org.gradle.internal.snapshot.RegularFileSnapshot;
+import org.gradle.internal.snapshot.SnapshotVisitResult;
+
+import java.util.HashSet;
+import java.util.Map;
+
+/**
+ * Fingerprint files ignoring the path.
+ *
+ * Ignores directories.
+ */
+public class IgnoredPathFingerprintingStrategy extends AbstractFingerprintingStrategy {
+ public static final IgnoredPathFingerprintingStrategy DEFAULT = new IgnoredPathFingerprintingStrategy();
+ public static final String IDENTIFIER = "IGNORED_PATH";
+ public static final String IGNORED_PATH = "";
+
+ private final FileSystemLocationSnapshotHasher normalizedContentHasher;
+
+ public IgnoredPathFingerprintingStrategy(FileSystemLocationSnapshotHasher normalizedContentHasher) {
+ super(IDENTIFIER, DirectorySensitivity.DEFAULT, normalizedContentHasher);
+ this.normalizedContentHasher = normalizedContentHasher;
+ }
+
+ private IgnoredPathFingerprintingStrategy() {
+ this(FileSystemLocationSnapshotHasher.DEFAULT);
+ }
+
+ @Override
+ public String normalizePath(FileSystemLocationSnapshot snapshot) {
+ return IGNORED_PATH;
+ }
+
+ @Override
+ public Map collectFingerprints(FileSystemSnapshot roots) {
+ ImmutableMap.Builder builder = ImmutableMap.builder();
+ HashSet processedEntries = new HashSet<>();
+ roots.accept(snapshot -> {
+ snapshot.accept(new FileSystemLocationSnapshotVisitor() {
+ @Override
+ public void visitRegularFile(RegularFileSnapshot fileSnapshot) {
+ visitNonDirectoryEntry(snapshot);
+ }
+
+ @Override
+ public void visitMissing(MissingFileSnapshot missingSnapshot) {
+ visitNonDirectoryEntry(snapshot);
+ }
+
+ private void visitNonDirectoryEntry(FileSystemLocationSnapshot snapshot) {
+ String absolutePath = snapshot.getAbsolutePath();
+ if (processedEntries.add(absolutePath)) {
+ HashCode normalizedContentHash = getNormalizedContentHash(snapshot, normalizedContentHasher);
+ if (normalizedContentHash != null) {
+ builder.put(absolutePath, IgnoredPathFileSystemLocationFingerprint.create(snapshot.getType(), normalizedContentHash));
+ }
+ }
+ }
+ });
+ return SnapshotVisitResult.CONTINUE;
+ });
+ return builder.build();
+ }
+
+ @Override
+ public FingerprintHashingStrategy getHashingStrategy() {
+ return FingerprintHashingStrategy.SORT;
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/NameOnlyFingerprintingStrategy.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/NameOnlyFingerprintingStrategy.java
new file mode 100644
index 00000000..df8c49f6
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/NameOnlyFingerprintingStrategy.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint.impl;
+
+import com.google.common.collect.ImmutableMap;
+import org.gradle.internal.file.FileType;
+import org.gradle.internal.fingerprint.DirectorySensitivity;
+import org.gradle.internal.fingerprint.FileSystemLocationFingerprint;
+import org.gradle.internal.fingerprint.FingerprintHashingStrategy;
+import org.gradle.internal.fingerprint.hashing.FileSystemLocationSnapshotHasher;
+import org.gradle.internal.hash.HashCode;
+import org.gradle.internal.snapshot.FileSystemLocationSnapshot;
+import org.gradle.internal.snapshot.FileSystemSnapshot;
+import org.gradle.internal.snapshot.RootTrackingFileSystemSnapshotHierarchyVisitor;
+import org.gradle.internal.snapshot.SnapshotVisitResult;
+
+import java.util.HashSet;
+import java.util.Map;
+
+/**
+ * Fingerprint files normalizing the path to the file name.
+ *
+ * File names for root directories are ignored.
+ */
+public class NameOnlyFingerprintingStrategy extends AbstractFingerprintingStrategy {
+ public static final NameOnlyFingerprintingStrategy DEFAULT = new NameOnlyFingerprintingStrategy(DirectorySensitivity.DEFAULT);
+ public static final NameOnlyFingerprintingStrategy IGNORE_DIRECTORIES = new NameOnlyFingerprintingStrategy(DirectorySensitivity.IGNORE_DIRECTORIES);
+ public static final String IDENTIFIER = "NAME_ONLY";
+ private final FileSystemLocationSnapshotHasher normalizedContentHasher;
+
+ public NameOnlyFingerprintingStrategy(DirectorySensitivity directorySensitivity, FileSystemLocationSnapshotHasher normalizedContentHasher) {
+ super(IDENTIFIER, directorySensitivity, normalizedContentHasher);
+ this.normalizedContentHasher = normalizedContentHasher;
+ }
+
+ private NameOnlyFingerprintingStrategy(DirectorySensitivity directorySensitivity) {
+ this(directorySensitivity, FileSystemLocationSnapshotHasher.DEFAULT);
+ }
+
+ @Override
+ public String normalizePath(FileSystemLocationSnapshot snapshot) {
+ return snapshot.getName();
+ }
+
+ @Override
+ public Map collectFingerprints(FileSystemSnapshot roots) {
+ ImmutableMap.Builder builder = ImmutableMap.builder();
+ HashSet processedEntries = new HashSet<>();
+ roots.accept(new RootTrackingFileSystemSnapshotHierarchyVisitor() {
+ @Override
+ public SnapshotVisitResult visitEntry(FileSystemLocationSnapshot snapshot, boolean isRoot) {
+ String absolutePath = snapshot.getAbsolutePath();
+ if (processedEntries.add(absolutePath) && getDirectorySensitivity().shouldFingerprint(snapshot)) {
+ if (isRoot && snapshot.getType() == FileType.Directory) {
+ builder.put(absolutePath, IgnoredPathFileSystemLocationFingerprint.DIRECTORY);
+ } else {
+ HashCode normalizedContentHash = getNormalizedContentHash(snapshot, normalizedContentHasher);
+ if (normalizedContentHash != null) {
+ builder.put(absolutePath, new DefaultFileSystemLocationFingerprint(snapshot.getName(), snapshot.getType(), normalizedContentHash));
+ }
+ }
+ }
+ return SnapshotVisitResult.CONTINUE;
+ }
+ });
+ return builder.build();
+ }
+
+ @Override
+ public FingerprintHashingStrategy getHashingStrategy() {
+ return FingerprintHashingStrategy.SORT;
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/RelativePathFingerprintingStrategy.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/RelativePathFingerprintingStrategy.java
new file mode 100644
index 00000000..68d33a7f
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/RelativePathFingerprintingStrategy.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.fingerprint.impl;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Interner;
+import org.gradle.internal.file.FileType;
+import org.gradle.internal.fingerprint.DirectorySensitivity;
+import org.gradle.internal.fingerprint.FileSystemLocationFingerprint;
+import org.gradle.internal.fingerprint.FingerprintHashingStrategy;
+import org.gradle.internal.fingerprint.hashing.FileSystemLocationSnapshotHasher;
+import org.gradle.internal.hash.HashCode;
+import org.gradle.internal.snapshot.FileSystemLocationSnapshot;
+import org.gradle.internal.snapshot.FileSystemSnapshot;
+import org.gradle.internal.snapshot.RelativePathTracker;
+import org.gradle.internal.snapshot.SnapshotVisitResult;
+
+import javax.annotation.Nullable;
+import java.util.HashSet;
+import java.util.Map;
+
+/**
+ * Fingerprint file system snapshots normalizing the path to the relative path in a hierarchy.
+ *
+ * File names for root directories are ignored. For root files, the file name is used as normalized path.
+ */
+public class RelativePathFingerprintingStrategy extends AbstractFingerprintingStrategy {
+ public static final String IDENTIFIER = "RELATIVE_PATH";
+
+ private final Interner stringInterner;
+ private final FileSystemLocationSnapshotHasher normalizedContentHasher;
+
+ public RelativePathFingerprintingStrategy(Interner stringInterner, DirectorySensitivity directorySensitivity, FileSystemLocationSnapshotHasher normalizedContentHasher) {
+ super(IDENTIFIER, directorySensitivity, normalizedContentHasher);
+ this.stringInterner = stringInterner;
+ this.normalizedContentHasher = normalizedContentHasher;
+ }
+
+ public RelativePathFingerprintingStrategy(Interner stringInterner, DirectorySensitivity directorySensitivity) {
+ this(stringInterner, directorySensitivity, FileSystemLocationSnapshotHasher.DEFAULT);
+ }
+
+ @Override
+ public String normalizePath(FileSystemLocationSnapshot snapshot) {
+ if (snapshot.getType() == FileType.Directory) {
+ return "";
+ } else {
+ return snapshot.getName();
+ }
+ }
+
+ @Override
+ public Map collectFingerprints(FileSystemSnapshot roots) {
+ ImmutableMap.Builder builder = ImmutableMap.builder();
+ HashSet processedEntries = new HashSet<>();
+ roots.accept(new RelativePathTracker(), (snapshot, relativePath) -> {
+ String absolutePath = snapshot.getAbsolutePath();
+ if (processedEntries.add(absolutePath) && getDirectorySensitivity().shouldFingerprint(snapshot)) {
+ FileSystemLocationFingerprint fingerprint;
+ if (relativePath.isRoot()) {
+ if (snapshot.getType() == FileType.Directory) {
+ fingerprint = IgnoredPathFileSystemLocationFingerprint.DIRECTORY;
+ } else {
+ fingerprint = fingerprint(snapshot.getName(), snapshot.getType(), snapshot);
+ }
+ } else {
+ fingerprint = fingerprint(stringInterner.intern(relativePath.toRelativePath()), snapshot.getType(), snapshot);
+ }
+
+ if (fingerprint != null) {
+ builder.put(absolutePath, fingerprint);
+ }
+ }
+ return SnapshotVisitResult.CONTINUE;
+ });
+ return builder.build();
+ }
+
+ @Nullable
+ FileSystemLocationFingerprint fingerprint(String name, FileType type, FileSystemLocationSnapshot snapshot) {
+ HashCode normalizedContentHash = getNormalizedContentHash(snapshot, normalizedContentHasher);
+ return normalizedContentHash == null ? null : new DefaultFileSystemLocationFingerprint(name, type, normalizedContentHash);
+ }
+
+ @Override
+ public FingerprintHashingStrategy getHashingStrategy() {
+ return FingerprintHashingStrategy.SORT;
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/package-info.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/package-info.java
new file mode 100644
index 00000000..a560448f
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/impl/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@NonNullApi
+package org.gradle.internal.fingerprint.impl;
+
+import org.gradle.api.NonNullApi;
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/package-info.java b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/package-info.java
new file mode 100644
index 00000000..ea215d8d
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/fingerprint/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@NonNullApi
+package org.gradle.internal.fingerprint;
+
+import org.gradle.api.NonNullApi;
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/isolation/Isolatable.java b/subprojects/snapshots/src/main/java/org/gradle/internal/isolation/Isolatable.java
new file mode 100644
index 00000000..37b7cdd2
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/isolation/Isolatable.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.isolation;
+
+import org.gradle.internal.hash.Hashable;
+import org.gradle.internal.snapshot.ValueSnapshot;
+
+import javax.annotation.Nullable;
+
+/**
+ * Isolatable objects can return an isolated instance of the given type T from which this object was created.
+ * An isolated instance has the same internal state as the original object on which this isolatable was based,
+ * but it is guaranteed not to retain any references to mutable state from the original instance.
+ *
+ * The primary reason to need such an isolated instance of an object is to ensure that work can be done in parallel using the instance without
+ * fear that its internal state is changing while the work is being carried out.
+ */
+public interface Isolatable extends Hashable {
+ /**
+ * Returns this value as a {@link ValueSnapshot}. The returned value should not hold any references to user ClassLoaders.
+ */
+ ValueSnapshot asSnapshot();
+
+ /**
+ * Returns an instance of T that is isolated from the original object and all other instances.
+ * When T is mutable, a new instance is created on each call. When T is immutable, a new instance may or may not be created on each call. This may potentially be expensive.
+ */
+ @Nullable
+ T isolate();
+
+ /**
+ * Returns an instance of S constructed from the state of the original object, if possible.
+ *
+ * @return null if not supported, or the value is null.
+ */
+ @Nullable
+ S coerce(Class type);
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/isolation/IsolatableFactory.java b/subprojects/snapshots/src/main/java/org/gradle/internal/isolation/IsolatableFactory.java
new file mode 100644
index 00000000..b9a39aeb
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/isolation/IsolatableFactory.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.isolation;
+
+import javax.annotation.Nullable;
+
+public interface IsolatableFactory {
+ /**
+ * Creates an {@link Isolatable} that reflects the current state of the given value. Any changes made to the value will not be visible to the {@link Isolatable} and vice versa.
+ */
+ Isolatable isolate(@Nullable T value);
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/isolation/package-info.java b/subprojects/snapshots/src/main/java/org/gradle/internal/isolation/package-info.java
new file mode 100644
index 00000000..9831e186
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/isolation/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@NonNullApi
+package org.gradle.internal.isolation;
+
+import org.gradle.api.NonNullApi;
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/AbstractFileSystemLocationSnapshot.java b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/AbstractFileSystemLocationSnapshot.java
new file mode 100644
index 00000000..9b826162
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/AbstractFileSystemLocationSnapshot.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.snapshot;
+
+import org.gradle.internal.file.FileMetadata.AccessType;
+
+import java.util.Optional;
+
+public abstract class AbstractFileSystemLocationSnapshot implements FileSystemLocationSnapshot {
+ private final String absolutePath;
+ private final String name;
+ private final AccessType accessType;
+
+ public AbstractFileSystemLocationSnapshot(String absolutePath, String name, AccessType accessType) {
+ this.absolutePath = absolutePath;
+ this.name = name;
+ this.accessType = accessType;
+ }
+
+ protected static MissingFileSnapshot missingSnapshotForAbsolutePath(String filePath) {
+ return new MissingFileSnapshot(filePath, AccessType.DIRECT);
+ }
+
+ @Override
+ public String getAbsolutePath() {
+ return absolutePath;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public AccessType getAccessType() {
+ return accessType;
+ }
+
+ public String getPathToParent() {
+ return getName();
+ }
+
+ @Override
+ public FileSystemLocationSnapshot store(VfsRelativePath targetPath, CaseSensitivity caseSensitivity, MetadataSnapshot snapshot, SnapshotHierarchy.NodeDiffListener diffListener) {
+ return this;
+ }
+
+ @Override
+ public void accept(SnapshotHierarchy.SnapshotVisitor snapshotVisitor) {
+ snapshotVisitor.visitSnapshotRoot(this);
+ }
+
+ @Override
+ public boolean hasDescendants() {
+ return true;
+ }
+
+ @Override
+ public FileSystemNode asFileSystemNode() {
+ return this;
+ }
+
+ @Override
+ public Optional getSnapshot() {
+ return Optional.of(this);
+ }
+
+ @Override
+ public Optional getSnapshot(VfsRelativePath relativePath, CaseSensitivity caseSensitivity) {
+ return getChildSnapshot(relativePath, caseSensitivity);
+ }
+
+ protected Optional getChildSnapshot(VfsRelativePath relativePath, CaseSensitivity caseSensitivity) {
+ return Optional.of(missingSnapshotForAbsolutePath(relativePath.getAbsolutePath()));
+ }
+
+ @Override
+ public ReadOnlyFileSystemNode getNode(VfsRelativePath relativePath, CaseSensitivity caseSensitivity) {
+ return getChildNode(relativePath, caseSensitivity);
+ }
+
+ protected ReadOnlyFileSystemNode getChildNode(VfsRelativePath relativePath, CaseSensitivity caseSensitivity) {
+ return missingSnapshotForAbsolutePath(relativePath.getAbsolutePath());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ AbstractFileSystemLocationSnapshot that = (AbstractFileSystemLocationSnapshot) o;
+
+ if (accessType != that.accessType) {
+ return false;
+ }
+ if (!name.equals(that.name)) {
+ return false;
+ }
+ if (!absolutePath.equals(that.absolutePath)) {
+ return false;
+ }
+ return getHash().equals(that.getHash());
+ }
+
+ @Override
+ public int hashCode() {
+ int result = absolutePath.hashCode();
+ result = 31 * result + name.hashCode();
+ result = 31 * result + accessType.hashCode();
+ result = 31 * result + getHash().hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/AbstractIncompleteFileSystemNode.java b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/AbstractIncompleteFileSystemNode.java
new file mode 100644
index 00000000..63e943bf
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/AbstractIncompleteFileSystemNode.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.snapshot;
+
+import org.gradle.internal.file.FileType;
+
+import java.util.Optional;
+import java.util.function.Predicate;
+
+public abstract class AbstractIncompleteFileSystemNode implements FileSystemNode {
+ protected final ChildMap children;
+
+ @SuppressWarnings("unchecked")
+ public AbstractIncompleteFileSystemNode(ChildMap extends FileSystemNode> children) {
+ this.children = (ChildMap) children;
+ }
+
+ @Override
+ public ReadOnlyFileSystemNode getNode(VfsRelativePath targetPath, CaseSensitivity caseSensitivity) {
+ return SnapshotUtil.getChild(children, targetPath, caseSensitivity);
+ }
+
+ @Override
+ public Optional invalidate(VfsRelativePath targetPath, CaseSensitivity caseSensitivity, SnapshotHierarchy.NodeDiffListener diffListener) {
+ ChildMap newChildren = children.invalidate(targetPath, caseSensitivity, new ChildMap.InvalidationHandler() {
+ @Override
+ public Optional handleAsDescendantOfChild(VfsRelativePath pathInChild, FileSystemNode child) {
+ return child.invalidate(pathInChild, caseSensitivity, diffListener);
+ }
+
+ @Override
+ public void handleAsAncestorOfChild(String childPath, FileSystemNode child) {
+ diffListener.nodeRemoved(child);
+ }
+
+ @Override
+ public void handleExactMatchWithChild(FileSystemNode child) {
+ diffListener.nodeRemoved(child);
+ }
+
+ @Override
+ public void handleUnrelatedToAnyChild() {
+ }
+ });
+ if (newChildren.isEmpty()) {
+ return withAllChildrenRemoved();
+ }
+ if (newChildren == children) {
+ return Optional.of(withIncompleteChildren());
+ }
+ return Optional.of(withIncompleteChildren(newChildren));
+ }
+
+ @Override
+ public FileSystemNode store(VfsRelativePath targetPath, CaseSensitivity caseSensitivity, MetadataSnapshot snapshot, SnapshotHierarchy.NodeDiffListener diffListener) {
+ ChildMap newChildren = children.store(targetPath, caseSensitivity, new ChildMap.StoreHandler() {
+ @Override
+ public FileSystemNode handleAsDescendantOfChild(VfsRelativePath pathInChild, FileSystemNode child) {
+ return child.store(pathInChild, caseSensitivity, snapshot, diffListener);
+ }
+
+ @Override
+ public FileSystemNode handleAsAncestorOfChild(String childPath, FileSystemNode child) {
+ FileSystemNode newChild = snapshot.asFileSystemNode();
+ diffListener.nodeRemoved(child);
+ diffListener.nodeAdded(newChild);
+ return newChild;
+ }
+
+ @Override
+ public FileSystemNode mergeWithExisting(FileSystemNode child) {
+ if (snapshot instanceof FileSystemLocationSnapshot || !child.getSnapshot().map(oldSnapshot -> oldSnapshot instanceof FileSystemLocationSnapshot).orElse(false)) {
+ FileSystemNode newChild = snapshot.asFileSystemNode();
+ diffListener.nodeRemoved(child);
+ diffListener.nodeAdded(newChild);
+ return newChild;
+ } else {
+ return child;
+ }
+ }
+
+ @Override
+ public FileSystemNode createChild() {
+ FileSystemNode newChild = snapshot.asFileSystemNode();
+ diffListener.nodeAdded(newChild);
+ return newChild;
+ }
+
+ @Override
+ public FileSystemNode createNodeFromChildren(ChildMap children) {
+ boolean isDirectory = anyChildMatches(children, node -> node.getSnapshot().map(this::isRegularFileOrDirectory).orElse(false));
+ return isDirectory ? new PartialDirectoryNode(children) : new UnknownFileSystemNode(children);
+ }
+
+ private boolean isRegularFileOrDirectory(MetadataSnapshot metadataSnapshot) {
+ return metadataSnapshot.getType() != FileType.Missing;
+ }
+ });
+ if (newChildren == children) {
+ return this;
+ }
+ return withIncompleteChildren(newChildren);
+ }
+
+ @Override
+ public Optional getSnapshot(VfsRelativePath targetPath, CaseSensitivity caseSensitivity) {
+ return SnapshotUtil.getMetadataFromChildren(children, targetPath, caseSensitivity, Optional::empty);
+ }
+
+ /**
+ * Returns an updated node with the same children. The list of children are
+ * incomplete, even if they were complete before.
+ */
+ protected abstract FileSystemNode withIncompleteChildren();
+
+ /**
+ * Returns an updated node with an updated list of children.
+ *
+ * Caller must ensure the child list is not be mutated as the method
+ * doesn't make a defensive copy.
+ */
+ protected abstract FileSystemNode withIncompleteChildren(ChildMap extends FileSystemNode> newChildren);
+
+ /**
+ * Returns an updated node with all children removed, or {@link Optional#empty()}
+ * if the node without children would contain no useful information to keep around.
+ */
+ protected abstract Optional withAllChildrenRemoved();
+
+ @Override
+ public void accept(SnapshotHierarchy.SnapshotVisitor snapshotVisitor) {
+ children.visitChildren((childPath, child) -> child.accept(snapshotVisitor));
+ }
+
+ @Override
+ public boolean hasDescendants() {
+ return anyChildMatches(children, FileSystemNode::hasDescendants);
+ }
+
+ private static boolean anyChildMatches(ChildMap children, Predicate predicate) {
+ return children.entries().stream()
+ .map(ChildMap.Entry::getValue)
+ .anyMatch(predicate);
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/AbstractInvalidateChildHandler.java b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/AbstractInvalidateChildHandler.java
new file mode 100644
index 00000000..c0ab8402
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/AbstractInvalidateChildHandler.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.snapshot;
+
+import java.util.Optional;
+
+public abstract class AbstractInvalidateChildHandler implements ChildMap.NodeHandler> {
+
+ private final ChildMap.InvalidationHandler handler;
+
+ public AbstractInvalidateChildHandler(ChildMap.InvalidationHandler handler) {
+ this.handler = handler;
+ }
+
+ public abstract ChildMap getChildMap();
+
+ public abstract ChildMap withReplacedChild(RESULT newChild);
+
+ public abstract ChildMap withReplacedChild(String newChildPath, RESULT newChild);
+
+ public abstract ChildMap withRemovedChild();
+
+ @Override
+ public ChildMap handleAsDescendantOfChild(VfsRelativePath pathInChild, T child) {
+ Optional invalidatedChild = handler.handleAsDescendantOfChild(pathInChild, child);
+ return invalidatedChild
+ .map(this::withReplacedChild)
+ .orElseGet(this::withRemovedChild);
+ }
+
+ @Override
+ public ChildMap handleAsAncestorOfChild(String childPath, T child) {
+ handler.handleAsAncestorOfChild(childPath, child);
+ return withRemovedChild();
+ }
+
+ @Override
+ public ChildMap handleExactMatchWithChild(T child) {
+ handler.handleExactMatchWithChild(child);
+ return withRemovedChild();
+ }
+
+ @Override
+ public ChildMap handleUnrelatedToAnyChild() {
+ handler.handleUnrelatedToAnyChild();
+ return getChildMap();
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/AbstractListChildMap.java b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/AbstractListChildMap.java
new file mode 100644
index 00000000..a4d4bed9
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/AbstractListChildMap.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.snapshot;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
+
+public abstract class AbstractListChildMap implements ChildMap {
+ protected final List> entries;
+
+ protected AbstractListChildMap(List> entries) {
+ this.entries = entries;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ @Override
+ public List values() {
+ return entries.stream()
+ .map(Entry::getValue)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public List> entries() {
+ return entries;
+ }
+
+ @Override
+ public void visitChildren(BiConsumer visitor) {
+ for (Entry child : entries) {
+ visitor.accept(child.getPath(), child.getValue());
+ }
+ }
+
+ protected int findChildIndexWithCommonPrefix(VfsRelativePath targetPath, CaseSensitivity caseSensitivity) {
+ return SearchUtil.binarySearch(
+ entries,
+ candidate -> targetPath.compareToFirstSegment(candidate.getPath(), caseSensitivity)
+ );
+ }
+
+ @Override
+ public ChildMap invalidate(VfsRelativePath targetPath, CaseSensitivity caseSensitivity, InvalidationHandler handler) {
+ int childIndex = findChildIndexWithCommonPrefix(targetPath, caseSensitivity);
+ if (childIndex >= 0) {
+ Entry entry = entries.get(childIndex);
+ String childPath = entry.getPath();
+ return entry.withNode(targetPath, caseSensitivity, new AbstractInvalidateChildHandler(handler) {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public AbstractListChildMap getChildMap() {
+ return (AbstractListChildMap) AbstractListChildMap.this;
+ }
+
+ @Override
+ public ChildMap withReplacedChild(RESULT newChild) {
+ return withReplacedChild(childPath, newChild);
+ }
+
+ @Override
+ public ChildMap withReplacedChild(String newChildPath, RESULT newChild) {
+ return getChildMap().withReplacedChild(childIndex, newChildPath, newChild);
+ }
+
+ @Override
+ public ChildMap withRemovedChild() {
+ return getChildMap().withRemovedChild(childIndex);
+ }
+ });
+ } else {
+ handler.handleUnrelatedToAnyChild();
+ @SuppressWarnings("unchecked") AbstractListChildMap castedThis = (AbstractListChildMap) this;
+ return castedThis;
+ }
+ }
+
+ @Override
+ public ChildMap store(VfsRelativePath targetPath, CaseSensitivity caseSensitivity, StoreHandler storeHandler) {
+ int childIndex = findChildIndexWithCommonPrefix(targetPath, caseSensitivity);
+ if (childIndex >= 0) {
+ return entries.get(childIndex).handlePath(targetPath, caseSensitivity, new AbstractStorePathRelationshipHandler(caseSensitivity, storeHandler) {
+ @Override
+ public ChildMap withReplacedChild(T newChild) {
+ return withReplacedChild(entries.get(childIndex).getPath(), newChild);
+ }
+
+ @Override
+ public ChildMap withReplacedChild(String newChildPath, T newChild) {
+ return AbstractListChildMap.this.withReplacedChild(childIndex, newChildPath, newChild);
+ }
+
+ @Override
+ public ChildMap withNewChild(String newChildPath, T newChild) {
+ return AbstractListChildMap.this.withNewChild(childIndex, newChildPath, newChild);
+ }
+ });
+ } else {
+ T newChild = storeHandler.createChild();
+ return withNewChild(-childIndex - 1, targetPath.toString(), newChild);
+ }
+ }
+
+ protected ChildMap withNewChild(int insertBefore, String path, T newChild) {
+ List> newChildren = new ArrayList<>(entries);
+ newChildren.add(insertBefore, new Entry<>(path, newChild));
+ return ChildMapFactory.childMapFromSorted(newChildren);
+ }
+
+ protected ChildMap withReplacedChild(int childIndex, String newPath, T newChild) {
+ Entry oldEntry = entries.get(childIndex);
+ if (oldEntry.getPath().equals(newPath) && oldEntry.getValue().equals(newChild)) {
+ return this;
+ }
+ List> newChildren = new ArrayList<>(entries);
+ newChildren.set(childIndex, new Entry<>(newPath, newChild));
+ return ChildMapFactory.childMapFromSorted(newChildren);
+ }
+
+ protected ChildMap withRemovedChild(int childIndex) {
+ List> newChildren = new ArrayList<>(entries);
+ newChildren.remove(childIndex);
+ return ChildMapFactory.childMapFromSorted(newChildren);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ AbstractListChildMap> that = (AbstractListChildMap>) o;
+
+ return entries.equals(that.entries);
+ }
+
+ @Override
+ public int hashCode() {
+ return entries.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return entries.toString();
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/AbstractStorePathRelationshipHandler.java b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/AbstractStorePathRelationshipHandler.java
new file mode 100644
index 00000000..c0798b8a
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/AbstractStorePathRelationshipHandler.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.snapshot;
+
+public abstract class AbstractStorePathRelationshipHandler implements ChildMap.Entry.PathRelationshipHandler, T> {
+
+ private final CaseSensitivity caseSensitivity;
+ private final ChildMap.StoreHandler handler;
+
+ public AbstractStorePathRelationshipHandler(CaseSensitivity caseSensitivity, ChildMap.StoreHandler handler) {
+ this.caseSensitivity = caseSensitivity;
+ this.handler = handler;
+ }
+
+ public abstract ChildMap withReplacedChild(T newChild);
+
+ public abstract ChildMap withReplacedChild(String newChildPath, T newChild);
+
+ public abstract ChildMap withNewChild(String newChildPath, T newChild);
+
+ @Override
+ public ChildMap handleAsDescendantOfChild(VfsRelativePath targetPath, String childPath, T child) {
+ T newChild = handler.handleAsDescendantOfChild(targetPath.fromChild(childPath), child);
+ return withReplacedChild(newChild);
+ }
+
+ @Override
+ public ChildMap handleAsAncestorOfChild(VfsRelativePath targetPath, String childPath, T child) {
+ T newChild = handler.handleAsAncestorOfChild(childPath, child);
+ return withReplacedChild(targetPath.getAsString(), newChild);
+ }
+
+ @Override
+ public ChildMap handleExactMatchWithChild(VfsRelativePath targetPath, String childPath, T child) {
+ T newChild = handler.mergeWithExisting(child);
+ return withReplacedChild(newChild);
+ }
+
+ @Override
+ public ChildMap handleSiblingOfChild(VfsRelativePath targetPath, String childPath, T child, int commonPrefixLength) {
+ String commonPrefix = childPath.substring(0, commonPrefixLength);
+ String newChildPath = childPath.substring(commonPrefixLength + 1);
+ ChildMap.Entry newChild = new ChildMap.Entry<>(newChildPath, child);
+ String siblingPath = targetPath.suffixStartingFrom(commonPrefixLength + 1).getAsString();
+ ChildMap.Entry sibling = new ChildMap.Entry<>(siblingPath, handler.createChild());
+ ChildMap newChildren = ChildMapFactory.childMap(caseSensitivity, newChild, sibling);
+ return withReplacedChild(commonPrefix, handler.createNodeFromChildren(newChildren));
+ }
+
+ @Override
+ public ChildMap handleUnrelatedToAnyChild(VfsRelativePath targetPath) {
+ String path = targetPath.getAsString();
+ T newNode = handler.createChild();
+ return withNewChild(path, newNode);
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/CaseSensitivity.java b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/CaseSensitivity.java
new file mode 100644
index 00000000..41ff1b9a
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/CaseSensitivity.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.snapshot;
+
+/**
+ * The case sensitivity of a file system.
+ *
+ * Note that the method for actually comparing paths with a case sensitivity are in {@link PathUtil} instead of being on this enum,
+ * since it seems that the methods can be better inlined by the JIT compiler if they are static.
+ */
+public enum CaseSensitivity {
+ CASE_SENSITIVE,
+ CASE_INSENSITIVE
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/ChildMap.java b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/ChildMap.java
new file mode 100644
index 00000000..15eae95d
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/ChildMap.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.snapshot;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+
+public interface ChildMap {
+
+ boolean isEmpty();
+
+ List values();
+
+ List> entries();
+
+ void visitChildren(BiConsumer visitor);
+
+ RESULT withNode(VfsRelativePath targetPath, CaseSensitivity caseSensitivity, NodeHandler handler);
+
+ interface NodeHandler {
+ RESULT handleAsDescendantOfChild(VfsRelativePath pathInChild, T child);
+ RESULT handleAsAncestorOfChild(String childPath, T child);
+ RESULT handleExactMatchWithChild(T child);
+ RESULT handleUnrelatedToAnyChild();
+ }
+
+ ChildMap invalidate(VfsRelativePath targetPath, CaseSensitivity caseSensitivity, InvalidationHandler handler);
+
+ interface InvalidationHandler {
+ Optional handleAsDescendantOfChild(VfsRelativePath pathInChild, T child);
+ void handleAsAncestorOfChild(String childPath, T child);
+ void handleExactMatchWithChild(T child);
+ void handleUnrelatedToAnyChild();
+ }
+
+ ChildMap store(VfsRelativePath targetPath, CaseSensitivity caseSensitivity, StoreHandler storeHandler);
+
+ interface StoreHandler {
+ T handleAsDescendantOfChild(VfsRelativePath pathInChild, T child);
+ T handleAsAncestorOfChild(String childPath, T child);
+ T mergeWithExisting(T child);
+ T createChild();
+ T createNodeFromChildren(ChildMap children);
+ }
+
+ class Entry {
+ private final String path;
+ private final T value;
+
+ public Entry(String path, T value) {
+ this.path = path;
+ this.value = value;
+ }
+
+ public RESULT withNode(VfsRelativePath targetPath, CaseSensitivity caseSensitivity, NodeHandler handler) {
+ return handleAncestorDescendantOrExactMatch(targetPath, caseSensitivity, handler)
+ .orElseGet(handler::handleUnrelatedToAnyChild);
+ }
+
+ public Optional handleAncestorDescendantOrExactMatch(VfsRelativePath targetPath, CaseSensitivity caseSensitivity, NodeHandler handler) {
+ if (targetPath.hasPrefix(path, caseSensitivity)) {
+ if (targetPath.length() == path.length()) {
+ return Optional.of(handler.handleExactMatchWithChild(value));
+ } else {
+ return Optional.of(handler.handleAsDescendantOfChild(targetPath.fromChild(path), value));
+ }
+ } else if (targetPath.length() < path.length() && targetPath.isPrefixOf(path, caseSensitivity)) {
+ return Optional.of(handler.handleAsAncestorOfChild(path, value));
+ }
+ return Optional.empty();
+ }
+
+ public RESULT handlePath(VfsRelativePath targetPath, CaseSensitivity caseSensitivity, PathRelationshipHandler handler) {
+ int pathToParentLength = path.length();
+ int targetPathLength = targetPath.length();
+ int maxPos = Math.min(pathToParentLength, targetPathLength);
+ int commonPrefixLength = targetPath.lengthOfCommonPrefix(path, caseSensitivity);
+ if (commonPrefixLength == maxPos) {
+ if (pathToParentLength > targetPathLength) {
+ return handler.handleAsAncestorOfChild(targetPath, path, value);
+ }
+ if (pathToParentLength == targetPathLength) {
+ return handler.handleExactMatchWithChild(targetPath, path, value);
+ }
+ return handler.handleAsDescendantOfChild(targetPath, path, value);
+ }
+ if (commonPrefixLength == 0) {
+ return handler.handleUnrelatedToAnyChild(targetPath);
+ }
+ return handler.handleSiblingOfChild(targetPath, path, value, commonPrefixLength);
+ }
+
+ public interface PathRelationshipHandler {
+ RESULT handleAsDescendantOfChild(VfsRelativePath targetPath, String childPath, T child);
+ RESULT handleAsAncestorOfChild(VfsRelativePath targetPath, String childPath, T child);
+ RESULT handleExactMatchWithChild(VfsRelativePath targetPath, String childPath, T child);
+ RESULT handleSiblingOfChild(VfsRelativePath targetPath, String childPath, T child, int commonPrefixLength);
+ RESULT handleUnrelatedToAnyChild(VfsRelativePath targetPath);
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public T getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Entry> entry = (Entry>) o;
+
+ if (!path.equals(entry.path)) {
+ return false;
+ }
+ return value.equals(entry.value);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = path.hashCode();
+ result = 31 * result + value.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Entry{" + path + " : " + value + '}';
+ }
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/ChildMapFactory.java b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/ChildMapFactory.java
new file mode 100644
index 00000000..61cded95
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/ChildMapFactory.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.snapshot;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+
+public class ChildMapFactory {
+ /**
+ * If a node has fewer children, we use a linear search for the child.
+ * We use this limit since {@link VfsRelativePath#compareToFirstSegment(String, CaseSensitivity)}
+ * is about twice as slow as {@link VfsRelativePath#hasPrefix(String, CaseSensitivity)},
+ * so comparing the searched path to all of the children is actually faster than doing a binary search.
+ */
+ private static final int MINIMUM_CHILD_COUNT_FOR_BINARY_SEARCH = 10;
+
+ public static ChildMap childMap(CaseSensitivity caseSensitivity, Collection> entries) {
+ List> sortedEntries = new ArrayList<>(entries);
+ sortedEntries.sort(Comparator.comparing(ChildMap.Entry::getPath, PathUtil.getPathComparator(caseSensitivity)));
+ return childMapFromSorted(sortedEntries);
+ }
+
+ public static ChildMap childMapFromSorted(List> sortedEntries) {
+ int size = sortedEntries.size();
+ switch (size) {
+ case 0:
+ return EmptyChildMap.getInstance();
+ case 1:
+ return new SingletonChildMap<>(sortedEntries.get(0));
+ default:
+ return (size < MINIMUM_CHILD_COUNT_FOR_BINARY_SEARCH)
+ ? new MediumChildMap<>(sortedEntries)
+ : new LargeChildMap<>(sortedEntries);
+ }
+ }
+
+ static ChildMap childMap(CaseSensitivity caseSensitivity, ChildMap.Entry entry1, ChildMap.Entry entry2) {
+ int compared = PathUtil.getPathComparator(caseSensitivity).compare(entry1.getPath(), entry2.getPath());
+ List> sortedEntries = compared < 0
+ ? ImmutableList.of(entry1, entry2)
+ : ImmutableList.of(entry2, entry1);
+ return childMapFromSorted(sortedEntries);
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/CompositeFileSystemSnapshot.java b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/CompositeFileSystemSnapshot.java
new file mode 100644
index 00000000..13f4f52c
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/CompositeFileSystemSnapshot.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.snapshot;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Collection;
+import java.util.List;
+
+public class CompositeFileSystemSnapshot implements FileSystemSnapshot {
+ private final ImmutableList snapshots;
+
+ private CompositeFileSystemSnapshot(Collection extends FileSystemSnapshot> snapshots) {
+ this.snapshots = ImmutableList.copyOf(snapshots);
+ }
+
+ public static FileSystemSnapshot of(List extends FileSystemSnapshot> snapshots) {
+ switch (snapshots.size()) {
+ case 0:
+ return EMPTY;
+ case 1:
+ return snapshots.get(0);
+ default:
+ return new CompositeFileSystemSnapshot(snapshots);
+ }
+ }
+
+ @Override
+ public SnapshotVisitResult accept(FileSystemSnapshotHierarchyVisitor visitor) {
+ for (FileSystemSnapshot snapshot : snapshots) {
+ SnapshotVisitResult result = snapshot.accept(visitor);
+ if (result == SnapshotVisitResult.TERMINATE) {
+ return SnapshotVisitResult.TERMINATE;
+ }
+ }
+ return SnapshotVisitResult.CONTINUE;
+ }
+
+ @Override
+ public SnapshotVisitResult accept(RelativePathTracker pathTracker, RelativePathTrackingFileSystemSnapshotHierarchyVisitor visitor) {
+ for (FileSystemSnapshot snapshot : snapshots) {
+ SnapshotVisitResult result = snapshot.accept(pathTracker, visitor);
+ if (result == SnapshotVisitResult.TERMINATE) {
+ return SnapshotVisitResult.TERMINATE;
+ }
+ }
+ return SnapshotVisitResult.CONTINUE;
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ CompositeFileSystemSnapshot that = (CompositeFileSystemSnapshot) o;
+
+ return snapshots.equals(that.snapshots);
+ }
+
+ @Override
+ public int hashCode() {
+ return snapshots.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return snapshots.toString();
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/DirectorySnapshot.java b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/DirectorySnapshot.java
new file mode 100644
index 00000000..c395b7cc
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/DirectorySnapshot.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.snapshot;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.gradle.internal.file.FileMetadata.AccessType;
+import org.gradle.internal.file.FileType;
+import org.gradle.internal.hash.HashCode;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static org.gradle.internal.snapshot.ChildMapFactory.childMapFromSorted;
+import static org.gradle.internal.snapshot.SnapshotVisitResult.CONTINUE;
+
+/**
+ * A snapshot of an existing directory hierarchy.
+ *
+ * Includes snapshots of any child element and the Merkle tree hash.
+ */
+public class DirectorySnapshot extends AbstractFileSystemLocationSnapshot {
+ private final ChildMap children;
+ private final HashCode contentHash;
+
+ public DirectorySnapshot(String absolutePath, String name, AccessType accessType, HashCode contentHash, List children) {
+ this(absolutePath, name, accessType, contentHash, childMapFromSorted(children.stream()
+ .map(it -> new ChildMap.Entry<>(it.getName(), it))
+ .collect(Collectors.toList())));
+ }
+
+ public DirectorySnapshot(String absolutePath, String name, AccessType accessType, HashCode contentHash, ChildMap children) {
+ super(absolutePath, name, accessType);
+ this.contentHash = contentHash;
+ this.children = children;
+ }
+
+ @Override
+ public HashCode getHash() {
+ return contentHash;
+ }
+
+ @Override
+ public FileType getType() {
+ return FileType.Directory;
+ }
+
+ @Override
+ public boolean isContentAndMetadataUpToDate(FileSystemLocationSnapshot other) {
+ return isContentUpToDate(other);
+ }
+
+ @Override
+ public boolean isContentUpToDate(FileSystemLocationSnapshot other) {
+ return other instanceof DirectorySnapshot;
+ }
+
+ @Override
+ public SnapshotVisitResult accept(FileSystemSnapshotHierarchyVisitor visitor) {
+ SnapshotVisitResult result = visitor.visitEntry(this);
+ switch (result) {
+ case CONTINUE:
+ visitor.enterDirectory(this);
+ children.visitChildren((name, child) -> child.accept(visitor));
+ visitor.leaveDirectory(this);
+ return CONTINUE;
+ case SKIP_SUBTREE:
+ return CONTINUE;
+ case TERMINATE:
+ return SnapshotVisitResult.TERMINATE;
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ @Override
+ public SnapshotVisitResult accept(RelativePathTracker pathTracker, RelativePathTrackingFileSystemSnapshotHierarchyVisitor visitor) {
+ pathTracker.enter(getName());
+ try {
+ SnapshotVisitResult result = visitor.visitEntry(this, pathTracker);
+ switch (result) {
+ case CONTINUE:
+ visitor.enterDirectory(this, pathTracker);
+ children.visitChildren((name, child) -> child.accept(pathTracker, visitor));
+ visitor.leaveDirectory(this, pathTracker);
+ return CONTINUE;
+ case SKIP_SUBTREE:
+ return CONTINUE;
+ case TERMINATE:
+ return SnapshotVisitResult.TERMINATE;
+ default:
+ throw new AssertionError();
+ }
+ } finally {
+ pathTracker.leave();
+ }
+ }
+
+ @Override
+ public void accept(FileSystemLocationSnapshotVisitor visitor) {
+ visitor.visitDirectory(this);
+ }
+
+ @Override
+ public T accept(FileSystemLocationSnapshotTransformer transformer) {
+ return transformer.visitDirectory(this);
+ }
+
+ @VisibleForTesting
+ public List getChildren() {
+ return children.values();
+ }
+
+ @Override
+ protected Optional getChildSnapshot(VfsRelativePath targetPath, CaseSensitivity caseSensitivity) {
+ return Optional.of(
+ SnapshotUtil.getMetadataFromChildren(children, targetPath, caseSensitivity, Optional::empty)
+ .orElseGet(() -> missingSnapshotForAbsolutePath(targetPath.getAbsolutePath()))
+ );
+ }
+
+ @Override
+ protected ReadOnlyFileSystemNode getChildNode(VfsRelativePath targetPath, CaseSensitivity caseSensitivity) {
+ ReadOnlyFileSystemNode childNode = SnapshotUtil.getChild(children, targetPath, caseSensitivity);
+ return childNode == ReadOnlyFileSystemNode.EMPTY
+ ? missingSnapshotForAbsolutePath(targetPath.getAbsolutePath())
+ : childNode;
+ }
+
+ @Override
+ public Optional invalidate(VfsRelativePath targetPath, CaseSensitivity caseSensitivity, SnapshotHierarchy.NodeDiffListener diffListener) {
+ ChildMap newChildren = children.invalidate(targetPath, caseSensitivity, new ChildMap.InvalidationHandler() {
+ @Override
+ public Optional handleAsDescendantOfChild(VfsRelativePath pathInChild, FileSystemLocationSnapshot child) {
+ diffListener.nodeRemoved(DirectorySnapshot.this);
+ Optional invalidated = child.invalidate(pathInChild, caseSensitivity, new SnapshotHierarchy.NodeDiffListener() {
+ @Override
+ public void nodeRemoved(FileSystemNode node) {
+ // the parent already has been removed. No children need to be removed.
+ }
+
+ @Override
+ public void nodeAdded(FileSystemNode node) {
+ diffListener.nodeAdded(node);
+ }
+ });
+ children.visitChildren((__, existingChild) -> {
+ if (existingChild != child) {
+ diffListener.nodeAdded(existingChild);
+ }
+ });
+ return invalidated;
+ }
+
+ @Override
+ public void handleAsAncestorOfChild(String childPath, FileSystemLocationSnapshot child) {
+ throw new IllegalStateException("Can't have an ancestor of a single path element");
+ }
+
+ @Override
+ public void handleExactMatchWithChild(FileSystemLocationSnapshot child) {
+ diffListener.nodeRemoved(DirectorySnapshot.this);
+ children.visitChildren((__, existingChild) -> {
+ if (existingChild != child) {
+ diffListener.nodeAdded(existingChild);
+ }
+ });
+ }
+
+ @Override
+ public void handleUnrelatedToAnyChild() {
+ diffListener.nodeRemoved(DirectorySnapshot.this);
+ children.visitChildren((__, child) -> diffListener.nodeAdded(child));
+ }
+ });
+ return Optional.of(new PartialDirectoryNode(newChildren));
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s@%s/%s(%s)", super.toString(), contentHash, getName(), children);
+ }
+}
diff --git a/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/EmptyChildMap.java b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/EmptyChildMap.java
new file mode 100644
index 00000000..6eeb1ddd
--- /dev/null
+++ b/subprojects/snapshots/src/main/java/org/gradle/internal/snapshot/EmptyChildMap.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.snapshot;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.function.BiConsumer;
+
+public class EmptyChildMap implements ChildMap {
+ private static final EmptyChildMap