Skip to content

Commit 58838d4

Browse files
authored
Merge pull request #252 from nullvariant/feat/tilde-compression-ssh-key-path
feat: compress file picker SSH key paths to ~/ format
2 parents 47ef3ef + a08d15f commit 58838d4

9 files changed

Lines changed: 265 additions & 83 deletions

File tree

extensions/git-id-switcher/CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.16.8] - 2026-01-31
11+
12+
### Changed
13+
14+
- **SSH key file picker now stores paths in `~/` format instead of absolute paths**:
15+
- Selecting a key via the 📁 browse button now produces `~/.ssh/id_ed25519` instead of `/Users/name/.ssh/id_ed25519`
16+
- Improves privacy (no OS username in settings.json) and portability across machines
17+
- Works on both Unix and Windows (backslashes normalized to forward slashes)
18+
- Extracted shared `replaceHomeWithTilde()` utility to `pathUtils.ts`, removing duplicate from `pathSanitizer.ts`
19+
20+
### Documentation
21+
22+
- **Updated Japanese README for identity management UI**:
23+
- Added "Profile Management UI" as first item in Features section
24+
- Rewrote Quick Start Step 3 from settings.json-first to UI-first approach
25+
- Added new "Profile Management" section (add/edit/delete/reorder/SSH key path)
26+
- Added Delete Identity command to Commands table
27+
- Removed "Note: Basic Setup (No SSH)" section
28+
1029
## [0.16.7] - 2026-01-30
1130

1231
### Fixed

extensions/git-id-switcher/docs/i18n/ja/README.md

Lines changed: 95 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,12 @@ Gitプロフィール切り替えツールは数多く存在しますが、**Git
4040

4141
## 機能
4242

43+
- **プロフィール管理UI**: settings.jsonを編集せずに、プロフィールの追加・編集・削除・並べ替えが可能
44+
- **ワンクリックでプロフィール切り替え**: Git user.nameとuser.emailを瞬時に変更
45+
- **ステータスバー統合**: 現在のプロフィールを常に一目で確認
4346
- **サブモジュール対応**: Gitサブモジュールにも自動的にプロフィールを伝播
4447
- **SSH鍵管理**: ssh-agentのSSH鍵を自動的に切り替え
4548
- **GPG署名対応**: コミット署名用のGPG鍵を設定(オプション)
46-
- **ワンクリックでプロフィール切り替え**: Git user.nameとuser.emailを瞬時に変更
47-
- **ステータスバー統合**: 現在のプロフィールを常に一目で確認
4849
- **リッチなツールチップ**: 説明やSSHホストを含む詳細なプロフィール情報
4950
- **クロスプラットフォーム**: macOS、Linux、Windowsで動作
5051
- **多言語対応**: 17言語をサポート
@@ -103,36 +104,28 @@ Host github-work
103104

104105
### ステップ 3: 拡張機能を設定
105106

106-
拡張機能の設定を開き(`Cmd+,` / `Ctrl+,`)→「Git ID Switcher」を検索 →「settings.jsonで編集」をクリック:
107+
インストール直後はサンプルプロフィールが用意されています。これを自分用に編集しましょう。
107108

108-
```json
109-
{
110-
"gitIdSwitcher.identities": [
111-
{
112-
"id": "personal",
113-
"icon": "🏠",
114-
"name": "高橋カオル",
115-
"service": "GitHub",
116-
"email": "kaoru@personal.example.com",
117-
"description": "個人プロジェクト",
118-
"sshKeyPath": "~/.ssh/id_ed25519_personal"
119-
},
120-
{
121-
"id": "work",
122-
"icon": "💼",
123-
"name": "高橋カオル",
124-
"service": "GitHub 会社用",
125-
"email": "kaoru.takahashi@company.example.com",
126-
"description": "会社の開発用",
127-
"sshKeyPath": "~/.ssh/id_ed25519_work",
128-
"sshHost": "github-work"
129-
}
130-
],
131-
"gitIdSwitcher.defaultIdentity": "personal",
132-
"gitIdSwitcher.autoSwitchSshKey": true,
133-
"gitIdSwitcher.applyToSubmodules": true
134-
}
135-
```
109+
1. ステータスバー(右下)のプロフィールアイコンをクリック
110+
2. 一覧下部の「プロフィール管理」を選択
111+
3. サンプルプロフィールの「✏️」をクリックして編集
112+
4. ID・名前・メールアドレスを自分の情報に変更
113+
5. 「SSH Key Path」を選択し、📁ボタンでSSH秘密鍵を参照選択(パスが自動入力される)
114+
6. SSHホスト・その他のオプション項目も必要に応じて設定
115+
116+
<img src="https://assets.nullvariant.com/nullvariant-vscode-extensions/extensions/git-id-switcher/images/management-ui-ja.png" width="600" alt="プロフィール管理UI">
117+
118+
<img src="https://assets.nullvariant.com/nullvariant-vscode-extensions/extensions/git-id-switcher/images/ssh-key-browse-ja.png" width="600" alt="SSH鍵パス参照ボタン">
119+
120+
> **鍵ファイルは送信されません**: この操作で記録されるのは鍵ファイルのパス(場所)だけです。鍵ファイルの中身がアップロードされたり外部に送信されることはありません。
121+
122+
2つ目のプロフィールは「+」ボタンから新規追加してください。
123+
124+
> **ヒント**: settings.jsonから直接設定することもできます。
125+
> 拡張機能の設定を開き(`Cmd+,` / `Ctrl+,`)→「Git ID Switcher」を検索 →「settings.jsonで編集」をクリック。
126+
> JSON形式での設定例は「[フル設定例](#フル設定例-4アカウントとssh--gpg)」を参照してください。
127+
128+
SSH鍵パスの詳細な設定手順・プロフィールの編集・削除・並び替えについては「[プロフィール管理](#プロフィール管理)」を参照してください。
136129

137130
### ステップ 4: 使ってみる
138131

@@ -286,6 +279,76 @@ Host bitbucket.org
286279

287280
---
288281

282+
## プロフィール管理
283+
284+
ステータスバーをクリック → 一覧下部の「プロフィール管理」で管理画面を開きます。
285+
プロフィールの追加・編集・削除・並び替えはすべてUIから直接操作できます。
286+
287+
<img src="https://assets.nullvariant.com/nullvariant-vscode-extensions/extensions/git-id-switcher/images/management-ui-overview-ja.png" width="600" alt="プロフィール管理画面">
288+
289+
### プロフィールの追加
290+
291+
1. 管理画面の「+」ボタンをクリック
292+
2. 各フィールドを入力:
293+
- **ID** (必須): 一意の識別子(例: `personal`, `work`
294+
- **名前** (必須): Git user.name
295+
- **メール** (必須): Git user.email
296+
- その他のオプション項目(アイコン、サービス名、説明、SSH鍵パス、SSHホスト、GPG鍵ID)
297+
3. 「保存」を選択
298+
299+
保存後、編集画面に遷移するので、オプション項目も続けて設定できます。
300+
301+
### プロフィールの編集
302+
303+
1. 管理画面で対象プロフィールの「✏️」をクリック
304+
2. 編集したいフィールドを選択
305+
3. 新しい値を入力
306+
307+
フィールドを保存すると編集画面に戻るので、他のフィールドも続けて編集できます。
308+
309+
> **注意**: プロフィールIDは、プロフィールが1件のみの場合に限り変更可能です。
310+
> 2件以上の場合は参照整合性のためロックされます。
311+
312+
### プロフィールの削除
313+
314+
1. 管理画面で対象プロフィールの「🗑️」をクリック
315+
2. 確認ダイアログで「はい」を選択
316+
317+
コマンドパレットから `Git ID Switcher: Delete Identity` でも実行できます。
318+
319+
### プロフィールの並び替え
320+
321+
管理画面で対象プロフィールの「⬆」「⬇」をクリックして順序を変更できます。
322+
この順序はsettings.json内の配列順と連動します。
323+
324+
### SSH鍵パスの設定
325+
326+
SSH鍵パスの入力は、この拡張機能の設定で最も手間のかかる項目です。
327+
管理UIにはファイル参照機能が組み込まれており、パスを手入力する必要がありません。
328+
329+
1. プロフィールの編集画面で「SSH Key Path」を選択
330+
2. 入力欄の右側に「📁」(Browse for SSH key path...)ボタンが表示される
331+
3. ボタンをクリックすると、OSのファイルダイアログが `~/.ssh/` を初期表示して開く
332+
4. 使用したいSSH秘密鍵ファイルを選択(例: `id_ed25519_work`
333+
5. 選択したファイルのパスが `~/.ssh/...` 形式で入力欄に自動入力される
334+
6. パスのバリデーションが即座に実行される
335+
336+
<img src="https://assets.nullvariant.com/nullvariant-vscode-extensions/extensions/git-id-switcher/images/ssh-key-dialog-ja.png" width="600" alt="SSH鍵ファイルダイアログ">
337+
338+
<img src="https://assets.nullvariant.com/nullvariant-vscode-extensions/extensions/git-id-switcher/images/ssh-key-autofill-ja.png" width="600" alt="SSH鍵パス自動入力後">
339+
340+
> **鍵ファイルは送信されません**: この操作は鍵ファイルのパス(場所)を記録するだけです。鍵ファイルの中身がアップロードされたり外部に送信されることはありません。
341+
342+
**パス入力のポイント:**
343+
344+
- 📁ボタンで参照するのが最も確実(タイプミスの心配なし)
345+
- 手入力も可能(`~/.ssh/id_ed25519_work` のように `~` で始まるパスも使用可)
346+
- セキュリティ上、`~/.ssh/` ディレクトリ配下のファイルのみ指定可能
347+
348+
> **ヒント**: SSH鍵ファイルが `~/.ssh/` にない場合は、先にファイルを移動またはシンボリックリンクを作成してください。
349+
350+
---
351+
289352
## 設定リファレンス
290353

291354
### プロフィールのプロパティ
@@ -342,33 +405,6 @@ Host bitbucket.org
342405
| `false` | `高橋カオル` | `高橋カオル <email>` |
343406
| `true` | `👤 高橋カオル` | `👤 高橋カオル <email>` |
344407

345-
### 補足: 基本設定のみ(SSHなし)
346-
347-
SSH鍵切り替えが不要な場合(単一のGitHubアカウントで異なるコミット者情報を使い分けるなど)、最小構成で利用できます:
348-
349-
```json
350-
{
351-
"gitIdSwitcher.identities": [
352-
{
353-
"id": "personal",
354-
"icon": "🏠",
355-
"name": "高橋カオル",
356-
"email": "kaoru@personal.example.com",
357-
"description": "個人プロジェクト"
358-
},
359-
{
360-
"id": "work",
361-
"icon": "💼",
362-
"name": "高橋カオル",
363-
"email": "kaoru.takahashi@company.example.com",
364-
"description": "会社の開発用"
365-
}
366-
]
367-
}
368-
```
369-
370-
この設定では`git config user.name``user.email`のみを切り替えます。
371-
372408
---
373409

374410
## 仕組み
@@ -582,6 +618,7 @@ VS Codeなどのエディタは設定スキーマをメモリにキャッシュ
582618
| コマンド | 説明 |
583619
| ---------------------------------------- | ---------------------------- |
584620
| `Git ID Switcher: Select Identity` | プロフィールピッカーを開く |
621+
| `Git ID Switcher: Delete Identity` | プロフィールを削除 |
585622
| `Git ID Switcher: Show Current Identity` | 現在のプロフィール情報を表示 |
586623
| `Git ID Switcher: Show Documentation` | ドキュメントを表示 |
587624

extensions/git-id-switcher/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "git-id-switcher",
33
"displayName": "%extension.displayName%",
44
"description": "%extension.description%",
5-
"version": "0.16.7",
5+
"version": "0.16.8",
66
"publisher": "nullvariant",
77
"icon": "images/icon.png",
88
"engines": {

extensions/git-id-switcher/src/security/pathSanitizer.ts

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import * as path from 'node:path';
1111
import { PATH_MAX, MAX_PATTERN_CHECK_LENGTH } from '../core/constants';
1212
import { CONTROL_CHAR_REGEX_ALL } from '../validators/common';
13+
import { replaceHomeWithTilde } from './pathUtils';
1314

1415
/**
1516
* Platform-specific sensitive directory patterns
@@ -217,23 +218,6 @@ function tryRedactUncPath(normalizedPath: string): string | null {
217218
return null;
218219
}
219220

220-
/**
221-
* Replace home directory with ~ for privacy.
222-
*
223-
* @internal
224-
*/
225-
function replaceHomeWithTilde(normalizedPath: string, home: string): string | null {
226-
if (!home) {
227-
return null;
228-
}
229-
const normalizedHome = home.replaceAll('\\', '/');
230-
// SECURITY: Ensure we match at path component boundary
231-
if (normalizedPath === normalizedHome || normalizedPath.startsWith(normalizedHome + '/')) {
232-
return '~' + normalizedPath.slice(normalizedHome.length);
233-
}
234-
return null;
235-
}
236-
237221
/**
238222
* Sanitize path for logging (don't expose full path structure)
239223
*

extensions/git-id-switcher/src/security/pathUtils.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,3 +499,35 @@ export function isUnderSshDirectory(value: string): boolean {
499499

500500
return normalizedValue.startsWith(sshDirWithSep);
501501
}
502+
503+
/**
504+
* Replace home directory prefix with ~ for privacy and portability.
505+
*
506+
* Normalizes path separators to forward slashes so the result is always
507+
* in `~/...` form regardless of platform. Returns null if the path is
508+
* not under the home directory.
509+
*
510+
* @param absolutePath - Absolute path (may contain backslashes on Windows)
511+
* @param homeDir - Home directory path
512+
* @returns Path with ~ prefix, or null if not under home directory
513+
*
514+
* @example
515+
* replaceHomeWithTilde('/Users/me/.ssh/key', '/Users/me') // '~/.ssh/key'
516+
* replaceHomeWithTilde('C:\\Users\\me\\.ssh\\key', 'C:\\Users\\me') // '~/.ssh/key'
517+
* replaceHomeWithTilde('/other/path', '/Users/me') // null
518+
*/
519+
export function replaceHomeWithTilde(absolutePath: string, homeDir: string): string | null {
520+
if (!homeDir) {
521+
return null;
522+
}
523+
const normalizedPath = absolutePath.replaceAll('\\', '/');
524+
const normalizedHome = homeDir.replaceAll('\\', '/');
525+
// SECURITY: Ensure we match at path component boundary
526+
if (normalizedPath === normalizedHome) {
527+
return '~';
528+
}
529+
if (normalizedPath.startsWith(normalizedHome + '/')) {
530+
return '~' + normalizedPath.slice(normalizedHome.length);
531+
}
532+
return null;
533+
}

extensions/git-id-switcher/src/test/e2e/identityManager.test.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2556,7 +2556,7 @@ describe('identityManager E2E Test Suite', function () {
25562556
});
25572557

25582558
describe('File selection updates InputBox value', () => {
2559-
it('should update InputBox value after file selection', async () => {
2559+
it('should update InputBox value after file selection (Unix)', async () => {
25602560
// Save original env
25612561
const originalHome = process.env.HOME;
25622562
process.env.HOME = '/home/testuser';
@@ -2598,12 +2598,68 @@ describe('identityManager E2E Test Suite', function () {
25982598

25992599
try {
26002600
await showEditProfileFlow(TEST_IDENTITIES.work);
2601-
assert.strictEqual(inputBoxValueAfterFilePick, '/home/testuser/.ssh/selected_key',
2602-
'InputBox value should be updated to selected file path');
2601+
assert.strictEqual(inputBoxValueAfterFilePick, '~/.ssh/selected_key',
2602+
'InputBox value should be updated to selected file path with ~ prefix');
26032603
} finally {
26042604
process.env.HOME = originalHome;
26052605
}
26062606
});
2607+
2608+
it('should normalize Windows backslashes to forward slashes after tilde compression', async () => {
2609+
// Save original env
2610+
const originalHome = process.env.HOME;
2611+
const originalUserProfile = process.env.USERPROFILE;
2612+
2613+
// Simulate Windows environment
2614+
delete process.env.HOME;
2615+
process.env.USERPROFILE = 'C:\\Users\\testuser';
2616+
2617+
let inputBoxValueAfterFilePick: string | undefined;
2618+
2619+
const mockVSCode = createMockVSCode({
2620+
identities: [TEST_IDENTITIES.work],
2621+
quickPickSelections: [
2622+
{ field: 'sshKeyPath' },
2623+
undefined,
2624+
],
2625+
inputBoxSelections: [FILE_PICKER_CLICK, undefined],
2626+
showOpenDialogResult: 'C:\\Users\\testuser\\.ssh\\selected_key',
2627+
});
2628+
2629+
const originalCreateInputBox = mockVSCode.window.createInputBox;
2630+
mockVSCode.window.createInputBox = () => {
2631+
const inputBox = originalCreateInputBox();
2632+
const originalValueSetter = Object.getOwnPropertyDescriptor(inputBox, 'value')?.set;
2633+
let internalValue = '';
2634+
2635+
Object.defineProperty(inputBox, 'value', {
2636+
get: () => internalValue,
2637+
set: (v: string) => {
2638+
internalValue = v;
2639+
inputBoxValueAfterFilePick = v;
2640+
if (originalValueSetter) {
2641+
originalValueSetter.call(inputBox, v);
2642+
}
2643+
},
2644+
});
2645+
return inputBox;
2646+
};
2647+
2648+
_setMockVSCode(mockVSCode as never);
2649+
2650+
try {
2651+
await showEditProfileFlow(TEST_IDENTITIES.work);
2652+
assert.strictEqual(inputBoxValueAfterFilePick, '~/.ssh/selected_key',
2653+
'Windows backslashes should be normalized to forward slashes after ~ compression');
2654+
} finally {
2655+
if (originalHome !== undefined) {
2656+
process.env.HOME = originalHome;
2657+
}
2658+
if (originalUserProfile !== undefined) {
2659+
process.env.USERPROFILE = originalUserProfile;
2660+
}
2661+
}
2662+
});
26072663
});
26082664

26092665
describe('Validation after file selection', () => {

0 commit comments

Comments
 (0)