diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 73dd322f3a..86655a053b 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -7,6 +7,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/2100[#2100]: Fix Python not available for Mac x64 +* https://github.com/devonfw/IDEasy/issues/2168[#2168]: Add support for git_remote property to configure additional remotes after cloning The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/48?closed=1[milestone 2026.08.001]. diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java index d01751b662..b0205a57d8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java @@ -254,7 +254,15 @@ default void reset(Path repository, String branch) { void saveCurrentCommitId(Path repository, Path trackedCommitIdPath); /** - * Commits the staged changes in the given repository. + * Adds a new git remote to the given repository using {@code git remote add}. + * + * @param repository the {@link Path} to the git repository. + * @param name the name of the remote to add (e.g. "upstream"). + * @param url the URL of the remote to add. + */ + void addRemote(Path repository, String name, String url); + /** + * Commits the staged changes in the given repository. * * @param repository the {@link Path} to the git repository. * @param message the commit message. diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java index 03eeb52a19..e815cdd4f4 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java @@ -525,6 +525,16 @@ public void saveCurrentCommitId(Path repository, Path trackedCommitIdPath) { } } + @Override + public void addRemote(Path repository, String name, String url) { + + LOG.debug("Adding remote '{}' with url '{}' to {}", name, url, repository); + ProcessResult result = runGitCommand(repository, ProcessMode.DEFAULT, "remote", "add", name, url); + if (!result.isSuccessful()) { + LOG.warn("Failed to add remote '{}' to {}", name, repository); + } + } + /** * @param repository the {@link Path} to the git repository. * @return the current commit ID of the given {@link Path repository}. @@ -559,5 +569,3 @@ public void push(Path repository, boolean followTags) { } } } - - diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryCommandlet.java index 1b8dd0b611..7bb032b0b3 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryCommandlet.java @@ -186,6 +186,9 @@ private void doImportRepository(RepositoryConfig config) { } Path linkRepositoryPath = config.isVirtualSettingsRepository() ? firstRepository : repositoryPath; if (Files.exists(linkRepositoryPath)) { + for (RepositoryRemote remote : config.remotes()) { + this.context.getGitContext().addRemote(linkRepositoryPath, remote.name(), remote.url()); + } for (RepositoryLink link : config.links()) { createRepositoryLink(link, linkRepositoryPath, workspacePath); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryConfig.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryConfig.java index 77fa86ebe4..6b32747645 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryConfig.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryConfig.java @@ -20,6 +20,7 @@ * @param buildPath The build path for the repository. * @param buildCmd The command to invoke to build the repository after clone or pull. If omitted no build is triggered. * @param imports list of IDEs where the repository will be imported to. + * @param remotes list of additional git remotes to add to the repository after cloning. * @param active {@code true} to setup the repository during setup, {@code false} to skip. */ public record RepositoryConfig( @@ -33,6 +34,7 @@ public record RepositoryConfig( String buildCmd, Set imports, List links, + List remotes, boolean active) { /** Wildcard to match all workspaces. */ @@ -65,7 +67,8 @@ public static RepositoryConfig loadProperties(Path filePath, IdeContext context) RepositoryProperties properties = new RepositoryProperties(filePath, context); String id = properties.getId(); RepositoryConfig config = new RepositoryConfig(id, properties.getPath(), properties.getWorkingSets(), properties.getWorkspaces(), properties.getGitUrl(), - properties.getGitBranch(), properties.getBuildPath(), properties.getBuildCmd(), properties.getImports(), properties.getLinks(), properties.isActive()); + properties.getGitBranch(), properties.getBuildPath(), properties.getBuildCmd(), properties.getImports(), properties.getLinks(), properties.getRemotes(), + properties.isActive()); if (properties.isInvalid()) { return null; } diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryProperties.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryProperties.java index 55020bca2e..187d0f29bc 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryProperties.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryProperties.java @@ -34,8 +34,11 @@ final class RepositoryProperties { private static final String PROPERTY_IMPORT = "import"; private static final String PROPERTY_LINK = "link"; private static final String PROPERTY_LINK_TARGET = "link (=)"; + private static final String PROPERTY_GIT_REMOTE = "git_remote"; private static final String PROPERTY_ECLIPSE = "eclipse"; + private static final Pattern REMOTE_NAME_PATTERN = Pattern.compile("[a-zA-Z]+"); + private static final Pattern PATH_PATTERN = Pattern.compile("[a-zA-Z0-9_.$/-]+"); private static final Pattern WORKSPACE_PATTERN = Pattern.compile("[a-zA-Z][a-zA-Z0-9_.-]+"); @@ -302,6 +305,36 @@ public List getLinks() { return List.copyOf(links); // make immutable for record } + public List getRemotes() { + + String remotes = getProperty(PROPERTY_GIT_REMOTE); + if (isEmpty(remotes)) { + return List.of(); + } + List remoteList = new ArrayList<>(); + for (String remoteItem : remotes.split(",")) { + String trimmed = remoteItem.trim(); + int colonIndex = trimmed.indexOf(':'); + if (colonIndex <= 0) { + LOG.warn("Ignoring invalid git_remote entry {} from {}", trimmed, PROPERTY_GIT_REMOTE); + continue; + } + String name = trimmed.substring(0, colonIndex).trim(); + String url = trimmed.substring(colonIndex + 1).trim(); + if (name.isBlank() || url.isBlank()) { + LOG.warn("Ignoring git_remote entry {} with empty name or url from {}", trimmed, PROPERTY_GIT_REMOTE); + continue; + } + if (!REMOTE_NAME_PATTERN.matcher(name).matches()) { + LOG.warn("Ignoring git_remote entry {} with invalid remote name \"{}\" — name must consist of latin letters only from {}", + trimmed, name, PROPERTY_GIT_REMOTE); + continue; + } + remoteList.add(new RepositoryRemote(name, url)); + } + return List.copyOf(remoteList); + } + private String sanatizeRelativePath(String path, String propertyName) { if (path == null) { return null; diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryRemote.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryRemote.java new file mode 100644 index 0000000000..78b7c22923 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryRemote.java @@ -0,0 +1,11 @@ +package com.devonfw.tools.ide.git.repository; + +/** + * Configuration for an additional git remote to add to a cloned repository. + * + * @param name the name of the remote (e.g. "upstream"). + * @param url the URL of the remote repository. + */ +public record RepositoryRemote(String name, String url) { + +} diff --git a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java index 33f53f0f64..b804ef52d9 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java @@ -119,12 +119,16 @@ public String determineRemote(Path repository) { return "origin"; } - @Override public void saveCurrentCommitId(Path repository, Path trackedCommitIdPath) { } + @Override + public void addRemote(Path repository, String name, String url) { + + } + @Override public void commit(Path repository, String message, boolean addAll) { diff --git a/cli/src/test/java/com/devonfw/tools/ide/git/repository/RepositoryPropertiesTest.java b/cli/src/test/java/com/devonfw/tools/ide/git/repository/RepositoryPropertiesTest.java index 74296c7754..06e512f139 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/git/repository/RepositoryPropertiesTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/git/repository/RepositoryPropertiesTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import java.nio.file.Path; +import java.util.List; import java.util.Properties; import org.junit.jupiter.api.Test; @@ -12,6 +13,20 @@ */ class RepositoryPropertiesTest { + // Test remote names + private static final String REMOTE_NAME_UPSTREAM = "upstream"; + private static final String REMOTE_NAME_FORK = "fork"; + private static final String REMOTE_NAME_GOOD = "good"; + private static final String REMOTE_NAME_VALID = "valid"; + + // Test remote URLs + private static final String URL_DEVONFW_SETTINGS = "https://github.com/devonfw/ide-settings.git"; + private static final String URL_DEVONFW_UPSTREAM = "https://github.com/devonfw/upstream.git"; + private static final String URL_DEVONFW_REPO = "https://github.com/devonfw/repo.git"; + private static final String URL_USER_FORK = "https://github.com/user/fork.git"; + private static final String URL_EXAMPLE_REPO = "https://example.com/repo.git"; + private static final String URL_SSH_DEVONFW = "git@github.com:devonfw/ide-settings.git"; + @Test void testGetId() { @@ -36,4 +51,97 @@ private static RepositoryProperties properties(String filename) { return new RepositoryProperties(Path.of(filename), new Properties()); } + + private static RepositoryProperties properties(String filename, Properties properties) { + + return new RepositoryProperties(Path.of(filename), properties); + } + + @Test + void testGetRemotesEmpty() { + + RepositoryProperties props = properties("test.properties"); + assertThat(props.getRemotes()).isEmpty(); + } + + @Test + void testGetRemotesSingle() { + + Properties props = new Properties(); + props.setProperty("git_remote", REMOTE_NAME_UPSTREAM + ":" + URL_DEVONFW_SETTINGS); + RepositoryProperties repositoryProperties = properties("test.properties", props); + + List remotes = repositoryProperties.getRemotes(); + assertThat(remotes).hasSize(1); + assertThat(remotes.get(0).name()).isEqualTo(REMOTE_NAME_UPSTREAM); + assertThat(remotes.get(0).url()).isEqualTo(URL_DEVONFW_SETTINGS); + } + + @Test + void testGetRemotesMultiple() { + + Properties props = new Properties(); + props.setProperty("git_remote", REMOTE_NAME_UPSTREAM + ":" + URL_DEVONFW_UPSTREAM + "," + REMOTE_NAME_FORK + ":" + URL_USER_FORK); + RepositoryProperties repositoryProperties = properties("test.properties", props); + + List remotes = repositoryProperties.getRemotes(); + assertThat(remotes).hasSize(2); + assertThat(remotes.get(0).name()).isEqualTo(REMOTE_NAME_UPSTREAM); + assertThat(remotes.get(0).url()).isEqualTo(URL_DEVONFW_UPSTREAM); + assertThat(remotes.get(1).name()).isEqualTo(REMOTE_NAME_FORK); + assertThat(remotes.get(1).url()).isEqualTo(URL_USER_FORK); + } + + @Test + void testGetRemotesInvalidEntriesSkipped() { + + Properties props = new Properties(); + props.setProperty("git_remote", "badentry,noColon," + REMOTE_NAME_GOOD + ":" + URL_EXAMPLE_REPO); + RepositoryProperties repositoryProperties = properties("test.properties", props); + + List remotes = repositoryProperties.getRemotes(); + assertThat(remotes).hasSize(1); + assertThat(remotes.get(0).name()).isEqualTo(REMOTE_NAME_GOOD); + assertThat(remotes.get(0).url()).isEqualTo(URL_EXAMPLE_REPO); + } + + @Test + void testGetRemotesWithWhitespace() { + + Properties props = new Properties(); + props.setProperty("git_remote", " " + REMOTE_NAME_UPSTREAM + " : " + URL_DEVONFW_REPO + " "); + RepositoryProperties repositoryProperties = properties("test.properties", props); + + List remotes = repositoryProperties.getRemotes(); + assertThat(remotes).hasSize(1); + assertThat(remotes.get(0).name()).isEqualTo(REMOTE_NAME_UPSTREAM); + assertThat(remotes.get(0).url()).isEqualTo(URL_DEVONFW_REPO); + } + + @Test + void testGetRemotesInvalidNameRejected() { + + Properties props = new Properties(); + props.setProperty("git_remote", + "my-remote:https://a.git,remote2:https://b.git,remote_one:https://c.git," + REMOTE_NAME_VALID + ":https://d.git"); + RepositoryProperties repositoryProperties = properties("test.properties", props); + + List remotes = repositoryProperties.getRemotes(); + assertThat(remotes).hasSize(1); + assertThat(remotes.get(0).name()).isEqualTo(REMOTE_NAME_VALID); + assertThat(remotes.get(0).url()).isEqualTo("https://d.git"); + } + + @Test + void testGetRemotesSshUrl() { + + Properties props = new Properties(); + props.setProperty("git_remote", REMOTE_NAME_UPSTREAM + ":" + URL_SSH_DEVONFW); + RepositoryProperties repositoryProperties = properties("test.properties", props); + + List remotes = repositoryProperties.getRemotes(); + assertThat(remotes).hasSize(1); + assertThat(remotes.get(0).name()).isEqualTo(REMOTE_NAME_UPSTREAM); + assertThat(remotes.get(0).url()).isEqualTo(URL_SSH_DEVONFW); + } }