11/*
2- * Copyright 2020-2021 DiffPlug
2+ * Copyright 2020-2022 DiffPlug
33 *
44 * Licensed under the Apache License, Version 2.0 (the "License");
55 * you may not use this file except in compliance with the License.
1717
1818import java .io .File ;
1919import java .io .IOException ;
20- import java .nio .charset .StandardCharsets ;
21- import java .nio .file .Files ;
2220
2321import javax .annotation .Nullable ;
2422
23+ import org .eclipse .jgit .errors .ConfigInvalidException ;
24+ import org .eclipse .jgit .lib .Config ;
25+ import org .eclipse .jgit .lib .ConfigConstants ;
26+ import org .eclipse .jgit .lib .Constants ;
27+ import org .eclipse .jgit .storage .file .FileBasedConfig ;
2528import org .eclipse .jgit .storage .file .FileRepositoryBuilder ;
29+ import org .eclipse .jgit .util .IO ;
30+ import org .eclipse .jgit .util .RawParseUtils ;
31+ import org .eclipse .jgit .util .SystemReader ;
32+
33+ import com .diffplug .common .base .Errors ;
34+
35+ import edu .umd .cs .findbugs .annotations .SuppressFBWarnings ;
2636
2737/**
2838 * Utility methods for Git workarounds.
2939 */
30- public class GitWorkarounds {
40+ public final class GitWorkarounds {
3141 private GitWorkarounds () {}
3242
3343 /**
@@ -40,46 +50,155 @@ private GitWorkarounds() {}
4050 * @return the path to the .git directory.
4151 */
4252 static @ Nullable File getDotGitDir (File projectDir ) {
43- return fileRepositoryBuilderForProject (projectDir ).getGitDir ();
53+ return fileRepositoryResolverForProject (projectDir ).getGitDir ();
4454 }
4555
4656 /**
47- * Creates a {@link FileRepositoryBuilder } for the given project directory.
57+ * Creates a {@link RepositorySpecificResolver } for the given project directory.
4858 *
4959 * This applies a workaround for JGit not supporting worktrees properly.
5060 *
5161 * @param projectDir the project directory.
5262 * @return the builder.
5363 */
54- static FileRepositoryBuilder fileRepositoryBuilderForProject (File projectDir ) {
55- FileRepositoryBuilder builder = new FileRepositoryBuilder ();
56- builder .findGitDir (projectDir );
57- File gitDir = builder . getGitDir ();
58- if (gitDir != null ) {
59- builder . setGitDir ( resolveRealGitDirIfWorktreeDir ( gitDir ) );
64+ static RepositorySpecificResolver fileRepositoryResolverForProject (File projectDir ) {
65+ RepositorySpecificResolver repositoryResolver = new RepositorySpecificResolver ();
66+ repositoryResolver .findGitDir (projectDir );
67+ repositoryResolver . readEnvironment ();
68+ if (repositoryResolver . getGitDir () != null || repositoryResolver . getWorkTree () != null ) {
69+ Errors . rethrow (). get ( repositoryResolver :: setup );
6070 }
61- return builder ;
71+ return repositoryResolver ;
6272 }
6373
6474 /**
65- * If the dir is a worktree directory (typically .git/worktrees/something) then
66- * returns the actual .git directory.
75+ * Piggyback on the {@link FileRepositoryBuilder} mechanics for finding the git directory.
6776 *
68- * @param dir the directory which may be a worktree directory or may be a .git directory.
69- * @return the .git directory .
77+ * Here we take into account that git repositories can share a common directory. This directory
78+ * will contain ./config ./objects/, ./info/, and ./refs/ .
7079 */
71- private static File resolveRealGitDirIfWorktreeDir (File dir ) {
72- File pointerFile = new File (dir , "gitdir" );
73- if (pointerFile .isFile ()) {
74- try {
75- String content = new String (Files .readAllBytes (pointerFile .toPath ()), StandardCharsets .UTF_8 ).trim ();
76- return new File (content );
77- } catch (IOException e ) {
78- System .err .println ("failed to parse git meta: " + e .getMessage ());
79- return dir ;
80+ static class RepositorySpecificResolver extends FileRepositoryBuilder {
81+ /**
82+ * The common directory file is used to define $GIT_COMMON_DIR if environment variable is not set.
83+ * https://github.com/git/git/blob/b23dac905bde28da47543484320db16312c87551/Documentation/gitrepository-layout.txt#L259
84+ */
85+ private static final String COMMON_DIR = "commondir" ;
86+ private static final String GIT_COMMON_DIR_ENV_KEY = "GIT_COMMON_DIR" ;
87+
88+ /**
89+ * Using an extension it is possible to have per-worktree config.
90+ * https://github.com/git/git/blob/b23dac905bde28da47543484320db16312c87551/Documentation/git-worktree.txt#L366
91+ */
92+ private static final String EXTENSIONS_WORKTREE_CONFIG = "worktreeConfig" ;
93+ private static final String EXTENSIONS_WORKTREE_CONFIG_FILENAME = "config.worktree" ;
94+
95+ private File commonDirectory ;
96+
97+ /** @return the repository specific configuration. */
98+ Config getRepositoryConfig () {
99+ return Errors .rethrow ().get (this ::getConfig );
100+ }
101+
102+ /**
103+ * @return the repository's configuration.
104+ * @throws IOException on errors accessing the configuration file.
105+ * @throws IllegalArgumentException on malformed configuration.
106+ */
107+ @ Override
108+ protected Config loadConfig () throws IOException {
109+ if (getGitDir () != null ) {
110+ File path = resolveWithCommonDir (Constants .CONFIG );
111+ FileBasedConfig cfg = new FileBasedConfig (path , safeFS ());
112+ try {
113+ cfg .load ();
114+
115+ // Check for per-worktree config, it should be parsed after the common config
116+ if (cfg .getBoolean (ConfigConstants .CONFIG_EXTENSIONS_SECTION , EXTENSIONS_WORKTREE_CONFIG , false )) {
117+ File worktreeSpecificConfig = safeFS ().resolve (getGitDir (), EXTENSIONS_WORKTREE_CONFIG_FILENAME );
118+ if (safeFS ().exists (worktreeSpecificConfig ) && safeFS ().isFile (worktreeSpecificConfig )) {
119+ // It is important to base this on the common config, as both the common config and the per-worktree config should be used
120+ cfg = new FileBasedConfig (cfg , worktreeSpecificConfig , safeFS ());
121+ try {
122+ cfg .load ();
123+ } catch (ConfigInvalidException err ) {
124+ throw new IllegalArgumentException ("Failed to parse config " + worktreeSpecificConfig .getAbsolutePath (), err );
125+ }
126+ }
127+ }
128+ } catch (ConfigInvalidException err ) {
129+ throw new IllegalArgumentException ("Failed to parse config " + path .getAbsolutePath (), err );
130+ }
131+ return cfg ;
132+ }
133+ return super .loadConfig ();
134+ }
135+
136+ @ Override
137+ protected void setupGitDir () throws IOException {
138+ super .setupGitDir ();
139+
140+ // Setup common directory
141+ if (commonDirectory == null ) {
142+ File commonDirFile = safeFS ().resolve (getGitDir (), COMMON_DIR );
143+ if (safeFS ().exists (commonDirFile ) && safeFS ().isFile (commonDirFile )) {
144+ byte [] content = IO .readFully (commonDirFile );
145+ if (content .length < 1 ) {
146+ throw emptyFile (commonDirFile );
147+ }
148+
149+ int lineEnd = RawParseUtils .nextLF (content , 0 );
150+ while (content [lineEnd - 1 ] == '\n' || (content [lineEnd - 1 ] == '\r' && SystemReader .getInstance ().isWindows ())) {
151+ lineEnd --;
152+ }
153+ if (lineEnd <= 1 ) {
154+ throw emptyFile (commonDirFile );
155+ }
156+
157+ String commonPath = RawParseUtils .decode (content , 0 , lineEnd );
158+ File common = new File (commonPath );
159+ if (common .isAbsolute ()) {
160+ commonDirectory = common ;
161+ } else {
162+ commonDirectory = safeFS ().resolve (getGitDir (), commonPath ).getCanonicalFile ();
163+ }
164+ }
165+ }
166+
167+ // Setup object directory
168+ if (getObjectDirectory () == null ) {
169+ setObjectDirectory (resolveWithCommonDir (Constants .OBJECTS ));
170+ }
171+ }
172+
173+ private static IOException emptyFile (File commonDir ) {
174+ return new IOException ("Empty 'commondir' file: " + commonDir .getAbsolutePath ());
175+ }
176+
177+ @ SuppressFBWarnings (value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE" )
178+ @ Override
179+ public FileRepositoryBuilder readEnvironment (SystemReader sr ) {
180+ super .readEnvironment (sr );
181+
182+ // Always overwrite, will trump over the common dir file
183+ String val = sr .getenv (GIT_COMMON_DIR_ENV_KEY );
184+ if (val != null ) {
185+ commonDirectory = new File (val );
186+ }
187+
188+ return self ();
189+ }
190+
191+ /**
192+ * For repository with multiple linked worktrees some data might be shared in a "common" directory.
193+ *
194+ * @param target the file we want to resolve.
195+ * @return a file resolved from the {@link #getGitDir()}, or possibly in the path specified by $GIT_COMMON_DIR or {@code commondir} file.
196+ */
197+ File resolveWithCommonDir (String target ) {
198+ if (commonDirectory != null ) {
199+ return safeFS ().resolve (commonDirectory , target );
80200 }
81- } else {
82- return dir ;
201+ return safeFS ().resolve (getGitDir (), target );
83202 }
84203 }
85204}
0 commit comments