This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 714cc76f19 create projects from git #6961 (#6962)
714cc76f19 is described below

commit 714cc76f19b84493f4d4ede2941aec02d167535c
Author: Bart Maertens <[email protected]>
AuthorDate: Mon Apr 6 12:36:21 2026 +0000

    create projects from git #6961 (#6962)
---
 docs/hop-user-manual/modules/ROOT/nav.adoc         |   1 +
 .../modules/ROOT/pages/technology/neo4j/index.adoc |   2 +
 .../main/java/org/apache/hop/git/model/UIGit.java  |  42 +-
 plugins/misc/projects/pom.xml                      |   2 +
 plugins/misc/projects/src/assembly/assembly.xml    |   5 +
 .../projects/git/BitbucketRepositoryBrowser.java   | 189 +++++++
 .../hop/projects/git/GitHubRepositoryBrowser.java  | 145 ++++++
 .../hop/projects/git/GitLabRepositoryBrowser.java  | 147 ++++++
 .../org/apache/hop/projects/git/GitRepoAuth.java   |  62 +++
 .../apache/hop/projects/git/GitRepoProvider.java   |  87 ++++
 .../hop/projects/git/GitRepositoryBrowser.java     |  57 +++
 .../apache/hop/projects/git/GitRepositoryInfo.java |  83 +++
 .../apache/hop/projects/git/GitRepositoryPage.java |  46 ++
 .../gui/CloneFromVersionControlDialog.java         | 378 ++++++++++++++
 .../apache/hop/projects/gui/ProjectsGuiPlugin.java |  80 ++-
 .../hop/projects/gui/SelectRepositoryDialog.java   | 567 +++++++++++++++++++++
 .../apache/hop/projects/util/GitCloneHelper.java   | 110 ++++
 .../main/resources/dependencies.xml}               |  18 +-
 .../gui/messages/messages_en_US.properties         |  83 +++
 .../org/apache/hop/ui/core/gui/GuiMenuWidgets.java |   9 +-
 20 files changed, 2090 insertions(+), 23 deletions(-)

diff --git a/docs/hop-user-manual/modules/ROOT/nav.adoc 
b/docs/hop-user-manual/modules/ROOT/nav.adoc
index 66e920aef9..90da4a2b7c 100644
--- a/docs/hop-user-manual/modules/ROOT/nav.adoc
+++ b/docs/hop-user-manual/modules/ROOT/nav.adoc
@@ -474,6 +474,7 @@ under the License.
 ** xref:technology/memgraph/index.adoc[Memgraph]
 ** xref:technology/neo4j/index.adoc[Neo4j]
 *** xref:technology/neo4j/working-with-neo4j-data.adoc[Working with Neo4j data]
+*** xref:technology/neo4j/neo4j-logging-schema.adoc[Neo4j Logging Schema]
 ** xref:technology/google/index.adoc[Google]
 * xref:hop-server/index.adoc[Hop Server]
 ** xref:hop-server/rest-api.adoc[REST api]
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/technology/neo4j/index.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/technology/neo4j/index.adoc
index bc01293401..5319316205 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/technology/neo4j/index.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/technology/neo4j/index.adoc
@@ -68,3 +68,5 @@ The Neo4j examples included in the release downloads is 
described in document:
 
 xref:technology/neo4j/working-with-neo4j-data.adoc[Working with Neo4j data]
 
+xref:technology/neo4j/neo4j-logging-schema.adoc[Neo4j Logging Schema] 
describes the graph schema used when logging executions to Neo4j.
+
diff --git a/plugins/misc/git/src/main/java/org/apache/hop/git/model/UIGit.java 
b/plugins/misc/git/src/main/java/org/apache/hop/git/model/UIGit.java
index eef85ef318..a8941f703b 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/model/UIGit.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/model/UIGit.java
@@ -803,11 +803,44 @@ public class UIGit extends VCS {
     }
   }
 
+  /**
+   * Create a CredentialsProvider for token-based authentication (GitHub, 
GitLab, etc.).
+   *
+   * @param token Personal access token
+   * @return CredentialsProvider, or null if token is null/empty
+   */
+  public static CredentialsProvider createTokenCredentialsProvider(String 
token) {
+    if (token == null || token.trim().isEmpty()) {
+      return null;
+    }
+    return new UsernamePasswordCredentialsProvider("x-oauth-basic", 
token.trim());
+  }
+
   public boolean cloneRepo(String directory, String uri) {
+    return cloneRepo(directory, uri, null, 0);
+  }
+
+  /**
+   * Clone a repository with optional credentials and shallow clone depth.
+   *
+   * @param directory Local directory to clone into
+   * @param uri Repository URI (e.g. https://github.com/user/repo.git)
+   * @param credentialsProvider Optional credentials for authentication (e.g. 
token). If null and
+   *     auth fails, user will be prompted.
+   * @param depth Shallow clone depth (0 = full clone, 1+ = shallow with that 
many commits)
+   * @return true if clone succeeded
+   */
+  public boolean cloneRepo(
+      String directory, String uri, CredentialsProvider credentialsProvider, 
int depth) {
+    CredentialsProvider provider =
+        credentialsProvider != null ? credentialsProvider : 
this.credentialsProvider;
     CloneCommand cmd = Git.cloneRepository();
     cmd.setDirectory(new File(directory));
     cmd.setURI(uri);
-    cmd.setCredentialsProvider(credentialsProvider);
+    cmd.setCredentialsProvider(provider);
+    if (depth > 0) {
+      cmd.setDepth(depth);
+    }
     try {
       Git gitClone = cmd.call();
       gitClone.close();
@@ -818,12 +851,11 @@ public class UIGit extends VCS {
                   .contains(
                       
CONST_AUTHENTICATION_IS_REQUIRED_BUT_NO_CREDENTIALS_PROVIDER_HAS_BEEN_REGISTERED)
               || e.getMessage().contains(CONST_NOT_AUTHORIZED))) {
-        if (promptUsernamePassword()) {
-          return cloneRepo(directory, uri);
+        if (provider == null && promptUsernamePassword()) {
+          return cloneRepo(directory, uri, null, depth);
         }
-      } else {
-        showMessageBox(BaseMessages.getString(PKG, CONST_DIALOG_ERROR), 
e.getMessage());
       }
+      showMessageBox(BaseMessages.getString(PKG, CONST_DIALOG_ERROR), 
e.getMessage());
     }
     return false;
   }
diff --git a/plugins/misc/projects/pom.xml b/plugins/misc/projects/pom.xml
index 0d8d4dc90a..c51ee5fc65 100644
--- a/plugins/misc/projects/pom.xml
+++ b/plugins/misc/projects/pom.xml
@@ -29,4 +29,6 @@
     <packaging>jar</packaging>
     <name>Hop Plugins Miscellaneous Projects</name>
 
+    <!-- Git plugin (hop-misc-git) is loaded at runtime; GitCloneHelper uses 
its classloader via reflection -->
+    <dependencies></dependencies>
 </project>
diff --git a/plugins/misc/projects/src/assembly/assembly.xml 
b/plugins/misc/projects/src/assembly/assembly.xml
index 3dec1aa567..96d8ab832c 100644
--- a/plugins/misc/projects/src/assembly/assembly.xml
+++ b/plugins/misc/projects/src/assembly/assembly.xml
@@ -31,6 +31,11 @@
             <outputDirectory>plugins/misc/projects</outputDirectory>
             <filtered>true</filtered>
         </file>
+        <file>
+            
<source>${project.basedir}/src/main/resources/dependencies.xml</source>
+            <outputDirectory>plugins/misc/projects</outputDirectory>
+            <filtered>true</filtered>
+        </file>
     </files>
 
     <fileSets>
diff --git 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/BitbucketRepositoryBrowser.java
 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/BitbucketRepositoryBrowser.java
new file mode 100644
index 0000000000..1aea2553f3
--- /dev/null
+++ 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/BitbucketRepositoryBrowser.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.git;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import org.apache.hop.core.exception.HopException;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+
+/**
+ * Fetches repository listings from Bitbucket Cloud using the 2.0 API.
+ *
+ * <p>Bitbucket uses Basic auth with a username + app password (not an OAuth 
token). The API returns
+ * all repos accessible to that user across their workspaces via {@code 
/repositories}.
+ */
+class BitbucketRepositoryBrowser implements GitRepositoryBrowser {
+
+  private static final String DEFAULT_API = "https://api.bitbucket.org/2.0";;
+  private static final HttpClient HTTP =
+      HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
+
+  @Override
+  public GitRepositoryPage listRepositories(
+      String apiBaseUrl, GitRepoAuth auth, String searchQuery, int page) 
throws HopException {
+
+    if (auth == null || !auth.isBasicAuth()) {
+      throw new HopException("Bitbucket requires a username and app 
password.");
+    }
+
+    String base =
+        (apiBaseUrl != null && !apiBaseUrl.isBlank())
+            ? apiBaseUrl.replaceAll("/+$", "")
+            : DEFAULT_API;
+
+    // /repositories/{workspace} lists repos for one workspace; /repositories 
lists all the user
+    // has access to (requires "account" scope on the app password).
+    StringBuilder url =
+        new StringBuilder(base)
+            .append("/repositories/")
+            .append(encode(auth.getUsername()))
+            .append("?pagelen=")
+            .append(PAGE_SIZE)
+            .append("&page=")
+            .append(page)
+            .append("&sort=-updated_on");
+
+    if (searchQuery != null && !searchQuery.isBlank()) {
+      try {
+        url.append("&q=name+%7E+%22")
+            .append(java.net.URLEncoder.encode(searchQuery, "UTF-8"))
+            .append("%22");
+      } catch (java.io.UnsupportedEncodingException e) {
+        // UTF-8 is always supported
+      }
+    }
+
+    String credentials =
+        Base64.getEncoder()
+            .encodeToString(
+                (auth.getUsername() + ":" + 
auth.getPassword()).getBytes(StandardCharsets.UTF_8));
+
+    HttpRequest request =
+        HttpRequest.newBuilder()
+            .uri(URI.create(url.toString()))
+            .timeout(Duration.ofSeconds(15))
+            .header("Accept", "application/json")
+            .header("Authorization", "Basic " + credentials)
+            .GET()
+            .build();
+
+    HttpResponse<String> response;
+    try {
+      response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
+    } catch (IOException | InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new HopException(
+          "Failed to connect to Bitbucket API at " + base + ": " + 
e.getMessage(), e);
+    }
+
+    if (response.statusCode() == 401) {
+      throw new HopException(
+          "Bitbucket authentication failed. Check your username and app 
password.");
+    }
+    if (response.statusCode() == 403) {
+      throw new HopException(
+          "Bitbucket access denied (HTTP 403). Your app password may lack the 
'Repositories: Read' permission.");
+    }
+    if (response.statusCode() != 200) {
+      throw new HopException(
+          "Bitbucket API returned HTTP " + response.statusCode() + ". URL: " + 
url);
+    }
+
+    return parseResponse(response.body(), page);
+  }
+
+  private GitRepositoryPage parseResponse(String json, int page) throws 
HopException {
+    JSONParser parser = new JSONParser();
+    JSONObject root;
+    try {
+      root = (JSONObject) parser.parse(json);
+    } catch (ParseException e) {
+      throw new HopException("Failed to parse Bitbucket API response: " + 
e.getMessage(), e);
+    }
+
+    JSONArray values = (JSONArray) root.get("values");
+    if (values == null) {
+      return new GitRepositoryPage(List.of(), page, false);
+    }
+
+    List<GitRepositoryInfo> result = new ArrayList<>();
+    for (Object item : values) {
+      JSONObject repo = (JSONObject) item;
+      String name = getString(repo, "name");
+      String owner = "";
+      JSONObject ws = (JSONObject) repo.get("workspace");
+      if (ws != null) {
+        owner = getString(ws, "slug");
+      }
+      String description = getString(repo, "description");
+      boolean isPrivate = Boolean.TRUE.equals(repo.get("is_private"));
+      String updatedOn = getString(repo, "updated_on");
+
+      String httpsCloneUrl = "";
+      String sshCloneUrl = "";
+      JSONObject links = (JSONObject) repo.get("links");
+      if (links != null) {
+        JSONArray cloneLinks = (JSONArray) links.get("clone");
+        if (cloneLinks != null) {
+          for (Object cl : cloneLinks) {
+            JSONObject cloneLink = (JSONObject) cl;
+            String linkName = getString(cloneLink, "name");
+            String href = getString(cloneLink, "href");
+            if ("https".equalsIgnoreCase(linkName)) {
+              httpsCloneUrl = href;
+            } else if ("ssh".equalsIgnoreCase(linkName)) {
+              sshCloneUrl = href;
+            }
+          }
+        }
+      }
+
+      result.add(
+          new GitRepositoryInfo(
+              name, owner, description, isPrivate, updatedOn, httpsCloneUrl, 
sshCloneUrl));
+    }
+
+    boolean hasMore = root.get("next") != null;
+    return new GitRepositoryPage(result, page, hasMore);
+  }
+
+  private static String getString(JSONObject obj, String key) {
+    Object val = obj.get(key);
+    return val != null ? val.toString() : "";
+  }
+
+  private static String encode(String value) {
+    try {
+      return java.net.URLEncoder.encode(value, "UTF-8");
+    } catch (java.io.UnsupportedEncodingException e) {
+      return value;
+    }
+  }
+}
diff --git 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitHubRepositoryBrowser.java
 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitHubRepositoryBrowser.java
new file mode 100644
index 0000000000..42650b361a
--- /dev/null
+++ 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitHubRepositoryBrowser.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.git;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.exception.HopException;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+
+/**
+ * Fetches repository listings from GitHub.com or a GitHub Enterprise server.
+ *
+ * <p>Uses {@code GET /user/repos} sorted by {@code updated} to return the 
most recently active
+ * repos first, matching the mental model users expect.
+ */
+class GitHubRepositoryBrowser implements GitRepositoryBrowser {
+
+  private static final String DEFAULT_API = "https://api.github.com";;
+  private static final HttpClient HTTP =
+      HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
+
+  @Override
+  public GitRepositoryPage listRepositories(
+      String apiBaseUrl, GitRepoAuth auth, String searchQuery, int page) 
throws HopException {
+
+    String base =
+        (apiBaseUrl != null && !apiBaseUrl.isBlank())
+            ? apiBaseUrl.replaceAll("/+$", "")
+            : DEFAULT_API;
+    String url =
+        base
+            + "/user/repos?per_page="
+            + PAGE_SIZE
+            + "&page="
+            + page
+            + 
"&sort=updated&direction=desc&affiliation=owner,collaborator,organization_member";
+
+    HttpRequest.Builder requestBuilder =
+        HttpRequest.newBuilder()
+            .uri(URI.create(url))
+            .timeout(Duration.ofSeconds(15))
+            .header("Accept", "application/vnd.github+json")
+            .header("X-GitHub-Api-Version", "2022-11-28")
+            .GET();
+
+    if (auth != null && auth.isTokenBased()) {
+      requestBuilder.header("Authorization", "token " + auth.getToken());
+    }
+
+    HttpResponse<String> response;
+    try {
+      response = HTTP.send(requestBuilder.build(), 
HttpResponse.BodyHandlers.ofString());
+    } catch (IOException | InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new HopException(
+          "Failed to connect to GitHub API at " + base + ": " + 
e.getMessage(), e);
+    }
+
+    if (response.statusCode() == 401) {
+      throw new HopException(
+          "GitHub authentication failed. Check that your personal access token 
is valid and has the 'repo' scope.");
+    }
+    if (response.statusCode() == 403) {
+      throw new HopException(
+          "GitHub access denied (HTTP 403). Your token may be missing the 
'repo' scope or rate-limited.");
+    }
+    if (response.statusCode() != 200) {
+      throw new HopException("GitHub API returned HTTP " + 
response.statusCode() + ". URL: " + url);
+    }
+
+    List<GitRepositoryInfo> repos = parseRepos(response.body(), searchQuery);
+
+    // GitHub uses Link header for pagination; if we got a full page there is 
likely more.
+    boolean hasMore = repos.size() == PAGE_SIZE;
+    return new GitRepositoryPage(repos, page, hasMore);
+  }
+
+  private List<GitRepositoryInfo> parseRepos(String json, String searchQuery) 
throws HopException {
+    JSONParser parser = new JSONParser();
+    JSONArray array;
+    try {
+      array = (JSONArray) parser.parse(json);
+    } catch (ParseException e) {
+      throw new HopException("Failed to parse GitHub API response: " + 
e.getMessage(), e);
+    }
+
+    String filter = (searchQuery != null) ? searchQuery.toLowerCase() : "";
+    List<GitRepositoryInfo> result = new ArrayList<>();
+
+    for (Object item : array) {
+      JSONObject repo = (JSONObject) item;
+      String name = getString(repo, "name");
+      String owner = "";
+      JSONObject ownerObj = (JSONObject) repo.get("owner");
+      if (ownerObj != null) {
+        owner = getString(ownerObj, "login");
+      }
+      String description = getString(repo, "description");
+      boolean isPrivate = Boolean.TRUE.equals(repo.get("private"));
+      String updatedAt = getString(repo, "pushed_at");
+      String httpsCloneUrl = getString(repo, "clone_url");
+      String sshCloneUrl = getString(repo, "ssh_url");
+
+      if (!filter.isEmpty()
+          && !name.toLowerCase().contains(filter)
+          && !owner.toLowerCase().contains(filter)) {
+        continue;
+      }
+
+      result.add(
+          new GitRepositoryInfo(
+              name, owner, description, isPrivate, updatedAt, httpsCloneUrl, 
sshCloneUrl));
+    }
+    return result;
+  }
+
+  private static String getString(JSONObject obj, String key) {
+    Object val = obj.get(key);
+    return val != null ? val.toString() : "";
+  }
+}
diff --git 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitLabRepositoryBrowser.java
 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitLabRepositoryBrowser.java
new file mode 100644
index 0000000000..aa49a07467
--- /dev/null
+++ 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitLabRepositoryBrowser.java
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.git;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.exception.HopException;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+
+/**
+ * Fetches repository (project) listings from GitLab.com or a self-hosted 
GitLab instance.
+ *
+ * <p>Uses {@code GET /projects?membership=true} so only repos accessible to 
the token are returned,
+ * sorted by last activity.
+ */
+class GitLabRepositoryBrowser implements GitRepositoryBrowser {
+
+  private static final String DEFAULT_API = "https://gitlab.com/api/v4";;
+  private static final HttpClient HTTP =
+      HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
+
+  @Override
+  public GitRepositoryPage listRepositories(
+      String apiBaseUrl, GitRepoAuth auth, String searchQuery, int page) 
throws HopException {
+
+    String base =
+        (apiBaseUrl != null && !apiBaseUrl.isBlank())
+            ? apiBaseUrl.replaceAll("/+$", "")
+            : DEFAULT_API;
+
+    StringBuilder url =
+        new StringBuilder(base)
+            
.append("/projects?membership=true&order_by=last_activity_at&sort=desc")
+            .append("&per_page=")
+            .append(PAGE_SIZE)
+            .append("&page=")
+            .append(page);
+
+    if (searchQuery != null && !searchQuery.isBlank()) {
+      try {
+        url.append("&search=").append(java.net.URLEncoder.encode(searchQuery, 
"UTF-8"));
+      } catch (java.io.UnsupportedEncodingException e) {
+        // UTF-8 is always supported
+      }
+    }
+
+    HttpRequest.Builder requestBuilder =
+        HttpRequest.newBuilder()
+            .uri(URI.create(url.toString()))
+            .timeout(Duration.ofSeconds(15))
+            .header("Accept", "application/json")
+            .GET();
+
+    if (auth != null && auth.isTokenBased()) {
+      requestBuilder.header("PRIVATE-TOKEN", auth.getToken());
+    }
+
+    HttpResponse<String> response;
+    try {
+      response = HTTP.send(requestBuilder.build(), 
HttpResponse.BodyHandlers.ofString());
+    } catch (IOException | InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new HopException(
+          "Failed to connect to GitLab API at " + base + ": " + 
e.getMessage(), e);
+    }
+
+    if (response.statusCode() == 401) {
+      throw new HopException(
+          "GitLab authentication failed. Check that your personal access token 
is valid and has the 'read_api' or 'read_repository' scope.");
+    }
+    if (response.statusCode() == 403) {
+      throw new HopException(
+          "GitLab access denied (HTTP 403). Your token may lack the required 
scope.");
+    }
+    if (response.statusCode() != 200) {
+      throw new HopException("GitLab API returned HTTP " + 
response.statusCode() + ". URL: " + url);
+    }
+
+    List<GitRepositoryInfo> repos = parseRepos(response.body());
+
+    // GitLab includes X-Next-Page header; if we got a full page assume there 
is more.
+    boolean hasMore = repos.size() == PAGE_SIZE;
+    return new GitRepositoryPage(repos, page, hasMore);
+  }
+
+  private List<GitRepositoryInfo> parseRepos(String json) throws HopException {
+    JSONParser parser = new JSONParser();
+    JSONArray array;
+    try {
+      array = (JSONArray) parser.parse(json);
+    } catch (ParseException e) {
+      throw new HopException("Failed to parse GitLab API response: " + 
e.getMessage(), e);
+    }
+
+    List<GitRepositoryInfo> result = new ArrayList<>();
+
+    for (Object item : array) {
+      JSONObject repo = (JSONObject) item;
+      String name = getString(repo, "name");
+      String owner = "";
+      JSONObject ns = (JSONObject) repo.get("namespace");
+      if (ns != null) {
+        owner = getString(ns, "full_path");
+      }
+      String description = getString(repo, "description");
+      String visibility = getString(repo, "visibility");
+      boolean isPrivate = "private".equalsIgnoreCase(visibility);
+      String updatedAt = getString(repo, "last_activity_at");
+      String httpsCloneUrl = getString(repo, "http_url_to_repo");
+      String sshCloneUrl = getString(repo, "ssh_url_to_repo");
+
+      result.add(
+          new GitRepositoryInfo(
+              name, owner, description, isPrivate, updatedAt, httpsCloneUrl, 
sshCloneUrl));
+    }
+    return result;
+  }
+
+  private static String getString(JSONObject obj, String key) {
+    Object val = obj.get(key);
+    return val != null ? val.toString() : "";
+  }
+}
diff --git 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitRepoAuth.java
 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitRepoAuth.java
new file mode 100644
index 0000000000..840634c987
--- /dev/null
+++ 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitRepoAuth.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.git;
+
+/** Holds credentials for authenticating to a Git provider's API. */
+public class GitRepoAuth {
+
+  private final String token;
+  private final String username;
+  private final String password;
+
+  private GitRepoAuth(String token, String username, String password) {
+    this.token = token;
+    this.username = username;
+    this.password = password;
+  }
+
+  /** Token-based auth (GitHub PAT, GitLab PAT, GHE PAT). */
+  public static GitRepoAuth forToken(String token) {
+    return new GitRepoAuth(token, null, null);
+  }
+
+  /** Basic auth (Bitbucket username + app password). */
+  public static GitRepoAuth forBasic(String username, String password) {
+    return new GitRepoAuth(null, username, password);
+  }
+
+  public String getToken() {
+    return token;
+  }
+
+  public String getUsername() {
+    return username;
+  }
+
+  public String getPassword() {
+    return password;
+  }
+
+  public boolean isTokenBased() {
+    return token != null && !token.isBlank();
+  }
+
+  public boolean isBasicAuth() {
+    return username != null && !username.isBlank();
+  }
+}
diff --git 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitRepoProvider.java
 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitRepoProvider.java
new file mode 100644
index 0000000000..d7bca1ba2b
--- /dev/null
+++ 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitRepoProvider.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.git;
+
+public enum GitRepoProvider {
+  GITHUB_CLOUD("GitHub.com", "https://api.github.com";, "https://github.com";, 
false),
+  GITHUB_ENTERPRISE("GitHub Enterprise", null, null, true),
+  GITLAB("GitLab", "https://gitlab.com/api/v4";, "https://gitlab.com";, true),
+  BITBUCKET("Bitbucket", "https://api.bitbucket.org/2.0";, 
"https://bitbucket.org";, false);
+
+  private final String displayName;
+  private final String defaultApiBaseUrl;
+  private final String defaultWebBaseUrl;
+
+  /** Whether a custom host URL field should be offered in the UI for this 
provider. */
+  private final boolean supportsCustomHost;
+
+  GitRepoProvider(
+      String displayName,
+      String defaultApiBaseUrl,
+      String defaultWebBaseUrl,
+      boolean supportsCustomHost) {
+    this.displayName = displayName;
+    this.defaultApiBaseUrl = defaultApiBaseUrl;
+    this.defaultWebBaseUrl = defaultWebBaseUrl;
+    this.supportsCustomHost = supportsCustomHost;
+  }
+
+  public String getDisplayName() {
+    return displayName;
+  }
+
+  public String getDefaultApiBaseUrl() {
+    return defaultApiBaseUrl;
+  }
+
+  public String getDefaultWebBaseUrl() {
+    return defaultWebBaseUrl;
+  }
+
+  public boolean isSupportsCustomHost() {
+    return supportsCustomHost;
+  }
+
+  /** Returns display names for all providers, in declaration order. */
+  public static String[] displayNames() {
+    GitRepoProvider[] values = values();
+    String[] names = new String[values.length];
+    for (int i = 0; i < values.length; i++) {
+      names[i] = values[i].displayName;
+    }
+    return names;
+  }
+
+  /** Best-effort detection of provider from a repository URL. */
+  public static GitRepoProvider detectFromUrl(String url) {
+    if (url == null || url.isBlank()) {
+      return GITHUB_CLOUD;
+    }
+    String lower = url.toLowerCase();
+    if (lower.contains("github.com")) {
+      return GITHUB_CLOUD;
+    }
+    if (lower.contains("gitlab.com") || lower.contains("/api/v4")) {
+      return GITLAB;
+    }
+    if (lower.contains("bitbucket.org")) {
+      return BITBUCKET;
+    }
+    return GITHUB_CLOUD;
+  }
+}
diff --git 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitRepositoryBrowser.java
 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitRepositoryBrowser.java
new file mode 100644
index 0000000000..ff08fbcba0
--- /dev/null
+++ 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitRepositoryBrowser.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.git;
+
+import org.apache.hop.core.exception.HopException;
+
+/**
+ * Fetches repository listings from a Git provider's REST API.
+ *
+ * <p>Implementations exist for GitHub (cloud + enterprise), GitLab, and 
Bitbucket.
+ */
+public interface GitRepositoryBrowser {
+
+  int PAGE_SIZE = 50;
+
+  /**
+   * Returns one page of repositories accessible with the given credentials.
+   *
+   * @param apiBaseUrl Provider API root (e.g. {@code https://api.github.com} 
or a custom GHE /
+   *     GitLab host). May be {@code null} for providers where the default is 
baked in.
+   * @param auth Authentication credentials.
+   * @param searchQuery Optional name filter (empty/null means no filter).
+   * @param page 1-based page number.
+   * @return A page of results; never {@code null}.
+   * @throws HopException on network or API errors (message is human-readable).
+   */
+  GitRepositoryPage listRepositories(
+      String apiBaseUrl, GitRepoAuth auth, String searchQuery, int page) 
throws HopException;
+
+  /**
+   * Returns the correct {@link GitRepositoryBrowser} implementation for 
{@code provider}.
+   *
+   * @throws IllegalArgumentException for unknown providers.
+   */
+  static GitRepositoryBrowser forProvider(GitRepoProvider provider) {
+    return switch (provider) {
+      case GITHUB_CLOUD, GITHUB_ENTERPRISE -> new GitHubRepositoryBrowser();
+      case GITLAB -> new GitLabRepositoryBrowser();
+      case BITBUCKET -> new BitbucketRepositoryBrowser();
+    };
+  }
+}
diff --git 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitRepositoryInfo.java
 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitRepositoryInfo.java
new file mode 100644
index 0000000000..5a42f5f37d
--- /dev/null
+++ 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitRepositoryInfo.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.git;
+
+/** Represents a single repository returned by a Git provider's API. */
+public class GitRepositoryInfo {
+
+  private final String name;
+  private final String owner;
+  private final String description;
+  private final boolean privateRepo;
+  private final String lastUpdated;
+  private final String httpsCloneUrl;
+  private final String sshCloneUrl;
+
+  public GitRepositoryInfo(
+      String name,
+      String owner,
+      String description,
+      boolean privateRepo,
+      String lastUpdated,
+      String httpsCloneUrl,
+      String sshCloneUrl) {
+    this.name = name;
+    this.owner = owner;
+    this.description = description;
+    this.privateRepo = privateRepo;
+    this.lastUpdated = lastUpdated;
+    this.httpsCloneUrl = httpsCloneUrl;
+    this.sshCloneUrl = sshCloneUrl;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public String getOwner() {
+    return owner;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+
+  public boolean isPrivateRepo() {
+    return privateRepo;
+  }
+
+  public String getLastUpdated() {
+    return lastUpdated;
+  }
+
+  public String getHttpsCloneUrl() {
+    return httpsCloneUrl;
+  }
+
+  public String getSshCloneUrl() {
+    return sshCloneUrl;
+  }
+
+  /** Short ISO date (first 10 chars) for display, or empty string if null. */
+  public String getLastUpdatedShort() {
+    if (lastUpdated == null || lastUpdated.length() < 10) {
+      return lastUpdated != null ? lastUpdated : "";
+    }
+    return lastUpdated.substring(0, 10);
+  }
+}
diff --git 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitRepositoryPage.java
 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitRepositoryPage.java
new file mode 100644
index 0000000000..bf524f470e
--- /dev/null
+++ 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/git/GitRepositoryPage.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.git;
+
+import java.util.List;
+
+/** A page of repository results from a Git provider's API. */
+public class GitRepositoryPage {
+
+  private final List<GitRepositoryInfo> repositories;
+  private final int currentPage;
+  private final boolean hasMore;
+
+  public GitRepositoryPage(List<GitRepositoryInfo> repositories, int 
currentPage, boolean hasMore) {
+    this.repositories = repositories;
+    this.currentPage = currentPage;
+    this.hasMore = hasMore;
+  }
+
+  public List<GitRepositoryInfo> getRepositories() {
+    return repositories;
+  }
+
+  public int getCurrentPage() {
+    return currentPage;
+  }
+
+  public boolean isHasMore() {
+    return hasMore;
+  }
+}
diff --git 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/CloneFromVersionControlDialog.java
 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/CloneFromVersionControlDialog.java
new file mode 100644
index 0000000000..b619ebebe7
--- /dev/null
+++ 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/CloneFromVersionControlDialog.java
@@ -0,0 +1,378 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.gui;
+
+import java.io.File;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.config.HopConfig;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.vfs.HopVfs;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.projects.config.ProjectsConfig;
+import org.apache.hop.projects.config.ProjectsConfigSingleton;
+import org.apache.hop.projects.git.GitRepoProvider;
+import org.apache.hop.projects.project.Project;
+import org.apache.hop.projects.project.ProjectConfig;
+import org.apache.hop.projects.util.GitCloneHelper;
+import org.apache.hop.ui.core.ConstUi;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.BaseDialog;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
+import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.core.gui.WindowProperty;
+import org.apache.hop.ui.core.widget.TextVar;
+import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+public class CloneFromVersionControlDialog extends Dialog {
+  private static final Class<?> PKG = CloneFromVersionControlDialog.class;
+
+  private String returnValue;
+  private Shell shell;
+  private final PropsUi props;
+  private final IVariables variables;
+
+  private Text wUrl;
+  private TextVar wDirectory;
+  private Text wProjectName;
+  private Text wToken;
+  private Button wShallowClone;
+  private Text wShallowDepth;
+
+  public CloneFromVersionControlDialog(Shell parent, IVariables variables) {
+    super(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | SWT.RESIZE);
+    this.variables = new Variables();
+    this.variables.initializeFrom(variables);
+    props = PropsUi.getInstance();
+  }
+
+  public String open() {
+    Shell parent = getParent();
+    shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | 
SWT.RESIZE);
+    shell.setImage(
+        GuiResource.getInstance()
+            .getImage(
+                "project.svg",
+                PKG.getClassLoader(),
+                ConstUi.SMALL_ICON_SIZE,
+                ConstUi.SMALL_ICON_SIZE));
+
+    PropsUi.setLook(shell);
+
+    int margin = PropsUi.getMargin() + 2;
+    int middle = props.getMiddlePct();
+
+    shell.setLayout(new FormLayout());
+    shell.setText(BaseMessages.getString(PKG, 
"CloneFromVersionControlDialog.Shell.Name"));
+
+    Button wOk = new Button(shell, SWT.PUSH);
+    wOk.setText(BaseMessages.getString(PKG, "System.Button.OK"));
+    wOk.addListener(SWT.Selection, event -> ok());
+    Button wCancel = new Button(shell, SWT.PUSH);
+    wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel"));
+    wCancel.addListener(SWT.Selection, event -> cancel());
+    BaseTransformDialog.positionBottomButtons(shell, new Button[] {wOk, 
wCancel}, margin * 3, null);
+
+    Composite comp = new Composite(shell, SWT.NONE);
+    comp.setLayout(new FormLayout());
+    PropsUi.setLook(comp);
+
+    FormData fdComp = new FormData();
+    fdComp.left = new FormAttachment(0, 0);
+    fdComp.right = new FormAttachment(100, 0);
+    fdComp.top = new FormAttachment(0, 0);
+    fdComp.bottom = new FormAttachment(wOk, -margin);
+    comp.setLayoutData(fdComp);
+
+    Control lastControl = null;
+
+    // URL
+    Label wlUrl = new Label(comp, SWT.RIGHT);
+    wlUrl.setText(BaseMessages.getString(PKG, 
"CloneFromVersionControlDialog.Label.Url"));
+    FormData fdlUrl = new FormData();
+    fdlUrl.left = new FormAttachment(0, 0);
+    fdlUrl.right = new FormAttachment(middle, 0);
+    fdlUrl.top = new FormAttachment(0, margin * 2);
+    wlUrl.setLayoutData(fdlUrl);
+    Button wBrowseRepo = new Button(comp, SWT.PUSH);
+    wBrowseRepo.setText(
+        BaseMessages.getString(PKG, 
"CloneFromVersionControlDialog.Button.BrowseRepo"));
+    FormData fdbBrowseRepo = new FormData();
+    fdbBrowseRepo.right = new FormAttachment(99, 0);
+    fdbBrowseRepo.top = new FormAttachment(wlUrl, 0, SWT.CENTER);
+    wBrowseRepo.setLayoutData(fdbBrowseRepo);
+    wBrowseRepo.addListener(SWT.Selection, e -> browseRepository());
+    wUrl = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.LEFT);
+    FormData fdUrl = new FormData();
+    fdUrl.left = new FormAttachment(middle, margin);
+    fdUrl.right = new FormAttachment(wBrowseRepo, -margin);
+    fdUrl.top = new FormAttachment(wlUrl, 0, SWT.CENTER);
+    wUrl.setLayoutData(fdUrl);
+    lastControl = wUrl;
+
+    // Token (optional) — placed directly below the URL / Browse row
+    Label wlToken = new Label(comp, SWT.RIGHT);
+    wlToken.setText(BaseMessages.getString(PKG, 
"CloneFromVersionControlDialog.Label.Token"));
+    FormData fdlToken = new FormData();
+    fdlToken.left = new FormAttachment(0, 0);
+    fdlToken.right = new FormAttachment(middle, 0);
+    fdlToken.top = new FormAttachment(lastControl, margin);
+    wlToken.setLayoutData(fdlToken);
+    wToken = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.LEFT | SWT.PASSWORD);
+    PropsUi.setLook(wToken);
+    FormData fdToken = new FormData();
+    fdToken.left = new FormAttachment(middle, margin);
+    fdToken.right = new FormAttachment(99, 0);
+    fdToken.top = new FormAttachment(wlToken, 0, SWT.CENTER);
+    wToken.setLayoutData(fdToken);
+    wToken.setToolTipText(
+        BaseMessages.getString(PKG, 
"CloneFromVersionControlDialog.Label.Token.Tooltip"));
+    lastControl = wToken;
+
+    // Directory
+    Label wlDir = new Label(comp, SWT.RIGHT);
+    wlDir.setText(BaseMessages.getString(PKG, 
"CloneFromVersionControlDialog.Label.Directory"));
+    FormData fdlDir = new FormData();
+    fdlDir.left = new FormAttachment(0, 0);
+    fdlDir.right = new FormAttachment(middle, 0);
+    fdlDir.top = new FormAttachment(lastControl, margin);
+    wlDir.setLayoutData(fdlDir);
+    Button wbDir = new Button(comp, SWT.PUSH);
+    wbDir.setText(BaseMessages.getString(PKG, 
"CloneFromVersionControlDialog.Button.Browse"));
+    FormData fdbDir = new FormData();
+    fdbDir.right = new FormAttachment(99, 0);
+    fdbDir.top = new FormAttachment(wlDir, 0, SWT.CENTER);
+    wbDir.setLayoutData(fdbDir);
+    wbDir.addListener(SWT.Selection, e -> browseDirectory());
+    wDirectory = new TextVar(variables, comp, SWT.SINGLE | SWT.BORDER | 
SWT.LEFT);
+    FormData fdDir = new FormData();
+    fdDir.left = new FormAttachment(middle, margin);
+    fdDir.right = new FormAttachment(wbDir, -margin);
+    fdDir.top = new FormAttachment(wlDir, 0, SWT.CENTER);
+    wDirectory.setLayoutData(fdDir);
+    lastControl = wDirectory;
+
+    // Project name
+    Label wlName = new Label(comp, SWT.RIGHT);
+    wlName.setText(BaseMessages.getString(PKG, 
"CloneFromVersionControlDialog.Label.ProjectName"));
+    FormData fdlName = new FormData();
+    fdlName.left = new FormAttachment(0, 0);
+    fdlName.right = new FormAttachment(middle, 0);
+    fdlName.top = new FormAttachment(lastControl, margin);
+    wlName.setLayoutData(fdlName);
+    wProjectName = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.LEFT);
+    FormData fdName = new FormData();
+    fdName.left = new FormAttachment(middle, margin);
+    fdName.right = new FormAttachment(99, 0);
+    fdName.top = new FormAttachment(wlName, 0, SWT.CENTER);
+    wProjectName.setLayoutData(fdName);
+    wUrl.addListener(SWT.Modify, e -> updateProjectNameFromUrl());
+    lastControl = wProjectName;
+
+    // Shallow clone
+    Label wlShallow = new Label(comp, SWT.RIGHT);
+    wlShallow.setText(
+        BaseMessages.getString(PKG, 
"CloneFromVersionControlDialog.Label.ShallowClone"));
+    FormData fdlShallow = new FormData();
+    fdlShallow.left = new FormAttachment(0, 0);
+    fdlShallow.right = new FormAttachment(middle, 0);
+    fdlShallow.top = new FormAttachment(lastControl, margin * 2);
+    wlShallow.setLayoutData(fdlShallow);
+    wShallowClone = new Button(comp, SWT.CHECK);
+    FormData fdShallow = new FormData();
+    fdShallow.left = new FormAttachment(middle, margin);
+    fdShallow.top = new FormAttachment(wlShallow, 0, SWT.CENTER);
+    wShallowClone.setLayoutData(fdShallow);
+    wShallowClone.addListener(SWT.Selection, e -> updateShallowDepthState());
+    wShallowDepth = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.LEFT);
+    PropsUi.setLook(wShallowDepth);
+    wShallowDepth.setText("1");
+    FormData fdDepth = new FormData();
+    fdDepth.left = new FormAttachment(wShallowClone, margin);
+    fdDepth.top = new FormAttachment(wlShallow, 0, SWT.CENTER);
+    fdDepth.width = 60;
+    wShallowDepth.setLayoutData(fdDepth);
+    Label wlCommits = new Label(comp, SWT.LEFT);
+    wlCommits.setText(
+        " "
+            + BaseMessages.getString(
+                PKG, 
"CloneFromVersionControlDialog.Label.ShallowClone.Commits"));
+    FormData fdlCommits = new FormData();
+    fdlCommits.left = new FormAttachment(wShallowDepth, margin);
+    fdlCommits.top = new FormAttachment(wlShallow, 0, SWT.CENTER);
+    wlCommits.setLayoutData(fdlCommits);
+
+    getData();
+    updateShallowDepthState();
+
+    shell.setDefaultButton(wOk);
+    wUrl.setFocus();
+    BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel());
+
+    return returnValue;
+  }
+
+  private void browseRepository() {
+    GitRepoProvider detectedProvider = 
GitRepoProvider.detectFromUrl(wUrl.getText().trim());
+    SelectRepositoryDialog dialog =
+        new SelectRepositoryDialog(shell, detectedProvider, 
wToken.getText().trim());
+    String cloneUrl = dialog.open();
+    if (cloneUrl != null) {
+      wUrl.setText(cloneUrl);
+      String repoName = dialog.getSelectedRepoName();
+      if (repoName != null && StringUtils.isEmpty(wProjectName.getText())) {
+        wProjectName.setText(repoName);
+      }
+    }
+  }
+
+  private void updateShallowDepthState() {
+    wShallowDepth.setEnabled(wShallowClone.getSelection());
+  }
+
+  private void updateProjectNameFromUrl() {
+    if (StringUtils.isEmpty(wProjectName.getText())) {
+      String url = wUrl.getText();
+      if (StringUtils.isNotEmpty(url)) {
+        String baseName = 
FilenameUtils.getBaseName(url.replaceFirst("\\.git$", ""));
+        if (StringUtils.isNotEmpty(baseName)) {
+          wProjectName.setText(baseName);
+        }
+      }
+    }
+  }
+
+  private void browseDirectory() {
+    BaseDialog.presentDirectoryDialog(shell, wDirectory, variables);
+  }
+
+  private void getData() {
+    ProjectsConfig config = ProjectsConfigSingleton.getConfig();
+    String standardFolder = 
variables.resolve(config.getStandardProjectsFolder());
+    wDirectory.setText(Const.NVL(standardFolder, ""));
+  }
+
+  private void ok() {
+    try {
+      String url = wUrl.getText().trim();
+      if (StringUtils.isEmpty(url)) {
+        throw new HopException("Please specify the repository URL");
+      }
+      String directory = variables.resolve(wDirectory.getText());
+      if (StringUtils.isEmpty(directory)) {
+        throw new HopException("Please specify the directory");
+      }
+      String projectName = wProjectName.getText().trim();
+      if (StringUtils.isEmpty(projectName)) {
+        projectName = FilenameUtils.getBaseName(url.replaceFirst("\\.git$", 
""));
+      }
+      if (StringUtils.isEmpty(projectName)) {
+        throw new HopException("Please specify a project name");
+      }
+
+      ProjectsConfig config = ProjectsConfigSingleton.getConfig();
+      if (config.findProjectConfig(projectName) != null) {
+        throw new HopException("Project '" + projectName + "' already exists");
+      }
+
+      String clonePath = directory + File.separator + projectName;
+      FileObject cloneDir = HopVfs.getFileObject(clonePath);
+      if (cloneDir.exists()) {
+        throw new HopException("Directory '" + clonePath + "' already exists");
+      }
+
+      int depth = 0;
+      if (wShallowClone.getSelection()) {
+        try {
+          depth = Integer.parseInt(wShallowDepth.getText().trim());
+          if (depth < 1) {
+            depth = 1;
+          }
+        } catch (NumberFormatException e) {
+          depth = 1;
+        }
+      }
+
+      if (!GitCloneHelper.cloneRepo(clonePath, url, wToken.getText(), depth)) {
+        return;
+      }
+
+      String defaultConfigFile = 
variables.resolve(config.getDefaultProjectConfigFile());
+      ProjectConfig projectConfig = new ProjectConfig(projectName, clonePath, 
defaultConfigFile);
+      Project project = new Project();
+      project.setParentProjectName(config.getStandardParentProject());
+
+      String configFilename = 
projectConfig.getActualProjectConfigFilename(variables);
+      FileObject configFile = HopVfs.getFileObject(configFilename);
+      if (!configFile.exists()) {
+        project.setConfigFilename(configFilename);
+        if (!configFile.getParent().exists()) {
+          configFile.getParent().createFolder();
+        }
+        project.saveToFile();
+      } else {
+        project.setConfigFilename(configFilename);
+        project.readFromFile();
+      }
+
+      config.addProjectConfig(projectConfig);
+      HopConfig.getInstance().saveToFile();
+
+      HopGui hopGui = HopGui.getInstance();
+      ProjectsGuiPlugin.updateProjectToolItem(projectName);
+      ProjectsGuiPlugin.enableHopGuiProject(projectName, project, null);
+
+      returnValue = projectName;
+      dispose();
+    } catch (Exception e) {
+      new ErrorDialog(
+          shell,
+          BaseMessages.getString(
+              ProjectsGuiPlugin.PKG, 
"ProjectGuiPlugin.AddProject.Error.Dialog.Header"),
+          BaseMessages.getString(
+              ProjectsGuiPlugin.PKG, 
"ProjectGuiPlugin.AddProject.Error.Dialog.Message"),
+          e);
+    }
+  }
+
+  private void cancel() {
+    returnValue = null;
+    dispose();
+  }
+
+  private void dispose() {
+    props.setScreen(new WindowProperty(shell));
+    shell.dispose();
+  }
+}
diff --git 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
index 77946a9fa2..979f46066b 100644
--- 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
+++ 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
@@ -116,6 +116,12 @@ public class ProjectsGuiPlugin {
   public static final String ID_TOOLBAR_ITEM_PROJECT = 
"toolbar-item-10000-project";
   public static final String ID_CONTEXT_MENU_PROJECT = "context-menu-project";
   public static final String ID_CONTEXT_MENU_PROJECT_ADD = 
"context-menu-project-40010-add";
+  public static final String ID_CONTEXT_MENU_PROJECT_ADD_FROM_VC =
+      "context-menu-project-40011-add-from-vc";
+  public static final String ID_CONTEXT_MENU_PROJECT_ADD_FROM_EXISTING =
+      "context-menu-project-40012-add-from-existing";
+  public static final String ID_CONTEXT_MENU_PROJECT_ADD_FROM_TEMPLATE =
+      "context-menu-project-40013-add-from-template";
   public static final String ID_CONTEXT_MENU_PROJECT_EDIT = 
"context-menu-project-40020-edit";
   public static final String ID_CONTEXT_MENU_PROJECT_DELETE = 
"context-menu-project-40030-delete";
 
@@ -363,7 +369,7 @@ public class ProjectsGuiPlugin {
     return info != null ? info.path : null;
   }
 
-  private static void updateProjectToolItem(String projectName) {
+  public static void updateProjectToolItem(String projectName) {
     GuiToolbarWidgets statusWidgets = 
HopGui.getInstance().getStatusToolbarWidgets();
     if (projectName == null || StringUtils.isEmpty(projectName)) {
       statusWidgets.setToolbarItemText(ID_TOOLBAR_ITEM_PROJECT, "");
@@ -916,7 +922,77 @@ public class ProjectsGuiPlugin {
       label = "i18n::HopGui.Toolbar.Project.Add.Label",
       toolTip = "i18n::HopGui.Toolbar.Project.Add.Tooltip",
       image = "ui/images/add.svg")
-  public void addNewProject() {
+  public void addProjectMenu() {
+    // Cascade menu - no action, submenu opens on hover
+  }
+
+  @GuiMenuElement(
+      root = ID_CONTEXT_MENU_PROJECT,
+      parentId = ID_CONTEXT_MENU_PROJECT_ADD,
+      id = ID_CONTEXT_MENU_PROJECT_ADD_FROM_VC,
+      label = "i18n::HopGui.Toolbar.Project.Add.FromVersionControl.Label",
+      toolTip = "i18n::HopGui.Toolbar.Project.Add.FromVersionControl.Tooltip",
+      image = "ui/images/add.svg")
+  public void addProjectFromVersionControl() {
+    addProjectFromVersionControlInternal();
+  }
+
+  @GuiMenuElement(
+      root = ID_CONTEXT_MENU_PROJECT,
+      parentId = ID_CONTEXT_MENU_PROJECT_ADD,
+      id = ID_CONTEXT_MENU_PROJECT_ADD_FROM_EXISTING,
+      label = "i18n::HopGui.Toolbar.Project.Add.FromExisting.Label",
+      toolTip = "i18n::HopGui.Toolbar.Project.Add.FromExisting.Tooltip",
+      image = "ui/images/add.svg")
+  public void addProjectFromExistingSources() {
+    addProjectFromExistingSourcesInternal();
+  }
+
+  private void addProjectFromVersionControlInternal() {
+    try {
+      HopGui hopGui = HopGui.getInstance();
+      if (hopGui == null) {
+        LogChannel.GENERAL.logError("HopGui is not available");
+        return;
+      }
+      Shell shell = hopGui.getActiveShell();
+      if (shell == null) {
+        shell = hopGui.getShell();
+      }
+      if (shell == null) {
+        org.eclipse.swt.widgets.Display display = 
org.eclipse.swt.widgets.Display.getCurrent();
+        if (display != null) {
+          shell = display.getActiveShell();
+        }
+      }
+      if (shell == null || shell.isDisposed()) {
+        LogChannel.GENERAL.logError(
+            "No valid shell available for dialog. HopGui may not be fully 
initialized.");
+        return;
+      }
+      IVariables variables = hopGui.getVariables();
+      if (variables == null) {
+        variables = Variables.getADefaultVariableSpace();
+      }
+      CloneFromVersionControlDialog dialog = new 
CloneFromVersionControlDialog(shell, variables);
+      dialog.open();
+    } catch (Exception e) {
+      LogChannel.GENERAL.logError("Error opening 'Add project from version 
control' dialog", e);
+      HopGui hopGui = HopGui.getInstance();
+      if (hopGui != null) {
+        Shell shell = hopGui.getShell();
+        if (shell != null && !shell.isDisposed()) {
+          new ErrorDialog(
+              shell,
+              BaseMessages.getString(PKG, 
"ProjectGuiPlugin.AddProject.Error.Dialog.Header"),
+              BaseMessages.getString(PKG, 
"ProjectGuiPlugin.AddProject.Error.Dialog.Message"),
+              e);
+        }
+      }
+    }
+  }
+
+  private void addProjectFromExistingSourcesInternal() {
     HopGui hopGui = HopGui.getInstance();
     IVariables variables = hopGui.getVariables();
 
diff --git 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/SelectRepositoryDialog.java
 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/SelectRepositoryDialog.java
new file mode 100644
index 0000000000..759f52adc8
--- /dev/null
+++ 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/SelectRepositoryDialog.java
@@ -0,0 +1,567 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.gui;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.projects.git.GitRepoAuth;
+import org.apache.hop.projects.git.GitRepoProvider;
+import org.apache.hop.projects.git.GitRepositoryBrowser;
+import org.apache.hop.projects.git.GitRepositoryInfo;
+import org.apache.hop.projects.git.GitRepositoryPage;
+import org.apache.hop.ui.core.ConstUi;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
+import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.core.gui.WindowProperty;
+import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Dialog that lets users browse and select a repository from GitHub, GitHub 
Enterprise, GitLab, or
+ * Bitbucket. Returns the selected HTTPS clone URL (and repo name) to the 
calling dialog.
+ */
+public class SelectRepositoryDialog extends Dialog {
+
+  private static final Class<?> PKG = SelectRepositoryDialog.class;
+
+  private static final int COL_NAME = 0;
+  private static final int COL_OWNER = 1;
+  private static final int COL_VISIBILITY = 2;
+  private static final int COL_UPDATED = 3;
+
+  private final PropsUi props;
+
+  /** Previously chosen provider (pre-selects the combo). */
+  private final GitRepoProvider initialProvider;
+
+  /** Previously entered token (pre-fills the token field). */
+  private final String initialToken;
+
+  private Shell shell;
+
+  private Combo wProvider;
+  private Label wlHost;
+  private Text wHost;
+  private Label wlToken;
+  private Text wToken;
+  private Label wlUsername;
+  private Text wUsername;
+  private Label wlPassword;
+  private Text wPassword;
+  private Text wFilter;
+  private Table wRepoTable;
+  private Button wLoadMore;
+  private Label wStatus;
+
+  /** The HTTPS clone URL of the selected repository, or {@code null} if 
cancelled. */
+  private String selectedCloneUrl;
+
+  /** The name of the selected repository, or {@code null} if cancelled. */
+  private String selectedRepoName;
+
+  private final List<GitRepositoryInfo> loadedRepos = new ArrayList<>();
+  private int currentPage = 1;
+  private boolean hasMore = false;
+
+  public SelectRepositoryDialog(
+      Shell parent, GitRepoProvider initialProvider, String initialToken) {
+    super(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | SWT.RESIZE);
+    this.initialProvider = initialProvider != null ? initialProvider : 
GitRepoProvider.GITHUB_CLOUD;
+    this.initialToken = initialToken != null ? initialToken : "";
+    this.props = PropsUi.getInstance();
+  }
+
+  /**
+   * Opens the dialog.
+   *
+   * @return the HTTPS clone URL of the selected repository, or {@code null} 
if cancelled.
+   */
+  public String open() {
+    Shell parent = getParent();
+    shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | 
SWT.RESIZE);
+    shell.setImage(
+        GuiResource.getInstance()
+            .getImage(
+                "project.svg",
+                PKG.getClassLoader(),
+                ConstUi.SMALL_ICON_SIZE,
+                ConstUi.SMALL_ICON_SIZE));
+
+    PropsUi.setLook(shell);
+    shell.setLayout(new FormLayout());
+    shell.setText(BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Shell.Name"));
+
+    int margin = PropsUi.getMargin() + 2;
+    int middle = props.getMiddlePct();
+
+    // --- Bottom buttons ---
+    Button wOk = new Button(shell, SWT.PUSH);
+    wOk.setText(BaseMessages.getString(PKG, "System.Button.OK"));
+    wOk.addListener(SWT.Selection, e -> ok());
+    wOk.setEnabled(false); // enabled only when a repo is selected
+
+    Button wCancel = new Button(shell, SWT.PUSH);
+    wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel"));
+    wCancel.addListener(SWT.Selection, e -> cancel());
+
+    BaseTransformDialog.positionBottomButtons(shell, new Button[] {wOk, 
wCancel}, margin * 3, null);
+
+    // Status bar just above buttons
+    wStatus = new Label(shell, SWT.LEFT);
+    wStatus.setText("");
+    PropsUi.setLook(wStatus);
+    FormData fdStatus = new FormData();
+    fdStatus.left = new FormAttachment(0, margin);
+    fdStatus.right = new FormAttachment(100, -margin);
+    fdStatus.bottom = new FormAttachment(wOk, -margin);
+    wStatus.setLayoutData(fdStatus);
+
+    // --- Scrollable content area ---
+    Composite comp = new Composite(shell, SWT.NONE);
+    comp.setLayout(new FormLayout());
+    PropsUi.setLook(comp);
+    FormData fdComp = new FormData();
+    fdComp.left = new FormAttachment(0, 0);
+    fdComp.right = new FormAttachment(100, 0);
+    fdComp.top = new FormAttachment(0, 0);
+    fdComp.bottom = new FormAttachment(wStatus, -margin);
+    comp.setLayoutData(fdComp);
+
+    // --- Provider ---
+    Label wlProvider = new Label(comp, SWT.RIGHT);
+    wlProvider.setText(BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Label.Provider"));
+    PropsUi.setLook(wlProvider);
+    FormData fdlProvider = new FormData();
+    fdlProvider.left = new FormAttachment(0, 0);
+    fdlProvider.right = new FormAttachment(middle, 0);
+    fdlProvider.top = new FormAttachment(0, margin * 2);
+    wlProvider.setLayoutData(fdlProvider);
+
+    wProvider = new Combo(comp, SWT.DROP_DOWN | SWT.READ_ONLY);
+    wProvider.setItems(GitRepoProvider.displayNames());
+    PropsUi.setLook(wProvider);
+    FormData fdProvider = new FormData();
+    fdProvider.left = new FormAttachment(middle, margin);
+    fdProvider.right = new FormAttachment(99, 0);
+    fdProvider.top = new FormAttachment(wlProvider, 0, SWT.CENTER);
+    wProvider.setLayoutData(fdProvider);
+    wProvider.addListener(SWT.Selection, e -> updateProviderState());
+
+    // --- Host (GHE / self-hosted GitLab) ---
+    wlHost = new Label(comp, SWT.RIGHT);
+    wlHost.setText(BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Label.Host"));
+    PropsUi.setLook(wlHost);
+    FormData fdlHost = new FormData();
+    fdlHost.left = new FormAttachment(0, 0);
+    fdlHost.right = new FormAttachment(middle, 0);
+    fdlHost.top = new FormAttachment(wProvider, margin);
+    wlHost.setLayoutData(fdlHost);
+
+    wHost = new Text(comp, SWT.SINGLE | SWT.BORDER);
+    PropsUi.setLook(wHost);
+    wHost.setToolTipText(BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Label.Host.Tooltip"));
+    FormData fdHost = new FormData();
+    fdHost.left = new FormAttachment(middle, margin);
+    fdHost.right = new FormAttachment(99, 0);
+    fdHost.top = new FormAttachment(wlHost, 0, SWT.CENTER);
+    wHost.setLayoutData(fdHost);
+
+    // --- Token (GitHub, GitLab) ---
+    wlToken = new Label(comp, SWT.RIGHT);
+    wlToken.setText(BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Label.Token"));
+    PropsUi.setLook(wlToken);
+    FormData fdlToken = new FormData();
+    fdlToken.left = new FormAttachment(0, 0);
+    fdlToken.right = new FormAttachment(middle, 0);
+    fdlToken.top = new FormAttachment(wHost, margin);
+    wlToken.setLayoutData(fdlToken);
+
+    wToken = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.PASSWORD);
+    PropsUi.setLook(wToken);
+    wToken.setToolTipText(
+        BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Label.Token.Tooltip"));
+    FormData fdToken = new FormData();
+    fdToken.left = new FormAttachment(middle, margin);
+    fdToken.right = new FormAttachment(99, 0);
+    fdToken.top = new FormAttachment(wlToken, 0, SWT.CENTER);
+    wToken.setLayoutData(fdToken);
+
+    // --- Username (Bitbucket) ---
+    wlUsername = new Label(comp, SWT.RIGHT);
+    wlUsername.setText(BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Label.Username"));
+    PropsUi.setLook(wlUsername);
+    FormData fdlUsername = new FormData();
+    fdlUsername.left = new FormAttachment(0, 0);
+    fdlUsername.right = new FormAttachment(middle, 0);
+    fdlUsername.top = new FormAttachment(wHost, margin);
+    wlUsername.setLayoutData(fdlUsername);
+
+    wUsername = new Text(comp, SWT.SINGLE | SWT.BORDER);
+    PropsUi.setLook(wUsername);
+    FormData fdUsername = new FormData();
+    fdUsername.left = new FormAttachment(middle, margin);
+    fdUsername.right = new FormAttachment(99, 0);
+    fdUsername.top = new FormAttachment(wlUsername, 0, SWT.CENTER);
+    wUsername.setLayoutData(fdUsername);
+
+    // --- App password (Bitbucket) ---
+    wlPassword = new Label(comp, SWT.RIGHT);
+    wlPassword.setText(BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Label.AppPassword"));
+    PropsUi.setLook(wlPassword);
+    FormData fdlPassword = new FormData();
+    fdlPassword.left = new FormAttachment(0, 0);
+    fdlPassword.right = new FormAttachment(middle, 0);
+    fdlPassword.top = new FormAttachment(wUsername, margin);
+    wlPassword.setLayoutData(fdlPassword);
+
+    wPassword = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.PASSWORD);
+    PropsUi.setLook(wPassword);
+    wPassword.setToolTipText(
+        BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Label.AppPassword.Tooltip"));
+    FormData fdPassword = new FormData();
+    fdPassword.left = new FormAttachment(middle, margin);
+    fdPassword.right = new FormAttachment(99, 0);
+    fdPassword.top = new FormAttachment(wlPassword, 0, SWT.CENTER);
+    wPassword.setLayoutData(fdPassword);
+
+    // --- Filter + Search button ---
+    Label wlFilter = new Label(comp, SWT.RIGHT);
+    wlFilter.setText(BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Label.Filter"));
+    PropsUi.setLook(wlFilter);
+    FormData fdlFilter = new FormData();
+    fdlFilter.left = new FormAttachment(0, 0);
+    fdlFilter.right = new FormAttachment(middle, 0);
+    fdlFilter.top = new FormAttachment(wToken, margin * 2);
+    wlFilter.setLayoutData(fdlFilter);
+
+    Button wSearch = new Button(comp, SWT.PUSH);
+    wSearch.setText(BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Button.Search"));
+    FormData fdSearch = new FormData();
+    fdSearch.right = new FormAttachment(99, 0);
+    fdSearch.top = new FormAttachment(wlFilter, 0, SWT.CENTER);
+    wSearch.setLayoutData(fdSearch);
+    wSearch.addListener(SWT.Selection, e -> loadRepos(true));
+
+    wFilter = new Text(comp, SWT.SINGLE | SWT.BORDER);
+    PropsUi.setLook(wFilter);
+    wFilter.setToolTipText(
+        BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Label.Filter.Tooltip"));
+    FormData fdFilter = new FormData();
+    fdFilter.left = new FormAttachment(middle, margin);
+    fdFilter.right = new FormAttachment(wSearch, -margin);
+    fdFilter.top = new FormAttachment(wlFilter, 0, SWT.CENTER);
+    wFilter.setLayoutData(fdFilter);
+    // Allow pressing Enter in filter field to trigger search
+    wFilter.addListener(SWT.DefaultSelection, e -> loadRepos(true));
+
+    // --- Repository table ---
+    wRepoTable = new Table(comp, SWT.BORDER | SWT.FULL_SELECTION | SWT.SINGLE 
| SWT.V_SCROLL);
+    wRepoTable.setHeaderVisible(true);
+    wRepoTable.setLinesVisible(true);
+    PropsUi.setLook(wRepoTable);
+
+    TableColumn colName = new TableColumn(wRepoTable, SWT.LEFT);
+    colName.setText(BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Table.Col.Name"));
+    colName.setWidth(220);
+
+    TableColumn colOwner = new TableColumn(wRepoTable, SWT.LEFT);
+    colOwner.setText(BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Table.Col.Owner"));
+    colOwner.setWidth(160);
+
+    TableColumn colVisibility = new TableColumn(wRepoTable, SWT.LEFT);
+    colVisibility.setText(
+        BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Table.Col.Visibility"));
+    colVisibility.setWidth(80);
+
+    TableColumn colUpdated = new TableColumn(wRepoTable, SWT.LEFT);
+    colUpdated.setText(BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Table.Col.Updated"));
+    colUpdated.setWidth(110);
+
+    FormData fdTable = new FormData();
+    fdTable.left = new FormAttachment(0, margin);
+    fdTable.right = new FormAttachment(99, 0);
+    fdTable.top = new FormAttachment(wSearch, margin);
+    fdTable.height = 300;
+    wRepoTable.setLayoutData(fdTable);
+
+    // Single click selects; double click selects and closes
+    wRepoTable.addListener(
+        SWT.Selection,
+        e -> {
+          wOk.setEnabled(wRepoTable.getSelectionCount() > 0);
+        });
+    wRepoTable.addListener(SWT.DefaultSelection, e -> ok());
+
+    // --- Load more button ---
+    wLoadMore = new Button(comp, SWT.PUSH);
+    wLoadMore.setText(BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Button.LoadMore"));
+    wLoadMore.setEnabled(false);
+    FormData fdLoadMore = new FormData();
+    fdLoadMore.left = new FormAttachment(middle, margin);
+    fdLoadMore.top = new FormAttachment(wRepoTable, margin);
+    wLoadMore.setLayoutData(fdLoadMore);
+    wLoadMore.addListener(SWT.Selection, e -> loadRepos(false));
+
+    // --- Initial data ---
+    selectProvider(initialProvider);
+    wToken.setText(initialToken);
+
+    shell.setSize(720, 580);
+    shell.open();
+
+    while (!shell.isDisposed()) {
+      if (!shell.getDisplay().readAndDispatch()) {
+        shell.getDisplay().sleep();
+      }
+    }
+    return selectedCloneUrl;
+  }
+
+  /** Returns the repo name of the last selection. */
+  public String getSelectedRepoName() {
+    return selectedRepoName;
+  }
+
+  // 
---------------------------------------------------------------------------
+  // UI helpers
+  // 
---------------------------------------------------------------------------
+
+  private void selectProvider(GitRepoProvider provider) {
+    GitRepoProvider[] values = GitRepoProvider.values();
+    for (int i = 0; i < values.length; i++) {
+      if (values[i] == provider) {
+        wProvider.select(i);
+        break;
+      }
+    }
+    updateProviderState();
+  }
+
+  private GitRepoProvider selectedProvider() {
+    int idx = wProvider.getSelectionIndex();
+    if (idx < 0) {
+      return GitRepoProvider.GITHUB_CLOUD;
+    }
+    return GitRepoProvider.values()[idx];
+  }
+
+  private void updateProviderState() {
+    GitRepoProvider provider = selectedProvider();
+    boolean isBitbucket = provider == GitRepoProvider.BITBUCKET;
+    boolean needsHost = provider.isSupportsCustomHost();
+
+    // Host field: shown for GHE and GitLab (self-hosted)
+    wlHost.setVisible(needsHost);
+    wHost.setVisible(needsHost);
+
+    // Token vs username+password
+    wlToken.setVisible(!isBitbucket);
+    wToken.setVisible(!isBitbucket);
+    wlUsername.setVisible(isBitbucket);
+    wUsername.setVisible(isBitbucket);
+    wlPassword.setVisible(isBitbucket);
+    wPassword.setVisible(isBitbucket);
+
+    // Update host placeholder text
+    if (provider == GitRepoProvider.GITHUB_ENTERPRISE) {
+      wHost.setMessage("https://git.mycorp.com";);
+    } else if (provider == GitRepoProvider.GITLAB) {
+      wHost.setMessage("https://gitlab.com (leave blank for GitLab.com)");
+    }
+
+    shell.layout(true, true);
+  }
+
+  // 
---------------------------------------------------------------------------
+  // Repository loading
+  // 
---------------------------------------------------------------------------
+
+  private void loadRepos(boolean resetPage) {
+    GitRepoProvider provider = selectedProvider();
+
+    GitRepoAuth auth;
+    if (provider == GitRepoProvider.BITBUCKET) {
+      String username = wUsername.getText().trim();
+      String password = wPassword.getText().trim();
+      if (username.isEmpty() || password.isEmpty()) {
+        wStatus.setText(
+            BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Status.NeedUsernamePassword"));
+        return;
+      }
+      auth = GitRepoAuth.forBasic(username, password);
+    } else {
+      String token = wToken.getText().trim();
+      if (token.isEmpty()) {
+        wStatus.setText(BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Status.NeedToken"));
+        return;
+      }
+      auth = GitRepoAuth.forToken(token);
+    }
+
+    String apiBaseUrl = resolveApiBaseUrl(provider);
+    String filter = wFilter.getText().trim();
+
+    if (resetPage) {
+      currentPage = 1;
+      loadedRepos.clear();
+      wRepoTable.removeAll();
+    }
+
+    wStatus.setText(BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Status.Loading"));
+    shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_WAIT));
+
+    final int pageToLoad = currentPage;
+    final GitRepoAuth finalAuth = auth;
+    final String finalApiBaseUrl = apiBaseUrl;
+    final String finalFilter = filter;
+
+    // Run the network call in a background thread and update UI on the SWT 
thread.
+    Thread worker =
+        new Thread(
+            () -> {
+              GitRepositoryPage result = null;
+              Exception error = null;
+              try {
+                GitRepositoryBrowser browser = 
GitRepositoryBrowser.forProvider(provider);
+                result =
+                    browser.listRepositories(finalApiBaseUrl, finalAuth, 
finalFilter, pageToLoad);
+              } catch (Exception e) {
+                error = e;
+              }
+
+              final GitRepositoryPage finalResult = result;
+              final Exception finalError = error;
+
+              shell
+                  .getDisplay()
+                  .asyncExec(
+                      () -> {
+                        if (shell.isDisposed()) {
+                          return;
+                        }
+                        shell.setCursor(null);
+                        if (finalError != null) {
+                          wStatus.setText(
+                              BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Status.Error"));
+                          new ErrorDialog(
+                              shell,
+                              BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Error.Header"),
+                              BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Error.Message"),
+                              finalError);
+                          return;
+                        }
+                        if (finalResult != null) {
+                          loadedRepos.addAll(finalResult.getRepositories());
+                          hasMore = finalResult.isHasMore();
+                          currentPage = pageToLoad + 1;
+                          populateTable(loadedRepos);
+                          wLoadMore.setEnabled(hasMore);
+                          wStatus.setText(
+                              String.format(
+                                  BaseMessages.getString(
+                                      PKG, 
"SelectRepositoryDialog.Status.Loaded"),
+                                  loadedRepos.size()));
+                        }
+                      });
+            });
+    worker.setDaemon(true);
+    worker.start();
+  }
+
+  private String resolveApiBaseUrl(GitRepoProvider provider) {
+    String hostValue = wHost.getText().trim();
+    return switch (provider) {
+      case GITHUB_ENTERPRISE -> {
+        if (!hostValue.isEmpty()) {
+          String base = hostValue.replaceAll("/+$", "");
+          yield base + "/api/v3";
+        }
+        yield null;
+      }
+      case GITLAB -> {
+        if (!hostValue.isEmpty()) {
+          String base = hostValue.replaceAll("/+$", "");
+          yield base + "/api/v4";
+        }
+        yield null; // default to gitlab.com
+      }
+      default -> null; // implementations use their own defaults
+    };
+  }
+
+  private void populateTable(List<GitRepositoryInfo> repos) {
+    wRepoTable.removeAll();
+    for (GitRepositoryInfo repo : repos) {
+      TableItem item = new TableItem(wRepoTable, SWT.NONE);
+      item.setText(COL_NAME, repo.getName());
+      item.setText(COL_OWNER, repo.getOwner());
+      item.setText(
+          COL_VISIBILITY,
+          repo.isPrivateRepo()
+              ? BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Visibility.Private")
+              : BaseMessages.getString(PKG, 
"SelectRepositoryDialog.Visibility.Public"));
+      item.setText(COL_UPDATED, repo.getLastUpdatedShort());
+      item.setData(repo);
+    }
+  }
+
+  // 
---------------------------------------------------------------------------
+  // OK / Cancel
+  // 
---------------------------------------------------------------------------
+
+  private void ok() {
+    int idx = wRepoTable.getSelectionIndex();
+    if (idx < 0) {
+      return;
+    }
+    TableItem item = wRepoTable.getItem(idx);
+    GitRepositoryInfo repo = (GitRepositoryInfo) item.getData();
+    if (repo == null) {
+      return;
+    }
+    selectedCloneUrl = repo.getHttpsCloneUrl();
+    selectedRepoName = repo.getName();
+    dispose();
+  }
+
+  private void cancel() {
+    selectedCloneUrl = null;
+    selectedRepoName = null;
+    dispose();
+  }
+
+  private void dispose() {
+    props.setScreen(new WindowProperty(shell));
+    shell.dispose();
+  }
+}
diff --git 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/GitCloneHelper.java
 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/GitCloneHelper.java
new file mode 100644
index 0000000000..a8cb432a13
--- /dev/null
+++ 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/GitCloneHelper.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.util;
+
+import java.lang.reflect.Method;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.gui.plugin.GuiPluginType;
+import org.apache.hop.core.plugins.IPlugin;
+import org.apache.hop.core.plugins.PluginRegistry;
+
+/**
+ * Helper to clone Git repositories using UIGit from the hop-misc-git plugin. 
Uses the git plugin's
+ * classloader to avoid ClassNotFoundException when plugins are loaded in 
isolation.
+ */
+public class GitCloneHelper {
+
+  private GitCloneHelper() {}
+
+  /**
+   * Clone a repository (full clone, no credentials).
+   *
+   * @param directory Local directory to clone into
+   * @param uri Repository URI (e.g. https://github.com/user/repo.git)
+   * @return true if clone succeeded
+   * @throws HopException if clone fails or git plugin is not installed
+   */
+  public static boolean cloneRepo(String directory, String uri) throws 
HopException {
+    return cloneRepo(directory, uri, null, 0);
+  }
+
+  /**
+   * Clone a repository with optional credentials and shallow clone depth.
+   *
+   * @param directory Local directory to clone into
+   * @param uri Repository URI (e.g. https://github.com/user/repo.git)
+   * @param token Optional token for authentication (null for none)
+   * @param depth Shallow clone depth (0 = full clone, 1+ = shallow with that 
many commits)
+   * @return true if clone succeeded
+   * @throws HopException if clone fails or git plugin is not installed
+   */
+  public static boolean cloneRepo(String directory, String uri, String token, 
int depth)
+      throws HopException {
+    try {
+      IPlugin gitPlugin = findGitPlugin();
+      if (gitPlugin == null) {
+        throw new HopException(
+            "Git plugin is not installed. Please add the hop-misc-git plugin 
to your plugins folder.");
+      }
+      ClassLoader gitClassLoader = 
PluginRegistry.getInstance().getClassLoader(gitPlugin);
+      Class<?> uiGitClass = 
gitClassLoader.loadClass("org.apache.hop.git.model.UIGit");
+      Object uiGit = uiGitClass.getConstructor().newInstance();
+
+      Object credentialsProvider = null;
+      if (StringUtils.isNotEmpty(token)) {
+        Method createTokenMethod =
+            uiGitClass.getMethod("createTokenCredentialsProvider", 
String.class);
+        credentialsProvider = createTokenMethod.invoke(null, token);
+      }
+
+      // Find cloneRepo(String, String, CredentialsProvider, int) by name and 
param count
+      // to avoid loading CredentialsProvider in our classloader (causes 
NoClassDefFoundError
+      // when projects plugin has dependencies.xml referencing git plugin)
+      Method cloneRepoMethod = null;
+      for (Method m : uiGitClass.getMethods()) {
+        if ("cloneRepo".equals(m.getName()) && m.getParameterCount() == 4) {
+          cloneRepoMethod = m;
+          break;
+        }
+      }
+      if (cloneRepoMethod == null) {
+        throw new 
HopException("UIGit.cloneRepo(String,String,CredentialsProvider,int) not 
found");
+      }
+      Boolean success =
+          (Boolean) cloneRepoMethod.invoke(uiGit, directory, uri, 
credentialsProvider, depth);
+      return Boolean.TRUE.equals(success);
+    } catch (Exception e) {
+      if (e.getCause() != null) {
+        throw new HopException("Clone failed: " + e.getCause().getMessage(), 
e.getCause());
+      }
+      throw new HopException("Clone failed: " + e.getMessage(), e);
+    }
+  }
+
+  private static IPlugin findGitPlugin() {
+    for (IPlugin plugin : 
PluginRegistry.getInstance().getPlugins(GuiPluginType.class)) {
+      for (String className : plugin.getClassMap().values()) {
+        if (className != null && className.startsWith("org.apache.hop.git.")) {
+          return plugin;
+        }
+      }
+    }
+    return null;
+  }
+}
diff --git a/plugins/misc/projects/pom.xml 
b/plugins/misc/projects/src/main/resources/dependencies.xml
similarity index 58%
copy from plugins/misc/projects/pom.xml
copy to plugins/misc/projects/src/main/resources/dependencies.xml
index 0d8d4dc90a..f1cedcfee7 100644
--- a/plugins/misc/projects/pom.xml
+++ b/plugins/misc/projects/src/main/resources/dependencies.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="UTF-8"?>
 <!--
   ~ Licensed to the Apache Software Foundation (ASF) under one or more
   ~ contributor license agreements.  See the NOTICE file distributed with
@@ -16,17 +15,8 @@
   ~ limitations under the License.
   ~
   -->
-<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
-    <modelVersion>4.0.0</modelVersion>
 
-    <parent>
-        <groupId>org.apache.hop</groupId>
-        <artifactId>hop-plugins-misc</artifactId>
-        <version>2.18.0-SNAPSHOT</version>
-    </parent>
-
-    <artifactId>hop-misc-projects</artifactId>
-    <packaging>jar</packaging>
-    <name>Hop Plugins Miscellaneous Projects</name>
-
-</project>
+<dependencies>
+    <!-- Git plugin folder: adds UIGit and JGit libs for "Add project from 
version control" -->
+    <folder>../git</folder>
+</dependencies>
diff --git 
a/plugins/misc/projects/src/main/resources/org/apache/hop/projects/gui/messages/messages_en_US.properties
 
b/plugins/misc/projects/src/main/resources/org/apache/hop/projects/gui/messages/messages_en_US.properties
index 7545f18d5c..9205198991 100644
--- 
a/plugins/misc/projects/src/main/resources/org/apache/hop/projects/gui/messages/messages_en_US.properties
+++ 
b/plugins/misc/projects/src/main/resources/org/apache/hop/projects/gui/messages/messages_en_US.properties
@@ -32,6 +32,12 @@ HopGui.Toolbar.Environment.Edit.Tooltip=Edit the selected 
environment
 HopGui.Toolbar.Project.Tooltip=Project {0} in ''{1}'' configured in ''{2}''
 HopGui.Toolbar.Project.Add.Label=Add project...
 HopGui.Toolbar.Project.Add.Tooltip=Add a new project
+HopGui.Toolbar.Project.Add.FromVersionControl.Label=From version control
+HopGui.Toolbar.Project.Add.FromVersionControl.Tooltip=Check out a project from 
a Git repository
+HopGui.Toolbar.Project.Add.FromExisting.Label=In a folder
+HopGui.Toolbar.Project.Add.FromExisting.Tooltip=Add a Hop project from an 
existing folder
+HopGui.Toolbar.Project.Add.FromTemplate.Label=From a template
+HopGui.Toolbar.Project.Add.FromTemplate.Tooltip=Create a new project from an 
existing template
 HopGui.Toolbar.Project.Delete.Label=Delete project...
 HopGui.Toolbar.Project.Delete.Tooltip=Delete the selected project
 HopGui.Toolbar.Project.Edit.Label=Edit project...
@@ -85,3 +91,80 @@ ProjectGuiPlugin.ZipDirectory.Error.Dialog.Message=Error 
zipping project
 ProjectGuiPlugin.ZipDirectory.Taskname.Text=Zipping project directory...
 ProjectGuiPlugin.IncludeVariables.Label=Include variables in export
 ProjectGuiPlugin.IncludeMetadata.Label=Include metadata in export
+EmptyProjectDialog.Shell.Name=Create empty project
+EmptyProjectDialog.Label.ProjectName=Project name
+EmptyProjectDialog.Label.HomeFolder=Home folder
+EmptyProjectDialog.Button.Browse=Browse...
+EmptyProjectDialog.Label.ConfigFile=Configuration file
+EmptyProjectDialog.Label.ConfigFile.Tooltip=Relative path within the home 
folder to the project configuration JSON file (e.g. config/project-config.json)
+EmptyProjectDialog.Label.ParentProject=Parent project
+EmptyProjectDialog.Label.Description=Description
+EmptyProjectDialog.Label.Company=Company
+EmptyProjectDialog.Label.Department=Department
+EmptyProjectDialog.Label.MetadataBaseFolder=Metadata base folder
+EmptyProjectDialog.Label.UnitTestsBasePath=Unit tests base path
+EmptyProjectDialog.Label.DataSetCsvFolder=Data set CSV folder
+EmptyProjectDialog.Label.EnforceHomeExecution=Enforce execution in home
+EmptyProjectDialog.Label.ProjectVariables=Project variables
+EmptyProjectDialog.Table.Col.VariableName=Variable name
+EmptyProjectDialog.Table.Col.VariableValue=Value
+EmptyProjectDialog.Table.Col.VariableDescription=Description
+EmptyProjectDialog.Label.ScaffoldOptions=Scaffold options
+EmptyProjectDialog.Label.CreatePipeline=Create pipeline
+EmptyProjectDialog.Label.PipelineName=Pipeline name
+EmptyProjectDialog.Label.CreateWorkflow=Create workflow
+EmptyProjectDialog.Label.WorkflowName=Workflow name
+EmptyProjectDialog.Label.CreateRunConfigs=Create run configurations
+EmptyProjectDialog.Label.CreateEnvironment=Create environment
+EmptyProjectDialog.Default.PipelineName=main.hpl
+EmptyProjectDialog.Default.WorkflowName=main.hwf
+CloneFromVersionControlDialog.Shell.Name=Clone project from Git
+CloneFromVersionControlDialog.Label.Url=Repository URL
+CloneFromVersionControlDialog.Label.Directory=Directory
+CloneFromVersionControlDialog.Label.ProjectName=Project name
+CloneFromVersionControlDialog.Label.Token=Authentication token (optional)
+CloneFromVersionControlDialog.Label.Token.Tooltip=Personal access token for 
GitHub, GitLab, etc. Leave empty for public repos or to be prompted.
+CloneFromVersionControlDialog.Label.ShallowClone=Shallow clone (history 
truncated to)
+CloneFromVersionControlDialog.Label.ShallowClone.Commits=commits
+CloneFromVersionControlDialog.Button.Browse=Browse...
+CloneFromVersionControlDialog.Button.BrowseRepo=Browse repositories...
+AddProjectFromTemplateDialog.Button.BrowseRepo=Browse repositories...
+SelectRepositoryDialog.Shell.Name=Select Repository
+SelectRepositoryDialog.Label.Provider=Provider
+SelectRepositoryDialog.Label.Host=Host URL
+SelectRepositoryDialog.Label.Host.Tooltip=Base URL of your self-hosted 
instance, e.g. https://git.mycorp.com
+SelectRepositoryDialog.Label.Token=Personal access token
+SelectRepositoryDialog.Label.Token.Tooltip=Personal access token with read 
access to your repositories (e.g. GitHub PAT with repo scope, GitLab PAT with 
read_api scope)
+SelectRepositoryDialog.Label.Username=Username
+SelectRepositoryDialog.Label.AppPassword=App password
+SelectRepositoryDialog.Label.AppPassword.Tooltip=Bitbucket app password with 
'Repositories: Read' permission
+SelectRepositoryDialog.Label.Filter=Filter
+SelectRepositoryDialog.Label.Filter.Tooltip=Filter by repository name or 
owner. Press Enter or click Search.
+SelectRepositoryDialog.Button.Search=Search
+SelectRepositoryDialog.Button.LoadMore=Load more...
+SelectRepositoryDialog.Table.Col.Name=Name
+SelectRepositoryDialog.Table.Col.Owner=Owner / Namespace
+SelectRepositoryDialog.Table.Col.Visibility=Visibility
+SelectRepositoryDialog.Table.Col.Updated=Last updated
+SelectRepositoryDialog.Visibility.Private=Private
+SelectRepositoryDialog.Visibility.Public=Public
+SelectRepositoryDialog.Status.Loading=Loading repositories...
+SelectRepositoryDialog.Status.Loaded=Showing {0} repositor(ies). Double-click 
or select and click OK.
+SelectRepositoryDialog.Status.NeedToken=Please enter a personal access token.
+SelectRepositoryDialog.Status.NeedUsernamePassword=Please enter a username and 
app password.
+SelectRepositoryDialog.Status.Error=Failed to load repositories.
+SelectRepositoryDialog.Error.Header=Error loading repositories
+SelectRepositoryDialog.Error.Message=An error occurred while fetching the 
repository list.
+AddProjectFromTemplateDialog.Shell.Name=Create project from template
+AddProjectFromTemplateDialog.Label.TemplateSource=Template source
+AddProjectFromTemplateDialog.Label.FromGit=Template from Git
+AddProjectFromTemplateDialog.Label.FromLocal=Local template
+AddProjectFromTemplateDialog.Label.GitUrl=Repository URL
+AddProjectFromTemplateDialog.Label.LocalFolder=Template folder
+AddProjectFromTemplateDialog.Label.ProjectName=Project name
+AddProjectFromTemplateDialog.Label.DestinationFolder=Destination folder
+AddProjectFromTemplateDialog.Label.Token=Authentication token (optional)
+AddProjectFromTemplateDialog.Label.Token.Tooltip=Personal access token for 
GitHub, GitLab, etc. Leave empty for public repos or to be prompted.
+AddProjectFromTemplateDialog.Label.ShallowClone=Shallow clone (history 
truncated to)
+AddProjectFromTemplateDialog.Label.ShallowClone.Commits=commits
+AddProjectFromTemplateDialog.Button.Browse=Browse...
diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiMenuWidgets.java 
b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiMenuWidgets.java
index 1abaff84ea..cb86d8ca9a 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiMenuWidgets.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiMenuWidgets.java
@@ -149,14 +149,19 @@ public class GuiMenuWidgets extends BaseGuiWidgets {
             try {
               executeMenuItem(guiMenuItem, instanceId);
             } catch (Exception ex) {
+              Throwable cause = ex.getCause() != null ? ex.getCause() : ex;
+              String msg = cause.getMessage();
+              if (msg == null || msg.isEmpty()) {
+                msg = cause.getClass().getSimpleName();
+              }
               LogChannel.UI.logError(
                   "Unable to call method "
                       + guiMenuItem.getListenerMethod()
                       + " in singleton "
                       + guiMenuItem.getListenerClassName()
                       + " : "
-                      + ex.getMessage(),
-                  e);
+                      + msg,
+                  ex);
             }
           });
 


Reply via email to