Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Release with new features and bugfixes:
* https://github.com/devonfw/IDEasy/issues/2140[#2140]: just install fails since already installed
* https://github.com/devonfw/IDEasy/issues/2137[#2137]: fix-vpn-tls-problem does not detect TLS certificate issues behind HTTP redirects
* https://github.com/devonfw/IDEasy/issues/1041[#1041]: native image has globbing active
* https://github.com/devonfw/IDEasy/issues/2133[#2133]: Add repository navigation option (-r) to icd command

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

Expand Down
102 changes: 92 additions & 10 deletions cli/src/main/package/functions
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ function icd() {
fi
local icd_project=
local icd_workspace=
local icd_repository=
local icd_repository_given=false
Comment thread
Ali-Shariati-Najafabadi marked this conversation as resolved.
# zsh aborts on unmatched globs by default (NOMATCH) - let the workspace-scan globs below behave like in bash
if [ -n "${ZSH_VERSION:-}" ]; then
setopt local_options null_glob
fi
while [ $# != 0 ]; do
case "$1" in
-p|--project)
Expand All @@ -83,8 +89,16 @@ function icd() {
icd_workspace=main
fi
;;
-r|--repository)
shift
icd_repository_given=true
if [ $# != 0 ] && [ "${1:0:1}" != "-" ]; then
icd_repository=$1
shift
fi
;;
-h|--help)
echo "USAGE: icd [-p «project»] [-w [«workspace»]]"
echo "USAGE: icd [-p «project»] [-w [«workspace»]] [-r [«repository»]]"
echo
echo "Change directory and initialize IDEasy environment."
echo "The icd command can be used as an alternative to the regular cd command."
Expand All @@ -93,9 +107,13 @@ function icd() {
echo "Without any arguments icd will navigate to your top-level project directory (IDE_HOME)."
echo
echo "OPTIONS:"
echo " -p | --project «project» The IDEasy project to cd to."
echo " -w | --workspace «workspace» The workspace inside your IDEasy project to cd to. Defaults to main."
echo " -h | --help Print this help text."
echo " -p | --project «project» The IDEasy project to cd to."
echo " -w | --workspace «workspace» The workspace inside your IDEasy project to cd to. Defaults to main."
echo " -r | --repository «repository» The repository inside the workspace to cd to. Defaults to the project name."
echo " If no workspace is given, 'main' is searched first and then all other workspaces."
echo " If no repository is given and no project-name folder is found, falls back to the"
echo " single non-hidden folder in the workspace, or the workspace itself with a warning."
echo " -h | --help Print this help text."
return
;;
*)
Expand All @@ -104,13 +122,13 @@ function icd() {
esac
done
local icd_path=${IDE_ROOT}

# Determine the project to use
if [ "${icd_project}" != "" ]; then
# Project explicitly specified
icd_path=${icd_path}/${icd_project}
elif [ "${icd_workspace}" != "" ]; then
# Workspace specified without project - infer project from current directory or IDE_HOME
elif [ "${icd_workspace}" != "" ] || [ "${icd_repository_given}" = true ]; then
# Workspace or repository specified without project - infer project from current directory or IDE_HOME
local current_project=""
local current_dir="$(pwd)"
if [[ "${current_dir}" == "${IDE_ROOT}"/* ]]; then
Expand All @@ -133,11 +151,75 @@ function icd() {
icd_path=${IDE_HOME}
fi

# Append workspace path if specified
if [ "${icd_workspace}" != "" ]; then
if [ "${icd_repository_given}" = true ]; then
# Repository specified (or implied by project name) - resolve it within the given (or searched) workspace
local icd_repo_name_given=true
local icd_repo_name="${icd_repository}"
if [ "${icd_repo_name}" = "" ]; then
icd_repo_name="$(basename "${icd_path}")"
icd_repo_name_given=false
fi
local icd_workspaces_dir="${icd_path}/workspaces"
local icd_found_workspace=
if [ "${icd_workspace}" != "" ]; then
if [ -d "${icd_workspaces_dir}/${icd_workspace}/${icd_repo_name}" ]; then
icd_found_workspace=${icd_workspace}
fi
else
if [ -d "${icd_workspaces_dir}/main/${icd_repo_name}" ]; then
icd_found_workspace=main
else
local icd_ws_dir icd_ws_name
for icd_ws_dir in "${icd_workspaces_dir}"/*/; do
[ -d "${icd_ws_dir}" ] || continue
icd_ws_name="$(basename "${icd_ws_dir}")"
if [ "${icd_ws_name}" = "main" ]; then
continue
fi
if [ -d "${icd_workspaces_dir}/${icd_ws_name}/${icd_repo_name}" ]; then
icd_found_workspace=${icd_ws_name}
break
fi
done
fi
fi
if [ "${icd_found_workspace}" != "" ]; then
icd_path=${icd_workspaces_dir}/${icd_found_workspace}/${icd_repo_name}
elif [ "${icd_repo_name_given}" = true ]; then
if [ "${icd_workspace}" != "" ]; then
echo "Error: Repository '${icd_repo_name}' not found in workspace '${icd_workspace}'." >&2
else
echo "Error: Repository '${icd_repo_name}' not found in any workspace." >&2
fi
return 1
else
local icd_fallback_workspace="${icd_workspace}"
if [ "${icd_fallback_workspace}" = "" ]; then
icd_fallback_workspace=main
fi
local icd_fallback_dir="${icd_workspaces_dir}/${icd_fallback_workspace}"
local icd_candidate= icd_candidate_count=0 icd_entry icd_entry_name
for icd_entry in "${icd_fallback_dir}"/*/; do
[ -d "${icd_entry}" ] || continue
icd_entry_name="$(basename "${icd_entry}")"
case "${icd_entry_name}" in
.*) continue ;;
esac
icd_candidate="${icd_entry}"
icd_candidate_count=$((icd_candidate_count + 1))
done
if [ "${icd_candidate_count}" = 1 ]; then
icd_path="${icd_candidate%/}"
else
echo "Warning: Repository '${icd_repo_name}' not found in any workspace and workspace '${icd_fallback_workspace}' does not contain a single unambiguous repository. Navigating to the workspace instead." >&2
icd_path="${icd_fallback_dir}"
fi
fi
elif [ "${icd_workspace}" != "" ]; then
# Append workspace path if specified
icd_path=${icd_path}/workspaces/${icd_workspace}
fi

cd "${icd_path}" || return 1
ide
return
Expand Down
102 changes: 102 additions & 0 deletions cli/src/test/functions-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,108 @@ check "legacy Git Bash (OSTYPE=msys, uname=MINGW) shows no warning" hidden msys
# MSYS environment -> no warning
check "MSYS environment shows no warning" hidden cygwin "MSYS_NT-10.0-26200"

echo
echo "Testing icd -r repository navigation in ${FUNCTIONS_FILE}"

ICD_ROOT="$(mktemp -d)"
trap 'rm -rf "${STUB_DIR}" "${ICD_ROOT}"' EXIT

# "proj" has a repo directly in main ("cli") and a nested repo in "dev" ("backend/core", with "backend" itself
# also being a valid top-level repo folder in "dev").
mkdir -p "${ICD_ROOT}/proj/workspaces/main/cli"
mkdir -p "${ICD_ROOT}/proj/workspaces/dev/backend/core"
# "proj-named" has a folder matching the project name, for the implicit "-r" fallback.
mkdir -p "${ICD_ROOT}/proj-named/workspaces/main/proj-named"
# "proj-empty" has no repo folders in main at all.
mkdir -p "${ICD_ROOT}/proj-empty/workspaces/main"
# "proj-single" has exactly one non-hidden folder in main (plus a hidden one that must be ignored).
mkdir -p "${ICD_ROOT}/proj-single/workspaces/main/onlyrepo"
mkdir -p "${ICD_ROOT}/proj-single/workspaces/main/.git"
# "proj-multi" has more than one folder in main, so the fallback is ambiguous.
mkdir -p "${ICD_ROOT}/proj-multi/workspaces/main/repoA"
mkdir -p "${ICD_ROOT}/proj-multi/workspaces/main/repoB"

# runIcd <shell> <icd-args...> : sources the functions file in a fresh <shell> with IDE_ROOT=ICD_ROOT
# and prints "<exit code>\n<final PWD>\n<combined stdout/stderr of icd>"
runIcd() {
local shell_bin="$1"
shift
FUNCTIONS_FILE="${FUNCTIONS_FILE}" STUB_DIR="${STUB_DIR}" IDE_ROOT="${ICD_ROOT}" \
"${shell_bin}" -c '
export PATH="${STUB_DIR}:${PATH}"
export IDE_HOME=""
source "${FUNCTIONS_FILE}" >/dev/null 2>&1
cd "${IDE_ROOT}" || exit 99
icd_out="$(mktemp)"
icd "$@" >"${icd_out}" 2>&1
code=$?
printf "%s\n%s\n" "${code}" "${PWD}"
cat "${icd_out}"
rm -f "${icd_out}"
' _ "$@"
}

# checkIcd <description> <shell> <expected exit code> <expected PWD suffix|""> <expected message substring|""> <icd-args...>
checkIcd() {
local description="$1" shell_bin="$2" expected_code="$3" expected_pwd_suffix="$4" expected_msg="$5"
shift 5
total=$((total + 1))
local result actual_code actual_pwd actual_msg ok=true
result="$(runIcd "${shell_bin}" "$@")"
actual_code="$(printf '%s\n' "${result}" | sed -n '1p')"
actual_pwd="$(printf '%s\n' "${result}" | sed -n '2p')"
actual_msg="$(printf '%s\n' "${result}" | tail -n +3)"
[ "${actual_code}" = "${expected_code}" ] || ok=false
if [ -n "${expected_pwd_suffix}" ]; then
case "${actual_pwd}" in
*"${expected_pwd_suffix}") ;;
*) ok=false ;;
esac
fi
if [ -n "${expected_msg}" ]; then
case "${actual_msg}" in
*"${expected_msg}"*) ;;
*) ok=false ;;
esac
fi
if [ "${ok}" = true ]; then
doSuccess "PASSED (${shell_bin}): ${description}"
else
doError "FAILED (${shell_bin}): ${description} - exit=${actual_code} (expected ${expected_code}), pwd=${actual_pwd} (expected suffix '${expected_pwd_suffix}'), msg=${actual_msg}"
failed=$((failed + 1))
fi
}

ICD_SHELLS=(bash)
if command -v zsh >/dev/null 2>&1; then
ICD_SHELLS+=(zsh)
else
echo "zsh not found on PATH - skipping zsh regression checks for icd -r (bash still covered)"
fi

for icd_shell in "${ICD_SHELLS[@]}"; do
checkIcd "-r <repo>: found directly in main" "${icd_shell}" 0 "/proj/workspaces/main/cli" "" \
-p proj -r cli
checkIcd "-r <repo>: not in main, found in another workspace" "${icd_shell}" 0 "/proj/workspaces/dev/backend" "" \
-p proj -r backend
checkIcd "-r <repo>: nested repository path within an explicit workspace" "${icd_shell}" 0 "/proj/workspaces/dev/backend/core" "" \
-p proj -w dev -r backend/core
checkIcd "-w restricts the search to that single workspace" "${icd_shell}" 1 "" "not found in workspace 'dev'" \
-p proj -w dev -r cli
checkIcd "-r <repo>: not found anywhere -> error" "${icd_shell}" 1 "" "not found in any workspace" \
-p proj -r nope
checkIcd "-r <repo>: explicit repo, nonexistent project -> error, no crash" "${icd_shell}" 1 "" "not found in any workspace" \
-p no-such-project -r foo
checkIcd "-r (implicit): resolves the project-name folder" "${icd_shell}" 0 "/proj-named/workspaces/main/proj-named" "" \
-p proj-named -r
checkIcd "-r (implicit): single unambiguous folder in main is used, hidden folder ignored" "${icd_shell}" 0 "/proj-single/workspaces/main/onlyrepo" "" \
-p proj-single -r
checkIcd "-r (implicit): multiple folders in main -> warning, cd into workspace" "${icd_shell}" 0 "/proj-multi/workspaces/main" "not contain a single unambiguous repository" \
-p proj-multi -r
checkIcd "-r (implicit): empty workspace -> warning, cd into workspace, no crash" "${icd_shell}" 0 "/proj-empty/workspaces/main" "not contain a single unambiguous repository" \
-p proj-empty -r
done

echo
if [ "${failed}" = 0 ]; then
doSuccess "All ${total} tests passed."
Expand Down
15 changes: 15 additions & 0 deletions documentation/cli.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,18 @@ IDE environment variables have been set for /~/projects/myproject in workspace m
IDE environment variables have been set for /~/projects/myproject in workspace main
[~/projects/myproject]$ echo "Wow! IDEasy is awesome!"
```

You can also use `-r «repository»` to jump straight into a repository (folder) inside a workspace.
Comment thread
Ali-Shariati-Najafabadi marked this conversation as resolved.
The repository can also be a nested path such as `-r backend/core`.
If `-w` is omitted, the workspace `main` is searched first and, if the repository is not found there, all other workspaces are searched next; `icd` then navigates into the first workspace containing the repository.
If the repository is given explicitly but not found in any workspace, `icd` fails with an error.

```bash
[/tmp]$ icd -p myproject -r myrepo
IDE environment variables have been set for /~/projects/myproject in workspace main
[~/projects/myproject/workspaces/main/myrepo]$ echo "Found and jumped straight into myrepo!"
```

If `-r` is given without a value, the project name is used as the repository name and resolved the same way.
If no folder matching the project name is found, `icd` falls back to the workspace `main` (or the workspace given via `-w`):
if that workspace contains exactly one non-hidden folder, `icd` navigates into it; otherwise it navigates into the workspace itself and prints a warning.
Loading