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 @@ -6,6 +6,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/2273[#2273]: Integrate Git as global tool

The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/52?closed=1[milestone 2026.11.001].

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import com.devonfw.tools.ide.tool.gcviewer.GcViewer;
import com.devonfw.tools.ide.tool.gemini.Gemini;
import com.devonfw.tools.ide.tool.gh.Gh;
import com.devonfw.tools.ide.tool.git.Git;
import com.devonfw.tools.ide.tool.go.Go;
import com.devonfw.tools.ide.tool.graalvm.GraalVm;
import com.devonfw.tools.ide.tool.gradle.Gradle;
Expand Down Expand Up @@ -198,6 +199,7 @@ public CommandletManagerImpl(IdeContext context) {
add(new CheckCommandlet(context));
add(new Obsidian(context));
add(new Gemini(context));
add(new Git(context));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import com.devonfw.tools.ide.commandlet.CommandletManagerImpl;
import com.devonfw.tools.ide.commandlet.ContextCommandlet;
import com.devonfw.tools.ide.commandlet.EnvironmentCommandlet;
import com.devonfw.tools.ide.commandlet.InstallCommandlet;
import com.devonfw.tools.ide.commandlet.UpgradeCommandlet;
import com.devonfw.tools.ide.commandlet.update.UpdateCommandlet;
import com.devonfw.tools.ide.common.SystemPath;
Expand Down Expand Up @@ -76,6 +77,7 @@
import com.devonfw.tools.ide.tool.ToolInstallation;
import com.devonfw.tools.ide.tool.custom.CustomToolRepository;
import com.devonfw.tools.ide.tool.custom.CustomToolRepositoryImpl;
import com.devonfw.tools.ide.tool.git.Git;
import com.devonfw.tools.ide.tool.mvn.MvnRepository;
import com.devonfw.tools.ide.tool.npm.NpmRepository;
import com.devonfw.tools.ide.tool.pip.PipRepository;
Expand Down Expand Up @@ -819,10 +821,11 @@ public UrlMetadata getUrls() {

if (this.urlMetadata == null) {
if (!isTest()) {
getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
updateUrlsRepository();
}
this.urlMetadata = new UrlMetadata(this);
}

return this.urlMetadata;
}

Expand Down Expand Up @@ -1461,20 +1464,24 @@ private ValidationResult applyAndRun(CliArguments arguments, AbstractCommandlet
if (!(cmd instanceof UpgradeCommandlet)) {
verifyIdeMinVersion(false);
}
Path settingsRepository = getSettingsGitRepository();
if (settingsRepository != null) {
if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
settingsRepository, getSettingsCommitIdPath()))) {

// Inform the user that an update is available. The update message is suppressed if we are already running the update
String msg = determineSettingsUpdateMessage(cmd);
if (msg != null) {
IdeLogLevel.INTERACTION.log(LOG, msg);

if (!isInstallingGit(cmd)) {
Path settingsRepository = getSettingsGitRepository();
if (settingsRepository != null) {
if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
settingsRepository, getSettingsCommitIdPath()))) {

// Inform the user that an update is available. The update message is suppressed if we are already running the update
String msg = determineSettingsUpdateMessage(cmd);
if (msg != null) {
IdeLogLevel.INTERACTION.log(LOG, msg);
}
}
}
}
}

boolean success = ensureLicenseAgreement(cmd);
if (!success) {
return ValidationResultValid.get();
Expand Down Expand Up @@ -1992,4 +1999,28 @@ public String getDefaultWindowsGitPath() {
return DEFAULT_WINDOWS_GIT_PATH;
}

private boolean isInstallingGit(AbstractCommandlet cmd) {

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.

isInstallingGit puts tool-specific knowledge into the generic context and reads InstallCommandlet.tool directly. A generic guard such as getGitContext().findGit() != null around the settings-update check would cover ide install git the same way, without coupling AbstractIdeContext to Git, and would also avoid the same failure for other commands run without Git.


if (cmd instanceof InstallCommandlet installCommandlet) {
return installCommandlet.tool.getValue() instanceof Git;
}
return false;
}

/**
* Updates the local {@code ide-urls} repository or, if Git is not available, falls back to the existing local metadata.
*/
public void updateUrlsRepository() {

GitContext gitContext = getGitContext();

Path urlsPath = getUrlsPath();
if (gitContext.findGit() != null) {
gitContext.pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, urlsPath, null);
} else if ((urlsPath != null) && Files.isDirectory(urlsPath)) {
LOG.debug("Git is not available. Using existing URL metadata without updating it.");
} else {
gitContext.findGitRequired();

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.

On a machine without Git and without _ide/urls (a fresh Windows setup, the use case of #2273), ide install git resolves its version via getUrls() and lands here. findGitRequired() then aborts with the new hint "Try running ide install git" from GitContextImpl.java:351, so the user is sent in a circle. Please either keep the download hint without the ide install git advice when the urls repository is missing, or provide a way to get the Git metadata without Git.

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,12 @@ public Path findGitRequired() {

Path gitPath = findGit();
if (gitPath == null) {
String message = "Git " + IdeContext.IS_NOT_INSTALLED_BUT_REQUIRED;
String message = "Git " + IdeContext.IS_NOT_INSTALLED_BUT_REQUIRED
+ " Try running `ide install git` to install it.";

if (SystemInfoImpl.INSTANCE.isWindows()) {
message += IdeContext.PLEASE_DOWNLOAD_AND_INSTALL_GIT + ":\n " + IdeContext.WINDOWS_GIT_DOWNLOAD_URL;
message += "\nAlternatively, " + IdeContext.PLEASE_DOWNLOAD_AND_INSTALL_GIT + ":\n "
+ IdeContext.WINDOWS_GIT_DOWNLOAD_URL;
}
throw new CliException(message);
}
Expand Down
53 changes: 50 additions & 3 deletions cli/src/main/java/com/devonfw/tools/ide/os/WindowsHelperImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -144,16 +145,62 @@ private String findUninstallKey(String appName) {
if (out == null) {
continue;
}

for (String line : out) {
line = line.trim();
if (line.startsWith("HKEY_")) {
return line; // exact registry path (key) for tool
String key = getMatchingUninstallKey(line, appName);
if (key != null) {
return key;
}
}
}

return null;
}

/**
* Checks whether the given registry search result represents an uninstallation key whose display name matches the requested application.
*
* @param line the registry search result line to check.
* @param appName the application name to match.
* @return the matching uninstall registry key, or {@code null} if the line does not represent a matching application.
*/
private String getMatchingUninstallKey(String line, String appName) {

String key = line.trim();
if (!key.startsWith("HKEY_")) {
return null;
}

List<String> values = runReg("query", key);
if (values == null) {
return null;
}

String displayName = retrieveRegString("DisplayName", values);
if (matchesAppName(displayName, appName)) {
return key;
}

return null;
}

/**
* Checks whether a Windows application display name matches the requested application name.
*
* @param displayName the application's display name from the Windows registry.
* @param appName the application name to match.
* @return {@code true} if the names match, {@code false} otherwise.
*/
private boolean matchesAppName(String displayName, String appName) {

if ((displayName == null) || (appName == null)) {
return false;
}

return displayName.equalsIgnoreCase(appName)
|| displayName.toLowerCase(Locale.ROOT).startsWith(appName.toLowerCase(Locale.ROOT) + " ");
}

/**
* Executes a Windows registry command and returns its output.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ protected boolean isExtract() {
protected ToolInstallation doInstall(ToolInstallRequest request) {

VersionIdentifier resolvedVersion = request.getRequested().getResolvedVersion();
if (this.context.getSystemInfo().isLinux()) {
// on Linux global tools are typically installed via the package manager of the OS
if (isPackageManagerBasedInstallation()) {
// global tools are typically installed via the package manager of the OS (e.g. apt on Linux, Homebrew on macOS)
// if a global tool implements getNativePackages() to return at least one NativePackage, then we will install this way.
List<PackageManagerCommand> commands = getInstallPackageManagerCommands(resolvedVersion);
if (!commands.isEmpty()) {
Expand Down Expand Up @@ -187,7 +187,7 @@ protected ToolInstallation doInstall(ToolInstallRequest request) {
fileAccess.extract(target, downloadBinaryPath);
executable = fileAccess.findFirst(downloadBinaryPath, Files::isExecutable, false);
}
pc = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING).executable(executable);
pc = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING).executable(executable).addArgs(getInstallerArguments());
int exitCode = pc.run(ProcessMode.BACKGROUND_SILENT).getExitCode();
if (tmpDir != null) {
fileAccess.delete(tmpDir);
Expand Down Expand Up @@ -266,7 +266,7 @@ protected Path getMacApplicationsPath() {
@Override
protected VersionIdentifier resolveVersionForInstall(String edition, GenericVersionRange version) {

if (this.context.getSystemInfo().isLinux() && !getNativePackages().isEmpty()) {
if (isPackageManagerBasedInstallation() && !getNativePackages().isEmpty()) {
if (version instanceof VersionIdentifier versionIdentifier && !versionIdentifier.isPattern()) {
return versionIdentifier;
}
Expand All @@ -286,6 +286,17 @@ protected List<NativePackage> getNativePackages() {
return List.of();
}

/**
* @return {@code true} if this tool is installed via the OS native package manager (see {@link #getNativePackages()}), so that the install, version
* resolution and installed-version detection flows of this class go through the package manager instead of the standard installer/artifact flow.
* <p>
* By default this is the case on Linux only, as that is where global tools are typically installed via the package manager of the OS. Subclasses may
* override this to support the package manager on further operating systems.
*/
protected boolean isPackageManagerBasedInstallation() {
return this.context.getSystemInfo().isLinux();
}

/**
* @return the {@link List} of {@link PackageManagerCommand}s to use on Linux to install this tool. If empty, no package manager installation will be
* triggered on Linux.
Expand Down Expand Up @@ -365,8 +376,8 @@ public Map<String, String> getWindowsRegistryAppNames() {
@Override
protected EditionAndVersion computeInstalledEditionAndVersion() {

if (this.context.getSystemInfo().isLinux()) {
// on Linux global tools are typically installed via the package manager of the OS
if (isPackageManagerBasedInstallation()) {

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.

This branch conflicts with main: #2500 rewrote computeInstalledEditionAndVersion() so that every non-Windows OS already goes through getInstalledVersionForEdition() and so through getNativePackageVersion(), which already covers macOS for Git. When rebasing, this hunk can be dropped in favour of main's version, and isPackageManagerBasedInstallation() is then only needed in doInstall and resolveVersionForInstall.

// global tools are typically installed via the package manager of the OS
VersionIdentifier version = getNativePackageVersion();
return (version != null) ? new EditionAndVersion(this.tool, version) : null;
}
Expand Down Expand Up @@ -491,4 +502,8 @@ private Path findMacApplicationBundle() {
public String getMacApplicationName() {
return null;
}

protected List<String> getInstallerArguments() {

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.

getInstallerArguments() is a new protected extension point but has no Javadoc, unlike its neighbours such as getNativePackages().

Suggested change
protected List<String> getInstallerArguments() {
/**
* @return the additional arguments passed to the downloaded installer executable (e.g. silent mode flags). The default is an empty {@link List}.
*/
protected List<String> getInstallerArguments() {

return List.of();
}
}
67 changes: 67 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/tool/git/Git.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.devonfw.tools.ide.tool.git;

import java.util.List;
import java.util.Set;

import com.devonfw.tools.ide.common.Tag;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.tool.GlobalToolCommandlet;
import com.devonfw.tools.ide.tool.NativePackage;
import com.devonfw.tools.ide.tool.NativePackageManager;

/**
* {@link GlobalToolCommandlet} for <a href="https://git-scm.com/">Git</a>.
*/
public class Git extends GlobalToolCommandlet {

/**
* The constructor.
*
* @param context the {@link IdeContext}.
*/
public Git(IdeContext context) {
super(context, "git", Set.of(Tag.GIT));
}

@Override
protected String getBinaryName() {
return "git";
}

@Override
protected List<NativePackage> getNativePackages() {
return List.of(
NativePackage.of(NativePackageManager.APT, "git"),
NativePackage.of(NativePackageManager.ZYPPER, "git"),
NativePackage.of(NativePackageManager.BREW, "git"));
}

@Override
public String getWindowsRegistryAppName() {
return "Git";
}

@Override
protected List<String> getInstallerArguments() {
if (this.context.getSystemInfo().isWindows()) {
return List.of(
"/VERYSILENT",
"/NORESTART",
"/NOCANCEL",
"/SP-"
);
}
return List.of();
}

/**
* On macOS Git is installed via Homebrew (see {@link #getNativePackages()}) rather than via the DMG installer artifact, so the package manager based
* installation flow is used there too - exactly like on Linux. On Windows the standard installer artifact is still used.
*
* @return {@code true} if Git is installed via the OS native package manager.
*/
@Override
protected boolean isPackageManagerBasedInstallation() {
return this.context.getSystemInfo().isLinux() || this.context.getSystemInfo().isMac();
}
}
2 changes: 2 additions & 0 deletions cli/src/main/resources/nls/Help.properties
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ cmd.get-version.opt.--configured=print only the configured version
cmd.get-version.opt.--installed=print only the installed version
cmd.gh=Tool commandlet for GitHub CLI.
cmd.gh.detail=GitHub CLI (Command Line Interface) allows to interact with GitHub repositories, issues, and pull requests from the command line. Detailed documentation can be found at https://cli.github.com/manual/
cmd.git=Tool commandlet for Git.
cmd.git.detail=Git is a distributed version control system for tracking changes in source code. Detailed documentation can be found at https://git-scm.com/doc
cmd.go=Tool commandlet for Go (programming language).
cmd.go.detail=Go is an open-source programming language for building simple, reliable, and efficient software. Detailed documentation can be found at https://go.dev/doc/
cmd.graalvm=Tool commandlet for GraalVm (Java with native-image).
Expand Down
2 changes: 2 additions & 0 deletions cli/src/main/resources/nls/Help_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ cmd.get-version.opt.--configured=zeigt nur die konfigurierte Version
cmd.get-version.opt.--installed=zeigt nur die installierte Version
cmd.gh=Werkzeug Kommando für die Github Kommandoschnittstelle.
cmd.gh.detail=GitHub CLI (Command Line Interface) ermöglicht die Interaktion mit GitHub-Repositories, Issues und Pull Requests über die Befehlszeile. Detaillierte Dokumentation ist zu finden unter https://cli.github.com/manual/
cmd.git=Werkzeug-Kommando für Git.
cmd.git.detail=Git ist ein verteiltes Versionsverwaltungssystem zum Nachverfolgen von Änderungen am Quellcode. Ausführliche Dokumentation ist unter https://git-scm.com/doc verfügbar.
cmd.go=Werkzeug Kommando für Go (Programmiersprache).
cmd.go.detail=Go ist eine Open-Source-Programmiersprache zum Erstellen einfacher, zuverlässiger und effizienter Software. Detaillierte Dokumentation ist zu finden unter https://go.dev/doc/
cmd.graalvm=Werkzeug Kommando für GraalVm.
Expand Down
Loading
Loading