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

devonfw / IDEasy / 43457934883

13 Nov 2026 03:58PM UTC coverage: 75.401% (-0.03%) from 75.434%
43457934883

push

github

devonfw-core
set next version to 2026.11.004-SNAPSHOT

6467 of 9501 branches covered (68.07%)

Branch coverage included in aggregate %.

16516 of 20980 relevant lines covered (78.72%)

3.39 hits per line

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

71.68
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.cli.CliAbortException;
15
import com.devonfw.tools.ide.commandlet.AbstractCommandlet;
16
import com.devonfw.tools.ide.context.IdeContext;
17
import com.devonfw.tools.ide.git.GitContext;
18
import com.devonfw.tools.ide.git.GitUrl;
19
import com.devonfw.tools.ide.io.FileAccess;
20
import com.devonfw.tools.ide.property.RepositoryProperty;
21
import com.devonfw.tools.ide.step.Step;
22
import com.devonfw.tools.ide.tool.AbstractToolCommandlet;
23
import com.devonfw.tools.ide.tool.ide.IdeToolCommandlet;
24

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

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

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

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

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

49
  @Override
50
  public String getName() {
51

52
    return REPOSITORY;
2 ✔
53
  }
54

55
  @Override
56
  protected void doRun() {
57

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

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

92
  /**
93
   * Like {@link #prepareActiveRepository(Path, boolean)} but ignores a single invalid repository instead of aborting the setup of all others. Resolving a
94
   * property value may fail for a malformed expression, and one broken file in the repositories folder must not prevent every other repository from being
95
   * set up. Only used when iterating all repositories - if the user explicitly requested a single repository, the failure is reported instead. A
96
   * {@link CliAbortException} is still propagated as the {@link Step} always re-throws it.
97
   *
98
   * @param repositoryFile the {@link Path} to the repository properties file.
99
   * @param forceMode - {@code true} to setup the repository even if it is not active, {@code false} otherwise.
100
   * @return the {@link RepositoryConfig} or {@code null} if the repository shall be skipped.
101
   */
102
  private RepositoryConfig prepareActiveRepositoryIgnoringInvalid(Path repositoryFile, boolean forceMode) {
103

104
    Step step = this.context.newStep("Prepare repository", repositoryFile);
11 ✔
105
    return step.call(() -> prepareActiveRepository(repositoryFile, forceMode), () -> null);
16 ✔
106
  }
107

108
  private RepositoryConfig prepareActiveRepository(Path repositoryFile, boolean forceMode) {
109

110
    // the active flag is evaluated before the remaining properties are read: reading a property resolves variables and expressions in its value, so an
111
    // expression like @ask-variable would otherwise ask the user for a repository that is skipped anyway
112
    RepositoryProperties properties = new RepositoryProperties(repositoryFile, this.context);
7 ✔
113
    if (!properties.isActive()) {
3 ✔
114
      if (forceMode) {
2 ✔
115
        LOG.info("Setup of repository {} is forced, hence proceeding ...", properties.getId());
6 ✔
116
      } else {
117
        LOG.info("Skipping repository {} because it is not active, use --force-repositories to setup all repositories ...", properties.getId());
5 ✔
118
        return null;
2 ✔
119
      }
120
    }
121
    RepositoryConfig config = RepositoryConfig.loadProperties(properties);
3 ✔
122
    if (config == null) {
2 ✔
123
      return null;
2 ✔
124
    }
125
    // prepare workspace creation for correct resolution of *
126
    List<String> workspaces = config.workspaces();
3 ✔
127
    for (String workspace : workspaces) {
10 ✔
128
      if (!RepositoryConfig.WORKSPACE_NAME_ALL.equals(workspace)) {
4 ✔
129
        Path workspacePath = this.context.getWorkspacePath(workspace);
5 ✔
130
        this.context.getFileAccess().mkdirs(workspacePath);
5 ✔
131
      }
132
    }
1 ✔
133
    return config;
2 ✔
134
  }
135

136
  private void importRepository(RepositoryConfig config) {
137

138
    this.context.newStep("Setup of repository " + config.id()).run(() -> {
11 ✔
139
      doImportRepository(config);
3 ✔
140
    });
1 ✔
141
  }
1 ✔
142

143
  private void doImportRepository(RepositoryConfig config) {
144
    LOG.debug("Repository configuration: {}", config);
4 ✔
145
    String repositoryRelativePath = config.path();
3 ✔
146
    if (repositoryRelativePath == null) {
2 ✔
147
      repositoryRelativePath = config.id();
3 ✔
148
    }
149
    Path ideStatusDir = this.context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE);
6 ✔
150
    FileAccess fileAccess = this.context.getFileAccess();
4 ✔
151
    fileAccess.mkdirs(ideStatusDir);
3 ✔
152

153
    List<String> workspaces = config.workspaces();
3 ✔
154
    if ((workspaces.size() == 1) && (RepositoryConfig.WORKSPACE_NAME_ALL.equals(workspaces.getFirst()))) {
9 ✔
155
      // if workspaces=* replace with all existing workspaces
156
      workspaces = fileAccess.listChildren(this.context.getWorkspacesBasePath(), Files::isDirectory).stream().map(Path::getFileName).map(Path::toString)
16 ✔
157
          .toList();
2 ✔
158
    }
159
    // if main is contained in workspaces, it should come first (to ensure physical cloning to main and linking to others)
160
    if (!workspaces.getFirst().equals(IdeContext.WORKSPACE_MAIN) && workspaces.contains(IdeContext.WORKSPACE_MAIN)) {
10 !
161
      workspaces = new ArrayList<>(workspaces); // mutable copy
×
162
      workspaces.remove(IdeContext.WORKSPACE_MAIN);
×
163
      workspaces.addFirst(IdeContext.WORKSPACE_MAIN);
×
164
    }
165

166
    Path firstRepository = null;
2 ✔
167

168
    if (config.isVirtualSettingsRepository()) {
3 ✔
169
      firstRepository = this.context.getSettingsPath();
4 ✔
170
    }
171

172
    for (String workspaceName : workspaces) {
10 ✔
173
      Path workspacePath = this.context.getWorkspacePath(workspaceName);
5 ✔
174
      Path repositoryPath = workspacePath.resolve(repositoryRelativePath);
4 ✔
175
      Path repositoryCreatedStatusFile = ideStatusDir.resolve("repository." + config.id() + "." + workspaceName);
7 ✔
176
      boolean createRepository = true;
2 ✔
177
      if (!config.isVirtualSettingsRepository() && Files.isDirectory(repositoryPath.resolve(GitContext.GIT_FOLDER))) {
10 !
178
        if (firstRepository == null) {
×
179
          firstRepository = repositoryPath;
×
180
        }
181
        LOG.info("Repository {} already exists in workspace {} at {}", config.id(), workspaceName, repositoryPath);
×
182
        if (!(this.context.isForceMode() || this.context.isForceRepositories())) {
×
183
          LOG.info("Ignoring repository {} in workspace {}, use --force-repositories to rerun setup.", config.id(), workspaceName);
×
184
          createRepository = false;
×
185
        }
186
      }
187
      if (Files.exists(repositoryCreatedStatusFile)) {
5 !
188
        if (!(this.context.isForceMode() || this.context.isForceRepositories())) {
×
189
          LOG.info("Ignoring repository {} in workspace {} because it was already setup before, use --force-repositories for recreation.",
×
190
              config.id(), workspaceName);
×
191
          createRepository = false;
×
192
        }
193
      }
194
      if (createRepository) {
2 !
195
        if (firstRepository == null) {
2 ✔
196
          GitUrl gitUrl = config.asGitUrl();
3 ✔
197
          boolean success = cloneOrPullRepository(repositoryPath, gitUrl, repositoryCreatedStatusFile);
6 ✔
198
          if (success) {
2 !
199
            firstRepository = repositoryPath;
2 ✔
200
            buildRepository(config, repositoryPath);
5 ✔
201
            importRepository(config, repositoryPath, config.id());
6 ✔
202
          }
203
        } else if (!config.isVirtualSettingsRepository()) {
4 ✔
204
          fileAccess.mkdirs(repositoryPath.getParent());
4 ✔
205
          fileAccess.symlink(firstRepository, repositoryPath);
4 ✔
206
        }
207
      }
208
      Path linkRepositoryPath = config.isVirtualSettingsRepository() ? firstRepository : repositoryPath;
7 ✔
209
      if (Files.exists(linkRepositoryPath)) {
5 !
210
        for (RepositoryRemote remote : config.remotes()) {
7 !
211
          this.context.getGitContext().addRemote(linkRepositoryPath, remote.name(), remote.url());
×
212
        }
×
213
        for (RepositoryLink link : config.links()) {
11 ✔
214
          createRepositoryLink(link, linkRepositoryPath, workspacePath);
5 ✔
215
        }
1 ✔
216
      }
217
    }
1 ✔
218
  }
1 ✔
219

220
  private boolean linkTargetExists(RepositoryLink link, Path linkTargetPath) {
221

222
    if (!Files.exists(linkTargetPath)) {
5 !
223
      LOG.error("Skipping link from '{}' to '{}' because target does not exist: {}", link.link(), link.target(), linkTargetPath);
×
224
      return false;
×
225
    }
226
    return true;
2 ✔
227
  }
228

229
  private void createRepositoryLink(RepositoryLink link, Path repositoryPath, Path workspacePath) {
230
    Path linkPath = workspacePath.resolve(link.link());
5 ✔
231
    String target = link.target();
3 ✔
232
    Path linkTargetPath;
233
    if ((target != null) && !target.isBlank()) {
5 !
234
      linkTargetPath = repositoryPath.resolve(target);
4 ✔
235
      if (!linkTargetExists(link, linkTargetPath)) {
5 !
236
        return;
×
237
      }
238
    } else {
239
      linkTargetPath = repositoryPath;
2 ✔
240
    }
241
    FileAccess fileAccess = this.context.getFileAccess();
4 ✔
242
    fileAccess.mkdirs(linkPath.getParent());
4 ✔
243
    fileAccess.symlink(linkTargetPath, linkPath);
4 ✔
244
  }
1 ✔
245

246
  private boolean cloneOrPullRepository(Path repositoryPath, GitUrl gitUrl, Path repositoryCreatedStatusFile) {
247

248
    FileAccess fileAccess = this.context.getFileAccess();
4 ✔
249
    return this.context.newStep("Clone or pull repository").run(() -> {
12 ✔
250
      fileAccess.mkdirs(repositoryPath);
3 ✔
251
      this.context.getGitContext().pullOrClone(gitUrl, repositoryPath);
6 ✔
252
      fileAccess.touch(repositoryCreatedStatusFile);
3 ✔
253
    });
1 ✔
254
  }
255

256
  private boolean buildRepository(RepositoryConfig repositoryConfig, Path repositoryPath) {
257
    String buildCmd = repositoryConfig.buildCmd();
3 ✔
258
    if (buildCmd != null && !buildCmd.isEmpty()) {
2 !
259
      return this.context.newStep("Build repository via: " + buildCmd).run(() -> {
×
260
        String[] command = buildCmd.split("\\s+");
×
261
        AbstractToolCommandlet commandlet = this.context.getCommandletManager().getToolCommandlet(command[0]);
×
262
        if (commandlet == null) {
×
263
          String displayName = (command[0] == null || command[0].isBlank()) ? "<empty>" : "'" + command[0] + "'";
×
264
          LOG.error("Cannot build repository. Required tool '{}' not found. Please check your repository's build_cmd configuration value.",
×
265
              displayName);
266
          return;
×
267
        }
268
        commandlet.reset();
×
269
        for (int i = 1; i < command.length; i++) {
×
270
          commandlet.arguments.addValue(command[i]);
×
271
        }
272
        Path executionDirectory = repositoryPath;
×
273
        String path = repositoryConfig.buildPath();
×
274
        if (path != null) {
×
275
          executionDirectory = executionDirectory.resolve(path);
×
276
        }
277
        commandlet.setExecutionDirectory(executionDirectory);
×
278
        commandlet.run();
×
279
      });
×
280
    } else {
281
      LOG.debug("Build command not set. Skipping build for repository.");
3 ✔
282
      return true;
2 ✔
283
    }
284
  }
285

286
  private void importRepository(RepositoryConfig repositoryConfig, Path repositoryPath, String repositoryId) {
287

288
    Set<String> imports = repositoryConfig.imports();
3 ✔
289
    if ((imports == null) || imports.isEmpty()) {
5 !
290
      LOG.debug("Repository {} has no IDE configured for import.", repositoryId);
4 ✔
291
      return;
1 ✔
292
    }
293
    for (String ide : imports) {
10 ✔
294
      Step step = this.context.newStep("Importing repository " + repositoryId + " into " + ide);
7 ✔
295
      step.run(() -> {
9 ✔
296
        AbstractToolCommandlet commandlet = this.context.getCommandletManager().getToolCommandlet(ide);
6 ✔
297
        if (commandlet == null) {
2 !
298
          String displayName = (ide == null || ide.isBlank()) ? "<empty>" : "'" + ide + "'";
×
299
          step.error("Cannot import repository '{}'. Required IDE '{}' not found. Please check your repository's imports configuration.", repositoryId,
×
300
              displayName);
301
        } else if (commandlet instanceof IdeToolCommandlet ideToolCommandlet) {
6 !
302
          ideToolCommandlet.importRepository(repositoryPath);
4 ✔
303
        } else {
304
          step.error("Repository {} has import {} configured that is not an IDE!", repositoryId, ide);
×
305
        }
306
      });
1 ✔
307
    }
1 ✔
308
  }
1 ✔
309
}
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