Skip to content

Commit

Permalink
Add support for worktrees
Browse files Browse the repository at this point in the history
Git supports using the same local repository for multiple checked-out worktrees. JGit does not fully support this, so we have to do some workarounds for it to work.

The previous workaround provided by diffplug#965 did not take `commondir` into consideration, which is the location of a few files.

More details are available at: https://git-scm.com/docs/git-worktree#_details
  • Loading branch information
klaren committed Feb 4, 2022
1 parent 3fe3fd1 commit 8529187
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package com.diffplug.spotless.extra;

import static com.diffplug.spotless.extra.LibExtraPreconditions.requireElementsNonNull;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
Expand Down Expand Up @@ -52,7 +54,6 @@
import com.diffplug.spotless.LineEnding;
import com.diffplug.spotless.extra.GitWorkarounds.RepositorySpecificResolver;

import static com.diffplug.spotless.extra.LibExtraPreconditions.requireElementsNonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
Expand Down Expand Up @@ -82,6 +83,13 @@ static class RepositorySpecificResolver extends FileRepositoryBuilder {
private static final String COMMON_DIR = "commondir";
private static final String GIT_COMMON_DIR_ENV_KEY = "GIT_COMMON_DIR";

/**
* Using an extension it is possible to have per-worktree config.
* https://github.com/git/git/blob/b23dac905bde28da47543484320db16312c87551/Documentation/git-worktree.txt#L366
*/
private static final String EXTENSIONS_WORKTREE_CONFIG = "worktreeConfig";
private static final String EXTENSIONS_WORKTREE_CONFIG_FILENAME = "config.worktree";

private File commonDirectory;

/** @return the repository specific configuration. */
Expand All @@ -101,6 +109,20 @@ protected Config loadConfig() throws IOException {
FileBasedConfig cfg = new FileBasedConfig(path, safeFS());
try {
cfg.load();

// Check for per-worktree config, it should be parsed after the common config
if (cfg.getBoolean(ConfigConstants.CONFIG_EXTENSIONS_SECTION, EXTENSIONS_WORKTREE_CONFIG, false)) {
File worktreeSpecificConfig = safeFS().resolve(getGitDir(), EXTENSIONS_WORKTREE_CONFIG_FILENAME);
if (safeFS().exists(worktreeSpecificConfig) && safeFS().isFile(worktreeSpecificConfig)) {
// It is important to base this on the common config, as both the common config and the per-worktree config should be used
cfg = new FileBasedConfig(cfg, worktreeSpecificConfig, safeFS());
try {
cfg.load();
} catch (ConfigInvalidException err) {
throw new IllegalArgumentException("Failed to parse config " + worktreeSpecificConfig.getAbsolutePath(), err);
}
}
}
} catch (ConfigInvalidException err) {
throw new IllegalArgumentException("Failed to parse config " + path.getAbsolutePath(), err);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Constants;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import com.diffplug.spotless.ResourceHarness;
Expand All @@ -47,38 +50,75 @@ void external() throws IOException, GitAPIException {
Assertions.assertThat(repositorySpecificResolver.getGitDir()).isEqualTo(gitDir);
}

@Test
void worktrees() throws IOException, GitAPIException {
File w1ProjectTree = newFolder("project-w1");
File w2ProjectDir = newFolder("project-w2");
File commonGitDir = newFolder("project.git");
Git.init().setDirectory(w1ProjectTree).setGitDir(commonGitDir).call();

// Setup worktrees manually since JGit does not support it
newFolder("project.git/worktrees/");

File w1GitDir = newFolder("project.git/worktrees/project-w1/");
setFile("project.git/worktrees/project-w1/gitdir").toContent(w1ProjectTree.getAbsolutePath() + "/.git");
setFile("project.git/worktrees/project-w1/commondir").toContent("../.."); // Test relative path
setFile("project-w1/.git").toContent("gitdir: " + w1GitDir.getAbsolutePath());

File w2GitDir = newFolder("project.git/worktrees/project-w2/");
setFile("project.git/worktrees/project-w2/gitdir").toContent(w2ProjectDir.getAbsolutePath() + "/.git");
setFile("project.git/worktrees/project-w2/commondir").toContent(commonGitDir.getAbsolutePath()); // Test absolute path
setFile("project-w2/.git").toContent("gitdir: " + w2GitDir.getAbsolutePath());

// Test worktree 1
{
RepositorySpecificResolver repositorySpecificResolver = GitWorkarounds.fileRepositoryResolverForProject(w1ProjectTree);
Assertions.assertThat(repositorySpecificResolver.getGitDir()).isEqualTo(w1GitDir);
Assertions.assertThat(repositorySpecificResolver.resolveWithCommonDir(Constants.CONFIG)).isEqualTo(new File(commonGitDir, Constants.CONFIG));
@Nested
@DisplayName("Worktrees")
class Worktrees {
private File project1Tree;
private File project1GitDir;
private File project2Tree;
private File project2GitDir;
private File commonGitDir;

@BeforeEach
void setUp() throws IOException, GitAPIException {
project1Tree = newFolder("project-w1");
project2Tree = newFolder("project-w2");
commonGitDir = newFolder("project.git");
Git.init().setDirectory(project1Tree).setGitDir(commonGitDir).call();

// Setup worktrees manually since JGit does not support it
newFolder("project.git/worktrees/");

project1GitDir = newFolder("project.git/worktrees/project-w1/");
setFile("project.git/worktrees/project-w1/gitdir").toContent(project1Tree.getAbsolutePath() + "/.git");
setFile("project.git/worktrees/project-w1/commondir").toContent("../.."); // Relative path
setFile("project-w1/.git").toContent("gitdir: " + project1GitDir.getAbsolutePath());

project2GitDir = newFolder("project.git/worktrees/project-w2/");
setFile("project.git/worktrees/project-w2/gitdir").toContent(project2Tree.getAbsolutePath() + "/.git");
setFile("project.git/worktrees/project-w2/commondir").toContent(commonGitDir.getAbsolutePath()); // Absolute path
setFile("project-w2/.git").toContent("gitdir: " + project2GitDir.getAbsolutePath());
}

@Test
void resolveGitDir() {
// Test worktree 1
{
RepositorySpecificResolver repositorySpecificResolver = GitWorkarounds.fileRepositoryResolverForProject(project1Tree);
Assertions.assertThat(repositorySpecificResolver.getGitDir()).isEqualTo(project1GitDir);
Assertions.assertThat(repositorySpecificResolver.resolveWithCommonDir(Constants.CONFIG)).isEqualTo(new File(commonGitDir, Constants.CONFIG));
}

// Test worktree 2
{
RepositorySpecificResolver repositorySpecificResolver = GitWorkarounds.fileRepositoryResolverForProject(project2Tree);
Assertions.assertThat(repositorySpecificResolver.getGitDir()).isEqualTo(project2GitDir);
Assertions.assertThat(repositorySpecificResolver.resolveWithCommonDir(Constants.CONFIG)).isEqualTo(new File(commonGitDir, Constants.CONFIG));
}
}

@Test
void perWorktreeConfig() throws IOException {
setFile("project.git/config").toLines("[core]", "mySetting = true");

Assertions.assertThat(getMySetting(project1Tree)).isTrue();
Assertions.assertThat(getMySetting(project2Tree)).isTrue();

// Override setting for project 1, but don't enable extension yet
setFile("project.git/worktrees/project-w1/config.worktree").toLines("[core]", "mySetting = false");

Assertions.assertThat(getMySetting(project1Tree)).isTrue();
Assertions.assertThat(getMySetting(project2Tree)).isTrue();

// Enable extension
setFile("project.git/config").toLines("[core]", "mySetting = true", "[extensions]", "worktreeConfig = true");

Assertions.assertThat(getMySetting(project1Tree)).isFalse(); // Should now be overridden by config.worktree
Assertions.assertThat(getMySetting(project2Tree)).isTrue();
}

// Test worktree 2
{
RepositorySpecificResolver repositorySpecificResolver = GitWorkarounds.fileRepositoryResolverForProject(w2ProjectDir);
Assertions.assertThat(repositorySpecificResolver.getGitDir()).isEqualTo(w2GitDir);
Assertions.assertThat(repositorySpecificResolver.resolveWithCommonDir(Constants.CONFIG)).isEqualTo(new File(commonGitDir, Constants.CONFIG));
private boolean getMySetting(File projectDir) {
return GitWorkarounds.fileRepositoryResolverForProject(projectDir).getRepositoryConfig().getBoolean("core", "mySetting", false);
}
}
}

0 comments on commit 8529187

Please sign in to comment.