diff options
author | Simon Eder <simon.eclipse@hotmail.com> | 2024-10-09 17:16:57 +0200 |
---|---|---|
committer | Matthias Sohn <matthias.sohn@sap.com> | 2025-01-29 00:57:38 +0100 |
commit | 32b9a9523c2a8100f9ac22b251a10c3afb7d1144 (patch) | |
tree | 1f6080f0ef700d325353f993742f1d3f6ba3896e | |
parent | 517a7b210fba00661bf42e3d4bc473cc1428c93b (diff) | |
download | jgit-32b9a9523c2a8100f9ac22b251a10c3afb7d1144.tar.gz jgit-32b9a9523c2a8100f9ac22b251a10c3afb7d1144.zip |
Submodules: Update submodule with deleted worktree
The current implementation throws an exception when a submodule is
updated for which the 'gitdir' still exists but the 'worktree' is
missing. The current implementation tries to clone the submodule but
fails as the 'gitdir' is not empty.
The commit adds:
- a check if the 'gitdir' is empty
- if the 'gitdir' is not empty:
- create a new '.git' file linking to the existing 'gitdir'
- fetch the submodule
- checkout the submodule unconditionally (ignore any configured
update mode)
- if the submodule is cloned checkout the submodule unconditionally
(ignore any configured update mode)
This change mimics the behavior of cgit.
Bug: jgit-79
Change-Id: Iffc8659d2a04d866a43815c78c7760c0f3d82640
3 files changed, 253 insertions, 55 deletions
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleUpdateTest.java index c5e9c2deaa..d54117005d 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleUpdateTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleUpdateTest.java @@ -17,21 +17,25 @@ import java.io.File; import java.io.IOException; import java.util.Collection; +import org.eclipse.jgit.api.CheckoutCommand; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.InitCommand; +import org.eclipse.jgit.api.SubmoduleAddCommand; import org.eclipse.jgit.api.SubmoduleUpdateCommand; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheEditor; import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit; import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; -import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.junit.Test; @@ -40,6 +44,91 @@ import org.junit.Test; */ public class SubmoduleUpdateTest extends RepositoryTestCase { + private Repository submoduleRepo; + + private Git git; + + private AnyObjectId subRepoCommit2; + + private void createSubmoduleRepo() throws IOException, GitAPIException { + File directory = createTempDirectory("submodule_repo"); + InitCommand init = Git.init(); + init.setDirectory(directory); + init.call(); + submoduleRepo = Git.open(directory).getRepository(); + try (Git sub = Git.wrap(submoduleRepo)) { + // commit something + JGitTestUtil.writeTrashFile(submoduleRepo, "commit1.txt", + "commit 1"); + sub.add().addFilepattern("commit1.txt").call(); + sub.commit().setMessage("commit 1").call().getId(); + + JGitTestUtil.writeTrashFile(submoduleRepo, "commit2.txt", + "commit 2"); + sub.add().addFilepattern("commit2.txt").call(); + subRepoCommit2 = sub.commit().setMessage("commit 2").call().getId(); + } + } + + private void addSubmodule(String path) throws GitAPIException { + SubmoduleAddCommand command = new SubmoduleAddCommand(db); + command.setPath(path); + String uri = submoduleRepo.getDirectory().toURI().toString(); + command.setURI(uri); + try (Repository repo = command.call()) { + assertNotNull(repo); + } + git.add().addFilepattern(path).addFilepattern(Constants.DOT_GIT_MODULES) + .call(); + git.commit().setMessage("adding submodule").call(); + recursiveDelete(new File(git.getRepository().getWorkTree(), path)); + recursiveDelete( + new File(new File(git.getRepository().getCommonDirectory(), + Constants.MODULES), path)); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + createSubmoduleRepo(); + + git = Git.wrap(db); + // commit something + writeTrashFile("initial.txt", "initial"); + git.add().addFilepattern("initial.txt").call(); + git.commit().setMessage("initial commit").call(); + } + + public void updateModeClonedRestoredSubmoduleTemplate(String mode) + throws Exception { + String path = "sub"; + addSubmodule(path); + + StoredConfig cfg = git.getRepository().getConfig(); + if (mode != null) { + cfg.load(); + cfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, + ConfigConstants.CONFIG_KEY_UPDATE, mode); + cfg.save(); + } + SubmoduleUpdateCommand update = new SubmoduleUpdateCommand(db); + update.call(); + try (Git subGit = Git.open(new File(db.getWorkTree(), path))) { + update.call(); + assertEquals(subRepoCommit2.getName(), + subGit.getRepository().getBranch()); + } + + recursiveDelete(new File(db.getWorkTree(), path)); + + update.call(); + try (Git subGit = Git.open(new File(db.getWorkTree(), path))) { + update.call(); + assertEquals(subRepoCommit2.getName(), + subGit.getRepository().getBranch()); + } + } + @Test public void repositoryWithNoSubmodules() throws GitAPIException { SubmoduleUpdateCommand command = new SubmoduleUpdateCommand(db); @@ -50,35 +139,9 @@ public class SubmoduleUpdateTest extends RepositoryTestCase { @Test public void repositoryWithSubmodule() throws Exception { - writeTrashFile("file.txt", "content"); - Git git = Git.wrap(db); - git.add().addFilepattern("file.txt").call(); - final RevCommit commit = git.commit().setMessage("create file").call(); final String path = "sub"; - DirCache cache = db.lockDirCache(); - DirCacheEditor editor = cache.editor(); - editor.add(new PathEdit(path) { - - @Override - public void apply(DirCacheEntry ent) { - ent.setFileMode(FileMode.GITLINK); - ent.setObjectId(commit); - } - }); - editor.commit(); - - StoredConfig config = db.getConfig(); - config.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, - ConfigConstants.CONFIG_KEY_URL, db.getDirectory().toURI() - .toString()); - config.save(); - - FileBasedConfig modulesConfig = new FileBasedConfig(new File( - db.getWorkTree(), Constants.DOT_GIT_MODULES), db.getFS()); - modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, - ConfigConstants.CONFIG_KEY_PATH, path); - modulesConfig.save(); + addSubmodule(path); SubmoduleUpdateCommand command = new SubmoduleUpdateCommand(db); Collection<String> updated = command.call(); @@ -90,7 +153,7 @@ public class SubmoduleUpdateTest extends RepositoryTestCase { assertTrue(generator.next()); try (Repository subRepo = generator.getRepository()) { assertNotNull(subRepo); - assertEquals(commit, subRepo.resolve(Constants.HEAD)); + assertEquals(subRepoCommit2, subRepo.resolve(Constants.HEAD)); String worktreeDir = subRepo.getConfig().getString( ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_WORKTREE); @@ -104,8 +167,8 @@ public class SubmoduleUpdateTest extends RepositoryTestCase { } @Test - public void repositoryWithUnconfiguredSubmodule() throws IOException, - GitAPIException { + public void repositoryWithUnconfiguredSubmodule() + throws IOException, GitAPIException { final ObjectId id = ObjectId .fromString("abcd1234abcd1234abcd1234abcd1234abcd1234"); final String path = "sub"; @@ -121,16 +184,14 @@ public class SubmoduleUpdateTest extends RepositoryTestCase { }); editor.commit(); - FileBasedConfig modulesConfig = new FileBasedConfig(new File( - db.getWorkTree(), Constants.DOT_GIT_MODULES), db.getFS()); + FileBasedConfig modulesConfig = new FileBasedConfig( + new File(db.getWorkTree(), Constants.DOT_GIT_MODULES), + db.getFS()); modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, ConfigConstants.CONFIG_KEY_PATH, path); String url = "git://server/repo.git"; modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, ConfigConstants.CONFIG_KEY_URL, url); - String update = "rebase"; - modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, - ConfigConstants.CONFIG_KEY_UPDATE, update); modulesConfig.save(); SubmoduleUpdateCommand command = new SubmoduleUpdateCommand(db); @@ -140,8 +201,8 @@ public class SubmoduleUpdateTest extends RepositoryTestCase { } @Test - public void repositoryWithInitializedSubmodule() throws IOException, - GitAPIException { + public void repositoryWithInitializedSubmodule() + throws IOException, GitAPIException { final ObjectId id = ObjectId .fromString("abcd1234abcd1234abcd1234abcd1234abcd1234"); final String path = "sub"; @@ -168,4 +229,77 @@ public class SubmoduleUpdateTest extends RepositoryTestCase { assertNotNull(updated); assertTrue(updated.isEmpty()); } + + @Test + public void updateModeMergeClonedRestoredSubmodule() throws Exception { + updateModeClonedRestoredSubmoduleTemplate( + ConfigConstants.CONFIG_KEY_MERGE); + } + + @Test + public void updateModeRebaseClonedRestoredSubmodule() throws Exception { + updateModeClonedRestoredSubmoduleTemplate( + ConfigConstants.CONFIG_KEY_REBASE); + } + + @Test + public void updateModeCheckoutClonedRestoredSubmodule() throws Exception { + updateModeClonedRestoredSubmoduleTemplate( + ConfigConstants.CONFIG_KEY_CHECKOUT); + } + + @Test + public void updateModeMissingClonedRestoredSubmodule() throws Exception { + updateModeClonedRestoredSubmoduleTemplate(null); + } + + @Test + public void updateMode() throws Exception { + String path = "sub"; + addSubmodule(path); + + StoredConfig cfg = git.getRepository().getConfig(); + cfg.load(); + cfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, + ConfigConstants.CONFIG_KEY_UPDATE, + ConfigConstants.CONFIG_KEY_REBASE); + cfg.save(); + + SubmoduleUpdateCommand update = new SubmoduleUpdateCommand(db); + update.call(); + try (Git subGit = Git.open(new File(db.getWorkTree(), path))) { + CheckoutCommand checkout = subGit.checkout(); + checkout.setName("master"); + checkout.call(); + update.call(); + assertEquals("master", subGit.getRepository().getBranch()); + } + + cfg.load(); + cfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, + ConfigConstants.CONFIG_KEY_UPDATE, + ConfigConstants.CONFIG_KEY_CHECKOUT); + cfg.save(); + + update.call(); + try (Git subGit = Git.open(new File(db.getWorkTree(), path))) { + assertEquals(subRepoCommit2.getName(), + subGit.getRepository().getBranch()); + } + + cfg.load(); + cfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, + ConfigConstants.CONFIG_KEY_UPDATE, + ConfigConstants.CONFIG_KEY_MERGE); + cfg.save(); + + update.call(); + try (Git subGit = Git.open(new File(db.getWorkTree(), path))) { + CheckoutCommand checkout = subGit.checkout(); + checkout.setName("master"); + checkout.call(); + update.call(); + assertEquals("master", subGit.getRepository().getBranch()); + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java index 3524984347..5e4b2ee0b7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java @@ -28,6 +28,7 @@ import org.eclipse.jgit.api.errors.RefNotFoundException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.internal.storage.file.LockFile; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; @@ -39,6 +40,7 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.submodule.SubmoduleWalk; import org.eclipse.jgit.treewalk.filter.PathFilterGroup; +import org.eclipse.jgit.util.FileUtils; /** * A class used to execute a submodule update command. @@ -62,6 +64,8 @@ public class SubmoduleUpdateCommand extends private boolean fetch = false; + private boolean clonedRestored; + /** * <p> * Constructor for SubmoduleUpdateCommand. @@ -116,26 +120,77 @@ public class SubmoduleUpdateCommand extends return this; } + private static boolean submoduleExists(File gitDir) { + if (gitDir != null && gitDir.isDirectory()) { + File[] files = gitDir.listFiles(); + return files != null && files.length != 0; + } + return false; + } + + private static void restoreSubmodule(File gitDir, File workingTree) + throws IOException { + LockFile dotGitLock = new LockFile( + new File(workingTree, Constants.DOT_GIT)); + if (dotGitLock.lock()) { + String content = Constants.GITDIR + + getRelativePath(gitDir, workingTree); + dotGitLock.write(Constants.encode(content)); + dotGitLock.commit(); + } + } + + private static String getRelativePath(File gitDir, File workingTree) { + File relPath; + try { + relPath = workingTree.toPath().relativize(gitDir.toPath()) + .toFile(); + } catch (IllegalArgumentException e) { + relPath = gitDir; + } + return FileUtils.pathToString(relPath); + } + + private String determineUpdateMode(String mode) { + if (clonedRestored) { + return ConfigConstants.CONFIG_KEY_CHECKOUT; + } + return mode; + } + private Repository getOrCloneSubmodule(SubmoduleWalk generator, String url) throws IOException, GitAPIException { Repository repository = generator.getRepository(); + boolean restored = false; + boolean cloned = false; if (repository == null) { - if (callback != null) { - callback.cloningSubmodule(generator.getPath()); - } - CloneCommand clone = Git.cloneRepository(); - configure(clone); - clone.setURI(url); - clone.setDirectory(generator.getDirectory()); - clone.setGitDir(new File( + File gitDir = new File( new File(repo.getCommonDirectory(), Constants.MODULES), - generator.getPath())); - clone.setRelativePaths(true); - if (monitor != null) { - clone.setProgressMonitor(monitor); + generator.getPath()); + if (submoduleExists(gitDir)) { + restoreSubmodule(gitDir, generator.getDirectory()); + restored = true; + clonedRestored = true; + repository = generator.getRepository(); + } else { + if (callback != null) { + callback.cloningSubmodule(generator.getPath()); + } + CloneCommand clone = Git.cloneRepository(); + configure(clone); + clone.setURI(url); + clone.setDirectory(generator.getDirectory()); + clone.setGitDir(gitDir); + clone.setRelativePaths(true); + if (monitor != null) { + clone.setProgressMonitor(monitor); + } + repository = clone.call().getRepository(); + cloned = true; + clonedRestored = true; } - repository = clone.call().getRepository(); - } else if (this.fetch) { + } + if ((this.fetch || restored) && !cloned) { if (fetchCallback != null) { fetchCallback.fetchingSubmodule(generator.getPath()); } @@ -172,15 +227,17 @@ public class SubmoduleUpdateCommand extends continue; // Skip submodules not registered in parent repository's config String url = generator.getConfigUrl(); - if (url == null) + if (url == null) { continue; - + } + clonedRestored = false; try (Repository submoduleRepo = getOrCloneSubmodule(generator, url); RevWalk walk = new RevWalk(submoduleRepo)) { RevCommit commit = walk .parseCommit(generator.getObjectId()); - String update = generator.getConfigUpdate(); + String update = determineUpdateMode( + generator.getConfigUpdate()); if (ConfigConstants.CONFIG_KEY_MERGE.equals(update)) { MergeCommand merge = new MergeCommand(submoduleRepo); merge.include(commit); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java index a57f1b714a..5068a6cce0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java @@ -446,6 +446,13 @@ public final class ConfigConstants { /** The "rebase" key */ public static final String CONFIG_KEY_REBASE = "rebase"; + /** + * The "checkout" key + * + * @since 7.2 + */ + public static final String CONFIG_KEY_CHECKOUT = "checkout"; + /** The "url" key */ public static final String CONFIG_KEY_URL = "url"; |