Skip to content

Commit cd8325a

Browse files
committed
fix: set upstream creates remote branch when it does not exist yet (closes #3)
- Run git push -u when remote branch does not exist, git branch --set-upstream-to otherwise - Show existing upstream as initial value in modal - Allow manual branch name input with a toggle when remote branches are available - Show notice when the typed branch name does not exist on the remote
1 parent 0dbba2d commit cd8325a

10 files changed

Lines changed: 136 additions & 29 deletions

File tree

src/git/git-service.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -503,11 +503,15 @@ export class GitService {
503503
await this.exec(['remote', 'remove', name]);
504504
}
505505

506-
async setUpstream(localBranch: string, remote: string, remoteBranch: string): Promise<void> {
507-
this.assertSafeRef(localBranch, 'branch --set-upstream-to');
508-
this.assertSafeRef(remote, 'branch --set-upstream-to');
509-
this.assertSafeRef(remoteBranch, 'branch --set-upstream-to');
510-
await this.exec(['branch', '--set-upstream-to', `${remote}/${remoteBranch}`, localBranch]);
506+
async setUpstream(localBranch: string, remote: string, remoteBranch: string, options?: { createRemote?: boolean }): Promise<void> {
507+
this.assertSafeRef(localBranch, 'setUpstream');
508+
this.assertSafeRef(remote, 'setUpstream');
509+
this.assertSafeRef(remoteBranch, 'setUpstream');
510+
if (options?.createRemote) {
511+
await this.exec(['push', '-u', remote, `${localBranch}:${remoteBranch}`]);
512+
} else {
513+
await this.exec(['branch', '--set-upstream-to', `${remote}/${remoteBranch}`, localBranch]);
514+
}
511515
}
512516

513517
async rebase(onto: string, options?: { autostash?: boolean }): Promise<void> {

src/panels/MainPanel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ export class MainPanel {
308308
break;
309309
}
310310
case 'setUpstream': {
311-
await this.gitService.setUpstream(message.payload.branch, message.payload.remote, message.payload.remoteBranch);
311+
await this.gitService.setUpstream(message.payload.branch, message.payload.remote, message.payload.remoteBranch, { createRemote: message.payload.createRemote });
312312
this.panel.webview.postMessage({ type: 'operationComplete', payload: { operation: 'setUpstream', success: true } });
313313
await this.refreshAll();
314314
break;

src/utils/message-bus.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export type WebviewMessage =
7474
| { type: 'stageFile'; payload: { file: string } }
7575
| { type: 'abortOperation' }
7676
| { type: 'openConflictFile'; payload: { file: string } }
77-
| { type: 'setUpstream'; payload: { branch: string; remote: string; remoteBranch: string } }
77+
| { type: 'setUpstream'; payload: { branch: string; remote: string; remoteBranch: string; createRemote?: boolean } }
7878
| { type: 'openWorktreeInNewWindow'; payload: { path: string } }
7979
| { type: 'showNotification'; payload: { message: string } }
8080
| { type: 'showTagDetails'; payload: { name: string } };

webview-ui/src/App.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,8 +532,9 @@
532532
{#if modalStore.setUpstream.show}
533533
<SetUpstreamModal
534534
branchName={modalStore.setUpstream.branchName}
535+
currentUpstream={modalStore.setUpstream.currentUpstream}
535536
onClose={() => { modalStore.closeSetUpstream(); }}
536-
onSet={(remote, remoteBranch) => { const branch = modalStore.setUpstream.branchName; modalStore.closeSetUpstream(); vscode.postMessage({ type: 'setUpstream', payload: { branch, remote, remoteBranch } }); }}
537+
onSet={(remote, remoteBranch, createRemote) => { const branch = modalStore.setUpstream.branchName; modalStore.closeSetUpstream(); vscode.postMessage({ type: 'setUpstream', payload: { branch, remote, remoteBranch, createRemote } }); }}
537538
/>
538539
{/if}
539540

webview-ui/src/components/graph/CommitGraph.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,8 @@
499499
{
500500
label: t('graph.setUpstream'),
501501
action: () => {
502-
modalStore.openSetUpstream(branchName);
502+
const branchInfo = branchStore.branches.find(b => !b.remote && b.name === branchName);
503+
modalStore.openSetUpstream(branchName, branchInfo?.upstream);
503504
},
504505
},
505506
{ separator: true, label: '', action: () => {} },

webview-ui/src/components/modals/SetUpstreamModal.svelte

Lines changed: 109 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,57 @@
66
77
interface Props {
88
branchName: string;
9+
currentUpstream?: string;
910
onClose: () => void;
10-
onSet: (remote: string, remoteBranch: string) => void;
11+
onSet: (remote: string, remoteBranch: string, createRemote: boolean) => void;
1112
}
1213
13-
let { branchName, onClose, onSet }: Props = $props();
14+
let { branchName, currentUpstream = '', onClose, onSet }: Props = $props();
1415
1516
const remoteNames = $derived([...new Set(branchStore.remotes.map(r => r.name))]);
17+
18+
function parseUpstream(upstream: string) {
19+
const slashIdx = upstream.indexOf('/');
20+
if (slashIdx === -1) return { remote: '', branch: upstream };
21+
return { remote: upstream.slice(0, slashIdx), branch: upstream.slice(slashIdx + 1) };
22+
}
23+
1624
// svelte-ignore state_referenced_locally
17-
let selectedRemote = $state(remoteNames[0] ?? 'origin');
25+
const parsed = parseUpstream(currentUpstream);
26+
// svelte-ignore state_referenced_locally
27+
let selectedRemote = $state(parsed.remote || remoteNames[0] || 'origin');
1828
1929
const remoteBranchesForRemote = $derived(
2030
branchStore.remoteBranches
2131
.filter(b => b.name.startsWith(selectedRemote + '/'))
2232
.map(b => b.name.substring(selectedRemote.length + 1))
2333
);
2434
35+
const remoteOptions = $derived(remoteNames.map(r => ({ value: r, label: r, color: '' })));
36+
const branchOptions = $derived(
37+
remoteBranchesForRemote.map(b => ({ value: b, label: `${selectedRemote}/${b}`, color: '' }))
38+
);
39+
40+
const hasBranchOptions = $derived(remoteBranchesForRemote.length > 0);
41+
42+
// svelte-ignore state_referenced_locally
43+
let dropdownBranch = $state(parsed.branch || branchName);
2544
// svelte-ignore state_referenced_locally
26-
let selectedRemoteBranch = $state(branchName);
45+
let textBranch = $state(currentUpstream ? '' : branchName);
46+
// upstream이 없으면 새로 만드는 케이스 → 텍스트 입력으로 시작
47+
// svelte-ignore state_referenced_locally
48+
let manualInput = $state(!currentUpstream);
2749
28-
const remoteOptions = $derived(remoteNames.map(r => ({ value: r, label: r, color: '' })));
29-
const branchOptions = $derived(remoteBranchesForRemote.map(b => ({ value: b, label: `${selectedRemote}/${b}`, color: '' })));
50+
const useDropdown = $derived(hasBranchOptions && !manualInput);
51+
const activeBranch = $derived(manualInput ? textBranch : dropdownBranch);
52+
53+
const remoteBranchExists = $derived(
54+
useDropdown || remoteBranchesForRemote.includes(activeBranch.trim())
55+
);
3056
3157
function handleSubmit() {
32-
if (selectedRemote && selectedRemoteBranch.trim()) {
33-
onSet(selectedRemote, selectedRemoteBranch.trim());
58+
if (selectedRemote && activeBranch.trim()) {
59+
onSet(selectedRemote, activeBranch.trim(), !remoteBranchExists);
3460
}
3561
}
3662
</script>
@@ -40,7 +66,7 @@
4066
<div class="modal-context-card">
4167
<span class="modal-pill modal-pill--target"><i class="codicon codicon-git-branch"></i>{branchName}</span>
4268
<i class="codicon codicon-arrow-both" style="color: var(--text-secondary);"></i>
43-
<span class="modal-pill modal-pill--source"><i class="codicon codicon-cloud"></i>{selectedRemote}/{selectedRemoteBranch || branchName}</span>
69+
<span class="modal-pill modal-pill--source"><i class="codicon codicon-cloud"></i>{selectedRemote}/{activeBranch || branchName}</span>
4470
</div>
4571

4672
{#if remoteNames.length > 1}
@@ -56,18 +82,84 @@
5682
{/if}
5783

5884
<div class="modal-form-group">
59-
<div class="modal-field-label">{t('setUpstream.remoteBranch')}</div>
60-
<ColorSelect
61-
options={branchOptions}
62-
value={selectedRemoteBranch}
63-
onChange={(v) => { selectedRemoteBranch = v; }}
64-
showDot={false}
65-
/>
85+
<div class="modal-field-label-row">
86+
<span class="modal-field-label">{t('setUpstream.remoteBranch')}</span>
87+
{#if hasBranchOptions}
88+
<button class="toggle-btn" onclick={() => { manualInput = !manualInput; }}>
89+
<i class="codicon {manualInput ? 'codicon-list-unordered' : 'codicon-edit'}"></i>
90+
<span>{manualInput ? t('setUpstream.selectFromList') : t('setUpstream.typeManually')}</span>
91+
</button>
92+
{/if}
93+
</div>
94+
{#if useDropdown}
95+
<ColorSelect
96+
options={branchOptions}
97+
value={dropdownBranch}
98+
onChange={(v) => { dropdownBranch = v; }}
99+
showDot={false}
100+
/>
101+
{:else}
102+
<input
103+
class="modal-input"
104+
type="text"
105+
bind:value={textBranch}
106+
/>
107+
{/if}
66108
</div>
67109

110+
{#if activeBranch.trim() && !remoteBranchExists}
111+
<p class="notice"><i class="codicon codicon-info"></i>{t('setUpstream.willCreate')}</p>
112+
{/if}
113+
68114
<div class="form-actions">
69115
<button onclick={onClose}>{t('common.cancel')}</button>
70-
<button class="primary" onclick={handleSubmit} disabled={!selectedRemoteBranch.trim()}>{t('setUpstream.set')}</button>
116+
<button class="primary" onclick={handleSubmit} disabled={!activeBranch.trim()}>{t('setUpstream.set')}</button>
71117
</div>
72118
</Modal>
73119

120+
<style>
121+
.modal-field-label-row {
122+
display: flex;
123+
align-items: center;
124+
justify-content: space-between;
125+
margin-bottom: 6px;
126+
}
127+
128+
.modal-field-label-row .modal-field-label {
129+
margin-bottom: 0;
130+
}
131+
132+
.toggle-btn {
133+
display: flex;
134+
align-items: center;
135+
gap: 3px;
136+
background: none;
137+
border: none;
138+
padding: 0;
139+
font-size: 11px;
140+
color: var(--text-secondary);
141+
cursor: pointer;
142+
font-family: inherit;
143+
}
144+
145+
.toggle-btn:hover {
146+
color: var(--text-primary);
147+
}
148+
149+
.toggle-btn:hover span {
150+
text-decoration: underline;
151+
}
152+
153+
.toggle-btn .codicon {
154+
font-size: 11px;
155+
}
156+
157+
.notice {
158+
display: flex;
159+
align-items: center;
160+
gap: 6px;
161+
font-size: 12px;
162+
color: var(--text-secondary);
163+
margin: 0;
164+
}
165+
</style>

webview-ui/src/lib/i18n/en.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,4 +459,7 @@ export const en: Record<string, string> = {
459459
'setUpstream.remote': 'Remote',
460460
'setUpstream.remoteBranch': 'Remote branch',
461461
'setUpstream.set': 'Set Upstream',
462+
'setUpstream.willCreate': 'Remote branch does not exist yet. Running git push -u will create it.',
463+
'setUpstream.typeManually': 'Type manually',
464+
'setUpstream.selectFromList': 'Select from list',
462465
};

webview-ui/src/lib/i18n/ko.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,4 +459,7 @@ export const ko: Record<string, string> = {
459459
'setUpstream.remote': '리모트',
460460
'setUpstream.remoteBranch': '리모트 브랜치',
461461
'setUpstream.set': '업스트림 설정',
462+
'setUpstream.willCreate': '리모트 브랜치가 존재하지 않습니다. git push -u를 실행하여 새로 생성합니다.',
463+
'setUpstream.typeManually': '직접 입력',
464+
'setUpstream.selectFromList': '목록에서 선택',
462465
};

webview-ui/src/lib/i18n/zh.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,4 +459,7 @@ export const zh: Record<string, string> = {
459459
'setUpstream.remote': '远程仓库',
460460
'setUpstream.remoteBranch': '远程分支',
461461
'setUpstream.set': '设置上游',
462+
'setUpstream.willCreate': '远程分支尚不存在,将执行 git push -u 进行创建。',
463+
'setUpstream.typeManually': '手动输入',
464+
'setUpstream.selectFromList': '从列表选择',
462465
};

webview-ui/src/lib/stores/modals.svelte.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ class ModalStore {
6666
closeStashSave() { this.stashSave = { show: false }; }
6767

6868
// ── Set Upstream ──
69-
setUpstream = $state({ show: false, branchName: '' });
70-
openSetUpstream(branchName: string) { this.setUpstream = { show: true, branchName }; }
71-
closeSetUpstream() { this.setUpstream = { show: false, branchName: '' }; }
69+
setUpstream = $state({ show: false, branchName: '', currentUpstream: '' });
70+
openSetUpstream(branchName: string, currentUpstream?: string) { this.setUpstream = { show: true, branchName, currentUpstream: currentUpstream ?? '' }; }
71+
closeSetUpstream() { this.setUpstream = { show: false, branchName: '', currentUpstream: '' }; }
7272

7373
// ── Fetch ──
7474
fetch = $state({ show: false, allRemotes: false, remote: 'origin' });

0 commit comments

Comments
 (0)