Attribution (#1049)

This commit is contained in:
Neil
2026-04-24 20:55:50 -07:00
committed by GitHub
parent 7ea0e8d0d1
commit f1a1f18cf8
11 changed files with 1276 additions and 6 deletions
@@ -0,0 +1,492 @@
/* eslint-disable max-lines -- Why: these tests exercise generated shell wrapper
scripts end-to-end, and keeping the regression fixtures adjacent makes the
attribution safety cases easier to audit. */
import { execFileSync } from 'node:child_process'
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { applyTerminalAttributionEnv } from './terminal-attribution'
describe('applyTerminalAttributionEnv', () => {
let tmpRoot: string | null = null
afterEach(() => {
if (tmpRoot) {
rmSync(tmpRoot, { force: true, recursive: true })
tmpRoot = null
}
})
function makeTmpRoot(): string {
tmpRoot = mkdtempSync(join(tmpdir(), 'orca-attribution-'))
return tmpRoot
}
function runGit(repo: string, args: string[], env?: Record<string, string>): string {
return execFileSync('git', args, {
cwd: repo,
encoding: 'utf8',
env: { ...process.env, ...env }
})
}
it('does not amend HEAD when git commit --dry-run exits successfully', () => {
const root = makeTmpRoot()
const repo = join(root, 'repo')
mkdirSync(repo)
runGit(repo, ['init'])
runGit(repo, ['config', 'user.name', 'Orca Test'])
runGit(repo, ['config', 'user.email', 'orca-test@example.com'])
writeFileSync(join(repo, 'README.md'), 'initial\n')
runGit(repo, ['add', 'README.md'])
runGit(repo, ['commit', '-m', 'initial'])
const attributionEnv = applyTerminalAttributionEnv(
{ PATH: process.env.PATH ?? '' },
{ enabled: true, userDataPath: join(root, 'user-data') }
)
const beforeHead = runGit(repo, ['rev-parse', 'HEAD']).trim()
writeFileSync(join(repo, 'second.txt'), 'second\n')
runGit(repo, ['add', 'second.txt'])
// Why: dry-run reports what would be committed but must not rewrite the
// existing HEAD just because the real git command returns success.
runGit(repo, ['commit', '--dry-run', '-m', 'second'], attributionEnv)
expect(runGit(repo, ['rev-parse', 'HEAD']).trim()).toBe(beforeHead)
expect(runGit(repo, ['log', '-1', '--format=%B'])).not.toContain('Co-authored-by: Orca')
runGit(repo, ['commit', '-m', 'second'], attributionEnv)
expect(runGit(repo, ['rev-parse', 'HEAD']).trim()).not.toBe(beforeHead)
expect(runGit(repo, ['log', '-1', '--format=%B'])).toContain(
'Co-authored-by: Orca <help@stably.ai>'
)
})
it('still adds the trailer when git commit uses --no-verify shorthand', () => {
const root = makeTmpRoot()
const repo = join(root, 'repo')
mkdirSync(repo)
runGit(repo, ['init'])
runGit(repo, ['config', 'user.name', 'Orca Test'])
runGit(repo, ['config', 'user.email', 'orca-test@example.com'])
writeFileSync(join(repo, 'README.md'), 'initial\n')
runGit(repo, ['add', 'README.md'])
const attributionEnv = applyTerminalAttributionEnv(
{ PATH: process.env.PATH ?? '' },
{ enabled: true, userDataPath: join(root, 'user-data') }
)
runGit(repo, ['commit', '-n', '-m', 'initial'], attributionEnv)
expect(runGit(repo, ['log', '-1', '--format=%B'])).toContain(
'Co-authored-by: Orca <help@stably.ai>'
)
})
it('does not rerun git hooks for the attribution-only amend', () => {
const root = makeTmpRoot()
const repo = join(root, 'repo')
mkdirSync(repo)
runGit(repo, ['init'])
runGit(repo, ['config', 'user.name', 'Orca Test'])
runGit(repo, ['config', 'user.email', 'orca-test@example.com'])
const hookPath = join(repo, '.git', 'hooks', 'commit-msg')
const hookCounterPath = join(repo, 'hook-count')
writeFileSync(
hookPath,
`#!/usr/bin/env bash
set -euo pipefail
count=0
if [[ -f "${hookCounterPath}" ]]; then
count="$(cat "${hookCounterPath}")"
fi
printf '%s\\n' "$((count + 1))" >"${hookCounterPath}"
`,
'utf8'
)
chmodSync(hookPath, 0o755)
writeFileSync(join(repo, 'README.md'), 'initial\n')
runGit(repo, ['add', 'README.md'])
const attributionEnv = applyTerminalAttributionEnv(
{ PATH: process.env.PATH ?? '' },
{ enabled: true, userDataPath: join(root, 'user-data') }
)
runGit(repo, ['commit', '-m', 'initial'], attributionEnv)
expect(readFileSync(hookCounterPath, 'utf8').trim()).toBe('1')
expect(runGit(repo, ['log', '-1', '--format=%B'])).toContain(
'Co-authored-by: Orca <help@stably.ai>'
)
})
it('skips git attribution when commit signing is enabled', () => {
const root = makeTmpRoot()
const binDir = join(root, 'bin')
const commitPath = join(root, 'commit-called')
const amendPath = join(root, 'amend-called')
mkdirSync(binDir)
writeFileSync(
join(binDir, 'git'),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$1 $2 $3" == "config --bool commit.gpgsign" ]]; then
printf '%s\\n' 'true'
exit 0
fi
if [[ "$1" == "commit" ]]; then
if [[ "\${2:-}" == "--amend" ]]; then
touch "${amendPath}"
else
touch "${commitPath}"
fi
exit 0
fi
exit 1
`,
'utf8'
)
chmodSync(join(binDir, 'git'), 0o755)
const attributionEnv = applyTerminalAttributionEnv(
{ PATH: `${binDir}:${process.env.PATH ?? ''}` },
{ enabled: true, userDataPath: join(root, 'user-data') }
)
// Why: attribution uses an amend; signed commits can prompt or fail during
// that second commit, so the wrapper skips attribution instead.
execFileSync('git', ['commit', '-m', 'signed commit'], {
encoding: 'utf8',
env: { ...process.env, ...attributionEnv }
})
expect(existsSync(commitPath)).toBe(true)
expect(existsSync(amendPath)).toBe(false)
})
it('preserves interactive gh pr create without guessing which PR to edit', () => {
const root = makeTmpRoot()
const binDir = join(root, 'bin')
const markerPath = join(root, 'gh-edit-called')
mkdirSync(binDir)
writeFileSync(
join(binDir, 'gh'),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$1 $2" == "pr create" ]]; then
printf '%s\\n' 'interactive create complete'
exit 0
fi
if [[ "$1 $2 $3 $4" == "pr view --json url" ]]; then
printf '%s\\n' 'https://github.com/stablyai/orca/pull/123'
exit 0
fi
if [[ "$1 $2" == "api repos/stablyai/orca/pulls/123" && "\${3:-}" == "--jq" ]]; then
printf '%s\\n' 'Existing body'
exit 0
fi
if [[ "$1 $2 $3 $4" == "api -X PATCH repos/stablyai/orca/pulls/123" ]]; then
touch "${markerPath}"
exit 0
fi
exit 1
`,
'utf8'
)
chmodSync(join(binDir, 'gh'), 0o755)
const attributionEnv = applyTerminalAttributionEnv(
{ PATH: `${binDir}:${process.env.PATH ?? ''}` },
{ enabled: true, userDataPath: join(root, 'user-data') }
)
const output = execFileSync('gh', ['pr', 'create'], {
encoding: 'utf8',
env: { ...process.env, ...attributionEnv }
})
expect(output).toBe('interactive create complete\n')
expect(existsSync(markerPath)).toBe(false)
})
it('adds gh attribution for noninteractive create output URLs', () => {
const root = makeTmpRoot()
const binDir = join(root, 'bin')
const prMarkerPath = join(root, 'pr-edit-called')
const issueMarkerPath = join(root, 'issue-edit-called')
mkdirSync(binDir)
writeFileSync(
join(binDir, 'gh'),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$1 $2" == "pr create" ]]; then
printf '%s\\n' 'https://github.com/stablyai/orca/pull/123'
exit 0
fi
if [[ "$1 $2" == "issue create" ]]; then
printf '%s\\n' 'https://github.com/stablyai/orca/issues/456'
exit 0
fi
if [[ "$1 $2" == "api repos/stablyai/orca/pulls/123" && "\${3:-}" == "--jq" ]]; then
printf '%s\\n' 'PR body'
exit 0
fi
if [[ "$1 $2" == "api repos/stablyai/orca/issues/456" && "\${3:-}" == "--jq" ]]; then
printf '%s\\n' 'Issue body'
exit 0
fi
if [[ "$1 $2 $3 $4" == "api -X PATCH repos/stablyai/orca/pulls/123" ]]; then
touch "${prMarkerPath}"
exit 0
fi
if [[ "$1 $2 $3 $4" == "api -X PATCH repos/stablyai/orca/issues/456" ]]; then
touch "${issueMarkerPath}"
exit 0
fi
exit 1
`,
'utf8'
)
chmodSync(join(binDir, 'gh'), 0o755)
const attributionEnv = applyTerminalAttributionEnv(
{ PATH: `${binDir}:${process.env.PATH ?? ''}` },
{ enabled: true, userDataPath: join(root, 'user-data') }
)
expect(
execFileSync('gh', ['pr', 'create', '--fill'], {
encoding: 'utf8',
env: { ...process.env, ...attributionEnv }
})
).toBe('https://github.com/stablyai/orca/pull/123\n')
expect(
execFileSync('gh', ['issue', 'create', '--title', 'Issue', '--body', 'Body'], {
encoding: 'utf8',
env: { ...process.env, ...attributionEnv }
})
).toBe('https://github.com/stablyai/orca/issues/456\n')
expect(existsSync(prMarkerPath)).toBe(true)
expect(existsSync(issueMarkerPath)).toBe(true)
})
it('passes gh create help through without editing existing PRs or issues', () => {
const root = makeTmpRoot()
const binDir = join(root, 'bin')
const markerPath = join(root, 'gh-edit-called')
mkdirSync(binDir)
writeFileSync(
join(binDir, 'gh'),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$1 $2 $3" == "pr create --help" ]]; then
printf '%s\\n' 'pr help'
exit 0
fi
if [[ "$1 $2 $3" == "issue create --help" ]]; then
printf '%s\\n' 'issue help'
exit 0
fi
if [[ "$1 $2 $3 $4" == "pr view --json url" ]]; then
printf '%s\\n' 'https://github.com/stablyai/orca/pull/123'
exit 0
fi
if [[ "$1 $2" == "issue list" ]]; then
printf '%s\\n' 'https://github.com/stablyai/orca/issues/456'
exit 0
fi
if [[ "$1 $2 $3 $4" == "api -X PATCH repos/stablyai/orca/pulls/123" ]]; then
touch "${markerPath}"
exit 0
fi
if [[ "$1 $2 $3 $4" == "api -X PATCH repos/stablyai/orca/issues/456" ]]; then
touch "${markerPath}"
exit 0
fi
exit 1
`,
'utf8'
)
chmodSync(join(binDir, 'gh'), 0o755)
const attributionEnv = applyTerminalAttributionEnv(
{ PATH: `${binDir}:${process.env.PATH ?? ''}` },
{ enabled: true, userDataPath: join(root, 'user-data') }
)
const output = execFileSync('gh', ['pr', 'create', '--help'], {
encoding: 'utf8',
env: { ...process.env, ...attributionEnv }
})
expect(output).toBe('pr help\n')
const issueOutput = execFileSync('gh', ['issue', 'create', '--help'], {
encoding: 'utf8',
env: { ...process.env, ...attributionEnv }
})
expect(issueOutput).toBe('issue help\n')
expect(existsSync(markerPath)).toBe(false)
})
it('preserves interactive gh issue create without guessing which issue to edit', () => {
const root = makeTmpRoot()
const binDir = join(root, 'bin')
const markerPath = join(root, 'gh-edit-called')
mkdirSync(binDir)
writeFileSync(
join(binDir, 'gh'),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$1 $2" == "issue create" ]]; then
printf '%s\\n' 'interactive issue create complete'
exit 0
fi
if [[ "$1 $2" == "issue list" ]]; then
printf '%s\\n' 'https://github.com/stablyai/orca/issues/456'
exit 0
fi
if [[ "$1 $2 $3 $4" == "api -X PATCH repos/stablyai/orca/issues/456" ]]; then
touch "${markerPath}"
exit 0
fi
exit 1
`,
'utf8'
)
chmodSync(join(binDir, 'gh'), 0o755)
const attributionEnv = applyTerminalAttributionEnv(
{ PATH: `${binDir}:${process.env.PATH ?? ''}` },
{ enabled: true, userDataPath: join(root, 'user-data') }
)
const output = execFileSync('gh', ['issue', 'create'], {
encoding: 'utf8',
env: { ...process.env, ...attributionEnv }
})
expect(output).toBe('interactive issue create complete\n')
expect(existsSync(markerPath)).toBe(false)
})
it('skips gh attribution edits when viewing the created item fails', () => {
const root = makeTmpRoot()
const binDir = join(root, 'bin')
const markerPath = join(root, 'gh-edit-called')
mkdirSync(binDir)
writeFileSync(
join(binDir, 'gh'),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$1 $2" == "pr create" ]]; then
printf '%s\\n' 'https://github.com/stablyai/orca/pull/123'
exit 0
fi
if [[ "$1 $2" == "api repos/stablyai/orca/pulls/123" && "\${3:-}" == "--jq" ]]; then
exit 7
fi
if [[ "$1 $2 $3 $4" == "api -X PATCH repos/stablyai/orca/pulls/123" ]]; then
touch "${markerPath}"
exit 0
fi
exit 1
`,
'utf8'
)
chmodSync(join(binDir, 'gh'), 0o755)
const attributionEnv = applyTerminalAttributionEnv(
{ PATH: `${binDir}:${process.env.PATH ?? ''}` },
{ enabled: true, userDataPath: join(root, 'user-data') }
)
const output = execFileSync('gh', ['pr', 'create', '--fill'], {
encoding: 'utf8',
env: { ...process.env, ...attributionEnv }
})
expect(output).toBe('https://github.com/stablyai/orca/pull/123\n')
expect(existsSync(markerPath)).toBe(false)
})
it('keeps gh create successful when the attribution edit fails', () => {
const root = makeTmpRoot()
const binDir = join(root, 'bin')
mkdirSync(binDir)
writeFileSync(
join(binDir, 'gh'),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$1 $2" == "pr create" ]]; then
printf '%s\\n' 'https://github.com/stablyai/orca/pull/123'
exit 0
fi
if [[ "$1 $2" == "api repos/stablyai/orca/pulls/123" && "\${3:-}" == "--jq" ]]; then
printf '%s\\n' 'Existing body'
exit 0
fi
if [[ "$1 $2 $3 $4" == "api -X PATCH repos/stablyai/orca/pulls/123" ]]; then
exit 9
fi
exit 1
`,
'utf8'
)
chmodSync(join(binDir, 'gh'), 0o755)
const attributionEnv = applyTerminalAttributionEnv(
{ PATH: `${binDir}:${process.env.PATH ?? ''}` },
{ enabled: true, userDataPath: join(root, 'user-data') }
)
const output = execFileSync('gh', ['pr', 'create', '--fill'], {
encoding: 'utf8',
env: { ...process.env, ...attributionEnv }
})
expect(output).toBe('https://github.com/stablyai/orca/pull/123\n')
})
it('fails open when shim files cannot be written', () => {
const root = makeTmpRoot()
const blockedUserDataPath = join(root, 'not-a-directory')
writeFileSync(blockedUserDataPath, 'blocked\n')
const baseEnv = { PATH: '/usr/bin' }
const env = applyTerminalAttributionEnv(baseEnv, {
enabled: true,
userDataPath: blockedUserDataPath
})
expect(env).toBe(baseEnv)
expect(env.ORCA_ENABLE_GIT_ATTRIBUTION).toBeUndefined()
expect(env.PATH).toBe('/usr/bin')
})
it('writes PowerShell wrappers without raw-template backslash escapes', () => {
const root = makeTmpRoot()
applyTerminalAttributionEnv(
{ PATH: process.env.PATH ?? '' },
{ enabled: true, userDataPath: join(root, 'user-data') }
)
const shimDir = join(root, 'user-data', 'orca-terminal-attribution', 'win32')
const gitWrapper = readFileSync(join(shimDir, 'git-wrapper.ps1'), 'utf8')
const ghWrapper = readFileSync(join(shimDir, 'gh-wrapper.ps1'), 'utf8')
expect(gitWrapper).toContain('$message.TrimEnd("`r", "`n")')
expect(gitWrapper).toContain('"`r`n`r`n"')
expect(ghWrapper).toContain('$body.TrimEnd("`r", "`n")')
expect(ghWrapper).toContain('"`r`n`r`n"')
expect(gitWrapper).not.toContain('"\\`r"')
expect(ghWrapper).not.toContain('"\\`r"')
})
})
@@ -0,0 +1,688 @@
/* eslint-disable max-lines -- Why: this module owns the generated git/gh wrapper
scripts for both POSIX shells and Windows shells. Keeping the scripts adjacent
to the env injection code makes the attribution behavior auditable as one unit
instead of scattering generated shell fragments across files. */
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
const ATTRIBUTION_ROOT_DIR = 'orca-terminal-attribution'
const ATTRIBUTION_SHIM_VERSION = '5'
const ORCA_PRODUCT_URL = 'https://github.com/orca-ide'
const ORCA_GIT_COMMIT_TRAILER = 'Co-authored-by: Orca <help@stably.ai>'
const ORCA_GH_FOOTER = `Made with [Orca](${ORCA_PRODUCT_URL}) 🐋`
const SHELL_DOLLAR = '$'
const POWERSHELL_TICK = '`'
const writtenRoots = new Set<string>()
type AttributionShimPaths = {
posixDir: string
win32Dir: string
}
export function applyTerminalAttributionEnv(
baseEnv: Record<string, string>,
options: { enabled: boolean; userDataPath: string }
): Record<string, string> {
if (!options.enabled) {
return baseEnv
}
let shimPaths: AttributionShimPaths
try {
shimPaths = ensureAttributionShims(options.userDataPath)
} catch {
return baseEnv
}
const pathDelimiter = process.platform === 'win32' ? ';' : ':'
const basePath = baseEnv.PATH ?? process.env.PATH ?? ''
// Why: resolve real Windows commands before prepending shims so cmd wrappers
// cannot recursively point ORCA_REAL_* at themselves.
const resolvedGit =
process.platform === 'win32' ? resolveWindowsExecutable('git', basePath) : null
const resolvedGh = process.platform === 'win32' ? resolveWindowsExecutable('gh', basePath) : null
const { posixDir, win32Dir } = shimPaths
// Why: Windows terminals may be cmd/PowerShell or Git Bash. Include both shim
// families; native shells ignore extensionless POSIX files, Git Bash can use them.
const prependDirs = process.platform === 'win32' ? [posixDir, win32Dir] : [posixDir]
// Why: these wrappers should affect only Orca-managed PTYs. Prepending the
// shim directory here keeps the attribution behavior scoped to Orca's live
// terminal environment instead of mutating global git/gh config or the
// user's external shell PATH.
baseEnv.PATH = [...prependDirs, basePath].filter(Boolean).join(pathDelimiter)
baseEnv.ORCA_ENABLE_GIT_ATTRIBUTION = '1'
baseEnv.ORCA_GIT_COMMIT_TRAILER = ORCA_GIT_COMMIT_TRAILER
baseEnv.ORCA_GH_PR_FOOTER = ORCA_GH_FOOTER
baseEnv.ORCA_GH_ISSUE_FOOTER = ORCA_GH_FOOTER
if (process.platform === 'win32') {
if (resolvedGit) {
baseEnv.ORCA_REAL_GIT = resolvedGit
}
if (resolvedGh) {
baseEnv.ORCA_REAL_GH = resolvedGh
}
}
return baseEnv
}
function ensureAttributionShims(userDataPath: string): AttributionShimPaths {
const rootDir = join(userDataPath, ATTRIBUTION_ROOT_DIR)
const posixDir = join(rootDir, 'posix')
const win32Dir = join(rootDir, 'win32')
const versionFile = join(rootDir, 'VERSION')
if (writtenRoots.has(rootDir)) {
return { posixDir, win32Dir }
}
if (readShimVersion(versionFile) === ATTRIBUTION_SHIM_VERSION) {
writtenRoots.add(rootDir)
return { posixDir, win32Dir }
}
mkdirSync(posixDir, { recursive: true })
mkdirSync(win32Dir, { recursive: true })
writeExecutable(join(posixDir, 'git'), POSIX_GIT_WRAPPER)
writeExecutable(join(posixDir, 'gh'), POSIX_GH_WRAPPER)
writeExecutable(join(win32Dir, 'git.cmd'), WIN32_GIT_CMD_WRAPPER)
writeExecutable(join(win32Dir, 'gh.cmd'), WIN32_GH_CMD_WRAPPER)
writeExecutable(join(win32Dir, 'git-wrapper.ps1'), WIN32_GIT_PS_WRAPPER)
writeExecutable(join(win32Dir, 'gh-wrapper.ps1'), WIN32_GH_PS_WRAPPER)
writeFileSync(versionFile, `${ATTRIBUTION_SHIM_VERSION}\n`, 'utf8')
writtenRoots.add(rootDir)
return { posixDir, win32Dir }
}
function readShimVersion(versionFile: string): string | null {
try {
return readFileSync(versionFile, 'utf8').trim()
} catch {
return null
}
}
function writeExecutable(filePath: string, contents: string): void {
writeFileSync(filePath, contents, 'utf8')
chmodSync(filePath, 0o755)
}
function resolveWindowsExecutable(command: string, pathValue: string): string | null {
const pathExt = (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD')
.split(';')
.map((ext) => ext.toLowerCase())
const searchDirs = pathValue.split(';').filter(Boolean)
for (const dir of searchDirs) {
for (const ext of pathExt) {
const candidate = join(dir, `${command}${ext}`)
if (existsSync(candidate)) {
return candidate
}
}
const bareCandidate = join(dir, command)
if (existsSync(bareCandidate)) {
return bareCandidate
}
}
return null
}
const POSIX_COMMON = String.raw`#!/usr/bin/env bash
set -euo pipefail
clean_path() {
local current_path="${SHELL_DOLLAR}{PATH:-}"
local script_dir
script_dir="$(cd -- "$(dirname "${SHELL_DOLLAR}{BASH_SOURCE[0]}")" && pwd)"
local cleaned=()
local entry
IFS=':' read -r -a entries <<<"$current_path"
for entry in "${SHELL_DOLLAR}{entries[@]}"; do
case "$entry" in
"$script_dir"|*/orca-terminal-attribution/posix|*/orca-terminal-attribution/win32|*\\orca-terminal-attribution\\posix|*\\orca-terminal-attribution\\win32)
;;
*)
cleaned+=("$entry")
;;
esac
done
(IFS=':'; printf '%s' "${SHELL_DOLLAR}{cleaned[*]:-}")
}
`
const POSIX_GIT_WRAPPER = `${POSIX_COMMON}
real_path="$(clean_path)"
real_git="$(PATH="$real_path" command -v git || true)"
if [[ -z "$real_git" ]]; then
echo "Orca attribution wrapper could not locate git on PATH." >&2
exit 127
fi
if [[ "\${ORCA_ENABLE_GIT_ATTRIBUTION:-0}" != "1" || "\${ORCA_ATTRIBUTION_BYPASS:-0}" == "1" || "\${1:-}" != "commit" ]]; then
PATH="$real_path" exec "$real_git" "$@"
fi
for arg in "$@"; do
case "$arg" in
--dry-run)
PATH="$real_path" exec "$real_git" "$@"
;;
esac
done
should_skip_signed_commit_attribution() {
local saw_no_gpg_sign=0
local arg
for arg in "$@"; do
case "$arg" in
--no-gpg-sign)
saw_no_gpg_sign=1
;;
--gpg-sign|--gpg-sign=*|-S|-S*)
return 0
;;
esac
done
if [[ $saw_no_gpg_sign -eq 1 ]]; then
return 1
fi
[[ "$(PATH="$real_path" "$real_git" config --bool commit.gpgsign 2>/dev/null || true)" == "true" ]]
}
if should_skip_signed_commit_attribution "$@"; then
PATH="$real_path" exec "$real_git" "$@"
fi
before_head="$(
PATH="$real_path" "$real_git" rev-parse --verify HEAD 2>/dev/null || true
)"
PATH="$real_path" "$real_git" "$@"
status=$?
if [[ $status -ne 0 ]]; then
exit $status
fi
after_head="$(
PATH="$real_path" "$real_git" rev-parse --verify HEAD 2>/dev/null || true
)"
if [[ -z "$after_head" || "$before_head" == "$after_head" ]]; then
exit 0
fi
message="$(
PATH="$real_path" "$real_git" log -1 --format=%B 2>/dev/null || true
)"
trailer="\${ORCA_GIT_COMMIT_TRAILER:-Co-authored-by: Orca <help@stably.ai>}"
if grep -Fqi "$trailer" <<<"$message"; then
exit 0
fi
tmp_file="$(mktemp)"
cleanup() {
rm -f "$tmp_file"
}
trap cleanup EXIT
if [[ -n "$message" ]]; then
printf '%s\n\n%s\n' "$message" "$trailer" >"$tmp_file"
else
printf '%s\n' "$trailer" >"$tmp_file"
fi
# Why: git commit has no generic "post-success message transformer" hook. The
# wrapper amends only the just-created commit so Orca can add attribution
# without mutating repo config or installing hooks into the user's checkout. The
# amend is best-effort so attribution cannot turn a successful user commit into
# a failed terminal command.
ORCA_ATTRIBUTION_BYPASS=1 PATH="$real_path" "$real_git" commit --amend --no-verify -F "$tmp_file" >/dev/null 2>/dev/null || true
`
const POSIX_GH_WRAPPER = `${POSIX_COMMON}
real_path="$(clean_path)"
real_gh="$(PATH="$real_path" command -v gh || true)"
if [[ -z "$real_gh" ]]; then
echo "Orca attribution wrapper could not locate gh on PATH." >&2
exit 127
fi
append_footer() {
local kind="$1"
local url_pattern="$2"
local footer="$3"
local stdout_capture="$4"
local stderr_capture="$5"
local url=""
url="$(printf '%s\n%s\n' "$stdout_capture" "$stderr_capture" | grep -Eo "$url_pattern" | tail -n 1 || true)"
append_footer_url "$kind" "$footer" "$url"
}
append_footer_url() {
local kind="$1"
local footer="$2"
local url="$3"
if [[ -z "$url" ]]; then
return 0
fi
local api_path
api_path="$(github_api_path "$kind" "$url" || true)"
if [[ -z "$api_path" ]]; then
return 0
fi
local body
if ! body="$(PATH="$real_path" "$real_gh" api "$api_path" --jq '.body // ""' 2>/dev/null)"; then
return 0
fi
if grep -Fqi "$footer" <<<"$body"; then
return 0
fi
local tmp_file
tmp_file="$(mktemp)"
if [[ -n "$body" ]]; then
printf '%s\n\n%s\n' "$body" "$footer" >"$tmp_file"
else
printf '%s\n' "$footer" >"$tmp_file"
fi
# Why: gh exposes create output as a URL, but does not provide a transactional
# body append. Use REST instead of gh pr/issue edit because those commands can
# hit unrelated GraphQL fields, while the URL maps directly to one REST item.
PATH="$real_path" "$real_gh" api -X PATCH "$api_path" -f "body=$(cat "$tmp_file")" >/dev/null || true
rm -f "$tmp_file"
}
github_api_path() {
local kind="$1"
local url="$2"
if [[ "$kind" == "pr" && "$url" =~ ^https://github[.]com/([^/]+)/([^/]+)/pull/([0-9]+) ]]; then
printf 'repos/%s/%s/pulls/%s' "${SHELL_DOLLAR}{BASH_REMATCH[1]}" "${SHELL_DOLLAR}{BASH_REMATCH[2]}" "${SHELL_DOLLAR}{BASH_REMATCH[3]}"
return 0
fi
if [[ "$kind" == "issue" && "$url" =~ ^https://github[.]com/([^/]+)/([^/]+)/issues/([0-9]+) ]]; then
printf 'repos/%s/%s/issues/%s' "${SHELL_DOLLAR}{BASH_REMATCH[1]}" "${SHELL_DOLLAR}{BASH_REMATCH[2]}" "${SHELL_DOLLAR}{BASH_REMATCH[3]}"
return 0
fi
return 1
}
has_noninteractive_create_args() {
local arg
for arg in "$@"; do
case "$arg" in
--title|-t|--title=*|--body|-b|--body=*|--body-file|-F|--body-file=*|--fill|--fill-first|--fill-verbose|--template|-T|--template=*|--recover|--recover=*|--web)
return 0
;;
esac
done
return 1
}
has_passthrough_create_args() {
local arg
for arg in "$@"; do
case "$arg" in
--help|-h|--version)
return 0
;;
esac
done
return 1
}
if [[ "\${ORCA_ENABLE_GIT_ATTRIBUTION:-0}" != "1" || "\${ORCA_ATTRIBUTION_BYPASS:-0}" == "1" ]]; then
PATH="$real_path" exec "$real_gh" "$@"
fi
if [[ "\${1:-}" == "pr" && "\${2:-}" == "create" ]]; then
footer="\${ORCA_GH_PR_FOOTER:-Made with [Orca](https://github.com/orca-ide) 🐋}"
if has_passthrough_create_args "$@"; then
PATH="$real_path" exec "$real_gh" "$@"
fi
if ! has_noninteractive_create_args "$@"; then
# Why: gh switches off interactive prompts when stdout/stderr are redirected,
# and post-create "pr view" can select the wrong PR in fork/multi-PR cases.
# Preserve interactive UX and skip attribution rather than guessing.
PATH="$real_path" exec "$real_gh" "$@"
fi
stdout_file="$(mktemp)"
stderr_file="$(mktemp)"
cleanup_capture() {
rm -f "$stdout_file" "$stderr_file"
}
trap cleanup_capture EXIT
if PATH="$real_path" "$real_gh" "$@" >"$stdout_file" 2>"$stderr_file"; then
status=0
else
status=$?
fi
stdout_capture="$(cat "$stdout_file")"
stderr_capture="$(cat "$stderr_file")"
cat "$stderr_file" >&2
cat "$stdout_file"
if [[ $status -eq 0 ]]; then
append_footer "pr" 'https://github.com/[^[:space:]]+/pull/[0-9]+' "$footer" "$stdout_capture" "$stderr_capture"
fi
cleanup_capture
trap - EXIT
exit $status
fi
if [[ "\${1:-}" == "issue" && "\${2:-}" == "create" ]]; then
footer="\${ORCA_GH_ISSUE_FOOTER:-Made with [Orca](https://github.com/orca-ide) 🐋}"
if has_passthrough_create_args "$@"; then
PATH="$real_path" exec "$real_gh" "$@"
fi
if ! has_noninteractive_create_args "$@"; then
# Why: gh issue create also requires a live TTY for prompts, but gh has no
# current-issue lookup equivalent to "pr view". Do not guess with issue list:
# that can edit an unrelated issue if the command printed no URL.
PATH="$real_path" exec "$real_gh" "$@"
fi
stdout_file="$(mktemp)"
stderr_file="$(mktemp)"
cleanup_capture() {
rm -f "$stdout_file" "$stderr_file"
}
trap cleanup_capture EXIT
if PATH="$real_path" "$real_gh" "$@" >"$stdout_file" 2>"$stderr_file"; then
status=0
else
status=$?
fi
stdout_capture="$(cat "$stdout_file")"
stderr_capture="$(cat "$stderr_file")"
cat "$stderr_file" >&2
cat "$stdout_file"
if [[ $status -eq 0 ]]; then
append_footer "issue" 'https://github.com/[^[:space:]]+/issues/[0-9]+' "$footer" "$stdout_capture" "$stderr_capture"
fi
cleanup_capture
trap - EXIT
exit $status
fi
PATH="$real_path" exec "$real_gh" "$@"
`
const WIN32_GIT_CMD_WRAPPER = String.raw`@echo off
setlocal
if not "%ORCA_ENABLE_GIT_ATTRIBUTION%"=="1" goto run
if "%ORCA_ATTRIBUTION_BYPASS%"=="1" goto run
if /I not "%~1"=="commit" goto run
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0git-wrapper.ps1" %*
exit /b %ERRORLEVEL%
:run
if defined ORCA_REAL_GIT (
"%ORCA_REAL_GIT%" %*
) else (
echo Orca attribution wrapper could not locate git on PATH. 1>&2
exit /b 127
)
exit /b %ERRORLEVEL%
`
const WIN32_GH_CMD_WRAPPER = String.raw`@echo off
setlocal
if not "%ORCA_ENABLE_GIT_ATTRIBUTION%"=="1" goto run
if "%ORCA_ATTRIBUTION_BYPASS%"=="1" goto run
if /I "%~1"=="pr" if /I "%~2"=="create" goto wrap
if /I "%~1"=="issue" if /I "%~2"=="create" goto wrap
goto run
:wrap
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0gh-wrapper.ps1" %*
exit /b %ERRORLEVEL%
:run
if defined ORCA_REAL_GH (
"%ORCA_REAL_GH%" %*
) else (
echo Orca attribution wrapper could not locate gh on PATH. 1>&2
exit /b 127
)
exit /b %ERRORLEVEL%
`
const WIN32_GIT_PS_WRAPPER = String.raw`$ErrorActionPreference = 'Stop'
$realGit = if ($env:ORCA_REAL_GIT) { $env:ORCA_REAL_GIT } else { 'git' }
$trailer = if ($env:ORCA_GIT_COMMIT_TRAILER) { $env:ORCA_GIT_COMMIT_TRAILER } else { 'Co-authored-by: Orca <help@stably.ai>' }
if ($args -contains '--dry-run') {
& $realGit @args
exit $LASTEXITCODE
}
function Test-SignedCommitAttributionSkip {
$sawNoGpgSign = $false
foreach ($arg in $args) {
if ($arg -eq '--no-gpg-sign') {
$sawNoGpgSign = $true
} elseif ($arg -eq '--gpg-sign' -or $arg.StartsWith('--gpg-sign=') -or $arg -eq '-S' -or $arg.StartsWith('-S')) {
return $true
}
}
if ($sawNoGpgSign) {
return $false
}
$gpgSign = (& $realGit config --bool commit.gpgsign 2>$null)
return $LASTEXITCODE -eq 0 -and $gpgSign -eq 'true'
}
if (Test-SignedCommitAttributionSkip) {
& $realGit @args
exit $LASTEXITCODE
}
$beforeHead = (& $realGit rev-parse --verify HEAD 2>$null)
& $realGit @args
$status = $LASTEXITCODE
if ($status -ne 0) {
exit $status
}
$afterHead = (& $realGit rev-parse --verify HEAD 2>$null)
if ([string]::IsNullOrWhiteSpace($afterHead) -or $beforeHead -eq $afterHead) {
exit 0
}
$message = (& $realGit log -1 --format=%B 2>$null) | Out-String
if ($message -match [Regex]::Escape($trailer)) {
exit 0
}
$tmpFile = [System.IO.Path]::GetTempFileName()
try {
$trimmed = $message.TrimEnd("${POWERSHELL_TICK}r", "${POWERSHELL_TICK}n")
if ([string]::IsNullOrWhiteSpace($trimmed)) {
Set-Content -LiteralPath $tmpFile -Value $trailer -NoNewline
} else {
Set-Content -LiteralPath $tmpFile -Value ($trimmed + "${POWERSHELL_TICK}r${POWERSHELL_TICK}n${POWERSHELL_TICK}r${POWERSHELL_TICK}n" + $trailer) -NoNewline
}
$env:ORCA_ATTRIBUTION_BYPASS = '1'
& $realGit commit --amend --no-verify -F $tmpFile 2>$null | Out-Null
exit 0
} finally {
Remove-Item -LiteralPath $tmpFile -Force -ErrorAction SilentlyContinue
}
`
const WIN32_GH_PS_WRAPPER = String.raw`$ErrorActionPreference = 'Stop'
$realGh = if ($env:ORCA_REAL_GH) { $env:ORCA_REAL_GH } else { 'gh' }
function Test-NonInteractiveCreateArgs {
param([string[]]$CommandArgs)
foreach ($arg in $CommandArgs) {
if ($arg -match '^(--title|-t|--body|-b|--body-file|-F|--fill|--fill-first|--fill-verbose|--template|-T|--recover|--web)(=|$)') {
return $true
}
}
return $false
}
function Test-PassthroughCreateArgs {
param([string[]]$CommandArgs)
foreach ($arg in $CommandArgs) {
if ($arg -eq '--help' -or $arg -eq '-h' -or $arg -eq '--version') {
return $true
}
}
return $false
}
function Add-Footer {
param([string]$Kind, [string]$CreatedUrl, [string]$Footer)
if (-not $CreatedUrl) {
return
}
$apiPath = Get-GitHubApiPath $Kind $CreatedUrl
if (-not $apiPath) {
return
}
$body = (& $realGh api $apiPath --jq '.body // ""' 2>$null) | Out-String
if ($LASTEXITCODE -ne 0 -or $body -match [Regex]::Escape($Footer)) {
return
}
$tmpFile = [System.IO.Path]::GetTempFileName()
try {
$trimmed = $body.TrimEnd("${POWERSHELL_TICK}r", "${POWERSHELL_TICK}n")
if ([string]::IsNullOrWhiteSpace($trimmed)) {
Set-Content -LiteralPath $tmpFile -Value $Footer -NoNewline
} else {
Set-Content -LiteralPath $tmpFile -Value ($trimmed + "${POWERSHELL_TICK}r${POWERSHELL_TICK}n${POWERSHELL_TICK}r${POWERSHELL_TICK}n" + $Footer) -NoNewline
}
try {
& $realGh api -X PATCH $apiPath -f "body=$(Get-Content -LiteralPath $tmpFile -Raw)" | Out-Null
} catch {
}
} finally {
Remove-Item -LiteralPath $tmpFile -Force -ErrorAction SilentlyContinue
}
}
function Get-GitHubApiPath {
param([string]$Kind, [string]$CreatedUrl)
if ($Kind -eq 'pr' -and $CreatedUrl -match '^https://github\.com/([^/]+)/([^/]+)/pull/([0-9]+)') {
return "repos/$($Matches[1])/$($Matches[2])/pulls/$($Matches[3])"
}
if ($Kind -eq 'issue' -and $CreatedUrl -match '^https://github\.com/([^/]+)/([^/]+)/issues/([0-9]+)') {
return "repos/$($Matches[1])/$($Matches[2])/issues/$($Matches[3])"
}
return $null
}
$commandText = ($args -join ' ').ToLowerInvariant()
if (($commandText.StartsWith('pr create') -or $commandText.StartsWith('issue create')) -and (Test-PassthroughCreateArgs $args)) {
& $realGh @args
exit $LASTEXITCODE
}
if (($commandText.StartsWith('pr create') -or $commandText.StartsWith('issue create')) -and -not (Test-NonInteractiveCreateArgs $args)) {
& $realGh @args
$status = $LASTEXITCODE
if ($status -ne 0) {
exit $status
}
if ($commandText.StartsWith('pr create')) {
exit 0
} else {
exit 0
}
exit 0
}
$stdoutFile = [System.IO.Path]::GetTempFileName()
$stderrFile = [System.IO.Path]::GetTempFileName()
& $realGh @args > $stdoutFile 2> $stderrFile
$status = $LASTEXITCODE
$stdoutCapture = if (Test-Path -LiteralPath $stdoutFile) { Get-Content -LiteralPath $stdoutFile -Raw } else { '' }
$stderrCapture = if (Test-Path -LiteralPath $stderrFile) { Get-Content -LiteralPath $stderrFile -Raw } else { '' }
if ($stderrCapture) {
[Console]::Error.Write($stderrCapture)
}
if ($status -ne 0) {
if ($stdoutCapture) {
[Console]::Out.Write($stdoutCapture)
}
Remove-Item -LiteralPath $stdoutFile, $stderrFile -Force -ErrorAction SilentlyContinue
exit $status
}
if ($stdoutCapture) {
[Console]::Out.Write($stdoutCapture)
}
if ($commandText.StartsWith('pr create')) {
$createdUrl = ([regex]::Matches(($stdoutCapture + [Environment]::NewLine + $stderrCapture), 'https://github.com/\S+/pull/\d+') | Select-Object -Last 1).Value
if ($createdUrl) {
$apiPath = Get-GitHubApiPath 'pr' $createdUrl
$body = if ($apiPath) { (& $realGh api $apiPath --jq '.body // ""' 2>$null) | Out-String } else { $null }
if ($LASTEXITCODE -ne 0) {
$body = $null
}
$footer = if ($env:ORCA_GH_PR_FOOTER) { $env:ORCA_GH_PR_FOOTER } else { 'Made with [Orca](https://github.com/orca-ide) 🐋' }
if ($null -ne $body -and $body -notmatch [Regex]::Escape($footer)) {
$tmpFile = [System.IO.Path]::GetTempFileName()
try {
$trimmed = $body.TrimEnd("${POWERSHELL_TICK}r", "${POWERSHELL_TICK}n")
if ([string]::IsNullOrWhiteSpace($trimmed)) {
Set-Content -LiteralPath $tmpFile -Value $footer -NoNewline
} else {
Set-Content -LiteralPath $tmpFile -Value ($trimmed + "${POWERSHELL_TICK}r${POWERSHELL_TICK}n${POWERSHELL_TICK}r${POWERSHELL_TICK}n" + $footer) -NoNewline
}
# Why: gh has no transactional body append for newly-created PRs. This
# immediate REST patch keeps attribution scoped to the URL gh returned.
try {
& $realGh api -X PATCH $apiPath -f "body=$(Get-Content -LiteralPath $tmpFile -Raw)" | Out-Null
} catch {
}
} finally {
Remove-Item -LiteralPath $tmpFile -Force -ErrorAction SilentlyContinue
}
}
}
}
if ($commandText.StartsWith('issue create')) {
$createdUrl = ([regex]::Matches(($stdoutCapture + [Environment]::NewLine + $stderrCapture), 'https://github.com/\S+/issues/\d+') | Select-Object -Last 1).Value
if ($createdUrl) {
$apiPath = Get-GitHubApiPath 'issue' $createdUrl
$body = if ($apiPath) { (& $realGh api $apiPath --jq '.body // ""' 2>$null) | Out-String } else { $null }
if ($LASTEXITCODE -ne 0) {
$body = $null
}
$footer = if ($env:ORCA_GH_ISSUE_FOOTER) { $env:ORCA_GH_ISSUE_FOOTER } else { 'Made with [Orca](https://github.com/orca-ide) 🐋' }
if ($null -ne $body -and $body -notmatch [Regex]::Escape($footer)) {
$tmpFile = [System.IO.Path]::GetTempFileName()
try {
$trimmed = $body.TrimEnd("${POWERSHELL_TICK}r", "${POWERSHELL_TICK}n")
if ([string]::IsNullOrWhiteSpace($trimmed)) {
Set-Content -LiteralPath $tmpFile -Value $footer -NoNewline
} else {
Set-Content -LiteralPath $tmpFile -Value ($trimmed + "${POWERSHELL_TICK}r${POWERSHELL_TICK}n${POWERSHELL_TICK}r${POWERSHELL_TICK}n" + $footer) -NoNewline
}
# Why: gh has no transactional body append for newly-created issues.
# This immediate REST patch keeps attribution scoped to the URL gh returned.
try {
& $realGh api -X PATCH $apiPath -f "body=$(Get-Content -LiteralPath $tmpFile -Raw)" | Out-Null
} catch {
}
} finally {
Remove-Item -LiteralPath $tmpFile -Force -ErrorAction SilentlyContinue
}
}
}
}
Remove-Item -LiteralPath $stdoutFile, $stderrFile -Force -ErrorAction SilentlyContinue
exit 0
`
@@ -89,6 +89,7 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
experimentalTerminalDaemonNoticeShown: false,
terminalForceHyperlink: true,
terminalWindowsShell: 'powershell.exe',
enableGitHubAttribution: true,
...overrides
}
}
+1
View File
@@ -83,6 +83,7 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
experimentalTerminalDaemonNoticeShown: false,
terminalForceHyperlink: true,
terminalWindowsShell: 'powershell.exe',
enableGitHubAttribution: true,
...overrides
}
}
+32 -2
View File
@@ -196,7 +196,8 @@ describe('registerPtyHandlers', () => {
async function spawnAndGetEnv(
argsEnv?: Record<string, string>,
processEnvOverrides?: Record<string, string | undefined>,
getSelectedCodexHomePath?: () => string | null
getSelectedCodexHomePath?: () => string | null,
getSettings?: () => { enableGitHubAttribution: boolean }
): Promise<Record<string, string>> {
const savedEnv: Record<string, string | undefined> = {}
if (processEnvOverrides) {
@@ -214,7 +215,12 @@ describe('registerPtyHandlers', () => {
// Clear previously registered handlers so re-registration doesn't
// accumulate stale state across calls within one test.
handlers.clear()
registerPtyHandlers(mainWindow as never, undefined, getSelectedCodexHomePath)
registerPtyHandlers(
mainWindow as never,
undefined,
getSelectedCodexHomePath,
getSettings as never
)
await handlers.get('pty:spawn')!(null, {
cols: 80,
rows: 24,
@@ -320,6 +326,30 @@ describe('registerPtyHandlers', () => {
expect(env.ORCA_AGENT_HOOK_TOKEN).toBe('agent-token')
})
it('prepends local git/gh attribution shims when attribution is enabled', async () => {
const env = await spawnAndGetEnv(undefined, undefined, undefined, () => ({
enableGitHubAttribution: true
}))
expect(env.ORCA_ENABLE_GIT_ATTRIBUTION).toBe('1')
expect(env.ORCA_GIT_COMMIT_TRAILER).toBe('Co-authored-by: Orca <help@stably.ai>')
expect(env.ORCA_GH_PR_FOOTER).toBe('Made with [Orca](https://github.com/orca-ide) 🐋')
expect(env.ORCA_GH_ISSUE_FOOTER).toBe('Made with [Orca](https://github.com/orca-ide) 🐋')
expect(env.PATH).toContain('/tmp/orca-user-data/orca-terminal-attribution/posix')
})
it('skips git/gh attribution shims when attribution is disabled', async () => {
const env = await spawnAndGetEnv(undefined, undefined, undefined, () => ({
enableGitHubAttribution: false
}))
expect(env.ORCA_ENABLE_GIT_ATTRIBUTION).toBeUndefined()
expect(env.ORCA_GIT_COMMIT_TRAILER).toBeUndefined()
expect(env.ORCA_GH_PR_FOOTER).toBeUndefined()
expect(env.ORCA_GH_ISSUE_FOOTER).toBeUndefined()
expect(env.PATH ?? '').not.toContain('/tmp/orca-user-data/orca-terminal-attribution/posix')
})
it('leaves ambient CODEX_HOME untouched when system default is selected', async () => {
const env = await spawnAndGetEnv(
undefined,
+10
View File
@@ -20,6 +20,7 @@ import {
markClaudePtyExited,
markClaudePtySpawned
} from '../claude-accounts/live-pty-gate'
import { applyTerminalAttributionEnv } from '../attribution/terminal-attribution'
// ─── Provider Registry ──────────────────────────────────────────────
// Routes PTY operations by connectionId. null = local provider.
@@ -240,6 +241,15 @@ export function registerPtyHandlers(
baseEnv.PATH = baseEnv.PATH ? `${devCliBin}${delimiter}${baseEnv.PATH}` : devCliBin
}
// Why: GitHub attribution should only affect commands launched from
// Orca's own PTYs. Injecting lightweight PATH shims at spawn-time keeps
// the behavior local to Orca instead of rewriting user git config or
// touching external shells.
applyTerminalAttributionEnv(baseEnv, {
enabled: getSettings?.()?.enableGitHubAttribution ?? true,
userDataPath: app.getPath('userData')
})
return baseEnv
},
onSpawned: (id) => runtime?.onPtySpawned(id),
+7 -4
View File
@@ -7,15 +7,15 @@ const { handleMock, removeHandlerMock, removeAllListenersMock } = vi.hoisted(()
}))
vi.mock('electron', () => ({
app: {
isPackaged: true,
getPath: vi.fn().mockReturnValue('/tmp/orca-test-userdata')
},
ipcMain: {
handle: handleMock,
on: vi.fn(),
removeHandler: removeHandlerMock,
removeAllListeners: removeAllListenersMock
},
app: {
isPackaged: true,
getPath: vi.fn().mockReturnValue('/tmp/orca-test-userdata')
}
}))
@@ -23,6 +23,9 @@ vi.mock('fs', () => ({
existsSync: () => true,
statSync: () => ({ isDirectory: () => true, mode: 0o755 }),
accessSync: () => undefined,
mkdirSync: vi.fn(),
readFileSync: vi.fn(() => ''),
writeFileSync: vi.fn(),
chmodSync: vi.fn(),
constants: { X_OK: 1 }
}))
@@ -109,6 +109,44 @@ export function GitPane({
/>
</button>
</SearchableSetting>
) : null,
matchesSettingsSearch(searchQuery, {
title: 'Orca Attribution',
description: 'Add Orca attribution to commits, PRs, and issues.',
keywords: ['github', 'gh', 'pr', 'issue', 'co-author', 'coauthored', 'attribution', 'orca']
}) ? (
<SearchableSetting
key="github-attribution"
title="Orca Attribution"
description="Add Orca attribution."
keywords={['github', 'gh', 'pr', 'issue', 'co-author', 'coauthored', 'attribution', 'orca']}
className="flex items-center justify-between gap-4 px-1 py-2"
>
<div className="space-y-0.5">
<Label>Orca Attribution</Label>
<p className="text-xs text-muted-foreground">
Add Orca attribution to commits, PRs, and issues.
</p>
</div>
<button
role="switch"
aria-checked={settings.enableGitHubAttribution}
onClick={() =>
updateSettings({
enableGitHubAttribution: !settings.enableGitHubAttribution
})
}
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
settings.enableGitHubAttribution ? 'bg-foreground' : 'bg-muted-foreground/30'
}`}
>
<span
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
settings.enableGitHubAttribution ? 'translate-x-4' : 'translate-x-0.5'
}`}
/>
</button>
</SearchableSetting>
) : null
].filter(Boolean)
@@ -10,5 +10,10 @@ export const GIT_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
title: 'Refresh Local Base Ref',
description: 'Optionally fast-forward local main or master when creating worktrees.',
keywords: ['main', 'master', 'origin/main', 'git diff', 'base ref', 'worktree']
},
{
title: 'Orca Attribution',
description: 'Add Orca attribution to commits, PRs, and issues.',
keywords: ['github', 'gh', 'pr', 'issue', 'co-author', 'coauthored', 'attribution', 'orca']
}
]
+1
View File
@@ -106,6 +106,7 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
refreshLocalBaseRefOnWorktreeCreate: false,
branchPrefix: 'git-username',
branchPrefixCustom: '',
enableGitHubAttribution: true,
theme: 'system',
editorAutoSave: false,
editorAutoSaveDelayMs: DEFAULT_EDITOR_AUTO_SAVE_DELAY_MS,
+1
View File
@@ -725,6 +725,7 @@ export type GlobalSettings = {
refreshLocalBaseRefOnWorktreeCreate: boolean
branchPrefix: 'git-username' | 'custom' | 'none'
branchPrefixCustom: string
enableGitHubAttribution: boolean
theme: 'system' | 'dark' | 'light'
editorAutoSave: boolean
editorAutoSaveDelayMs: number