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 ;
2634
2735/**
2836 * Utility methods for Git workarounds.
2937 */
30- public class GitWorkarounds {
38+ public final class GitWorkarounds {
3139 private GitWorkarounds () {}
3240
3341 /**
@@ -40,46 +48,154 @@ private GitWorkarounds() {}
4048 * @return the path to the .git directory.
4149 */
4250 static @ Nullable File getDotGitDir (File projectDir ) {
43- return fileRepositoryBuilderForProject (projectDir ).getGitDir ();
51+ return fileRepositoryResolverForProject (projectDir ).getGitDir ();
4452 }
4553
4654 /**
47- * Creates a {@link FileRepositoryBuilder } for the given project directory.
55+ * Creates a {@link RepositorySpecificResolver } for the given project directory.
4856 *
4957 * This applies a workaround for JGit not supporting worktrees properly.
5058 *
5159 * @param projectDir the project directory.
5260 * @return the builder.
5361 */
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 ) );
62+ static RepositorySpecificResolver fileRepositoryResolverForProject (File projectDir ) {
63+ RepositorySpecificResolver repositoryResolver = new RepositorySpecificResolver ();
64+ repositoryResolver .findGitDir (projectDir );
65+ repositoryResolver . readEnvironment ();
66+ if (repositoryResolver . getGitDir () != null || repositoryResolver . getWorkTree () != null ) {
67+ Errors . rethrow (). get ( repositoryResolver :: setup );
6068 }
61- return builder ;
69+ return repositoryResolver ;
6270 }
6371
6472 /**
65- * If the dir is a worktree directory (typically .git/worktrees/something) then
66- * returns the actual .git directory.
73+ * Piggyback on the {@link FileRepositoryBuilder} mechanics for finding the git directory.
6774 *
68- * @param dir the directory which may be a worktree directory or may be a .git directory.
69- * @return the .git directory .
75+ * Here we take into account that git repositories can share a common directory. This directory
76+ * will contain ./config ./objects/, ./info/, and ./refs/ .
7077 */
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 ;
78+ static class RepositorySpecificResolver extends FileRepositoryBuilder {
79+ /**
80+ * The common directory file is used to define $GIT_COMMON_DIR if environment variable is not set.
81+ * https://github.com/git/git/blob/b23dac905bde28da47543484320db16312c87551/Documentation/gitrepository-layout.txt#L259
82+ */
83+ private static final String COMMON_DIR = "commondir" ;
84+ private static final String GIT_COMMON_DIR_ENV_KEY = "GIT_COMMON_DIR" ;
85+
86+ /**
87+ * Using an extension it is possible to have per-worktree config.
88+ * https://github.com/git/git/blob/b23dac905bde28da47543484320db16312c87551/Documentation/git-worktree.txt#L366
89+ */
90+ private static final String EXTENSIONS_WORKTREE_CONFIG = "worktreeConfig" ;
91+ private static final String EXTENSIONS_WORKTREE_CONFIG_FILENAME = "config.worktree" ;
92+
93+ private File commonDirectory ;
94+
95+ /** @return the repository specific configuration. */
96+ Config getRepositoryConfig () {
97+ return Errors .rethrow ().get (this ::getConfig );
98+ }
99+
100+ /**
101+ * @return the repository's configuration.
102+ * @throws IOException on errors accessing the configuration file.
103+ * @throws IllegalArgumentException on malformed configuration.
104+ */
105+ @ Override
106+ protected Config loadConfig () throws IOException {
107+ if (getGitDir () != null ) {
108+ File path = resolveWithCommonDir (Constants .CONFIG );
109+ FileBasedConfig cfg = new FileBasedConfig (path , safeFS ());
110+ try {
111+ cfg .load ();
112+
113+ // Check for per-worktree config, it should be parsed after the common config
114+ if (cfg .getBoolean (ConfigConstants .CONFIG_EXTENSIONS_SECTION , EXTENSIONS_WORKTREE_CONFIG , false )) {
115+ File worktreeSpecificConfig = safeFS ().resolve (getGitDir (), EXTENSIONS_WORKTREE_CONFIG_FILENAME );
116+ if (safeFS ().exists (worktreeSpecificConfig ) && safeFS ().isFile (worktreeSpecificConfig )) {
117+ // It is important to base this on the common config, as both the common config and the per-worktree config should be used
118+ cfg = new FileBasedConfig (cfg , worktreeSpecificConfig , safeFS ());
119+ try {
120+ cfg .load ();
121+ } catch (ConfigInvalidException err ) {
122+ throw new IllegalArgumentException ("Failed to parse config " + worktreeSpecificConfig .getAbsolutePath (), err );
123+ }
124+ }
125+ }
126+ } catch (ConfigInvalidException err ) {
127+ throw new IllegalArgumentException ("Failed to parse config " + path .getAbsolutePath (), err );
128+ }
129+ return cfg ;
130+ }
131+ return super .loadConfig ();
132+ }
133+
134+ @ Override
135+ protected void setupGitDir () throws IOException {
136+ super .setupGitDir ();
137+
138+ // Setup common directory
139+ if (commonDirectory == null ) {
140+ File commonDirFile = safeFS ().resolve (getGitDir (), COMMON_DIR );
141+ if (safeFS ().exists (commonDirFile ) && safeFS ().isFile (commonDirFile )) {
142+ byte [] content = IO .readFully (commonDirFile );
143+ if (content .length < 1 ) {
144+ throw emptyFile (commonDirFile );
145+ }
146+
147+ int lineEnd = RawParseUtils .nextLF (content , 0 );
148+ while (content [lineEnd - 1 ] == '\n' || (content [lineEnd - 1 ] == '\r' && SystemReader .getInstance ().isWindows ())) {
149+ lineEnd --;
150+ }
151+ if (lineEnd <= 1 ) {
152+ throw emptyFile (commonDirFile );
153+ }
154+
155+ String commonPath = RawParseUtils .decode (content , 0 , lineEnd );
156+ File common = new File (commonPath );
157+ if (common .isAbsolute ()) {
158+ commonDirectory = common ;
159+ } else {
160+ commonDirectory = safeFS ().resolve (getGitDir (), commonPath ).getCanonicalFile ();
161+ }
162+ }
163+ }
164+
165+ // Setup object directory
166+ if (getObjectDirectory () == null ) {
167+ setObjectDirectory (resolveWithCommonDir (Constants .OBJECTS ));
168+ }
169+ }
170+
171+ private static IOException emptyFile (File commonDir ) {
172+ return new IOException ("Empty 'commondir' file: " + commonDir .getAbsolutePath ());
173+ }
174+
175+ @ Override
176+ public FileRepositoryBuilder readEnvironment (SystemReader sr ) {
177+ super .readEnvironment (sr );
178+
179+ // Always overwrite, will trump over the common dir file
180+ String val = sr .getenv (GIT_COMMON_DIR_ENV_KEY );
181+ if (val != null ) {
182+ commonDirectory = new File (val );
183+ }
184+
185+ return self ();
186+ }
187+
188+ /**
189+ * For repository with multiple linked worktrees some data might be shared in a "common" directory.
190+ *
191+ * @param target the file we want to resolve.
192+ * @return a file resolved from the {@link #getGitDir()}, or possibly in the path specified by $GIT_COMMON_DIR or {@code commondir} file.
193+ */
194+ File resolveWithCommonDir (String target ) {
195+ if (commonDirectory != null ) {
196+ return safeFS ().resolve (commonDirectory , target );
80197 }
81- } else {
82- return dir ;
198+ return safeFS ().resolve (getGitDir (), target );
83199 }
84200 }
85201}
0 commit comments