• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

devonfw / IDEasy / 32487370728

21 Aug 2026 01:32PM UTC coverage: 73.596% (-0.04%) from 73.638%
32487370728

push

github

web-flow
#2168: repository remote feature (#2198)

Co-authored-by: Quang Anh Le <quang-anh.le@capgemini.com>
Co-authored-by: Jörg Hohwiller <hohwille@users.noreply.github.com>
Co-authored-by: Malte Brunnlieb <maybeec@users.noreply.github.com>

5238 of 7935 branches covered (66.01%)

Branch coverage included in aggregate %.

13769 of 17891 relevant lines covered (76.96%)

3.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.83
cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryCommandlet.java
1
package com.devonfw.tools.ide.git.repository;
2

3
import java.nio.file.Files;
4
import java.nio.file.Path;
5
import java.util.ArrayList;
6
import java.util.HashMap;
7
import java.util.List;
8
import java.util.Map;
9
import java.util.Set;
10

11
import org.slf4j.Logger;
12
import org.slf4j.LoggerFactory;
13

14
import com.devonfw.tools.ide.commandlet.Commandlet;
15
import com.devonfw.tools.ide.context.IdeContext;
16
import com.devonfw.tools.ide.git.GitContext;
17
import com.devonfw.tools.ide.git.GitUrl;
18
import com.devonfw.tools.ide.io.FileAccess;
19
import com.devonfw.tools.ide.property.RepositoryProperty;
20
import com.devonfw.tools.ide.step.Step;
21
import com.devonfw.tools.ide.tool.ToolCommandlet;
22
import com.devonfw.tools.ide.tool.ide.IdeToolCommandlet;
23

24
/**
25
 * {@link Commandlet} to setup one or multiple GIT repositories for development.
26
 */
27
public class RepositoryCommandlet extends Commandlet {
28

29
  private static final Logger LOG = LoggerFactory.getLogger(RepositoryCommandlet.class);
4✔
30
  private static final String REPOSITORY = "repository";
31

32
  /** the repository to setup. */
33
  public final RepositoryProperty repository;
34

35
  /**
36
   * The constructor.
37
   *
38
   * @param context the {@link IdeContext}.
39
   */
40
  public RepositoryCommandlet(IdeContext context) {
41

42
    super(context);
3✔
43
    addKeyword(getName());
4✔
44
    addKeyword("setup");
3✔
45
    this.repository = add(new RepositoryProperty("", false, REPOSITORY));
11✔
46
  }
1✔
47

48
  @Override
49
  public String getName() {
50

51
    return REPOSITORY;
2✔
52
  }
53

54
  @Override
55
  protected void doRun() {
56

57
    Path repositoryFile = this.repository.getValue();
5✔
58

59
    if (repositoryFile != null) {
2✔
60
      // Handle the case when a specific repository is provided
61
      RepositoryConfig config = prepareActiveRepository(repositoryFile, true);
5✔
62
      if (config != null) {
2✔
63
        importRepository(config);
3✔
64
      }
65
    } else {
1✔
66
      // If no specific repository is provided, check for repositories folder
67
      Path repositoriesPath = this.context.getRepositoriesPath();
4✔
68
      if (repositoriesPath == null) {
2✔
69
        LOG.warn("Cannot find folder 'repositories' nor 'projects' in your settings.");
3✔
70
        return;
1✔
71
      }
72
      List<Path> propertiesFiles = this.context.getFileAccess()
5✔
73
          .listChildren(repositoriesPath, path -> path.getFileName().toString().endsWith(".properties"));
8✔
74
      boolean forceMode = this.context.isForceMode() || this.context.isForceRepositories();
12!
75
      Map<Path, RepositoryConfig> repositoryConfigMap = new HashMap<>(propertiesFiles.size());
6✔
76
      for (Path propertiesFile : propertiesFiles) {
10✔
77
        RepositoryConfig config = prepareActiveRepository(propertiesFile, forceMode);
5✔
78
        if (config != null) {
2✔
79
          repositoryConfigMap.put(propertiesFile, config);
5✔
80
        }
81
      }
1✔
82
      for (Path propertiesFile : propertiesFiles) {
10✔
83
        RepositoryConfig config = repositoryConfigMap.get(propertiesFile);
5✔
84
        if (config != null) {
2✔
85
          importRepository(config);
3✔
86
        }
87
      }
1✔
88
    }
89
  }
1✔
90

91
  private RepositoryConfig prepareActiveRepository(Path repositoryFile, boolean forceMode) {
92

93
    RepositoryConfig config = RepositoryConfig.loadProperties(repositoryFile, this.context);
5✔
94
    if (config == null) {
2✔
95
      return null;
2✔
96
    }
97
    if (!config.active()) {
3✔
98
      if (forceMode) {
2✔
99
        LOG.info("Setup of repository {} is forced, hence proceeding ...", config.id());
6✔
100
      } else {
101
        LOG.info("Skipping repository {} because it is not active, use --force-repositories to setup all repositories ...", config.id());
5✔
102
        return null;
2✔
103
      }
104
    }
105
    // prepare workspace creation for correct resolution of *
106
    List<String> workspaces = config.workspaces();
3✔
107
    for (String workspace : workspaces) {
10✔
108
      if (!RepositoryConfig.WORKSPACE_NAME_ALL.equals(workspace)) {
4✔
109
        Path workspacePath = this.context.getWorkspacePath(workspace);
5✔
110
        this.context.getFileAccess().mkdirs(workspacePath);
5✔
111
      }
112
    }
1✔
113
    return config;
2✔
114
  }
115

116
  private void importRepository(RepositoryConfig config) {
117

118
    this.context.newStep("Setup of repository " + config.id()).run(() -> {
11✔
119
      doImportRepository(config);
3✔
120
    });
1✔
121
  }
1✔
122

123
  private void doImportRepository(RepositoryConfig config) {
124
    LOG.debug("Repository configuration: {}", config);
4✔
125
    String repositoryRelativePath = config.path();
3✔
126
    if (repositoryRelativePath == null) {
2✔
127
      repositoryRelativePath = config.id();
3✔
128
    }
129
    Path ideStatusDir = this.context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE);
6✔
130
    FileAccess fileAccess = this.context.getFileAccess();
4✔
131
    fileAccess.mkdirs(ideStatusDir);
3✔
132

133
    List<String> workspaces = config.workspaces();
3✔
134
    if ((workspaces.size() == 1) && (RepositoryConfig.WORKSPACE_NAME_ALL.equals(workspaces.getFirst()))) {
9✔
135
      // if workspaces=* replace with all existing workspaces
136
      workspaces = fileAccess.listChildren(this.context.getWorkspacesBasePath(), Files::isDirectory).stream().map(Path::getFileName).map(Path::toString)
16✔
137
          .toList();
2✔
138
    }
139
    // if main is contained in workspaces, it should come first (to ensure physical cloning to main and linking to others)
140
    if (!workspaces.getFirst().equals(IdeContext.WORKSPACE_MAIN) && workspaces.contains(IdeContext.WORKSPACE_MAIN)) {
10✔
141
      workspaces = new ArrayList<>(workspaces); // mutable copy
5✔
142
      workspaces.remove(IdeContext.WORKSPACE_MAIN);
4✔
143
      workspaces.addFirst(IdeContext.WORKSPACE_MAIN);
3✔
144
    }
145

146
    Path firstRepository = null;
2✔
147

148
    if (config.isVirtualSettingsRepository()) {
3✔
149
      firstRepository = this.context.getSettingsPath();
4✔
150
    }
151

152
    for (String workspaceName : workspaces) {
10✔
153
      Path workspacePath = this.context.getWorkspacePath(workspaceName);
5✔
154
      Path repositoryPath = workspacePath.resolve(repositoryRelativePath);
4✔
155
      Path repositoryCreatedStatusFile = ideStatusDir.resolve("repository." + config.id() + "." + workspaceName);
7✔
156
      boolean createRepository = true;
2✔
157
      if (!config.isVirtualSettingsRepository() && Files.isDirectory(repositoryPath.resolve(GitContext.GIT_FOLDER))) {
10!
158
        if (firstRepository == null) {
×
159
          firstRepository = repositoryPath;
×
160
        }
161
        LOG.info("Repository {} already exists in workspace {} at {}", config.id(), workspaceName, repositoryPath);
×
162
        if (!(this.context.isForceMode() || this.context.isForceRepositories())) {
×
163
          LOG.info("Ignoring repository {} in workspace {}, use --force-repositories to rerun setup.", config.id(), workspaceName);
×
164
          createRepository = false;
×
165
        }
166
      }
167
      if (Files.exists(repositoryCreatedStatusFile)) {
5!
168
        if (!(this.context.isForceMode() || this.context.isForceRepositories())) {
×
169
          LOG.info("Ignoring repository {} in workspace {} because it was already setup before, use --force-repositories for recreation.",
×
170
              config.id(), workspaceName);
×
171
          createRepository = false;
×
172
        }
173
      }
174
      if (createRepository) {
2!
175
        if (firstRepository == null) {
2✔
176
          GitUrl gitUrl = config.asGitUrl();
3✔
177
          boolean success = cloneOrPullRepository(repositoryPath, gitUrl, repositoryCreatedStatusFile);
6✔
178
          if (success) {
2!
179
            firstRepository = repositoryPath;
2✔
180
            buildRepository(config, repositoryPath);
5✔
181
            importRepository(config, repositoryPath, config.id());
6✔
182
          }
183
        } else if (!config.isVirtualSettingsRepository()) {
4✔
184
          fileAccess.mkdirs(repositoryPath.getParent());
4✔
185
          fileAccess.symlink(firstRepository, repositoryPath);
4✔
186
        }
187
      }
188
      Path linkRepositoryPath = config.isVirtualSettingsRepository() ? firstRepository : repositoryPath;
7✔
189
      if (Files.exists(linkRepositoryPath)) {
5!
190
        for (RepositoryRemote remote : config.remotes()) {
7!
191
          this.context.getGitContext().addRemote(linkRepositoryPath, remote.name(), remote.url());
×
192
        }
×
193
        for (RepositoryLink link : config.links()) {
11✔
194
          createRepositoryLink(link, linkRepositoryPath, workspacePath);
5✔
195
        }
1✔
196
      }
197
    }
1✔
198
  }
1✔
199

200
  private boolean linkTargetExists(RepositoryLink link, Path linkTargetPath) {
201

202
    if (!Files.exists(linkTargetPath)) {
5!
203
      LOG.error("Skipping link from '{}' to '{}' because target does not exist: {}", link.link(), link.target(), linkTargetPath);
×
204
      return false;
×
205
    }
206
    return true;
2✔
207
  }
208

209
  private void createRepositoryLink(RepositoryLink link, Path repositoryPath, Path workspacePath) {
210
    Path linkPath = workspacePath.resolve(link.link());
5✔
211
    String target = link.target();
3✔
212
    Path linkTargetPath;
213
    if ((target != null) && !target.isBlank()) {
5!
214
      linkTargetPath = repositoryPath.resolve(target);
4✔
215
      if (!linkTargetExists(link, linkTargetPath)) {
5!
216
        return;
×
217
      }
218
    } else {
219
      linkTargetPath = repositoryPath;
2✔
220
    }
221
    FileAccess fileAccess = this.context.getFileAccess();
4✔
222
    fileAccess.mkdirs(linkPath.getParent());
4✔
223
    fileAccess.symlink(linkTargetPath, linkPath);
4✔
224
  }
1✔
225

226
  private boolean cloneOrPullRepository(Path repositoryPath, GitUrl gitUrl, Path repositoryCreatedStatusFile) {
227

228
    FileAccess fileAccess = this.context.getFileAccess();
4✔
229
    return this.context.newStep("Clone or pull repository").run(() -> {
12✔
230
      fileAccess.mkdirs(repositoryPath);
3✔
231
      this.context.getGitContext().pullOrClone(gitUrl, repositoryPath);
6✔
232
      fileAccess.touch(repositoryCreatedStatusFile);
3✔
233
    });
1✔
234
  }
235

236
  private boolean buildRepository(RepositoryConfig repositoryConfig, Path repositoryPath) {
237
    String buildCmd = repositoryConfig.buildCmd();
3✔
238
    if (buildCmd != null && !buildCmd.isEmpty()) {
2!
239
      return this.context.newStep("Build repository via: " + buildCmd).run(() -> {
×
240
        String[] command = buildCmd.split("\\s+");
×
241
        ToolCommandlet commandlet = this.context.getCommandletManager().getToolCommandlet(command[0]);
×
242
        if (commandlet == null) {
×
243
          String displayName = (command[0] == null || command[0].isBlank()) ? "<empty>" : "'" + command[0] + "'";
×
244
          LOG.error("Cannot build repository. Required tool '{}' not found. Please check your repository's build_cmd configuration value.",
×
245
              displayName);
246
          return;
×
247
        }
248
        commandlet.reset();
×
249
        for (int i = 1; i < command.length; i++) {
×
250
          commandlet.arguments.addValue(command[i]);
×
251
        }
252
        Path executionDirectory = repositoryPath;
×
253
        String path = repositoryConfig.buildPath();
×
254
        if (path != null) {
×
255
          executionDirectory = executionDirectory.resolve(path);
×
256
        }
257
        commandlet.setExecutionDirectory(executionDirectory);
×
258
        commandlet.run();
×
259
      });
×
260
    } else {
261
      LOG.debug("Build command not set. Skipping build for repository.");
3✔
262
      return true;
2✔
263
    }
264
  }
265

266
  private void importRepository(RepositoryConfig repositoryConfig, Path repositoryPath, String repositoryId) {
267

268
    Set<String> imports = repositoryConfig.imports();
3✔
269
    if ((imports == null) || imports.isEmpty()) {
5!
270
      LOG.debug("Repository {} has no IDE configured for import.", repositoryId);
4✔
271
      return;
1✔
272
    }
273
    for (String ide : imports) {
10✔
274
      Step step = this.context.newStep("Importing repository " + repositoryId + " into " + ide);
7✔
275
      step.run(() -> {
9✔
276
        ToolCommandlet commandlet = this.context.getCommandletManager().getToolCommandlet(ide);
6✔
277
        if (commandlet == null) {
2!
278
          String displayName = (ide == null || ide.isBlank()) ? "<empty>" : "'" + ide + "'";
×
279
          step.error("Cannot import repository '{}'. Required IDE '{}' not found. Please check your repository's imports configuration.", repositoryId,
×
280
              displayName);
281
        } else if (commandlet instanceof IdeToolCommandlet ideCommandlet) {
6!
282
          ideCommandlet.importRepository(repositoryPath);
4✔
283
        } else {
284
          step.error("Repository {} has import {} configured that is not an IDE!", repositoryId, ide);
×
285
        }
286
      });
1✔
287
    }
1✔
288
  }
1✔
289
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc