Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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].

Expand Down
10 changes: 9 additions & 1 deletion cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +263 to +265

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
void addRemote(Path repository, String name, String url);
/**
* Commits the staged changes in the given repository.
void addRemote(Path repository, String name, String url);
/**
* Commits the staged changes in the given repository.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a blank line after the opening brace('{'). Also the spaces in the comments are a bit off.

*
* @param repository the {@link Path} to the git repository.
* @param message the commit message.
Expand Down
12 changes: 10 additions & 2 deletions cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down Expand Up @@ -559,5 +569,3 @@ public void push(Path repository, boolean followTags) {
}
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -33,6 +34,7 @@ public record RepositoryConfig(
String buildCmd,
Set<String> imports,
List<RepositoryLink> links,
List<RepositoryRemote> remotes,
boolean active) {

/** Wildcard to match all workspaces. */
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (=<target>)";
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_.-]+");
Expand Down Expand Up @@ -302,6 +305,36 @@ public List<RepositoryLink> getLinks() {
return List.copyOf(links); // make immutable for record
}

public List<RepositoryRemote> getRemotes() {

String remotes = getProperty(PROPERTY_GIT_REMOTE);
if (isEmpty(remotes)) {
return List.of();
}
List<RepositoryRemote> 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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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() {

Expand All @@ -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<com.devonfw.tools.ide.git.repository.RepositoryRemote> 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<com.devonfw.tools.ide.git.repository.RepositoryRemote> 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<com.devonfw.tools.ide.git.repository.RepositoryRemote> 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<com.devonfw.tools.ide.git.repository.RepositoryRemote> 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<com.devonfw.tools.ide.git.repository.RepositoryRemote> 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<com.devonfw.tools.ide.git.repository.RepositoryRemote> 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);
}
}
Loading