Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ A fast, reliable command-line tool for managing multiple GitHub accounts. Switch
- **Instant Switching**: Change GitHub accounts with a single command
- **SSH Key Management**: Automatically configures SSH hosts for each profile
- **Git Config Integration**: Updates user.name and user.email per repository
- **GPG Signing Support**: Configure GPG signing keys per profile for verified commits
- **Profile Storage**: Securely stores multiple account profiles
- **Auto-Detection**: Automatically detects and switches profiles based on repository
- **Remote URL Updates**: Automatically updates git remote URLs when switching
Expand Down Expand Up @@ -85,6 +86,9 @@ gh-switch add personal
# - Git name: John Doe
# - Git email: john@personal.com
# - GitHub username: johndoe
# - Configure GPG signing? (optional)
# - GPG key ID: (if GPG is configured)
# - Enable automatic GPG signing? (if GPG is configured)
```

#### Listing Profiles
Expand All @@ -99,6 +103,8 @@ gh-switch list
# * personal (active)
# User: John Doe
# Email: john@personal.com
# GPG Key: ABCD1234EFGH5678
# GPG Signing: Enabled
#
# work
# User: John Smith
Expand Down Expand Up @@ -221,6 +227,8 @@ Each profile stores:
- Git email address
- GitHub username
- SSH host alias
- GPG signing key ID (optional)
- GPG auto-signing preference (optional)

## 🎨 Command Reference

Expand Down Expand Up @@ -269,6 +277,7 @@ ghclone() {
- **OpenSSH**: Standard SSH client
- **Bash**: Version 4.0+ (macOS/Linux)
- **GitHub Account**: With SSH keys configured
- **GPG** (optional): For commit signing (gpg or gpg2)

### Setting Up SSH Keys

Expand All @@ -288,6 +297,129 @@ ssh-add ~/.ssh/id_ed25519_work

Then add each public key to the corresponding GitHub account's settings.

### Setting Up GPG Keys for Signed Commits

GitHub supports GPG-signed commits to verify your identity. gh-switch can manage GPG keys per profile.

#### Generating GPG Keys

```bash
# Generate a GPG key for personal account
gpg --full-generate-key
# Select: (1) RSA and RSA or (9) ECC and ECC
# Key size: 4096 (for RSA) or use default (for ECC)
# Expiration: Choose based on your security policy
# Real name: Your Name
# Email: personal@email.com

# Generate a GPG key for work account
gpg --full-generate-key
# Use your work email: work@company.com
```

#### List Your GPG Keys

```bash
# List all GPG keys with their IDs
gpg --list-secret-keys --keyid-format=long

# Output example:
# sec rsa4096/ABCD1234EFGH5678 2024-01-01 [SC]
# uid [ultimate] Your Name <personal@email.com>
```

The key ID is the part after the `/` (e.g., `ABCD1234EFGH5678`).

#### Add GPG Key to GitHub

```bash
# Export your public GPG key
gpg --armor --export ABCD1234EFGH5678

# Copy the output (including BEGIN and END lines)
# Then add it to GitHub:
# 1. Go to GitHub Settings → SSH and GPG keys
# 2. Click "New GPG key"
# 3. Paste your public key
```

#### Configure GPG in gh-switch

When adding a new profile, gh-switch will prompt you to configure GPG signing:

```bash
gh-switch add personal

# You'll be prompted:
# - SSH key path
# - Git name
# - Git email
# - GitHub username
# - Configure GPG signing? (y/N)
# - GPG key ID: ABCD1234EFGH5678
# - Enable GPG signing for all commits? (Y/n)
```

For existing profiles, you can delete and re-add them with GPG support:

```bash
gh-switch delete personal
gh-switch add personal
```

#### Verify GPG Signing is Working

```bash
# Switch to your profile
gh-switch use personal

# Check current configuration
gh-switch current
# Should show:
# GPG key: ABCD1234EFGH5678
# GPG signing: Enabled

# Make a commit
git commit -m "Test signed commit"

# Verify the commit is signed
git log --show-signature -1
# Should show "Good signature from ..."
```

#### Manual GPG Configuration

If you prefer to manually sign specific commits:

1. When configuring GPG in gh-switch, answer "n" to "Enable GPG signing for all commits?"
2. This sets up the signing key but doesn't auto-sign
3. Sign individual commits with: `git commit -S -m "Your message"`

#### GPG Troubleshooting

**Issue: "gpg: signing failed: No secret key"**
```bash
# Ensure the key is in your keyring
gpg --list-secret-keys

# If missing, import it
gpg --import your-private-key.asc
```

**Issue: "gpg: signing failed: Inappropriate ioctl for device"**
```bash
# Add to ~/.bashrc or ~/.zshrc
export GPG_TTY=$(tty)

# Or use gpg-agent
echo 'use-agent' >> ~/.gnupg/gpg.conf
```

**Issue: Commits not showing as verified on GitHub**
- Ensure the GPG key email matches the commit email
- Check that the public key is added to your GitHub account
- The key must not be expired or revoked

## 🧪 Typical Workflows

### Daily Development
Expand Down
114 changes: 111 additions & 3 deletions bin/gh-switch-standalone
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,29 @@ ensure_config_dir() {
# === Profile Functions ===
create_profile() {
local name="$1" ssh_key="$2" git_name="$3" git_email="$4" github_user="$5"
local gpg_key_id="${6:-}" gpg_sign_commits="${7:-false}"
cat > "${PROFILES_DIR}/${name}" <<EOF
SSH_KEY="${ssh_key}"
GIT_NAME="${git_name}"
GIT_EMAIL="${git_email}"
GITHUB_USER="${github_user}"
HOST_ALIAS="github.com-${name}"
GPG_KEY_ID="${gpg_key_id}"
GPG_SIGN_COMMITS="${gpg_sign_commits}"
EOF
log_success "Profile '${name}' created"
}

load_profile() {
local name="$1"
local profile_file="${PROFILES_DIR}/${name}"
[[ -f "${profile_file}" ]] && source "${profile_file}" && return 0
if [[ -f "${profile_file}" ]]; then
source "${profile_file}"
# Set defaults for backward compatibility
GPG_KEY_ID="${GPG_KEY_ID:-}"
GPG_SIGN_COMMITS="${GPG_SIGN_COMMITS:-false}"
return 0
fi
return 1
}

Expand Down Expand Up @@ -103,6 +112,28 @@ apply_git_profile() {
set_git_config "user.email" "${2}" "${3:-local}"
}

apply_git_gpg_config() {
local gpg_key_id="$1" gpg_sign_commits="$2" scope="${3:-local}"
if [[ -n "$gpg_key_id" ]]; then
set_git_config "user.signingkey" "${gpg_key_id}" "${scope}"
log_info "Set GPG signing key: ${gpg_key_id}"
if [[ "$gpg_sign_commits" == "true" ]]; then
set_git_config "commit.gpgsign" "true" "${scope}"
log_info "Enabled automatic GPG commit signing"
else
set_git_config "commit.gpgsign" "false" "${scope}"
fi
else
if [[ "$scope" == "local" ]] && git rev-parse --git-dir &>/dev/null; then
git config --${scope} --unset user.signingkey 2>/dev/null || true
git config --${scope} --unset commit.gpgsign 2>/dev/null || true
elif [[ "$scope" == "global" ]]; then
git config --${scope} --unset user.signingkey 2>/dev/null || true
git config --${scope} --unset commit.gpgsign 2>/dev/null || true
fi
fi
}

update_remote_url() {
local profile="$1"
local current_url=$(git remote get-url origin 2>/dev/null)
Expand All @@ -113,6 +144,51 @@ update_remote_url() {
fi
}

# === GPG Functions ===
check_gpg_installed() {
command -v gpg &>/dev/null || command -v gpg2 &>/dev/null
}

get_gpg_cmd() {
command -v gpg2 &>/dev/null && echo "gpg2" || command -v gpg &>/dev/null && echo "gpg" || echo ""
}

list_gpg_keys() {
local gpg_cmd=$(get_gpg_cmd)
[[ -z "$gpg_cmd" ]] && log_error "GPG is not installed" && return 1
echo "Available GPG keys:"
echo "==================="
$gpg_cmd --list-secret-keys --keyid-format LONG 2>/dev/null | grep -E "^(sec|uid)" | sed 's/^/ /'
echo ""
echo "Hint: Use the key ID after 'sec' (e.g., 'rsa4096/ABCD1234EFGH5678')"
echo " Extract just the ID part: ABCD1234EFGH5678"
}

validate_gpg_key() {
local key_id="$1" gpg_cmd=$(get_gpg_cmd)
[[ -z "$gpg_cmd" ]] && log_error "GPG is not installed" && return 1
[[ -z "$key_id" ]] && return 0
$gpg_cmd --list-secret-keys "$key_id" &>/dev/null || (log_error "GPG key '$key_id' not found in keyring" && return 1)
}

select_gpg_key_interactive() {
local gpg_cmd=$(get_gpg_cmd)
[[ -z "$gpg_cmd" ]] && log_info "GPG is not installed. Skipping GPG key setup." && echo "" && return 0
! $gpg_cmd --list-secret-keys &>/dev/null && log_info "No GPG keys found in keyring. Skipping GPG key setup." && echo "" && return 0
echo ""
list_gpg_keys
local gpg_key=""
read -p "GPG key ID (optional, press Enter to skip): " gpg_key
if [[ -n "$gpg_key" ]]; then
validate_gpg_key "$gpg_key" && echo "$gpg_key" && return 0
log_error "Invalid GPG key. Profile will be created without GPG signing."
echo ""
return 1
fi
echo ""
return 0
}

# === Shell Detection Functions ===
detect_shell() {
# Check parent process
Expand Down Expand Up @@ -320,10 +396,26 @@ cmd_add() {
read -p "Git email: " git_email
read -p "GitHub username: " github_user

create_profile "$name" "$ssh_key" "$git_name" "$git_email" "$github_user"
local gpg_key="" gpg_sign="false"
if check_gpg_installed; then
echo ""
log_info "GPG Signing Configuration (Optional)"
echo "Would you like to configure GPG commit signing for this profile?"
read -p "Configure GPG signing? (y/N): " configure_gpg
if [[ "$configure_gpg" =~ ^[Yy]$ ]]; then
gpg_key=$(select_gpg_key_interactive)
if [[ -n "$gpg_key" ]]; then
read -p "Enable GPG signing for all commits? (Y/n): " enable_signing
[[ ! "$enable_signing" =~ ^[Nn]$ ]] && gpg_sign="true"
fi
fi
fi

create_profile "$name" "$ssh_key" "$git_name" "$git_email" "$github_user" "$gpg_key" "$gpg_sign"
add_ssh_host "$name" "$ssh_key"

log_success "Profile ${name} added successfully!"
[[ -n "$gpg_key" ]] && log_info "GPG signing configured with key: ${gpg_key}"
echo "Use 'gh-switch use ${name}' to activate"
}

Expand All @@ -335,6 +427,7 @@ cmd_use() {
load_profile "$profile_name" || (log_error "Profile '${profile_name}' not found" && exit 1)

apply_git_profile "${GIT_NAME}" "${GIT_EMAIL}" "${scope}"
apply_git_gpg_config "${GPG_KEY_ID}" "${GPG_SIGN_COMMITS}" "${scope}"
echo "${profile_name}" > "${GH_SWITCH_DIR}/current"

git rev-parse --git-dir &>/dev/null && update_remote_url "${profile_name}"
Expand All @@ -351,11 +444,20 @@ cmd_current() {
echo " Git user: ${GIT_NAME}"
echo " Git email: ${GIT_EMAIL}"
echo " GitHub user: ${GITHUB_USER}"

if [[ -n "${GPG_KEY_ID:-}" ]]; then
echo " GPG key: ${GPG_KEY_ID}"
[[ "${GPG_SIGN_COMMITS:-false}" == "true" ]] && echo " GPG signing: Enabled" || echo " GPG signing: Manual"
fi

if git rev-parse --git-dir &>/dev/null; then
echo -e "\nRepository config:"
echo " user.name: $(get_git_config user.name)"
echo " user.email: $(get_git_config user.email)"
local signing_key=$(get_git_config user.signingkey)
local gpg_sign=$(get_git_config commit.gpgsign)
[[ -n "$signing_key" ]] && echo " user.signingkey: ${signing_key}"
[[ -n "$gpg_sign" ]] && echo " commit.gpgsign: ${gpg_sign}"
fi
else
log_error "Current profile '${profile_name}' not found"
Expand All @@ -374,7 +476,13 @@ cmd_list() {
source "$profile_file"

[[ "$profile_name" == "$current_profile" ]] && echo -e "${GREEN}* ${profile_name}${NC} (active)" || echo " ${profile_name}"
echo -e " User: ${GIT_NAME}\n Email: ${GIT_EMAIL}\n"
echo " User: ${GIT_NAME}"
echo " Email: ${GIT_EMAIL}"
if [[ -n "${GPG_KEY_ID:-}" ]]; then
echo " GPG Key: ${GPG_KEY_ID}"
[[ "${GPG_SIGN_COMMITS:-false}" == "true" ]] && echo " GPG Signing: Enabled" || echo " GPG Signing: Manual"
fi
echo ""
fi
done
}
Expand Down
Loading