7
7
import org .gradle .api .Project ;
8
8
import org .gradle .api .plugins .ExtraPropertiesExtension ;
9
9
import org .gradle .internal .jvm .Jvm ;
10
- import org .gradle .process .ExecResult ;
11
10
12
11
import java .io .BufferedReader ;
13
12
import java .io .ByteArrayOutputStream ;
16
15
import java .io .IOException ;
17
16
import java .io .InputStreamReader ;
18
17
import java .io .UncheckedIOException ;
18
+ import java .nio .charset .StandardCharsets ;
19
+ import java .nio .file .Files ;
20
+ import java .nio .file .Path ;
21
+ import java .nio .file .Paths ;
19
22
import java .time .ZoneOffset ;
20
23
import java .time .ZonedDateTime ;
21
24
import java .util .ArrayList ;
22
25
import java .util .Arrays ;
23
26
import java .util .HashMap ;
24
27
import java .util .List ;
25
28
import java .util .Map ;
26
- import java .util .concurrent .atomic .AtomicReference ;
27
29
import java .util .stream .Collectors ;
28
30
29
- import static java .nio .charset .StandardCharsets .UTF_8 ;
30
-
31
31
public class GlobalBuildInfoPlugin implements Plugin <Project > {
32
32
private static final String GLOBAL_INFO_EXTENSION_NAME = "globalInfo" ;
33
33
private static Integer _defaultParallel = null ;
@@ -46,8 +46,6 @@ public void apply(Project project) {
46
46
File compilerJavaHome = findCompilerJavaHome ();
47
47
File runtimeJavaHome = findRuntimeJavaHome (compilerJavaHome );
48
48
49
- Object gitRevisionResolver = createGitRevisionResolver (project );
50
-
51
49
final List <JavaHome > javaVersions = new ArrayList <>();
52
50
for (int version = 8 ; version <= Integer .parseInt (minimumCompilerVersion .getMajorVersion ()); version ++) {
53
51
if (System .getenv (getJavaHomeEnvVarName (Integer .toString (version ))) != null ) {
@@ -95,7 +93,7 @@ public void apply(Project project) {
95
93
ext .set ("minimumCompilerVersion" , minimumCompilerVersion );
96
94
ext .set ("minimumRuntimeVersion" , minimumRuntimeVersion );
97
95
ext .set ("gradleJavaVersion" , Jvm .current ().getJavaVersion ());
98
- ext .set ("gitRevision" , gitRevisionResolver );
96
+ ext .set ("gitRevision" , gitRevision ( project ) );
99
97
ext .set ("buildDate" , ZonedDateTime .now (ZoneOffset .UTC ));
100
98
});
101
99
}
@@ -206,35 +204,65 @@ private static int findDefaultParallel(Project project) {
206
204
return _defaultParallel ;
207
205
}
208
206
209
- private Object createGitRevisionResolver (final Project project ) {
210
- return new Object () {
211
- private final AtomicReference <String > gitRevision = new AtomicReference <>();
212
-
213
- @ Override
214
- public String toString () {
215
- if (gitRevision .get () == null ) {
216
- final ByteArrayOutputStream stdout = new ByteArrayOutputStream ();
217
- final ByteArrayOutputStream stderr = new ByteArrayOutputStream ();
218
- final ExecResult result = project .exec (spec -> {
219
- spec .setExecutable ("git" );
220
- spec .setArgs (Arrays .asList ("rev-parse" , "HEAD" ));
221
- spec .setStandardOutput (stdout );
222
- spec .setErrorOutput (stderr );
223
- spec .setIgnoreExitValue (true );
224
- });
225
-
226
- final String revision ;
227
- if (result .getExitValue () != 0 ) {
228
- revision = "unknown" ;
229
- } else {
230
- revision = stdout .toString (UTF_8 ).trim ();
231
- }
232
- this .gitRevision .compareAndSet (null , revision );
207
+ private String gitRevision (final Project project ) {
208
+ try {
209
+ /*
210
+ * We want to avoid forking another process to run git rev-parse HEAD. Instead, we will read the refs manually. The
211
+ * documentation for this follows from https://git-scm.com/docs/gitrepository-layout and https://git-scm.com/docs/git-worktree.
212
+ *
213
+ * There are two cases to consider:
214
+ * - a plain repository with .git directory at the root of the working tree
215
+ * - a worktree with a plain text .git file at the root of the working tree
216
+ *
217
+ * In each case, our goal is to parse the HEAD file to get either a ref or a bare revision (in the case of being in detached
218
+ * HEAD state).
219
+ *
220
+ * In the case of a plain repository, we can read the HEAD file directly, resolved directly from the .git directory.
221
+ *
222
+ * In the case of a worktree, we read the gitdir from the plain text .git file. This resolves to a directory from which we read
223
+ * the HEAD file and resolve commondir to the plain git repository.
224
+ */
225
+ final Path dotGit = project .getRootProject ().getRootDir ().toPath ().resolve (".git" );
226
+ final String revision ;
227
+ if (Files .exists (dotGit ) == false ) {
228
+ return "unknown" ;
229
+ }
230
+ final Path head ;
231
+ final Path gitDir ;
232
+ if (Files .isDirectory (dotGit )) {
233
+ // this is a git repository, we can read HEAD directly
234
+ head = dotGit .resolve ("HEAD" );
235
+ gitDir = dotGit ;
236
+ } else {
237
+ // this is a git worktree, follow the pointer to the repository
238
+ final Path workTree = Paths .get (readFirstLine (dotGit ).substring ("gitdir:" .length ()).trim ());
239
+ head = workTree .resolve ("HEAD" );
240
+ final Path commonDir = Paths .get (readFirstLine (workTree .resolve ("commondir" )));
241
+ if (commonDir .isAbsolute ()) {
242
+ gitDir = commonDir ;
243
+ } else {
244
+ // this is the common case
245
+ gitDir = workTree .resolve (commonDir );
233
246
}
234
- return gitRevision .get ();
235
247
}
236
- };
248
+ final String ref = readFirstLine (head );
249
+ if (ref .startsWith ("ref:" )) {
250
+ revision = readFirstLine (gitDir .resolve (ref .substring ("ref:" .length ()).trim ()));
251
+ } else {
252
+ // we are in detached HEAD state
253
+ revision = ref ;
254
+ }
255
+ return revision ;
256
+ } catch (final IOException e ) {
257
+ // for now, do not be lenient until we have better understanding of real-world scenarios where this happens
258
+ throw new GradleException ("unable to read the git revision" , e );
259
+ }
260
+ }
237
261
262
+ private String readFirstLine (final Path path ) throws IOException {
263
+ return Files .lines (path , StandardCharsets .UTF_8 )
264
+ .findFirst ()
265
+ .orElseThrow (() -> new IOException ("file [" + path + "] is empty" ));
238
266
}
239
267
240
268
}
0 commit comments