From 23df37fff68fd5699c0af5aea56f11f7dd00c35a Mon Sep 17 00:00:00 2001 From: Silberengel Date: Wed, 25 Feb 2026 09:58:29 +0100 Subject: [PATCH] fix cli sync and refine commit workflow Nostr-Signature: ddf0b49bb68139efbdacd6308b95b4a5329a37f479b319d609d712bee83e2d45 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc aacc22f02a3129d18cd2bdcfc4e2dda66e9358e552eac507cd4c4808bb47cd582298aed7d28f21b677418e1a91f3f1553c08f02671df8f1f43681cf7b19a744e --- docs/CustomKinds.md | 13 ++- docs/specs.md | 3 +- nostr/commit-signatures.jsonl | 1 + scripts/sync-cli.sh | 154 ++++++++++++++++++++++++-- src/lib/services/git/commit-signer.ts | 67 +++++------ src/lib/services/git/file-manager.ts | 6 +- 6 files changed, 179 insertions(+), 65 deletions(-) diff --git a/docs/CustomKinds.md b/docs/CustomKinds.md index f50ec14..92eeff6 100644 --- a/docs/CustomKinds.md +++ b/docs/CustomKinds.md @@ -17,7 +17,6 @@ Git commit signature events are used to cryptographically sign git commits using "created_at": 1234567890, "content": "Signed commit: \n\n", "tags": [ - ["commit", "abc123def456..."], // Final commit hash (added after commit is created) ["author", "John Doe"], // Author name ["author", "john@example.com"], // Author email (second author tag) ["message", "Fix bug in feature"], // Commit message @@ -30,11 +29,15 @@ Git commit signature events are used to cryptographically sign git commits using ### Tag Descriptions -- **`commit`** (required): The final commit hash after the commit is created. This tag is added after the commit is created, as the hash is not known beforehand. - **`author`** (required, appears twice): First occurrence contains the author name, second contains the author email. -- **`message`** (required): The commit message text. +- **`message`** (required): The commit message text. Used for verification by matching with the actual commit message. - **`e`** (optional): Reference to a NIP-98 authentication event if the commit was made via HTTP git operations. +**Note:** Commit signature events do not include the commit hash because: +- The commit-msg hook runs **before** the commit is created, so the hash doesn't exist yet +- Nostr events are **immutable** - once created and signed, they cannot be modified +- Verification matches events to commits by comparing the commit message instead + ### Usage in GitRepublic 1. **Client-Side Signing**: When users make commits through the web interface, they can sign commits using NIP-07 (browser extension). The signature event is created client-side and keys never leave the browser. @@ -47,8 +50,8 @@ Git commit signature events are used to cryptographically sign git commits using ``` 4. **Verification**: Commit signatures can be verified by: - - Checking the event signature - - Verifying the commit hash matches + - Checking the event signature (cryptographic verification) + - Matching the commit message in the `message` tag with the actual commit message - Confirming the author information matches the commit ### Rationale diff --git a/docs/specs.md b/docs/specs.md index 1b93c2d..f985e6a 100644 --- a/docs/specs.md +++ b/docs/specs.md @@ -112,12 +112,13 @@ GitRepublic uses custom event kinds not defined in any standard NIP: Cryptographically sign git commits using Nostr keys. **Tags**: -- `commit`: Final commit hash - `author`: Author name (first occurrence) - `author`: Author email (second occurrence) - `message`: Commit message - `e`: Optional reference to NIP-98 auth event +**Note:** Commit signature events do not include the commit hash because the commit-msg hook runs before the commit is created. Verification matches events to commits by comparing the commit message. + **Status**: Custom implementation (may be proposed as NIP in future) See [Custom Event Kinds](./CustomKinds.md#kind-1640-commit-signature) for complete documentation. diff --git a/nostr/commit-signatures.jsonl b/nostr/commit-signatures.jsonl index 3a1f879..e3db639 100644 --- a/nostr/commit-signatures.jsonl +++ b/nostr/commit-signatures.jsonl @@ -85,3 +85,4 @@ {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772005973,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","update the API"]],"content":"Signed commit: update the API","id":"09329cf7eb8c228e87e365b0d7a4d052ddb08b3cf7f75162b2e9b8dd77e917a0","sig":"1a1b40b18dbd744bd4043f0f18d5945ba7d1f738d36bb8457c4ec806832cd1b44ed36417c24d01511fa7fddfa33c376bf24c2fdf478bf1ab015cf4c524aac7e8"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772008194,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","revamp tutoral and ReadMe documentation\nupdate API docs"]],"content":"Signed commit: revamp tutoral and ReadMe documentation\nupdate API docs","id":"9d1fb9db75e26a5fcdcab54253ef1c6126ea1e01e98728554435e655354f6238","sig":"0cfde0ac8a7083479c982c7f212a91eee7e8ed33b43b30291677fe3a126638ae4fde03893aa5d492f8f6fc8e066b7c46bd6f07246376a5d9e56becc73e4e48d8"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772008707,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix bugs\nsign sync commits"]],"content":"Signed commit: fix bugs\nsign sync commits","id":"3b05eb0074772bd7d3322e0a32ef8932dbafa2334ff51a75ed5159fcdfdb3558","sig":"b7bdc3272a6daddf409dc519de6e06a4ee85407a14790996be7c5039a00358106285a8fc00084d9016587df529d7026f28108384976198789aa1fd60140a5738"} +{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772009058,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix doc page redicrects"]],"content":"Signed commit: fix doc page redicrects","id":"be18739cf8e9062e7163dca11c6768086cbf834d52f9758c884a420e4d9dceb7","sig":"2f068184caa9d921f38d6b132992614f39bab6ec6ea8040ac0d337db4c16de4e66de44c4d78162787f1cf8bf13978d01927b8152405f0b48adf608ca6bf34295"} diff --git a/scripts/sync-cli.sh b/scripts/sync-cli.sh index aa4e318..7dafe94 100755 --- a/scripts/sync-cli.sh +++ b/scripts/sync-cli.sh @@ -30,9 +30,18 @@ if [ ! -d "$SEPARATE_REPO" ]; then echo " git remote add origin " fi -# Change to separate repo directory +# Change to separate repo directory and verify we're in a git repo cd "$SEPARATE_REPO" || exit 1 +# Verify we're in the correct git repository +if ! git rev-parse --git-dir >/dev/null 2>&1; then + echo "Error: $SEPARATE_REPO is not a git repository" + exit 1 +fi + +# Store the absolute path to ensure we stay in this directory +SEPARATE_REPO_ABS="$(cd "$SEPARATE_REPO" && pwd)" + # Copy files using rsync rsync -av --delete \ --exclude='node_modules' \ @@ -114,8 +123,48 @@ else echo "✅ No changes to commit (files are up to date)" fi -# Get current branch -CURRENT_BRANCH="$(git branch --show-current || echo 'master')" +# Get current branch - try multiple methods to detect the actual branch +CURRENT_BRANCH="" +if CURRENT_BRANCH=$(git branch --show-current 2>/dev/null); then + # Successfully got branch name + : +elif CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null); then + # Alternative method + : +elif CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null); then + # Another alternative + : +else + # Try to detect from existing branches + if MAIN_BRANCH=$(git branch -l | grep -E '^\*?\s+(main|master|develop)' | head -1 | sed 's/^\*\?\s*//' | sed 's/^.*\s//'); then + CURRENT_BRANCH="$MAIN_BRANCH" + else + # Last resort: try main, then master + if git show-ref --verify --quiet refs/heads/main 2>/dev/null; then + CURRENT_BRANCH="main" + elif git show-ref --verify --quiet refs/heads/master 2>/dev/null; then + CURRENT_BRANCH="master" + else + echo "⚠️ Warning: Could not detect current branch, defaulting to 'main'" + CURRENT_BRANCH="main" + fi + fi +fi + +if [ -z "$CURRENT_BRANCH" ]; then + echo "⚠️ Warning: Could not detect current branch, defaulting to 'main'" + CURRENT_BRANCH="main" +fi + +echo "Detected branch: $CURRENT_BRANCH" + +# Verify the branch actually exists +if ! git show-ref --verify --quiet "refs/heads/$CURRENT_BRANCH" 2>/dev/null; then + echo "⚠️ Error: Branch '$CURRENT_BRANCH' does not exist locally" + echo " Available branches:" + git branch -l | sed 's/^/ /' + exit 1 +fi # Check if gitrep is available if ! command -v gitrep >/dev/null 2>&1 && ! command -v gitrepublic >/dev/null 2>&1; then @@ -150,16 +199,97 @@ else echo "" echo "Pushing to all remotes using $GITREP_CMD push-all..." + echo "Working directory: $SEPARATE_REPO_ABS" + echo "Git directory: $(git rev-parse --git-dir)" + echo "Git remotes:" + git remote -v | sed 's/^/ /' - # Use gitrep push-all to push to all remotes - # This handles reachability checks, error handling, and provides better output - if $GITREP_CMD push-all "$CURRENT_BRANCH" 2>&1; then - echo "✅ Successfully pushed to all remotes" - else - PUSH_EXIT=$? - echo "⚠️ Push to some remotes may have failed (exit code: $PUSH_EXIT)" - # Don't exit with error - gitrep push-all may have succeeded for some remotes - fi + # Ensure we're in the separate repo directory when calling gitrep push-all + # Use a subshell with explicit directory change to ensure all git commands run correctly + # Clear any GIT_DIR or GIT_WORK_TREE that might interfere + ( + # Change to the separate repo directory + cd "$SEPARATE_REPO_ABS" || { echo "Error: Failed to change to $SEPARATE_REPO_ABS"; exit 1; } + + # Clear environment variables that might interfere + unset GIT_DIR + unset GIT_WORK_TREE + + # Verify we're in the correct directory and using the correct git config + ACTUAL_GIT_DIR="$(git rev-parse --show-toplevel 2>/dev/null)" + if [ "$ACTUAL_GIT_DIR" != "$SEPARATE_REPO_ABS" ]; then + echo "⚠️ Warning: Git directory mismatch!" + echo " Expected: $SEPARATE_REPO_ABS" + echo " Actual: $ACTUAL_GIT_DIR" + fi + + echo "Using git config from: $ACTUAL_GIT_DIR" + echo "Verifying remotes before push:" + git remote -v | sed 's/^/ /' + + # Verify remotes point to gitrepublic-cli, not gitrepublic-web + WRONG_REMOTE=false + for remote in $(git remote); do + REMOTE_URL="$(git remote get-url "$remote" 2>/dev/null)" + if echo "$REMOTE_URL" | grep -q "gitrepublic-web\.git"; then + echo "⚠️ Error: Remote '$remote' points to gitrepublic-web instead of gitrepublic-cli!" + echo " URL: $REMOTE_URL" + WRONG_REMOTE=true + fi + done + + if [ "$WRONG_REMOTE" = true ]; then + echo "❌ Cannot push: Remotes are pointing to the wrong repository" + exit 1 + fi + + # Double-check we're in the right directory + if [ "$(pwd)" != "$SEPARATE_REPO_ABS" ]; then + echo "❌ Error: Not in the correct directory!" + echo " Expected: $SEPARATE_REPO_ABS" + echo " Actual: $(pwd)" + exit 1 + fi + + # Get list of remotes to push to + REMOTES="$(git remote)" + if [ -z "$REMOTES" ]; then + echo "ℹ️ No remotes configured" + exit 0 + fi + + # Push to each remote using regular git push + # This ensures we use the correct git config from this directory + SUCCESS_COUNT=0 + FAIL_COUNT=0 + + for remote in $REMOTES; do + echo "" + echo "Pushing to $remote ($CURRENT_BRANCH)..." + REMOTE_URL="$(git remote get-url "$remote" 2>/dev/null)" + echo " Remote URL: $REMOTE_URL" + + if git push "$remote" "$CURRENT_BRANCH" 2>&1; then + echo "✅ Successfully pushed to $remote" + SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) + else + echo "⚠️ Failed to push to $remote" + FAIL_COUNT=$((FAIL_COUNT + 1)) + fi + done + + echo "" + echo "======================================================================" + echo "Push Summary: $SUCCESS_COUNT succeeded, $FAIL_COUNT failed out of $(echo "$REMOTES" | wc -l) remotes" + echo "======================================================================" + + if [ $FAIL_COUNT -gt 0 ]; then + exit 1 + else + exit 0 + fi + ) + PUSH_EXIT=$? fi echo "" diff --git a/src/lib/services/git/commit-signer.ts b/src/lib/services/git/commit-signer.ts index bf6bee8..a1f8944 100644 --- a/src/lib/services/git/commit-signer.ts +++ b/src/lib/services/git/commit-signer.ts @@ -88,10 +88,15 @@ export function decodeNostrId(id: string): string { /** * Create a Nostr event for commit signing * This creates a kind 1640 (commit signature) event that can be used to sign commits + * + * Note: The commit hash is not included in the event because: + * - For commit-msg hooks: The hook runs before the commit is created, so the hash doesn't exist yet + * - Nostr events are immutable: Once created and signed, they cannot be modified + * + * Verification matches events to commits by comparing the commit message. */ export function createCommitSignatureEvent( privateKey: string, - commitHash: string, commitMessage: string, authorName: string, authorEmail: string, @@ -107,11 +112,10 @@ export function createCommitSignatureEvent( pubkey, created_at: timestamp, tags: [ - ['commit', commitHash], ['author', authorName, authorEmail], ['message', commitMessage] ], - content: `Signed commit` + content: `Signed commit: ${commitMessage}` }; // Finalize and sign the event @@ -256,43 +260,16 @@ export async function createGitCommitSignature( return { signedMessage, signatureEvent: signedEvent }; } -/** - * Update commit signature with actual commit hash after commit is created - */ -export function updateCommitSignatureWithHash( - signatureEvent: NostrEvent, - commitHash: string -): NostrEvent { - // Add commit hash tag - const commitTag = signatureEvent.tags.find(t => t[0] === 'commit'); - if (!commitTag) { - signatureEvent.tags.push(['commit', commitHash]); - } else { - commitTag[1] = commitHash; - } - - // Recalculate event ID with updated tags - const serialized = JSON.stringify([ - 0, - signatureEvent.pubkey, - signatureEvent.created_at, - signatureEvent.kind, - signatureEvent.tags, - signatureEvent.content - ]); - signatureEvent.id = createHash('sha256').update(serialized).digest('hex'); - - // Note: Re-signing would require the private key, which we don't have here - // The signature in the original event is still valid for the commit hash tag - return signatureEvent; -} - /** * Verify a commit signature from a Nostr event and return original information + * + * Verification matches events to commits by comparing the commit message, + * since commit signature events don't include the commit hash (the hook runs + * before the commit is created, and Nostr events are immutable). */ export function verifyCommitSignature( signatureEvent: NostrEvent, - commitHash: string + commitMessage: string ): { valid: boolean; error?: string; @@ -308,10 +285,17 @@ export function verifyCommitSignature( return { valid: false, error: `Invalid event kind for commit signature. Expected ${KIND.COMMIT_SIGNATURE}, got ${signatureEvent.kind}` }; } - // Check commit hash tag - const commitTag = signatureEvent.tags.find(t => t[0] === 'commit'); - if (!commitTag || commitTag[1] !== commitHash) { - return { valid: false, error: 'Commit hash mismatch' }; + // Verify by matching the commit message + const messageTag = signatureEvent.tags.find(t => t[0] === 'message'); + if (!messageTag || !messageTag[1]) { + return { valid: false, error: 'Commit signature event missing message tag' }; + } + + // Compare commit message (normalize whitespace for comparison) + const eventMessage = messageTag[1].trim(); + const actualMessage = commitMessage.replace(/Nostr-Signature:.*$/s, '').trim(); + if (eventMessage !== actualMessage) { + return { valid: false, error: 'Commit message mismatch - signature event message does not match actual commit message' }; } // Verify event signature cryptographically @@ -327,7 +311,6 @@ export function verifyCommitSignature( // Extract original information from tags const authorTag = signatureEvent.tags.find(t => t[0] === 'author'); - const messageTag = signatureEvent.tags.find(t => t[0] === 'message'); return { valid: true, @@ -444,8 +427,8 @@ export async function verifyCommitFromMessage( signatureEvent = events[0] as NostrEvent; } - // Verify the signature - const verification = verifyCommitSignature(signatureEvent, commitHash); + // Verify the signature by matching the commit message + const verification = verifyCommitSignature(signatureEvent, commitMessage); if (!verification.valid) { return { ...verification, hasSignature: true }; diff --git a/src/lib/services/git/file-manager.ts b/src/lib/services/git/file-manager.ts index 3cf5edf..e48b672 100644 --- a/src/lib/services/git/file-manager.ts +++ b/src/lib/services/git/file-manager.ts @@ -1318,12 +1318,8 @@ export class FileManager { signatureEvent = event; } - // Update signature event with actual commit hash - const { updateCommitSignatureWithHash } = await import('./commit-signer.js'); - const updatedEvent = updateCommitSignatureWithHash(signatureEvent, commitHash); - // Save to nostr/commit-signatures.jsonl (use workDir since we have it) - await this.saveCommitSignatureEventToWorktree(workDir, updatedEvent); + await this.saveCommitSignatureEventToWorktree(workDir, signatureEvent); // Check if repo is private - only publish to relays if public const isPrivate = await this.isRepoPrivate(npub, repoName);