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

devonfw / IDEasy / 23003890195

12 Mar 2026 01:17PM UTC coverage: 70.474% (+0.1%) from 70.347%
23003890195

Pull #1739

github

web-flow
Merge a4cf3627a into a965db6c8
Pull Request #1739: #1735: implement repo link feature, #1736: fix link creation

4138 of 6464 branches covered (64.02%)

Branch coverage included in aggregate %.

10730 of 14633 relevant lines covered (73.33%)

3.09 hits per line

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

70.98
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

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

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

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

47
  @Override
48
  public String getName() {
49

50
    return "repository";
2✔
51
  }
52

53
  @Override
54
  protected void doRun() {
55

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

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

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

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

115
  private void importRepository(RepositoryConfig config) {
116

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

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

132
    List<String> workspaces = config.workspaces();
3✔
133
    if ((workspaces.size() == 1) && (RepositoryConfig.WORKSPACE_NAME_ALL.equals(workspaces.getFirst()))) {
9✔
134
      // if workspaces=* replace with all existing workspaces
135
      workspaces = fileAccess.listChildren(this.context.getWorkspacesBasePath(), Files::isDirectory).stream().map(Path::getFileName).map(Path::toString)
16✔
136
          .toList();
2✔
137
    }
138
    // if main is contained in workspaces, it should come first (to ensure physical cloning to main and linking to others)
139
    if (!workspaces.getFirst().equals(IdeContext.WORKSPACE_MAIN) && workspaces.contains(IdeContext.WORKSPACE_MAIN)) {
10!
140
      workspaces = new ArrayList<>(workspaces); // mutable copy
×
141
      workspaces.remove(IdeContext.WORKSPACE_MAIN);
×
142
      workspaces.addFirst(IdeContext.WORKSPACE_MAIN);
×
143
    }
144
    Path firstRepository = null;
2✔
145
    for (String workspaceName : workspaces) {
10✔
146
      Path workspacePath = this.context.getWorkspacePath(workspaceName);
5✔
147
      Path repositoryPath = workspacePath.resolve(repositoryRelativePath);
4✔
148
      Path repositoryCreatedStatusFile = ideStatusDir.resolve("repository." + config.id() + "." + workspaceName);
7✔
149
      boolean createRepository = true;
2✔
150
      if (Files.isDirectory(repositoryPath.resolve(GitContext.GIT_FOLDER))) {
7!
151
        if (firstRepository == null) {
×
152
          firstRepository = repositoryPath;
×
153
        }
154
        LOG.info("Repository {} already exists in workspace {} at {}", config.id(), workspaceName, repositoryPath);
×
155
        if (!(this.context.isForceMode() || this.context.isForceRepositories())) {
×
156
          LOG.info("Ignoring repository {} in workspace {}, use --force-repositories to rerun setup.", config.id(), workspaceName);
×
157
          createRepository = false;
×
158
        }
159
      }
160
      if (Files.exists(repositoryCreatedStatusFile)) {
5!
161
        if (!(this.context.isForceMode() || this.context.isForceRepositories())) {
×
162
          LOG.info("Ignoring repository {} in workspace {} because it was already setup before, use --force-repositories for recreation.",
×
163
              config.id(), workspaceName);
×
164
          createRepository = false;
×
165
        }
166
      }
167
      if (createRepository) {
2!
168
        if (firstRepository == null) {
2✔
169
          GitUrl gitUrl = config.asGitUrl();
3✔
170
          boolean success = cloneOrPullRepository(repositoryPath, gitUrl, repositoryCreatedStatusFile);
6✔
171
          if (success) {
2!
172
            firstRepository = repositoryPath;
2✔
173
            buildRepository(config, repositoryPath);
5✔
174
            importRepository(config, repositoryPath, config.id());
6✔
175
          }
176
        } else {
1✔
177
          fileAccess.mkdirs(repositoryPath.getParent());
4✔
178
          fileAccess.symlink(firstRepository, repositoryPath);
4✔
179
        }
180
      }
181
      if (Files.exists(repositoryPath)) {
5!
182
        for (RepositoryLink link : config.links()) {
11✔
183
          createRepositoryLink(link, repositoryPath, workspacePath);
5✔
184
        }
1✔
185
      }
186
    }
1✔
187
  }
1✔
188

189
  private void createRepositoryLink(RepositoryLink link, Path repositoryPath, Path workspacePath) {
190
    Path linkPath = workspacePath.resolve(link.link());
5✔
191
    String target = link.target();
3✔
192
    Path linkTargetPath;
193
    if ((target != null) && !target.isBlank()) {
5!
194
      linkTargetPath = repositoryPath.resolve(target);
4✔
195
      if (!Files.exists(linkTargetPath)) {
5!
196
        LOG.error("Skipping link from '{}' to '{}' because target does not exist: {}", link.link(), target, linkTargetPath);
×
197
        return;
×
198
      }
199
    } else {
200
      linkTargetPath = repositoryPath;
2✔
201
    }
202
    FileAccess fileAccess = this.context.getFileAccess();
4✔
203
    fileAccess.mkdirs(linkPath.getParent());
4✔
204
    fileAccess.symlink(linkTargetPath, linkPath);
4✔
205
  }
1✔
206

207
  private boolean cloneOrPullRepository(Path repositoryPath, GitUrl gitUrl, Path repositoryCreatedStatusFile) {
208

209
    FileAccess fileAccess = this.context.getFileAccess();
4✔
210
    return this.context.newStep("Clone or pull repository").run(() -> {
12✔
211
      fileAccess.mkdirs(repositoryPath);
3✔
212
      this.context.getGitContext().pullOrClone(gitUrl, repositoryPath);
6✔
213
      fileAccess.touch(repositoryCreatedStatusFile);
3✔
214
    });
1✔
215
  }
216

217
  private boolean buildRepository(RepositoryConfig repositoryConfig, Path repositoryPath) {
218
    String buildCmd = repositoryConfig.buildCmd();
3✔
219
    if (buildCmd != null && !buildCmd.isEmpty()) {
2!
220
      return this.context.newStep("Build repository via: " + buildCmd).run(() -> {
×
221
        String[] command = buildCmd.split("\\s+");
×
222
        ToolCommandlet commandlet = this.context.getCommandletManager().getToolCommandlet(command[0]);
×
223
        if (commandlet == null) {
×
224
          String displayName = (command[0] == null || command[0].isBlank()) ? "<empty>" : "'" + command[0] + "'";
×
225
          LOG.error("Cannot build repository. Required tool '{}' not found. Please check your repository's build_cmd configuration value.",
×
226
              displayName);
227
          return;
×
228
        }
229
        commandlet.reset();
×
230
        for (int i = 1; i < command.length; i++) {
×
231
          commandlet.arguments.addValue(command[i]);
×
232
        }
233
        Path executionDirectory = repositoryPath;
×
234
        String path = repositoryConfig.buildPath();
×
235
        if (path != null) {
×
236
          executionDirectory = executionDirectory.resolve(path);
×
237
        }
238
        commandlet.setExecutionDirectory(executionDirectory);
×
239
        commandlet.run();
×
240
      });
×
241
    } else {
242
      LOG.debug("Build command not set. Skipping build for repository.");
3✔
243
      return true;
2✔
244
    }
245
  }
246

247
  private void importRepository(RepositoryConfig repositoryConfig, Path repositoryPath, String repositoryId) {
248

249
    Set<String> imports = repositoryConfig.imports();
3✔
250
    if ((imports == null) || imports.isEmpty()) {
5!
251
      LOG.debug("Repository {} has no IDE configured for import.", repositoryId);
4✔
252
      return;
1✔
253
    }
254
    for (String ide : imports) {
10✔
255
      Step step = this.context.newStep("Importing repository " + repositoryId + " into " + ide);
7✔
256
      step.run(() -> {
9✔
257
        ToolCommandlet commandlet = this.context.getCommandletManager().getToolCommandlet(ide);
6✔
258
        if (commandlet == null) {
2!
259
          String displayName = (ide == null || ide.isBlank()) ? "<empty>" : "'" + ide + "'";
×
260
          step.error("Cannot import repository '{}'. Required IDE '{}' not found. Please check your repository's imports configuration.", repositoryId,
×
261
              displayName);
262
        } else if (commandlet instanceof IdeToolCommandlet ideCommandlet) {
6!
263
          ideCommandlet.importRepository(repositoryPath);
4✔
264
        } else {
265
          step.error("Repository {} has import {} configured that is not an IDE!", repositoryId, ide);
×
266
        }
267
      });
1✔
268
    }
1✔
269
  }
1✔
270
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc