Skip to content

Commit

Permalink
devonfw#757: Settings in code repository (devonfw#850)
Browse files Browse the repository at this point in the history
  • Loading branch information
salimbouch authored Jan 14, 2025
1 parent 241ce06 commit c62e383
Show file tree
Hide file tree
Showing 14 changed files with 198 additions and 11 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ 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/757[#757]: Support to allow settings in code repository
* https://github.com/devonfw/IDEasy/issues/826[#826]: Fix git settings check when settings folder is empty
* https://github.com/devonfw/IDEasy/issues/894[#894]: Fix ide.bat printing for initialization and error output
* https://github.com/devonfw/IDEasy/issues/759[#759]: Add UpgradeSettingsCommandlet for the upgrade of legacy devonfw-ide settings to IDEasy

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,11 @@ private void setupConf(Path template, Path conf) {
}
}

private void updateSettings() {
/**
* Updates the settings repository in IDE_HOME/settings by either cloning if no such repository exists or pulling
* if the repository exists then saves the latest current commit ID in the file ".commit.id".
*/
protected void updateSettings() {

Path settingsPath = this.context.getSettingsPath();
GitContext gitContext = this.context.getGitContext();
Expand All @@ -132,6 +136,7 @@ private void updateSettings() {
}
gitContext.pullOrClone(GitUrl.of(repository), settingsPath);
}
this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath());
step.success("Successfully updated settings repository.");
} finally {
if (step != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.devonfw.tools.ide.commandlet;

import java.nio.file.Files;
import java.nio.file.Path;

import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.git.GitUrl;
import com.devonfw.tools.ide.io.FileAccess;
import com.devonfw.tools.ide.property.FlagProperty;
import com.devonfw.tools.ide.property.StringProperty;
Expand All @@ -18,6 +20,9 @@ public class CreateCommandlet extends AbstractUpdateCommandlet {
/** {@link FlagProperty} for skipping the setup of git repositories */
public final FlagProperty skipRepositories;

/** {@link FlagProperty} for creating a project with settings inside a code repository */
public final FlagProperty codeRepositoryFlag;

/**
* The constructor.
*
Expand All @@ -28,6 +33,7 @@ public CreateCommandlet(IdeContext context) {
super(context);
this.newProject = add(new StringProperty("", true, "project"));
this.skipRepositories = add(new FlagProperty("--skip-repositories"));
this.codeRepositoryFlag = add(new FlagProperty("--code"));
add(this.settingsRepo);
}

Expand Down Expand Up @@ -59,12 +65,32 @@ public void run() {
initializeProject(newProjectPath);
this.context.setIdeHome(newProjectPath);
super.run();

if (this.skipRepositories.isTrue()) {
this.context.info("Skipping the cloning of project repositories as specified by the user.");
} else {
updateRepositories();
}
this.context.success("Successfully created new project '{}'.", newProjectName);

}

private void initializeCodeRepository(String repoUrl) {

// clone the given repository into IDE_HOME/workspaces/main
GitUrl gitUrl = GitUrl.of(repoUrl);
Path codeRepoPath = this.context.getWorkspacePath().resolve(gitUrl.getProjectName());
this.context.getGitContext().pullOrClone(gitUrl, codeRepoPath);

// check for settings folder and create symlink to IDE_HOME/settings
Path settingsFolder = codeRepoPath.resolve(IdeContext.FOLDER_SETTINGS);
if (Files.exists(settingsFolder)) {
this.context.getFileAccess().symlink(settingsFolder, this.context.getSettingsPath());
// create a file in IDE_HOME with the current local commit id
this.context.getGitContext().saveCurrentCommitId(codeRepoPath, this.context.getSettingsCommitIdPath());
} else {
this.context.warning("No settings folder was found inside the code repository.");
}
}

private void initializeProject(Path newInstancePath) {
Expand All @@ -79,4 +105,25 @@ private void updateRepositories() {

this.context.getCommandletManager().getCommandlet(RepositoryCommandlet.class).run();
}

@Override
protected void updateSettings() {

if (codeRepositoryFlag.isTrue()) {
String codeRepository = this.settingsRepo.getValue();
if (codeRepository == null || codeRepository.isBlank()) {
String message = """
No code repository was given after '--code'.
Please give the code repository below that includes your settings folder.
Further details can be found here: https://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc
Code repository URL:
""";
codeRepository = this.context.askForInput(message);
}
initializeCodeRepository(codeRepository);
} else {
super.updateSettings();
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ private void logSettingsLegacyStatus() {
}

private void logSettingsGitStatus() {
Path settingsPath = this.context.getSettingsPath();
Path settingsPath = this.context.getSettingsGitRepository();
if (settingsPath != null) {
GitContext gitContext = this.context.getGitContext();
if (gitContext.isRepositoryUpdateAvailable(settingsPath)) {
if (gitContext.isRepositoryUpdateAvailable(settingsPath, this.context.getSettingsCommitIdPath())) {
this.context.warning("Your settings are not up-to-date, please run 'ide update'.");
} else {
this.context.success("Your settings are up-to-date.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ public abstract class AbstractIdeContext implements IdeContext {

protected Path settingsPath;

private Path settingsCommitIdPath;

private Path softwarePath;

private Path softwareExtraPath;
Expand Down Expand Up @@ -241,6 +243,7 @@ public void setCwd(Path userDir, String workspace, Path ideHome) {
this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
this.confPath = this.ideHome.resolve(FOLDER_CONF);
this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
this.softwarePath = this.ideHome.resolve(FOLDER_SOFTWARE);
this.softwareExtraPath = this.softwarePath.resolve(FOLDER_EXTRA);
this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
Expand Down Expand Up @@ -409,6 +412,31 @@ public Path getSettingsPath() {
return this.settingsPath;
}

@Override
public Path getSettingsGitRepository() {

Path settingsPath = getSettingsPath();

if (settingsPath == null) {
error("No settings repository was found.");
return null;
}

// check whether the settings path has a .git folder only if its not a symbolic link
if (!Files.exists(settingsPath.resolve(".git")) && !Files.isSymbolicLink(settingsPath)) {
error("Settings repository exists but is not a git repository.");
return null;
}

return settingsPath;
}

@Override
public Path getSettingsCommitIdPath() {

return this.settingsCommitIdPath;
}

@Override
public Path getConfPath() {

Expand Down Expand Up @@ -836,10 +864,11 @@ private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
if (cmd.isIdeHomeRequired()) {
debug(getMessageIdeHomeFound());
}
if (this.settingsPath != null) {
if (getGitContext().isRepositoryUpdateAvailable(this.settingsPath) ||
(getGitContext().fetchIfNeeded(this.settingsPath) && getGitContext().isRepositoryUpdateAvailable(this.settingsPath))) {
interaction("Updates are available for the settings repository. If you want to pull the latest changes, call ide update.");
Path settingsRepository = getSettingsGitRepository();
if (settingsRepository != null) {
if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) ||
(getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()))) {
interaction("Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
}
}
}
Expand Down
15 changes: 15 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ public interface IdeContext extends IdeStartContext {
/** The filename of the configuration file in the settings for this {@link CustomToolRepository}. */
String FILE_CUSTOM_TOOLS = "ide-custom-tools.json";

/**
* file containing the current local commit hash of the settings repository. */
String SETTINGS_COMMIT_ID = ".commit.id";

/**
* @return {@code true} if {@link #isOfflineMode() offline mode} is active or we are NOT {@link #isOnline() online}, {@code false} otherwise.
*/
Expand Down Expand Up @@ -354,6 +358,17 @@ default void requireOnline(String purpose) {
*/
Path getSettingsPath();

/**
*
* @return the {@link Path} to the {@code settings} folder with the cloned git repository containing the project configuration only if the settings repository is in fact a git repository.
*/
Path getSettingsGitRepository();

/**
* @return the {@link Path} to the file containing the last tracked commit Id of the settings repository.
*/
Path getSettingsCommitIdPath();

/**
* @return the {@link Path} to the templates folder inside the {@link #getSettingsPath() settings}. The relative directory structure in this templates folder
* is to be applied to {@link #getIdeHome() IDE_HOME} when the project is set up.
Expand Down
21 changes: 19 additions & 2 deletions cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,23 @@ public interface GitContext {
* Checks if there are updates available for the Git repository in the specified target folder by comparing the local commit hash with the remote commit
* hash.
*
* @param repository the {@link Path} to the target folder where the git repository is located. This should be the folder containing the ".git"
* subfolder.
* @param repository the {@link Path} to the target folder where the git repository is located.
* @return {@code true} if the remote repository contains commits that are not present in the local repository, indicating that updates are available.
* {@code false} if the local and remote repositories are in sync, or if there was an issue retrieving the commit hashes.
*/
boolean isRepositoryUpdateAvailable(Path repository);

/**
* Checks if there are updates available for the Git repository in the specified target folder by comparing the local commit hash with the remote commit
* hash.
*
* @param repository the {@link Path} to the target folder where the git repository is located.
* @param trackedCommitIdPath the {@link Path} to a file containing the last tracked commit ID of this repository.
* @return {@code true} if the remote repository contains commits that are not present in the local repository, indicating that updates are available.
* {@code false} if the local and remote repositories are in sync, or if there was an issue retrieving the commit hashes.
*/
boolean isRepositoryUpdateAvailable(Path repository, Path trackedCommitIdPath);

/**
* Attempts a git pull and reset if required.
*
Expand Down Expand Up @@ -175,4 +185,11 @@ default void reset(Path repository, String branch) {
*/
String determineRemote(Path repository);

/**
* Saves the current git commit ID of a repository to a file given as an argument.
*
* @param repository the path to the git repository
* @param trackedCommitIdPath the path to the file where the commit Id will be written.
*/
void saveCurrentCommitId(Path repository, Path trackedCommitIdPath);
}
34 changes: 34 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.devonfw.tools.ide.git;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
Expand Down Expand Up @@ -59,6 +60,21 @@ public boolean isRepositoryUpdateAvailable(Path repository) {
return !localCommitId.equals(remoteCommitId);
}

@Override
public boolean isRepositoryUpdateAvailable(Path repository, Path trackedCommitIdPath) {

verifyGitInstalled();
String trackedCommitId;
try {
trackedCommitId = Files.readString(trackedCommitIdPath);
} catch (IOException e) {
return false;
}

String remoteCommitId = runGitCommandAndGetSingleOutput("Failed to get the remote commit id.", repository, "rev-parse", "@{u}");
return !trackedCommitId.equals(remoteCommitId);
}

@Override
public void pullOrCloneAndResetIfNeeded(GitUrl gitUrl, Path repository, String remoteName) {

Expand Down Expand Up @@ -292,6 +308,24 @@ private ProcessResult runGitCommand(Path directory, ProcessMode mode, ProcessErr
processContext.addArgs(args);
return processContext.run(mode);
}

@Override
public void saveCurrentCommitId(Path repository, Path trackedCommitIdPath) {

if ((repository == null) || (trackedCommitIdPath == null)) {
this.context.warning("Invalid usage of saveCurrentCommitId with null value");
return;
}
this.context.trace("Saving commit Id of {} into {}", repository, trackedCommitIdPath);
String currentCommitId = runGitCommandAndGetSingleOutput("Failed to get current commit id.", repository, "rev-parse", "HEAD");
if (currentCommitId != null) {
try {
Files.writeString(trackedCommitIdPath, currentCommitId);
} catch (IOException e) {
throw new IllegalStateException("Failed to save commit ID", e);
}
}
}
}


15 changes: 15 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/git/GitUrl.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ public String toString() {
return this.url + "#" + this.branch;
}

/**
* Extracts the project name from an git URL.
* For URLs like "https://github.com/devonfw/ide-urls.git" returns "ide-urls"
*
* @return the project name without ".git" extension
*/
public String getProjectName() {
String path = this.url.substring(this.url.indexOf("://") + 3);
if (path.endsWith(".git")) {
path = path.substring(0, path.length() - 4);
}
String[] parts = path.split("/");
return parts[parts.length - 1];
}

/**
* @param gitUrl the {@link #toString() string representation} of a {@link GitUrl}. May contain a branch name as {@code «url»#«branch»}.
* @return the parsed {@link GitUrl}.
Expand Down
1 change: 1 addition & 0 deletions cli/src/main/resources/nls/Help.properties
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ cmd.vscode=Tool commandlet for Visual Studio Code (IDE).
cmd.vscode.detail=Visual Studio Code (VS Code) is a popular code editor developed by Microsoft. Detailed documentation can be found at https://code.visualstudio.com/docs/
commandlets=Available commandlets:
opt.--batch=enable batch mode (non-interactive).
opt.--code=clone given code repository containing a settings folder into workspaces so that settings can be committed alongside code changes.
opt.--debug=enable debug logging.
opt.--force=enable force mode.
opt.--locale=the locale (e.g. '--locale=de' for German language).
Expand Down
1 change: 1 addition & 0 deletions cli/src/main/resources/nls/Help_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ cmd.vscode=Werkzeug Kommando für Visual Studio Code (IDE).
cmd.vscode.detail=Visual Studio Code (VS Code) ist ein beliebter Code-Editor, der von Microsoft entwickelt wurde. Detaillierte Dokumentation ist zu finden unter https://code.visualstudio.com/docs/
commandlets=Verfügbare Kommandos:
opt.--batch=Aktiviert den Batch-Modus (nicht-interaktive Stapelverarbeitung).
opt.--code=Git-Repository sowohl als Code- als auch als Settings-Repository verwenden.
opt.--debug=Aktiviert Debug-Ausgaben (Fehleranalyse).
opt.--force=Aktiviert den Force-Modus (Erzwingen).
opt.--locale=Die Spracheinstellungen (z.B. 'en' für Englisch).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class HelpCommandletTest extends AbstractIdeContextTest {
* Test of {@link HelpCommandlet} does not require home.
*/
@Test
public void testThatHomeIsNotReqired() {
public void testThatHomeIsNotRequired() {

// arrange
IdeContext context = IdeTestContextMock.get();
Expand Down
12 changes: 12 additions & 0 deletions cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ public boolean isRepositoryUpdateAvailable(Path repository) {
return false;
}

@Override
public boolean isRepositoryUpdateAvailable(Path repository, Path trackedCommitIdPath) {

return false;
}

@Override
public String determineCurrentBranch(Path repository) {

Expand All @@ -84,4 +90,10 @@ public String determineRemote(Path repository) {

return "origin";
}


@Override
public void saveCurrentCommitId(Path repository, Path trackedCommitIdPath) {

}
}
Loading

0 comments on commit c62e383

Please sign in to comment.