Gittyup icon indicating copy to clipboard operation
Gittyup copied to clipboard

Gittyup Fails to Switch SSH Keys Across Repositories with Custom Hostnames

Open cakemonitor opened this issue 8 months ago • 1 comments

Description: Gittyup inconsistently handles multiple SSH keys associated with different repositories that use custom hostnames mapped in the ~/.ssh/config file. The issue is observed in both the AppImage and Flatpak versions across the stable release (1.4.0) and the latest development release. Fetching only succeeds for repositories associated with the SSH key loaded first by the ssh agent - i.e. listed first in the output of ssh-add -l - regardless of the ~/.ssh/config setup.

  • Expected Behavior: Gittyup should respect the SSH configuration in ~/.ssh/config and allow fetching from repositories associated with distinct hostnames and SSH keys.
  • Observed Behavior: Gittyup fetches successfully from repositories associated with the first key listed by the SSH agent, but fails to fetch for repositories requiring other keys. For example:
Unable to fetch from 'origin' - remote: 
remote: ========================================================================
remote: 
remote: ERROR: The project you were looking for could not be found or you don't have permission to view it.
remote: 
remote: ========================================================================
  • git-gui and terminal-based git operations successfully fetch from all repositories regardless of hostname and associated SSH key. Moreover, launching a shell within the context of the Gittyup Flatpak runtime (flatpak run --command=sh com.github.Murmele.Gittyup) also allows successful fetching from all repositories regardless of hostname and associated SSH key, confirming correct handling of ~/.ssh/config in environments outside of Gittyup’s GUI. This indicates that Gittyup itself is not respecting ~/.ssh/config or dynamically switching keys during fetch operations.
  • Having tested on different systems, one system loads the personal key first while another loads the work key first. This issue has been observed on both systems, Gittyup will only utilise the SSH key loaded first, and the issue remains across reboots.

Steps to Reproduce:

  1. Set up two SSH keys and configure custom hostname mappings in ~/.ssh/config:
Host gitlab.com
    HostName gitlab.com
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes

Host work-gitlab.com
    HostName gitlab.com
    IdentityFile ~/.ssh/id_ed25519_work
    IdentitiesOnly yes
  1. Add the public keys to separate user accounts on GitLab (e.g., personal account for id_ed25519 and work account for id_ed25519_work).
  2. Clone two repositories, one for each account:
git clone [email protected]:<namespace>/<repo>.git
git clone [email protected]:<namespace>/<repo>.git
  1. Open the repositories in Gittyup and attempt to fetch from their remotes.
  2. Observe that Gittyup will only fetch from one repo.

System Details:

  • OS: Linux Mint 22.1 Cinnamon
  • Gittyup Versions: 1.4.0 stable (AppImage and Flatpak) and the latest development release (AppImage)

cakemonitor avatar Apr 18 '25 13:04 cakemonitor

I've dug into this issue a bit more and I think I've identified the problem. I don't have time to clone the repo and setup the build environment in order to open a PR myself, but here's my proposed fix:

Create test/ssh_config3:

Host example.com
    HostName example.com
    User defaultuser

Host special-example.com
    HostName example.com
    User specialuser

Add additional test cases to test/SshConfig.cpp:

void TestSshConfig::resolveSsh() {

  // snip...

  QCOMPARE(transformUrl("example.com:/repo", "ssh_config3"),
           QString("ssh://[email protected]/repo"));

  QCOMPARE(transformUrl("special-example.com:/repo", "ssh_config3"),
           QString("ssh://[email protected]/repo"));
}

I believe the latter comparison will fail because when matching hosts Gittyup exits early after finding any match, and here "example.com" is matched as a substring of "special-example.com" despite the better match appearing later in the ssh config.

My proposed fix is to modify ConfigFile::apply within src/git/Remote.cpp:

void apply(const QString &hostname,
           std::function<void(const Host &)> handler) const {
  const Host* mostSpecificHost = nullptr;
  QString mostSpecificPattern;

  for (const Host &host : parse()) {
    for (const QString &pattern : host.patterns) {
      QRegularExpression re{
          QRegularExpression::wildcardToRegularExpression(pattern)};
      if (re.match(hostname).hasMatch()) {
        if (!mostSpecificHost || pattern.length() > mostSpecificPattern.length()) {
          mostSpecificHost = &host;
          mostSpecificPattern = pattern;
        }
      }
    }
  }

  if (mostSpecificHost) {
    handler(*mostSpecificHost);
  }
}

cakemonitor avatar Apr 20 '25 12:04 cakemonitor