Browse Source

fix cli sync and refine commit workflow

Nostr-Signature: ddf0b49bb68139efbdacd6308b95b4a5329a37f479b319d609d712bee83e2d45 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc aacc22f02a3129d18cd2bdcfc4e2dda66e9358e552eac507cd4c4808bb47cd582298aed7d28f21b677418e1a91f3f1553c08f02671df8f1f43681cf7b19a744e
main
Silberengel 3 weeks ago
parent
commit
23df37fff6
  1. 13
      docs/CustomKinds.md
  2. 3
      docs/specs.md
  3. 1
      nostr/commit-signatures.jsonl
  4. 150
      scripts/sync-cli.sh
  5. 67
      src/lib/services/git/commit-signer.ts
  6. 6
      src/lib/services/git/file-manager.ts

13
docs/CustomKinds.md

@ -17,7 +17,6 @@ Git commit signature events are used to cryptographically sign git commits using @@ -17,7 +17,6 @@ Git commit signature events are used to cryptographically sign git commits using
"created_at": 1234567890,
"content": "Signed commit: <commit-message>\n\n<optional-additional-info>",
"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 @@ -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 @@ -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

3
docs/specs.md

@ -112,12 +112,13 @@ GitRepublic uses custom event kinds not defined in any standard NIP: @@ -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.

1
nostr/commit-signatures.jsonl

@ -85,3 +85,4 @@ @@ -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"}

150
scripts/sync-cli.sh

@ -30,9 +30,18 @@ if [ ! -d "$SEPARATE_REPO" ]; then @@ -30,9 +30,18 @@ if [ ! -d "$SEPARATE_REPO" ]; then
echo " git remote add origin <your-repo-url>"
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 @@ -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 @@ -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/^/ /'
# 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/^/ /'
# 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"
# 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
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
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 ""

67
src/lib/services/git/commit-signer.ts

@ -88,10 +88,15 @@ export function decodeNostrId(id: string): string { @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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 };

6
src/lib/services/git/file-manager.ts

@ -1318,12 +1318,8 @@ export class FileManager { @@ -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);

Loading…
Cancel
Save