feat: currentAuthor() carries git user.email — fail-open identity (F5 SLICE-2, #14)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import * as vscode from "vscode";
|
|||||||
import { CoauthorStore } from "./store";
|
import { CoauthorStore } from "./store";
|
||||||
import { newId, type AttributionRecord, type Provenance } from "./model";
|
import { newId, type AttributionRecord, type Provenance } from "./model";
|
||||||
import { buildFingerprint, resolve, type OffsetRange } from "./anchorer";
|
import { buildFingerprint, resolve, type OffsetRange } from "./anchorer";
|
||||||
|
import { gitUserEmail } from "./identity";
|
||||||
import { applyChange, coalesce, type LiveSpan } from "./attributionTracker";
|
import { applyChange, coalesce, type LiveSpan } from "./attributionTracker";
|
||||||
import { minimizeReplace, PendingEditRegistry } from "./pendingEdits";
|
import { minimizeReplace, PendingEditRegistry } from "./pendingEdits";
|
||||||
|
|
||||||
@@ -79,7 +80,8 @@ export class AttributionController implements vscode.Disposable {
|
|||||||
}
|
}
|
||||||
private currentAuthor(): Provenance {
|
private currentAuthor(): Provenance {
|
||||||
const id = vscode.workspace.getConfiguration("git").get<string>("user.name") || process.env.USER || "human";
|
const id = vscode.workspace.getConfiguration("git").get<string>("user.name") || process.env.USER || "human";
|
||||||
return { kind: "human", id };
|
const email = gitUserEmail(this.rootDir);
|
||||||
|
return { kind: "human", id, ...(email !== undefined ? { email } : {}) };
|
||||||
}
|
}
|
||||||
private state(docPath: string): DocAttribution {
|
private state(docPath: string): DocAttribution {
|
||||||
let s = this.docs.get(docPath);
|
let s = this.docs.get(docPath);
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Cross-rung identity (F5, spec §6.3 fork c): `email` is git's own join key —
|
||||||
|
* forges already map email → account. Resolved from the workspace git config,
|
||||||
|
* cached per root, FAIL-OPEN: absence just omits the optional field and the
|
||||||
|
* sidecar stays valid (spec §6.9). vscode-free, so it is unit-testable.
|
||||||
|
*/
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
|
||||||
|
const cache = new Map<string, string | undefined>();
|
||||||
|
|
||||||
|
export function gitUserEmail(rootDir: string): string | undefined {
|
||||||
|
if (!cache.has(rootDir)) {
|
||||||
|
try {
|
||||||
|
const out = execFileSync("git", ["config", "user.email"], {
|
||||||
|
cwd: rootDir,
|
||||||
|
encoding: "utf8",
|
||||||
|
stdio: ["ignore", "pipe", "ignore"],
|
||||||
|
}).trim();
|
||||||
|
cache.set(rootDir, out || undefined);
|
||||||
|
} catch {
|
||||||
|
cache.set(rootDir, undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cache.get(rootDir);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import * as vscode from "vscode";
|
|||||||
import { CoauthorStore } from "./store";
|
import { CoauthorStore } from "./store";
|
||||||
import { emptyArtifact, type Artifact, type Provenance } from "./model";
|
import { emptyArtifact, type Artifact, type Provenance } from "./model";
|
||||||
import { buildFingerprint, resolve, shift, type OffsetRange } from "./anchorer";
|
import { buildFingerprint, resolve, shift, type OffsetRange } from "./anchorer";
|
||||||
|
import { gitUserEmail } from "./identity";
|
||||||
import { addThread, appendMessage, setStatus } from "./threadModel";
|
import { addThread, appendMessage, setStatus } from "./threadModel";
|
||||||
|
|
||||||
/** Test-facing snapshot of what is currently rendered for a document. */
|
/** Test-facing snapshot of what is currently rendered for a document. */
|
||||||
@@ -72,7 +73,8 @@ export class ThreadController implements vscode.Disposable {
|
|||||||
|
|
||||||
private currentAuthor(): Provenance {
|
private currentAuthor(): Provenance {
|
||||||
const id = vscode.workspace.getConfiguration("git").get<string>("user.name") || process.env.USER || "human";
|
const id = vscode.workspace.getConfiguration("git").get<string>("user.name") || process.env.USER || "human";
|
||||||
return { kind: "human", id };
|
const email = gitUserEmail(this.rootDir);
|
||||||
|
return { kind: "human", id, ...(email !== undefined ? { email } : {}) };
|
||||||
}
|
}
|
||||||
|
|
||||||
private persist(state: DocState): void {
|
private persist(state: DocState): void {
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { gitUserEmail } from "../src/identity";
|
||||||
|
|
||||||
|
let repo: string;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
repo = fs.mkdtempSync(path.join(os.tmpdir(), "cowriting-identity-"));
|
||||||
|
execFileSync("git", ["init"], { cwd: repo, stdio: "ignore" });
|
||||||
|
execFileSync("git", ["config", "user.email", "rung@example.org"], { cwd: repo });
|
||||||
|
});
|
||||||
|
afterAll(() => fs.rmSync(repo, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
describe("gitUserEmail (F5 SLICE-2)", () => {
|
||||||
|
it("reads the workspace git config user.email", () => {
|
||||||
|
expect(gitUserEmail(repo)).toBe("rung@example.org");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails open (undefined) when git cannot run there", () => {
|
||||||
|
expect(gitUserEmail("/nonexistent-dir-xyz")).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user