Skip to content

Commit 1a76326

Browse files
Add git push support with NIP-98 auth
- Add pushRepo() with NIP-98 onAuth callback - Add commitChanges() to stage and commit files - Add writeFile() helper - Add push button (↑) to repo list - Add "Create Commit & Push" test in settings modal - Creates browser-test.txt, commits, and pushes to verify auth
1 parent 66940c4 commit 1a76326

1 file changed

Lines changed: 163 additions & 4 deletions

File tree

index.html

Lines changed: 163 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
}
8282
.repo-item:hover .sync-btn { opacity: 1; }
8383
.repo-item.syncing .sync-btn { opacity: 1; animation: spin 1s linear infinite; }
84+
.repo-item .sync-btn.pushing { opacity: 1; color: #2da44e; }
8485
@keyframes spin { to { transform: rotate(360deg); } }
8586
.add-btn {
8687
margin: 8px;
@@ -412,6 +413,67 @@
412413
console.log('Pull complete');
413414
}
414415

416+
async function pushRepo(repoId, cloneUrl, branch = 'main') {
417+
const dir = `${SYNC_DIR}/${repoId}`;
418+
const privkey = getPrivkey();
419+
420+
if (!privkey) {
421+
throw new Error('No private key configured. Open Settings to add one.');
422+
}
423+
424+
console.log(`Pushing ${repoId} to ${cloneUrl}...`);
425+
await git.push({
426+
fs,
427+
http,
428+
dir,
429+
url: cloneUrl,
430+
ref: branch,
431+
onAuth: async (url) => {
432+
// Create NIP-98 token for this URL
433+
const event = {
434+
kind: 27235,
435+
created_at: Math.floor(Date.now() / 1000),
436+
tags: [
437+
['u', url],
438+
['method', 'POST'] // Git uses POST for push
439+
],
440+
content: ''
441+
};
442+
const signed = await signEvent(event, privkey);
443+
const token = btoa(JSON.stringify(signed));
444+
return { username: 'nostr', password: token };
445+
}
446+
});
447+
console.log('Push complete');
448+
}
449+
450+
async function commitChanges(repoId, message) {
451+
const dir = `${SYNC_DIR}/${repoId}`;
452+
const privkey = getPrivkey();
453+
const pubkey = getPubkeyFromPrivkey(privkey);
454+
455+
// Stage all changes
456+
await git.add({ fs, dir, filepath: '.' });
457+
458+
// Create commit
459+
const sha = await git.commit({
460+
fs,
461+
dir,
462+
message,
463+
author: {
464+
name: pubkey ? `nostr:${pubkey.slice(0, 8)}` : 'browser',
465+
email: pubkey ? `${pubkey.slice(0, 8)}@nostr` : 'browser@local'
466+
}
467+
});
468+
console.log('Commit:', sha);
469+
return sha;
470+
}
471+
472+
async function writeFile(repoId, filePath, content) {
473+
const fullPath = `${SYNC_DIR}/${repoId}/${filePath}`;
474+
await pfs.writeFile(fullPath, content, 'utf8');
475+
}
476+
415477
async function repoExists(repoId) {
416478
try {
417479
await pfs.stat(`${SYNC_DIR}/${repoId}/.git`);
@@ -531,7 +593,7 @@ <h1>🔗 Nostr Git Browser</h1>
531593
`;
532594
}
533595

534-
function RepoList({ repos, selected, syncing, onSelect, onSync, onDelete, onAdd }) {
596+
function RepoList({ repos, selected, syncing, pushing, onSelect, onSync, onPush, onDelete, onAdd }) {
535597
return html`
536598
<div class="sidebar">
537599
<h2>Repositories</h2>
@@ -544,7 +606,17 @@ <h2>Repositories</h2>
544606
>
545607
<span>📁</span>
546608
<span class="name">${id}</span>
547-
<button class="sync-btn" onClick=${(e) => { e.stopPropagation(); onSync(id); }}></button>
609+
<button
610+
class="sync-btn"
611+
onClick=${(e) => { e.stopPropagation(); onSync(id); }}
612+
title="Pull"
613+
></button>
614+
<button
615+
class="sync-btn ${pushing === id ? 'pushing' : ''}"
616+
onClick=${(e) => { e.stopPropagation(); onPush(id); }}
617+
title="Push"
618+
style="margin-left: 2px;"
619+
></button>
548620
</div>
549621
`)}
550622
${Object.keys(repos).length === 0 && html`
@@ -688,11 +760,14 @@ <h2>Add Repository</h2>
688760
return html`<div class="toast">${message}</div>`;
689761
}
690762

691-
function SettingsModal({ onClose, onToast }) {
763+
function SettingsModal({ onClose, onToast, repos }) {
692764
const [privkey, setPrivkeyState] = useState(getPrivkey);
693765
const [testUrl, setTestUrl] = useState('https://melvin.me/public/git/sync/20260202/counter.txt');
694766
const [testResult, setTestResult] = useState(null);
695767
const [testing, setTesting] = useState(false);
768+
const [gitTestResult, setGitTestResult] = useState(null);
769+
const [gitTesting, setGitTesting] = useState(false);
770+
const [selectedRepo, setSelectedRepo] = useState(Object.keys(repos || {})[0] || '');
696771

697772
const pubkey = useMemo(() => getPubkeyFromPrivkey(privkey), [privkey]);
698773
const isValid = pubkey.length === 64;
@@ -742,9 +817,41 @@ <h2>Add Repository</h2>
742817
}
743818
};
744819

820+
const handleGitTest = async () => {
821+
if (!isValid || !selectedRepo) {
822+
setGitTestResult({ success: false, message: 'Need valid key and repo' });
823+
return;
824+
}
825+
826+
setGitTesting(true);
827+
setGitTestResult(null);
828+
829+
try {
830+
const repo = repos[selectedRepo];
831+
if (!repo) throw new Error('Repo not found');
832+
833+
// Create/update a test file with timestamp
834+
const timestamp = new Date().toISOString();
835+
await writeFile(selectedRepo, 'browser-test.txt', `Browser push test: ${timestamp}\n`);
836+
837+
// Commit
838+
await commitChanges(selectedRepo, `Browser test: ${timestamp}`);
839+
840+
// Push
841+
await pushRepo(selectedRepo, repo.cloneUrl, repo.branch);
842+
843+
setGitTestResult({ success: true, message: `Pushed to ${selectedRepo}!` });
844+
} catch (err) {
845+
console.error('Git test error:', err);
846+
setGitTestResult({ success: false, message: err.message });
847+
} finally {
848+
setGitTesting(false);
849+
}
850+
};
851+
745852
return html`
746853
<div class="modal-overlay" onClick=${onClose}>
747-
<div class="modal" onClick=${e => e.stopPropagation()}>
854+
<div class="modal" onClick=${e => e.stopPropagation()} style="max-height: 90vh; overflow-y: auto;">
748855
<h2>Settings</h2>
749856
<label>
750857
<span>Private Key (hex)</span>
@@ -785,6 +892,32 @@ <h2>Settings</h2>
785892
${testResult.message}
786893
</div>
787894
`}
895+
<hr style="margin: 16px 0; border: none; border-top: 1px solid #d0d7de;" />
896+
<label>
897+
<span>Test Git Push</span>
898+
<select
899+
value=${selectedRepo}
900+
onChange=${e => setSelectedRepo(e.target.value)}
901+
style="width: 100%; padding: 8px; border: 1px solid #d0d7de; border-radius: 6px;"
902+
>
903+
${Object.keys(repos || {}).map(id => html`
904+
<option key=${id} value=${id}>${id}</option>
905+
`)}
906+
</select>
907+
</label>
908+
<button
909+
class="test-btn"
910+
onClick=${handleGitTest}
911+
disabled=${!isValid || !selectedRepo || gitTesting}
912+
style="margin-top: 8px;"
913+
>
914+
${gitTesting ? 'Pushing...' : 'Create Commit & Push'}
915+
</button>
916+
${gitTestResult && html`
917+
<div class="test-result ${gitTestResult.success ? 'success' : 'error'}">
918+
${gitTestResult.message}
919+
</div>
920+
`}
788921
<div class="buttons">
789922
<button type="button" onClick=${onClose}>Cancel</button>
790923
<button type="submit" onClick=${handleSave}>Save</button>
@@ -799,6 +932,7 @@ <h2>Settings</h2>
799932
const [config, setConfig] = useState(loadConfig);
800933
const [selected, setSelected] = useState(null);
801934
const [syncing, setSyncing] = useState(null);
935+
const [pushing, setPushing] = useState(null);
802936
const [files, setFiles] = useState([]);
803937
const [expanded, setExpanded] = useState(new Set());
804938
const [selectedFile, setSelectedFile] = useState(null);
@@ -922,6 +1056,28 @@ <h2>Settings</h2>
9221056
}
9231057
}
9241058

1059+
async function handlePush(repoId) {
1060+
const repo = config.repos[repoId];
1061+
if (!repo) return;
1062+
1063+
const privkey = getPrivkey();
1064+
if (!privkey) {
1065+
showToast('No private key configured. Open Settings first.');
1066+
return;
1067+
}
1068+
1069+
setPushing(repoId);
1070+
try {
1071+
await pushRepo(repoId, repo.cloneUrl, repo.branch);
1072+
showToast(`Pushed: ${repoId}`);
1073+
} catch (err) {
1074+
console.error('Push error:', err);
1075+
showToast(`Push error: ${err.message}`);
1076+
} finally {
1077+
setPushing(null);
1078+
}
1079+
}
1080+
9251081
async function handleToggle(path) {
9261082
const newExpanded = new Set(expanded);
9271083
if (newExpanded.has(path)) {
@@ -993,8 +1149,10 @@ <h2>Settings</h2>
9931149
repos=${config.repos}
9941150
selected=${selected}
9951151
syncing=${syncing}
1152+
pushing=${pushing}
9961153
onSelect=${setSelected}
9971154
onSync=${syncRepo}
1155+
onPush=${handlePush}
9981156
onDelete=${handleDeleteRepo}
9991157
onAdd=${() => setShowAddModal(true)}
10001158
/>
@@ -1023,6 +1181,7 @@ <h2>Settings</h2>
10231181
<${SettingsModal}
10241182
onClose=${() => setShowSettingsModal(false)}
10251183
onToast=${showToast}
1184+
repos=${config.repos}
10261185
/>
10271186
`}
10281187
<${Toast} message=${toast} />

0 commit comments

Comments
 (0)