1
1
/*
2
- * Copyright 2020-2021 DiffPlug
2
+ * Copyright 2020-2022 DiffPlug
3
3
*
4
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
5
* you may not use this file except in compliance with the License.
17
17
18
18
import java .io .File ;
19
19
import java .io .IOException ;
20
- import java .nio .charset .StandardCharsets ;
21
- import java .nio .file .Files ;
22
20
23
21
import javax .annotation .Nullable ;
24
22
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 ;
25
28
import 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 ;
26
36
27
37
/**
28
38
* Utility methods for Git workarounds.
29
39
*/
30
- public class GitWorkarounds {
40
+ public final class GitWorkarounds {
31
41
private GitWorkarounds () {}
32
42
33
43
/**
@@ -40,46 +50,155 @@ private GitWorkarounds() {}
40
50
* @return the path to the .git directory.
41
51
*/
42
52
static @ Nullable File getDotGitDir (File projectDir ) {
43
- return fileRepositoryBuilderForProject (projectDir ).getGitDir ();
53
+ return fileRepositoryResolverForProject (projectDir ).getGitDir ();
44
54
}
45
55
46
56
/**
47
- * Creates a {@link FileRepositoryBuilder } for the given project directory.
57
+ * Creates a {@link RepositorySpecificResolver } for the given project directory.
48
58
*
49
59
* This applies a workaround for JGit not supporting worktrees properly.
50
60
*
51
61
* @param projectDir the project directory.
52
62
* @return the builder.
53
63
*/
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 );
60
70
}
61
- return builder ;
71
+ return repositoryResolver ;
62
72
}
63
73
64
74
/**
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.
67
76
*
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/ .
70
79
*/
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 );
80
200
}
81
- } else {
82
- return dir ;
201
+ return safeFS ().resolve (getGitDir (), target );
83
202
}
84
203
}
85
204
}
0 commit comments