Log slow API requests with server spans and client timing

This commit is contained in:
Ian Gulliver
2026-10-21 14:45:19 -07:00
parent 671165ec71
commit b4c1dbab37
31 changed files with 458 additions and 185 deletions
+4
View File
@@ -18,6 +18,10 @@ The service always runs from the app: `/Applications/Byre.app/Contents/Helpers/b
`byre serve` exits when its executable is replaced (logging `executable replaced, exiting`) and launchd starts the new one. A missing path counts as not yet replaced, so the moment in `install.sh` between removing the old bundle and moving the new one in doesn't trigger it. The service log is owner-only (0600), including existing logs: Tailscale can put a sign-in URL there. Logs go to `~/Library/Logs/byre.log`; `launchctl print gui/$(id -u)/run.fc.byre` shows the service.
## Slow requests
An API request that takes 200 ms or more is logged on both ends. The server's line, `request slow`, in `byre.log`, has the handler's time and its spans: each git process, SQL begin, exec and query, the wait for the store's write lock, and the tailnet `whois`. Every response also carries these spans in a `Server-Timing` header. On the client, `log show --start <time> --predicate 'subsystem == "run.fc.byre.mac" AND eventMessage BEGINSWITH "request slow"'` shows the app's line, which splits the request's time into `queue` (waiting for a connection), `connect`, `network` (time to the first byte, less the server's time), `server`, `download` and `app` (how long the main actor took to pick up work once the response ended). Event streams and websockets are left out.
## Tailnet
byre runs its own tailnet node with `tsnet`; it doesn't use the Tailscale app. The app connects at the root of the node's `ts.net` name on 443, not under a path: the API uses absolute paths and the app takes an origin with no path. Tailnet access requires a user identity; a tagged node gets 403. Local scripts can also check server status and run client commands through the authenticated loopback address.
+1 -1
View File
@@ -77,7 +77,7 @@ func (m *Manager) startAudit(ctx context.Context, projectID string) error {
if err != nil || active {
return err
}
head, err := git.BranchHead(project.RepoPath, project.DefaultBranch)
head, err := git.BranchHead(ctx, project.RepoPath, project.DefaultBranch)
if err != nil {
return err
}
+2 -2
View File
@@ -54,7 +54,7 @@ func (m *Manager) recoverMergeTurns(ctx context.Context, running []api.AgentSess
if err != nil {
return err
}
merging, err := git.MergeInProgress(task.WorktreePath)
merging, err := git.MergeInProgress(ctx, task.WorktreePath)
if err != nil {
return err
}
@@ -74,7 +74,7 @@ func (m *Manager) endMergeTurn(sessionID string, abort bool) {
return
}
if abort {
err := git.AbortMerge(t.dir)
err := git.AbortMerge(context.Background(), t.dir)
if err != nil {
slog.Error("abort unfinished merge", "session", sessionID, "err", err)
}
+2 -2
View File
@@ -161,7 +161,7 @@ func (m *Manager) ask(ctx context.Context, l *live, raw json.RawMessage) (string
if err != nil {
return "", err
}
behind, err := git.Behind(task.WorktreePath, project.DefaultBranch)
behind, err := git.Behind(ctx, task.WorktreePath, project.DefaultBranch)
if err != nil {
slog.Error("count commits behind default branch", "task", task.Id, "err", err)
}
@@ -182,7 +182,7 @@ func (m *Manager) ask(ctx context.Context, l *live, raw json.RawMessage) (string
func (m *Manager) commitBeforeSync(ctx context.Context, l *live, branch string) error {
unlock := m.lockTask(l.taskID)
defer unlock()
_, err := git.CommitAll(l.dir, "Work before merging "+branch, l.gitName, l.gitEmail)
_, err := git.CommitAll(ctx, l.dir, "Work before merging "+branch, l.gitName, l.gitEmail)
if err != nil {
return err
}
+2 -2
View File
@@ -278,7 +278,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
if err != nil {
return "", err
}
base, err = git.Head(from.WorktreePath)
base, err = git.Head(ctx, from.WorktreePath)
if err != nil {
return "", err
}
@@ -970,7 +970,7 @@ func (m *Manager) listTasksText(ctx context.Context) (string, error) {
if !listed {
b.WriteString("- no active tasks\n")
}
src, found, err := git.ReadFile(p.RepoPath, p.DefaultBranch, todo.File)
src, found, err := git.ReadFile(ctx, p.RepoPath, p.DefaultBranch, todo.File)
if err != nil {
return "", err
}
+4 -4
View File
@@ -15,7 +15,7 @@ func (m *Manager) prepareMainCommit(ctx context.Context, task api.Task, project
return "", "", err
}
env := git.IdentityEnv(user.GitName, user.GitEmail)
commit, err := git.SquashCommit(project.RepoPath, task.WorktreePath, project.DefaultBranch, squashMessage(task.MergeTitle, coAuthors), env, m.signer(ctx, user))
commit, err := git.SquashCommit(ctx, project.RepoPath, task.WorktreePath, project.DefaultBranch, squashMessage(task.MergeTitle, coAuthors), env, m.signer(ctx, user))
if err != nil || commit == "" {
return "", "", err
}
@@ -23,17 +23,17 @@ func (m *Manager) prepareMainCommit(ctx context.Context, task api.Task, project
if err != nil {
return "", "", err
}
notes, err := git.NoteCommit(project.RepoPath, commit, note, env)
notes, err := git.NoteCommit(ctx, project.RepoPath, commit, note, env)
return commit, notes, err
}
func advanceMainCheckout(project api.Project, commit, notes string) error {
pushed := git.Remote + "/" + project.DefaultBranch
err := git.AdvanceNotes(project.RepoPath, notes)
err := git.AdvanceNotes(context.Background(), project.RepoPath, notes)
if err != nil {
return fmt.Errorf("pushed %s to %s, but updating %s in the project checkout failed: %w", commit, pushed, git.NotesRef, err)
}
err = git.FastForward(project.RepoPath, commit)
err = git.FastForward(context.Background(), project.RepoPath, commit)
if err != nil {
return fmt.Errorf("pushed %s to %s, but fast-forwarding the project checkout failed: %w", commit, pushed, err)
}
+4 -4
View File
@@ -722,7 +722,7 @@ func (m *Manager) recordMessage(ctx context.Context, sess api.AgentSession, task
payload := map[string]any{"text": text}
if implementer {
head, err := git.Head(task.WorktreePath)
head, err := git.Head(ctx, task.WorktreePath)
if err != nil {
return nil, false, err
}
@@ -1190,7 +1190,7 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
return err
}
if (owner || landingBy != "") && !finished {
err := git.AbortMerge(l.dir)
err := git.AbortMerge(ctx, l.dir)
if err != nil {
slog.Error("abort unfinished merge", "session", l.id, "err", err)
}
@@ -1209,13 +1209,13 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
return nil
}
}
sha, err := git.CommitAll(l.dir, commitMessage(res), l.gitName, l.gitEmail)
sha, err := git.CommitAll(ctx, l.dir, commitMessage(res), l.gitName, l.gitEmail)
if err != nil {
slog.Error("commit turn", "session", l.id, "err", err)
m.recordError(l, fmt.Sprintf("byre couldn't commit the turn's work, so the turn counts as unfinished: %v", err))
finished = false
if owner {
err = git.AbortMerge(l.dir)
err = git.AbortMerge(ctx, l.dir)
if err != nil {
slog.Error("abort unfinished merge", "session", l.id, "err", err)
}
+3 -3
View File
@@ -123,15 +123,15 @@ func (m *Manager) gitFacts(ctx context.Context, t api.Task, p api.Project) (stor
if t.WorktreePath == "" {
return g, nil
}
tree, err := git.Snapshot(t.WorktreePath)
tree, err := git.Snapshot(ctx, t.WorktreePath)
if err != nil {
return g, err
}
g.UnmergedCommits, err = git.Ahead(t.WorktreePath, p.DefaultBranch)
g.UnmergedCommits, err = git.Ahead(ctx, t.WorktreePath, p.DefaultBranch)
if err != nil {
return g, err
}
g.UnmergedFiles, err = git.ChangedFileCount(t.WorktreePath, p.DefaultBranch, tree)
g.UnmergedFiles, err = git.ChangedFileCount(ctx, t.WorktreePath, p.DefaultBranch, tree)
if err != nil {
return g, err
}
+2 -2
View File
@@ -223,7 +223,7 @@ func pushLanded(ctx context.Context, project api.Project, commit string) (bool,
if err != nil {
return false, &unfinishedMergeError{fmt.Errorf("checking whether %s reached %s failed: %w", commit, target, err)}
}
landed, err := git.IsAncestor(project.RepoPath, commit, target)
landed, err := git.IsAncestor(ctx, project.RepoPath, commit, target)
if err != nil {
return false, &unfinishedMergeError{fmt.Errorf("checking whether %s reached %s failed: %w", commit, target, err)}
}
@@ -405,7 +405,7 @@ func (m *Manager) RemoveTask(ctx context.Context, task api.Task, project api.Pro
if err != nil {
return err
}
return git.RemoveWorktree(project.RepoPath, task.WorktreePath, task.Branch)
return git.RemoveWorktree(ctx, project.RepoPath, task.WorktreePath, task.Branch)
}
type setTitlesArgs struct {
+3 -3
View File
@@ -115,7 +115,7 @@ func (m *Manager) pushToMain(ctx context.Context, l *live, task api.Task, projec
if err != nil {
return "", err
}
_, err = git.CommitAll(l.dir, title, l.gitName, l.gitEmail)
_, err = git.CommitAll(ctx, l.dir, title, l.gitName, l.gitEmail)
if err != nil {
return "", err
}
@@ -130,7 +130,7 @@ func (m *Manager) pushToMain(ctx context.Context, l *live, task api.Task, projec
return "", err
}
if commit == "" {
err = git.Merge(l.dir, project.DefaultBranch, "Merge "+project.DefaultBranch, l.gitName, l.gitEmail)
err = git.Merge(ctx, l.dir, project.DefaultBranch, "Merge "+project.DefaultBranch, l.gitName, l.gitEmail)
return "", err
}
@@ -147,7 +147,7 @@ func (m *Manager) pushToMain(ctx context.Context, l *live, task api.Task, projec
if err != nil {
return "", err
}
err = git.Merge(l.dir, commit, fmt.Sprintf("Merge %s from %s", commit[:7], project.DefaultBranch), l.gitName, l.gitEmail)
err = git.Merge(ctx, l.dir, commit, fmt.Sprintf("Merge %s from %s", commit[:7], project.DefaultBranch), l.gitName, l.gitEmail)
if err != nil {
return "", fmt.Errorf("pushed %s to %s, but merging it back into this branch failed: %w", commit, pushed, err)
}
+4 -4
View File
@@ -119,7 +119,7 @@ func (m *Manager) beginReviews(ctx context.Context, ref store.SnapshotRef) error
if project.ReviewWorkflow == "" {
return finished(api.ReviewStateFailed, "", fmt.Sprintf("project %s has no review workflow", project.Name))
}
changes, err := git.Compare(project.RepoPath, project.DefaultBranch, snap.CommitSha, snap.TreeSha, "")
changes, err := git.Compare(ctx, project.RepoPath, project.DefaultBranch, snap.CommitSha, snap.TreeSha, "")
if err != nil {
return finished(api.ReviewStateFailed, project.ReviewWorkflow, fmt.Sprintf("byre couldn't compare snapshot %d with %s: %v", snap.N, project.DefaultBranch, err))
}
@@ -213,7 +213,7 @@ func (m *Manager) startReviewers(ctx context.Context, task api.Task, project api
func (m *Manager) launchReviewer(ctx context.Context, task api.Task, project api.Project, snap api.Snapshot, rv store.Reviewer, prompt string) {
r := rv.Review
err := git.AddDetachedWorktree(project.RepoPath, m.reviewDir(project.Id, r.Id), snap.CommitSha)
err := git.AddDetachedWorktree(ctx, project.RepoPath, m.reviewDir(project.Id, r.Id), snap.CommitSha)
if err == nil {
err = os.MkdirAll(m.reviewMediaDir(project.Id, r.Id), 0o755)
}
@@ -230,7 +230,7 @@ func (m *Manager) launchReviewer(ctx context.Context, task api.Task, project api
}
func (m *Manager) reviewPrompt(ctx context.Context, task api.Task, project api.Project, snap api.Snapshot, def api.WorkflowDefinition, stage string, inputs []api.Review) (string, error) {
changes, err := git.Compare(project.RepoPath, project.DefaultBranch, snap.CommitSha, snap.TreeSha, "")
changes, err := git.Compare(ctx, project.RepoPath, project.DefaultBranch, snap.CommitSha, snap.TreeSha, "")
if err != nil {
return "", err
}
@@ -600,7 +600,7 @@ func (m *Manager) cleanUpReviewer(r store.EndedReviewer) {
}
func (m *Manager) removeReviewDir(project api.Project, reviewID string) {
err := git.RemoveDetachedWorktree(project.RepoPath, m.reviewDir(project.Id, reviewID))
err := git.RemoveDetachedWorktree(context.Background(), project.RepoPath, m.reviewDir(project.Id, reviewID))
if err != nil {
slog.Error("remove review worktree", "review", reviewID, "err", err)
}
+3 -3
View File
@@ -134,7 +134,7 @@ func (m *Manager) takeSnapshot(ctx context.Context, t snapTarget, trigger api.Sn
}
}
tree, err := git.Snapshot(t.dir)
tree, err := git.Snapshot(ctx, t.dir)
if err != nil {
return nil, err
}
@@ -152,7 +152,7 @@ func (m *Manager) takeSnapshot(ctx context.Context, t snapTarget, trigger api.Sn
}
n := last.N + 1
commit, err := git.CommitSnapshot(t.dir, tree, git.SnapshotRef(t.taskID, n), fmt.Sprintf("byre snapshot %d (%s)", n, trigger), git.IdentityEnv(t.gitName, t.gitEmail))
commit, err := git.CommitSnapshot(ctx, t.dir, tree, git.SnapshotRef(t.taskID, n), fmt.Sprintf("byre snapshot %d (%s)", n, trigger), git.IdentityEnv(t.gitName, t.gitEmail))
if err != nil {
return nil, err
}
@@ -279,7 +279,7 @@ func (m *Manager) dropSnapshots(repo, taskID string) error {
st.mu.Lock()
defer st.mu.Unlock()
return git.DeleteSnapshotRefs(repo, taskID)
return git.DeleteSnapshotRefs(context.Background(), repo, taskID)
}
func (m *Manager) afterTool(l *live) claude.HookFunc {
+3 -3
View File
@@ -65,7 +65,7 @@ func (m *Manager) subtaskTools(l *live) []mcp.Tool {
if err != nil {
return "", err
}
base, err := git.Head(l.dir)
base, err := git.Head(ctx, l.dir)
if err != nil {
return "", err
}
@@ -372,7 +372,7 @@ func (m *Manager) closeMergedSubtask(ctx context.Context, t api.Task, parentID,
if err != nil || t.Status != api.TaskStatusActive {
return err
}
merged, err := git.IsAncestor(dir, t.Branch, "HEAD")
merged, err := git.IsAncestor(ctx, dir, t.Branch, "HEAD")
if err != nil || !merged {
return err
}
@@ -384,7 +384,7 @@ func (m *Manager) closeMergedSubtask(ctx context.Context, t api.Task, parentID,
if err != nil {
return err
}
head, err := git.Head(dir)
head, err := git.Head(ctx, dir)
if err != nil {
return err
}
+3 -3
View File
@@ -52,7 +52,7 @@ func (m *Manager) StartTodo(ctx context.Context, project api.Project, creator ap
if creator.GitName == "" || creator.GitEmail == "" {
return nil, ErrNoGitIdentity
}
src, found, err := git.ReadFile(project.RepoPath, project.DefaultBranch, todo.File)
src, found, err := git.ReadFile(ctx, project.RepoPath, project.DefaultBranch, todo.File)
if err != nil {
return nil, err
}
@@ -120,14 +120,14 @@ func (m *Manager) createTask(ctx context.Context, project api.Project, t api.Tas
t.CreatedAt = time.Now().UTC()
var err error
t.BaseRef, err = git.AddWorktree(project.RepoPath, t.WorktreePath, t.Branch, base)
t.BaseRef, err = git.AddWorktree(ctx, project.RepoPath, t.WorktreePath, t.Branch, base)
if err != nil {
return api.Task{}, err
}
err = m.store.CreateTask(ctx, t, nil, actor)
if err != nil {
rerr := git.RemoveWorktree(project.RepoPath, t.WorktreePath, t.Branch)
rerr := git.RemoveWorktree(ctx, project.RepoPath, t.WorktreePath, t.Branch)
if rerr != nil {
slog.Error("remove worktree of task not created", "task", t.Id, "err", rerr)
}
+2 -2
View File
@@ -355,7 +355,7 @@ func (m *Manager) recordStage(ctx context.Context, l *live) error {
if err != nil {
return err
}
head, err := git.Head(l.dir)
head, err := git.Head(ctx, l.dir)
if err != nil {
return err
}
@@ -411,7 +411,7 @@ func (m *Manager) nextStage(ctx context.Context, sessionID string) ([]string, er
return nil, err
}
if recorded == "" {
head, err := git.Head(task.WorktreePath)
head, err := git.Head(ctx, task.WorktreePath)
if err != nil {
return nil, err
}
+19 -18
View File
@@ -1,6 +1,7 @@
package git
import (
"context"
"fmt"
"os"
"strconv"
@@ -17,25 +18,25 @@ var statuses = map[byte]api.FileChangeStatus{
'T': api.TypeChanged,
}
func Changes(dir, defaultBranch, since string) (api.TaskChanges, error) {
tree, err := Snapshot(dir)
func Changes(ctx context.Context, dir, defaultBranch, since string) (api.TaskChanges, error) {
tree, err := Snapshot(ctx, dir)
if err != nil {
return api.TaskChanges{}, err
}
return Compare(dir, defaultBranch, "HEAD", tree, since)
return Compare(ctx, dir, defaultBranch, "HEAD", tree, since)
}
func Compare(dir, defaultBranch, head, tree, since string) (api.TaskChanges, error) {
func Compare(ctx context.Context, dir, defaultBranch, head, tree, since string) (api.TaskChanges, error) {
base := since
if base == "" {
mb, err := run(dir, "merge-base", head, defaultBranch)
mb, err := run(ctx, dir, "merge-base", head, defaultBranch)
if err != nil {
return api.TaskChanges{}, err
}
base = mb
}
files, err := stats(dir, base, tree)
files, err := stats(ctx, dir, base, tree)
if err != nil {
return api.TaskChanges{}, err
}
@@ -43,20 +44,20 @@ func Compare(dir, defaultBranch, head, tree, since string) (api.TaskChanges, err
return api.TaskChanges{Base: base, Tree: tree, Files: files}, nil
}
func ChangedFileCount(dir, defaultBranch, tree string) (int, error) {
base, err := run(dir, "merge-base", "HEAD", defaultBranch)
func ChangedFileCount(ctx context.Context, dir, defaultBranch, tree string) (int, error) {
base, err := run(ctx, dir, "merge-base", "HEAD", defaultBranch)
if err != nil {
return 0, err
}
paths, err := run(dir, "diff", "--find-renames", "--name-only", "-z", base, tree)
paths, err := run(ctx, dir, "diff", "--find-renames", "--name-only", "-z", base, tree)
if err != nil {
return 0, err
}
return strings.Count(paths, "\x00"), nil
}
func Snapshot(dir string) (string, error) {
index, err := run(dir, "rev-parse", "--path-format=absolute", "--git-path", "index")
func Snapshot(ctx context.Context, dir string) (string, error) {
index, err := run(ctx, dir, "rev-parse", "--path-format=absolute", "--git-path", "index")
if err != nil {
return "", err
}
@@ -90,27 +91,27 @@ func Snapshot(dir string) (string, error) {
}
env := []string{"GIT_INDEX_FILE=" + tmp.Name()}
_, err = runEnv(dir, env, "add", "--all")
_, err = runEnv(ctx, dir, env, "add", "--all")
if err != nil {
return "", err
}
return runEnv(dir, env, "write-tree")
return runEnv(ctx, dir, env, "write-tree")
}
func FileDiff(dir, base, tree, path, oldPath string) (string, error) {
func FileDiff(ctx context.Context, dir, base, tree, path, oldPath string) (string, error) {
args := []string{"diff", "--find-renames", "--no-ext-diff", "--no-color", base, tree, "--", path}
if oldPath != "" {
args = append(args, oldPath)
}
return run(dir, args...)
return run(ctx, dir, args...)
}
func stats(dir, base, tree string) ([]api.FileChange, error) {
nameStatus, err := run(dir, "diff", "--find-renames", "--name-status", "-z", base, tree)
func stats(ctx context.Context, dir, base, tree string) ([]api.FileChange, error) {
nameStatus, err := run(ctx, dir, "diff", "--find-renames", "--name-status", "-z", base, tree)
if err != nil {
return nil, err
}
numstat, err := run(dir, "diff", "--find-renames", "--numstat", "-z", base, tree)
numstat, err := run(ctx, dir, "diff", "--find-renames", "--numstat", "-z", base, tree)
if err != nil {
return nil, err
}
+70 -68
View File
@@ -14,6 +14,7 @@ import (
"time"
"g.fc.run/ai/byre/internal/procgroup"
"g.fc.run/ai/byre/internal/trace"
)
type RepoInfo struct {
@@ -22,13 +23,13 @@ type RepoInfo struct {
Remote string
}
func Inspect(path string) (RepoInfo, error) {
func Inspect(ctx context.Context, path string) (RepoInfo, error) {
abs, err := filepath.Abs(path)
if err != nil {
return RepoInfo{}, err
}
root, err := run(abs, "rev-parse", "--show-toplevel")
root, err := run(ctx, abs, "rev-parse", "--show-toplevel")
if err != nil {
return RepoInfo{}, err
}
@@ -47,12 +48,12 @@ func Inspect(path string) (RepoInfo, error) {
return RepoInfo{}, fmt.Errorf("%s is inside the repo at %s; register the repo root", abs, root)
}
branch, err := run(root, "symbolic-ref", "--short", "HEAD")
branch, err := run(ctx, root, "symbolic-ref", "--short", "HEAD")
if err != nil {
return RepoInfo{}, err
}
remote, err := run(root, "remote", "get-url", "origin")
remote, err := run(ctx, root, "remote", "get-url", "origin")
if err != nil {
return RepoInfo{}, err
}
@@ -60,13 +61,13 @@ func Inspect(path string) (RepoInfo, error) {
return RepoInfo{Root: root, DefaultBranch: branch, Remote: remote}, nil
}
func AddWorktree(repo, path, branch, base string) (string, error) {
sha, err := run(repo, "rev-parse", "--verify", base+"^{commit}")
func AddWorktree(ctx context.Context, repo, path, branch, base string) (string, error) {
sha, err := run(ctx, repo, "rev-parse", "--verify", base+"^{commit}")
if err != nil {
return "", err
}
_, err = run(repo, "worktree", "add", "-b", branch, path, sha)
_, err = run(ctx, repo, "worktree", "add", "-b", branch, path, sha)
if err != nil {
return "", err
}
@@ -74,38 +75,38 @@ func AddWorktree(repo, path, branch, base string) (string, error) {
return sha, nil
}
func AddDetachedWorktree(repo, path, commit string) error {
_, err := run(repo, "worktree", "add", "--detach", path, commit)
func AddDetachedWorktree(ctx context.Context, repo, path, commit string) error {
_, err := run(ctx, repo, "worktree", "add", "--detach", path, commit)
return err
}
func RemoveDetachedWorktree(repo, path string) error {
func RemoveDetachedWorktree(ctx context.Context, repo, path string) error {
_, err := os.Stat(path)
if errors.Is(err, os.ErrNotExist) {
return nil
}
_, err = run(repo, "worktree", "remove", "--force", path)
_, err = run(ctx, repo, "worktree", "remove", "--force", path)
return err
}
func RemoveWorktree(repo, path, branch string) error {
list, err := run(repo, "worktree", "list", "--porcelain")
func RemoveWorktree(ctx context.Context, repo, path, branch string) error {
list, err := run(ctx, repo, "worktree", "list", "--porcelain")
if err != nil {
return err
}
_, statErr := os.Stat(path)
if statErr == nil || slices.Contains(strings.Split(list, "\n"), "worktree "+path) {
_, err = run(repo, "worktree", "remove", "--force", path)
_, err = run(ctx, repo, "worktree", "remove", "--force", path)
if err != nil {
return err
}
}
_, _, code, err := runCode(repo, nil, "rev-parse", "--verify", "--quiet", "refs/heads/"+branch)
_, _, code, err := runCode(ctx, repo, nil, "rev-parse", "--verify", "--quiet", "refs/heads/"+branch)
if err != nil || code != 0 {
return err
}
_, err = run(repo, "branch", "-D", branch)
_, err = run(ctx, repo, "branch", "-D", branch)
return err
}
@@ -117,24 +118,24 @@ func (e *ConflictError) Error() string {
return "merge conflicts in " + strings.Join(e.Files, ", ")
}
func Ahead(worktree, target string) (int, error) {
count, err := run(worktree, "rev-list", "--count", target+"..HEAD")
func Ahead(ctx context.Context, worktree, target string) (int, error) {
count, err := run(ctx, worktree, "rev-list", "--count", target+"..HEAD")
if err != nil {
return 0, err
}
return strconv.Atoi(count)
}
func Behind(worktree, target string) (int, error) {
count, err := run(worktree, "rev-list", "--count", "HEAD.."+target)
func Behind(ctx context.Context, worktree, target string) (int, error) {
count, err := run(ctx, worktree, "rev-list", "--count", "HEAD.."+target)
if err != nil {
return 0, err
}
return strconv.Atoi(count)
}
func SquashCommit(repo, worktree, target, message string, env []string, sign func(payload []byte) ([]byte, error)) (string, error) {
checkedOut, err := run(repo, "symbolic-ref", "--short", "HEAD")
func SquashCommit(ctx context.Context, repo, worktree, target, message string, env []string, sign func(payload []byte) ([]byte, error)) (string, error) {
checkedOut, err := run(ctx, repo, "symbolic-ref", "--short", "HEAD")
if err != nil {
return "", err
}
@@ -142,20 +143,20 @@ func SquashCommit(repo, worktree, target, message string, env []string, sign fun
return "", fmt.Errorf("%s has %s checked out, not %s", repo, checkedOut, target)
}
tree, err := Snapshot(worktree)
tree, err := Snapshot(ctx, worktree)
if err != nil {
return "", err
}
base, err := run(worktree, "merge-base", target, "HEAD")
base, err := run(ctx, worktree, "merge-base", target, "HEAD")
if err != nil {
return "", err
}
head, err := run(repo, "rev-parse", target)
head, err := run(ctx, repo, "rev-parse", target)
if err != nil {
return "", err
}
out, stderr, code, err := runCode(repo, nil, "merge-tree", "--write-tree", "--name-only", "--merge-base="+base, head, tree)
out, stderr, code, err := runCode(ctx, repo, nil, "merge-tree", "--write-tree", "--name-only", "--merge-base="+base, head, tree)
if err != nil {
return "", err
}
@@ -174,7 +175,7 @@ func SquashCommit(repo, worktree, target, message string, env []string, sign fun
return "", fmt.Errorf("git merge-tree exited %d: %s", code, stderr)
}
headTree, err := run(repo, "rev-parse", head+"^{tree}")
headTree, err := run(ctx, repo, "rev-parse", head+"^{tree}")
if err != nil {
return "", err
}
@@ -182,11 +183,11 @@ func SquashCommit(repo, worktree, target, message string, env []string, sign fun
return "", nil
}
commit, err := runEnv(repo, env, "commit-tree", lines[0], "-p", head, "-m", message)
commit, err := runEnv(ctx, repo, env, "commit-tree", lines[0], "-p", head, "-m", message)
if err != nil {
return "", err
}
signing, stderr, code, err := runCode(repo, nil, "config", "--type=bool", "--get", "commit.gpgsign")
signing, stderr, code, err := runCode(ctx, repo, nil, "config", "--type=bool", "--get", "commit.gpgsign")
if err != nil {
return "", err
}
@@ -196,11 +197,11 @@ func SquashCommit(repo, worktree, target, message string, env []string, sign fun
if signing != "true" {
return commit, nil
}
return signCommit(repo, commit, sign)
return signCommit(ctx, repo, commit, sign)
}
func signCommit(repo, commit string, sign func(payload []byte) ([]byte, error)) (string, error) {
payload, err := runRaw(repo, nil, "", "cat-file", "commit", commit)
func signCommit(ctx context.Context, repo, commit string, sign func(payload []byte) ([]byte, error)) (string, error) {
payload, err := runRaw(ctx, repo, nil, "", "cat-file", "commit", commit)
if err != nil {
return "", err
}
@@ -214,7 +215,7 @@ func signCommit(repo, commit string, sign func(payload []byte) ([]byte, error))
}
header := "gpgsig " + strings.ReplaceAll(strings.TrimSuffix(string(sig), "\n"), "\n", "\n ") + "\n"
signed := payload[:eoh+1] + header + payload[eoh+1:]
return runInput(repo, nil, signed, "hash-object", "-t", "commit", "-w", "--stdin")
return runInput(ctx, repo, nil, signed, "hash-object", "-t", "commit", "-w", "--stdin")
}
const Remote = "origin"
@@ -244,8 +245,8 @@ func Push(ctx context.Context, repo, commit, branch, notes string) error {
return fmt.Errorf("pushing %s to %s: exit status %d: %s", branch, Remote, code, strings.TrimSpace(out+"\n"+stderr))
}
func FastForward(repo, rev string) error {
_, err := run(repo, "merge", "--ff-only", rev)
func FastForward(ctx context.Context, repo, rev string) error {
_, err := run(ctx, repo, "merge", "--ff-only", rev)
return err
}
@@ -265,19 +266,19 @@ func FetchFastForward(ctx context.Context, repo, branch string) error {
if err != nil {
return err
}
return FastForward(repo, Remote+"/"+branch)
return FastForward(ctx, repo, Remote+"/"+branch)
}
func Head(dir string) (string, error) {
return run(dir, "rev-parse", "HEAD")
func Head(ctx context.Context, dir string) (string, error) {
return run(ctx, dir, "rev-parse", "HEAD")
}
func BranchHead(repo, branch string) (string, error) {
return run(repo, "rev-parse", "--verify", "refs/heads/"+branch+"^{commit}")
func BranchHead(ctx context.Context, repo, branch string) (string, error) {
return run(ctx, repo, "rev-parse", "--verify", "refs/heads/"+branch+"^{commit}")
}
func IsAncestor(dir, ancestor, descendant string) (bool, error) {
_, stderr, code, err := runCode(dir, nil, "merge-base", "--is-ancestor", ancestor, descendant)
func IsAncestor(ctx context.Context, dir, ancestor, descendant string) (bool, error) {
_, stderr, code, err := runCode(ctx, dir, nil, "merge-base", "--is-ancestor", ancestor, descendant)
if err != nil {
return false, err
}
@@ -290,38 +291,38 @@ func IsAncestor(dir, ancestor, descendant string) (bool, error) {
return false, fmt.Errorf("git merge-base --is-ancestor %s %s: exit status %d: %s", ancestor, descendant, code, stderr)
}
func MergeInProgress(dir string) (bool, error) {
_, _, code, err := runCode(dir, nil, "rev-parse", "--quiet", "--verify", "MERGE_HEAD")
func MergeInProgress(ctx context.Context, dir string) (bool, error) {
_, _, code, err := runCode(ctx, dir, nil, "rev-parse", "--quiet", "--verify", "MERGE_HEAD")
return err == nil && code == 0, err
}
func AbortMerge(dir string) error {
merging, err := MergeInProgress(dir)
func AbortMerge(ctx context.Context, dir string) error {
merging, err := MergeInProgress(ctx, dir)
if err != nil || !merging {
return err
}
_, err = run(dir, "merge", "--abort")
_, err = run(ctx, dir, "merge", "--abort")
return err
}
func ReadFile(repo, rev, path string) (string, bool, error) {
_, _, code, err := runCode(repo, nil, "cat-file", "-e", rev+":"+path)
func ReadFile(ctx context.Context, repo, rev, path string) (string, bool, error) {
_, _, code, err := runCode(ctx, repo, nil, "cat-file", "-e", rev+":"+path)
if err != nil {
return "", false, err
}
if code != 0 {
return "", false, nil
}
out, err := run(repo, "cat-file", "blob", rev+":"+path)
out, err := run(ctx, repo, "cat-file", "blob", rev+":"+path)
return out, true, err
}
func CommitAll(dir, message, name, email string) (string, error) {
status, err := run(dir, "status", "--porcelain", "--untracked-files=all")
func CommitAll(ctx context.Context, dir, message, name, email string) (string, error) {
status, err := run(ctx, dir, "status", "--porcelain", "--untracked-files=all")
if err != nil {
return "", err
}
merging, err := MergeInProgress(dir)
merging, err := MergeInProgress(ctx, dir)
if err != nil {
return "", err
}
@@ -330,38 +331,38 @@ func CommitAll(dir, message, name, email string) (string, error) {
}
env := IdentityEnv(name, email)
_, err = runEnv(dir, env, "add", "--all")
_, err = runEnv(ctx, dir, env, "add", "--all")
if err != nil {
return "", err
}
_, err = runEnv(dir, env, "commit", "--quiet", "--message", message)
_, err = runEnv(ctx, dir, env, "commit", "--quiet", "--message", message)
if err != nil {
return "", err
}
return Head(dir)
return Head(ctx, dir)
}
func Merge(dir, rev, message, name, email string) error {
func Merge(ctx context.Context, dir, rev, message, name, email string) error {
env := IdentityEnv(name, email)
_, err := runEnv(dir, env, "merge", "--no-ff", "--quiet", "--message", message, rev)
_, err := runEnv(ctx, dir, env, "merge", "--no-ff", "--quiet", "--message", message, rev)
return err
}
func run(dir string, args ...string) (string, error) {
return runEnv(dir, nil, args...)
func run(ctx context.Context, dir string, args ...string) (string, error) {
return runEnv(ctx, dir, nil, args...)
}
func runEnv(dir string, env []string, args ...string) (string, error) {
return runInput(dir, env, "", args...)
func runEnv(ctx context.Context, dir string, env []string, args ...string) (string, error) {
return runInput(ctx, dir, env, "", args...)
}
func runInput(dir string, env []string, input string, args ...string) (string, error) {
out, err := runRaw(dir, env, input, args...)
func runInput(ctx context.Context, dir string, env []string, input string, args ...string) (string, error) {
out, err := runRaw(ctx, dir, env, input, args...)
return strings.TrimSpace(out), err
}
func runRaw(dir string, env []string, input string, args ...string) (string, error) {
out, stderr, code, err := runCodeInput(context.Background(), dir, env, strings.NewReader(input), args...)
func runRaw(ctx context.Context, dir string, env []string, input string, args ...string) (string, error) {
out, stderr, code, err := runCodeInput(context.WithoutCancel(ctx), dir, env, strings.NewReader(input), args...)
if err != nil {
return "", err
}
@@ -371,8 +372,8 @@ func runRaw(dir string, env []string, input string, args ...string) (string, err
return out, nil
}
func runCode(dir string, env []string, args ...string) (string, string, int, error) {
out, stderr, code, err := runCodeInput(context.Background(), dir, env, nil, args...)
func runCode(ctx context.Context, dir string, env []string, args ...string) (string, string, int, error) {
out, stderr, code, err := runCodeInput(context.WithoutCancel(ctx), dir, env, nil, args...)
return strings.TrimSpace(out), stderr, code, err
}
@@ -384,6 +385,7 @@ func runNetwork(ctx context.Context, dir string, args ...string) (string, string
}
func runCodeInput(ctx context.Context, dir string, env []string, stdin io.Reader, args ...string) (string, string, int, error) {
defer trace.Start(ctx, "git "+args[0])()
argv := args
if args[0] != "config" {
argv = append([]string{"-c", "commit.gpgsign=false"}, args...)
+2 -2
View File
@@ -6,13 +6,13 @@ import (
"strings"
)
func Ignored(worktree string, paths []string) (map[string]bool, error) {
func Ignored(ctx context.Context, worktree string, paths []string) (map[string]bool, error) {
ignored := map[string]bool{}
if len(paths) == 0 {
return ignored, nil
}
out, stderr, code, err := runCodeInput(context.Background(), worktree, nil, strings.NewReader(strings.Join(paths, "\x00")+"\x00"), "check-ignore", "--stdin", "-z")
out, stderr, code, err := runCodeInput(context.WithoutCancel(ctx), worktree, nil, strings.NewReader(strings.Join(paths, "\x00")+"\x00"), "check-ignore", "--stdin", "-z")
if err != nil {
return nil, err
}
+15 -14
View File
@@ -1,6 +1,7 @@
package git
import (
"context"
"fmt"
"os"
"path/filepath"
@@ -12,9 +13,9 @@ const NotesRef = "refs/notes/byre"
const notesFetch = NotesRef + "*:" + NotesRef + "*"
func ConfigureNotesFetch(repo string) error {
func ConfigureNotesFetch(ctx context.Context, repo string) error {
key := "remote." + Remote + ".fetch"
out, stderr, code, err := runCode(repo, nil, "config", "--get-all", key)
out, stderr, code, err := runCode(ctx, repo, nil, "config", "--get-all", key)
if err != nil {
return err
}
@@ -24,17 +25,17 @@ func ConfigureNotesFetch(repo string) error {
if slices.Contains(strings.Split(out, "\n"), notesFetch) {
return nil
}
_, err = run(repo, "config", "--add", key, notesFetch)
_, err = run(ctx, repo, "config", "--add", key, notesFetch)
return err
}
func NoteCommit(repo, commit, note string, env []string) (string, error) {
blob, err := runInput(repo, nil, note, "hash-object", "-w", "--stdin")
func NoteCommit(ctx context.Context, repo, commit, note string, env []string) (string, error) {
blob, err := runInput(ctx, repo, nil, note, "hash-object", "-w", "--stdin")
if err != nil {
return "", err
}
parent, stderr, code, err := runCode(repo, nil, "rev-parse", "--verify", "--quiet", NotesRef)
parent, stderr, code, err := runCode(ctx, repo, nil, "rev-parse", "--verify", "--quiet", NotesRef)
if err != nil {
return "", err
}
@@ -51,26 +52,26 @@ func NoteCommit(repo, commit, note string, env []string) (string, error) {
commitArgs := []string{"commit-tree", "-m", "Notes added by byre"}
if code == 0 {
_, err = runEnv(repo, index, "read-tree", parent)
_, err = runEnv(ctx, repo, index, "read-tree", parent)
if err != nil {
return "", err
}
commitArgs = append(commitArgs, "-p", parent)
}
_, err = runEnv(repo, index, "update-index", "--add", "--cacheinfo", "100644,"+blob+","+commit)
_, err = runEnv(ctx, repo, index, "update-index", "--add", "--cacheinfo", "100644,"+blob+","+commit)
if err != nil {
return "", err
}
tree, err := runEnv(repo, index, "write-tree")
tree, err := runEnv(ctx, repo, index, "write-tree")
if err != nil {
return "", err
}
return runEnv(repo, env, append(commitArgs, tree)...)
return runEnv(ctx, repo, env, append(commitArgs, tree)...)
}
func AdvanceNotes(repo, notes string) error {
current, stderr, code, err := runCode(repo, nil, "rev-parse", "--verify", "--quiet", NotesRef)
func AdvanceNotes(ctx context.Context, repo, notes string) error {
current, stderr, code, err := runCode(ctx, repo, nil, "rev-parse", "--verify", "--quiet", NotesRef)
if err != nil {
return err
}
@@ -78,7 +79,7 @@ func AdvanceNotes(repo, notes string) error {
return fmt.Errorf("git rev-parse %s: exit status %d: %s", NotesRef, code, stderr)
}
if code == 0 {
contained, err := IsAncestor(repo, notes, current)
contained, err := IsAncestor(ctx, repo, notes, current)
if err != nil {
return err
}
@@ -86,6 +87,6 @@ func AdvanceNotes(repo, notes string) error {
return nil
}
}
_, err = run(repo, "update-ref", NotesRef, notes)
_, err = run(ctx, repo, "update-ref", NotesRef, notes)
return err
}
+21 -20
View File
@@ -2,6 +2,7 @@ package git
import (
"bufio"
"context"
"fmt"
"io"
"os"
@@ -10,8 +11,8 @@ import (
"strings"
)
func RenameRefs(repo string, rename func(string) string) error {
branches, err := run(repo, "for-each-ref", "--format=%(refname:short)", "refs/heads/byre/")
func RenameRefs(ctx context.Context, repo string, rename func(string) string) error {
branches, err := run(ctx, repo, "for-each-ref", "--format=%(refname:short)", "refs/heads/byre/")
if err != nil {
return err
}
@@ -19,13 +20,13 @@ func RenameRefs(repo string, rename func(string) string) error {
if rename(b) == b {
continue
}
_, err = run(repo, "branch", "-m", b, rename(b))
_, err = run(ctx, repo, "branch", "-m", b, rename(b))
if err != nil {
return err
}
}
snaps, err := run(repo, "for-each-ref", "--format=%(objectname) %(refname)", "refs/byre/tasks/")
snaps, err := run(ctx, repo, "for-each-ref", "--format=%(objectname) %(refname)", "refs/byre/tasks/")
if err != nil {
return err
}
@@ -40,20 +41,20 @@ func RenameRefs(repo string, rename func(string) string) error {
if b.Len() == 0 {
return nil
}
_, err = runInput(repo, nil, b.String(), "update-ref", "--stdin")
_, err = runInput(ctx, repo, nil, b.String(), "update-ref", "--stdin")
return err
}
func RepairWorktrees(repo string, paths []string) error {
func RepairWorktrees(ctx context.Context, repo string, paths []string) error {
if len(paths) == 0 {
return nil
}
_, err := run(repo, append([]string{"worktree", "repair"}, paths...)...)
_, err := run(ctx, repo, append([]string{"worktree", "repair"}, paths...)...)
return err
}
func RewriteNotes(repo string, rewrite func(string) string) error {
parent, stderr, code, err := runCode(repo, nil, "rev-parse", "--verify", "--quiet", NotesRef)
func RewriteNotes(ctx context.Context, repo string, rewrite func(string) string) error {
parent, stderr, code, err := runCode(ctx, repo, nil, "rev-parse", "--verify", "--quiet", NotesRef)
if err != nil {
return err
}
@@ -64,7 +65,7 @@ func RewriteNotes(repo string, rewrite func(string) string) error {
return fmt.Errorf("git rev-parse %s: exit status %d: %s", NotesRef, code, stderr)
}
listing, err := run(repo, "ls-tree", "-r", parent)
listing, err := run(ctx, repo, "ls-tree", "-r", parent)
if err != nil {
return err
}
@@ -80,7 +81,7 @@ func RewriteNotes(repo string, rewrite func(string) string) error {
return nil
}
contents, err := catBlobs(repo, shas)
contents, err := catBlobs(ctx, repo, shas)
if err != nil {
return err
}
@@ -110,12 +111,12 @@ func RewriteNotes(repo string, rewrite func(string) string) error {
return nil
}
blobs, err := runInput(repo, nil, strings.Join(files, "\n")+"\n", "hash-object", "-w", "--stdin-paths")
blobs, err := runInput(ctx, repo, nil, strings.Join(files, "\n")+"\n", "hash-object", "-w", "--stdin-paths")
if err != nil {
return err
}
index := []string{"GIT_INDEX_FILE=" + filepath.Join(dir, "index")}
_, err = runEnv(repo, index, "read-tree", parent)
_, err = runEnv(ctx, repo, index, "read-tree", parent)
if err != nil {
return err
}
@@ -123,30 +124,30 @@ func RewriteNotes(repo string, rewrite func(string) string) error {
for i, blob := range splitLines(blobs) {
fmt.Fprintf(b, "100644 %s\t%s\n", blob, paths[changed[i]])
}
_, err = runInput(repo, index, b.String(), "update-index", "--index-info")
_, err = runInput(ctx, repo, index, b.String(), "update-index", "--index-info")
if err != nil {
return err
}
tree, err := runEnv(repo, index, "write-tree")
tree, err := runEnv(ctx, repo, index, "write-tree")
if err != nil {
return err
}
who, err := run(repo, "log", "-1", "--format=%cn%n%ce", parent)
who, err := run(ctx, repo, "log", "-1", "--format=%cn%n%ce", parent)
if err != nil {
return err
}
name, email, _ := strings.Cut(who, "\n")
env := IdentityEnv(name, email)
commit, err := runEnv(repo, env, "commit-tree", "-p", parent, "-m", "Rewrite byre ids in notes", tree)
commit, err := runEnv(ctx, repo, env, "commit-tree", "-p", parent, "-m", "Rewrite byre ids in notes", tree)
if err != nil {
return err
}
_, err = run(repo, "update-ref", NotesRef, commit, parent)
_, err = run(ctx, repo, "update-ref", NotesRef, commit, parent)
return err
}
func catBlobs(repo string, shas []string) (map[string]string, error) {
out, err := runRaw(repo, nil, strings.Join(shas, "\n")+"\n", "cat-file", "--batch")
func catBlobs(ctx context.Context, repo string, shas []string) (map[string]string, error) {
out, err := runRaw(ctx, repo, nil, strings.Join(shas, "\n")+"\n", "cat-file", "--batch")
if err != nil {
return nil, err
}
+10 -9
View File
@@ -1,6 +1,7 @@
package git
import (
"context"
"fmt"
"strings"
)
@@ -13,24 +14,24 @@ func SnapshotRef(taskID string, n int) string {
return fmt.Sprintf("%s%d", snapshotPrefix(taskID), n)
}
func CommitSnapshot(dir, tree, ref, message string, env []string) (string, error) {
head, err := Head(dir)
func CommitSnapshot(ctx context.Context, dir, tree, ref, message string, env []string) (string, error) {
head, err := Head(ctx, dir)
if err != nil {
return "", err
}
commit, err := runEnv(dir, env, "commit-tree", tree, "-p", head, "-m", message)
commit, err := runEnv(ctx, dir, env, "commit-tree", tree, "-p", head, "-m", message)
if err != nil {
return "", err
}
_, err = run(dir, "update-ref", ref, commit, "")
_, err = run(ctx, dir, "update-ref", ref, commit, "")
if err != nil {
return "", err
}
return commit, nil
}
func SnapshotRefs(repo, taskID string) ([]string, error) {
out, err := run(repo, "for-each-ref", "--format=%(refname)", snapshotPrefix(taskID))
func SnapshotRefs(ctx context.Context, repo, taskID string) ([]string, error) {
out, err := run(ctx, repo, "for-each-ref", "--format=%(refname)", snapshotPrefix(taskID))
if err != nil {
return nil, err
}
@@ -40,8 +41,8 @@ func SnapshotRefs(repo, taskID string) ([]string, error) {
return strings.Split(out, "\n"), nil
}
func DeleteSnapshotRefs(repo, taskID string) error {
refs, err := SnapshotRefs(repo, taskID)
func DeleteSnapshotRefs(ctx context.Context, repo, taskID string) error {
refs, err := SnapshotRefs(ctx, repo, taskID)
if err != nil {
return err
}
@@ -53,6 +54,6 @@ func DeleteSnapshotRefs(repo, taskID string) error {
for _, ref := range refs {
fmt.Fprintf(b, "delete %s\n", ref)
}
_, err = runInput(repo, nil, b.String(), "update-ref", "--stdin")
_, err = runInput(ctx, repo, nil, b.String(), "update-ref", "--stdin")
return err
}
+3 -3
View File
@@ -40,7 +40,7 @@ func (s *Server) GetTaskChanges(w http.ResponseWriter, r *http.Request, taskID s
}
}
changes, err := git.Changes(t.WorktreePath, p.DefaultBranch, since)
changes, err := git.Changes(r.Context(), t.WorktreePath, p.DefaultBranch, since)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
@@ -64,7 +64,7 @@ func (s *Server) snapshotChanges(w http.ResponseWriter, r *http.Request, t api.T
}
}
changes, err := git.Compare(t.WorktreePath, p.DefaultBranch, snap.CommitSha, snap.TreeSha, since)
changes, err := git.Compare(r.Context(), t.WorktreePath, p.DefaultBranch, snap.CommitSha, snap.TreeSha, since)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
@@ -97,7 +97,7 @@ func (s *Server) GetTaskDiff(w http.ResponseWriter, r *http.Request, taskID stri
if params.OldPath != nil {
oldPath = *params.OldPath
}
patch, err := git.FileDiff(t.WorktreePath, params.Base, params.Tree, params.Path, oldPath)
patch, err := git.FileDiff(r.Context(), t.WorktreePath, params.Base, params.Tree, params.Path, oldPath)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
+1 -1
View File
@@ -102,7 +102,7 @@ func (s *Server) GetTaskDir(w http.ResponseWriter, r *http.Request, taskID strin
for _, e := range listing.Entries {
paths = append(paths, path.Join(real, e.Name))
}
ignored, err := git.Ignored(t.WorktreePath, paths)
ignored, err := git.Ignored(r.Context(), t.WorktreePath, paths)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
+8 -5
View File
@@ -22,6 +22,7 @@ import (
"g.fc.run/ai/byre/internal/store"
"g.fc.run/ai/byre/internal/terminal"
"g.fc.run/ai/byre/internal/todo"
"g.fc.run/ai/byre/internal/trace"
"g.fc.run/ai/byre/internal/workflow"
"tailscale.com/client/local"
"tailscale.com/tsnet"
@@ -86,7 +87,7 @@ func New(dataDir string, portFirst int, ts *tsnet.Server, token string) (*Server
return nil, err
}
for _, p := range projects {
err = git.ConfigureNotesFetch(p.RepoPath)
err = git.ConfigureNotesFetch(context.Background(), p.RepoPath)
if err != nil {
slog.Error("configure notes fetch", "project", p.Id, "repo", p.RepoPath, "err", err)
}
@@ -131,7 +132,7 @@ func (s *Server) Handler() http.Handler {
writeError(w, http.StatusBadRequest, err)
},
})
return http.NewCrossOriginProtection().Handler(mux)
return timing(http.NewCrossOriginProtection().Handler(mux))
}
func (s *Server) LoopbackHandler() http.Handler {
@@ -167,7 +168,9 @@ func (s *Server) GetTailnet(w http.ResponseWriter, r *http.Request) {
func (s *Server) identify(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
identified := trace.Start(r.Context(), "whois")
who, err := s.tailnet.WhoIs(r.Context(), r.RemoteAddr)
identified()
if err != nil {
writeError(w, http.StatusUnauthorized, fmt.Errorf("identify %s on the tailnet: %w", r.RemoteAddr, err))
return
@@ -301,12 +304,12 @@ func (s *Server) createProject(ctx context.Context, name, repoPath, createdBy st
return api.Project{}, fmt.Errorf("%w: name is required", agents.ErrInvalid)
}
info, err := git.Inspect(repoPath)
info, err := git.Inspect(ctx, repoPath)
if err != nil {
return api.Project{}, fmt.Errorf("%w: %w", agents.ErrInvalid, err)
}
err = git.ConfigureNotesFetch(info.Root)
err = git.ConfigureNotesFetch(ctx, info.Root)
if err != nil {
return api.Project{}, err
}
@@ -439,7 +442,7 @@ func (s *Server) GetTodo(w http.ResponseWriter, r *http.Request, projectID strin
return
}
src, found, err := git.ReadFile(p.RepoPath, p.DefaultBranch, todo.File)
src, found, err := git.ReadFile(r.Context(), p.RepoPath, p.DefaultBranch, todo.File)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
+50
View File
@@ -0,0 +1,50 @@
package server
import (
"log/slog"
"net/http"
"time"
"g.fc.run/ai/byre/internal/trace"
)
const slowRequest = 200 * time.Millisecond
type timedWriter struct {
http.ResponseWriter
start time.Time
trace *trace.Trace
status int
}
func (w *timedWriter) WriteHeader(status int) {
if w.status == 0 {
w.status = status
w.Header().Set("Server-Timing", w.trace.Header(time.Since(w.start)))
}
w.ResponseWriter.WriteHeader(status)
}
func (w *timedWriter) Write(b []byte) (int, error) {
if w.status == 0 {
w.WriteHeader(http.StatusOK)
}
return w.ResponseWriter.Write(b)
}
func (w *timedWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
}
func timing(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, t := trace.New(r.Context())
tw := &timedWriter{ResponseWriter: w, start: time.Now(), trace: t}
next.ServeHTTP(tw, r.WithContext(ctx))
took := time.Since(tw.start)
if took < slowRequest || tw.status == http.StatusSwitchingProtocols || tw.Header().Get("Content-Type") == "text/event-stream" {
return
}
slog.Info("request slow", "method", r.Method, "path", r.URL.Path, "status", tw.status, "took", took, "spans", t.String())
})
}
+4 -3
View File
@@ -1,6 +1,7 @@
package store
import (
"context"
"database/sql"
"errors"
"fmt"
@@ -85,15 +86,15 @@ func migratePrefixIDs(tx *sql.Tx, dataDir string) error {
for i, w := range worktrees {
worktrees[i] = filepath.Dir(w)
}
err = git.RepairWorktrees(repo, worktrees)
err = git.RepairWorktrees(context.Background(), repo, worktrees)
if err != nil {
return err
}
err = git.RenameRefs(repo, rewrite)
err = git.RenameRefs(context.Background(), repo, rewrite)
if err != nil {
return err
}
err = git.RewriteNotes(repo, rewrite)
err = git.RewriteNotes(context.Background(), repo, rewrite)
if err != nil {
return err
}
+5 -3
View File
@@ -14,9 +14,8 @@ import (
"sync"
"time"
_ "modernc.org/sqlite"
"g.fc.run/ai/byre/internal/api"
"g.fc.run/ai/byre/internal/trace"
)
var ErrNotFound = errors.New("not found")
@@ -85,10 +84,11 @@ type Store struct {
func Open(path string, portFirst int, onEvent func([]api.Event)) (*Store, error) {
dsn := "file:" + path + "?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)&_txlock=immediate"
db, err := sql.Open("sqlite", dsn)
connector, err := newTracedConnector(dsn)
if err != nil {
return nil, err
}
db := sql.OpenDB(connector)
err = migrate(db, filepath.Dir(path))
if err != nil {
@@ -1505,7 +1505,9 @@ func (s *Store) Counts(ctx context.Context) (Counts, error) {
type emitFunc func(api.Event) error
func (s *Store) write(ctx context.Context, fn func(tx *sql.Tx, emit emitFunc) error) error {
locked := trace.Start(ctx, "store lock")
s.writeMu.Lock()
locked()
defer s.writeMu.Unlock()
tx, err := s.db.BeginTx(ctx, nil)
+95
View File
@@ -0,0 +1,95 @@
package store
import (
"context"
"database/sql/driver"
"time"
"modernc.org/sqlite"
"g.fc.run/ai/byre/internal/trace"
)
type sqliteConn interface {
driver.Conn
driver.ConnBeginTx
driver.ConnPrepareContext
driver.ExecerContext
driver.QueryerContext
driver.Pinger
driver.SessionResetter
driver.Validator
}
type tracedConnector struct {
driver.Connector
}
func newTracedConnector(dsn string) (driver.Connector, error) {
c, err := sqlite.NewConnector(dsn)
if err != nil {
return nil, err
}
return tracedConnector{c}, nil
}
func (c tracedConnector) Connect(ctx context.Context) (driver.Conn, error) {
conn, err := c.Connector.Connect(ctx)
if err != nil {
return nil, err
}
return tracedConn{conn.(sqliteConn)}, nil
}
type tracedConn struct {
sqliteConn
}
func (c tracedConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
defer trace.Start(ctx, "sql begin")()
return c.sqliteConn.BeginTx(ctx, opts)
}
func (c tracedConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
defer trace.Start(ctx, "sql exec")()
return c.sqliteConn.ExecContext(ctx, query, args)
}
func (c tracedConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
start := time.Now()
rows, err := c.sqliteConn.QueryContext(ctx, query, args)
if err != nil {
trace.Add(ctx, "sql query", time.Since(start))
return nil, err
}
return &tracedRows{sqliteRows: rows.(sqliteRows), ctx: ctx, took: time.Since(start)}, nil
}
type sqliteRows interface {
driver.Rows
driver.RowsColumnTypeDatabaseTypeName
driver.RowsColumnTypeLength
driver.RowsColumnTypeNullable
driver.RowsColumnTypePrecisionScale
driver.RowsColumnTypeScanType
}
type tracedRows struct {
sqliteRows
ctx context.Context
took time.Duration
}
func (r *tracedRows) Next(dest []driver.Value) error {
start := time.Now()
err := r.sqliteRows.Next(dest)
r.took += time.Since(start)
return err
}
func (r *tracedRows) Close() error {
start := time.Now()
err := r.sqliteRows.Close()
trace.Add(r.ctx, "sql query", r.took+time.Since(start))
return err
}
+71
View File
@@ -0,0 +1,71 @@
package trace
import (
"context"
"fmt"
"strings"
"sync"
"time"
)
type span struct {
name string
dur time.Duration
}
type Trace struct {
mu sync.Mutex
spans []span
}
type ctxKey struct{}
func New(ctx context.Context) (context.Context, *Trace) {
t := &Trace{}
return context.WithValue(ctx, ctxKey{}, t), t
}
func Start(ctx context.Context, name string) func() {
start := time.Now()
return func() {
Add(ctx, name, time.Since(start))
}
}
func Add(ctx context.Context, name string, dur time.Duration) {
t, _ := ctx.Value(ctxKey{}).(*Trace)
if t == nil {
return
}
t.mu.Lock()
t.spans = append(t.spans, span{name: name, dur: dur})
t.mu.Unlock()
}
func (t *Trace) Header(total time.Duration) string {
t.mu.Lock()
defer t.mu.Unlock()
parts := []string{fmt.Sprintf("total;dur=%.1f", ms(total))}
for _, s := range t.spans {
parts = append(parts, fmt.Sprintf("%s;dur=%.1f", token(s.name), ms(s.dur)))
}
return strings.Join(parts, ", ")
}
func (t *Trace) String() string {
t.mu.Lock()
defer t.mu.Unlock()
parts := []string{}
for _, s := range t.spans {
parts = append(parts, fmt.Sprintf("%s %s", s.name, s.dur.Round(time.Millisecond)))
}
return strings.Join(parts, ", ")
}
func ms(d time.Duration) float64 {
return float64(d.Microseconds()) / 1000
}
func token(name string) string {
return strings.ReplaceAll(name, " ", "-")
}
+1 -1
View File
@@ -118,7 +118,7 @@ final class API: Sendable {
"Byre-Host": macName,
]
config.proxyConfigurations = [tailnetNode.proxy()]
session = URLSession(configuration: config)
session = URLSession(configuration: config, delegate: RequestTiming(), delegateQueue: nil)
client = Client(
serverURL: origin,
configuration: Configuration(dateTranscoder: ByreDates()),
+41
View File
@@ -0,0 +1,41 @@
import Foundation
final class RequestTiming: NSObject, URLSessionTaskDelegate, Sendable {
private static let slow: TimeInterval = 0.2
func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
guard let t = metrics.transactionMetrics.last, let response = t.response as? HTTPURLResponse, response.statusCode != 101, response.value(forHTTPHeaderField: "Content-Type") != "text/event-stream", let fetched = t.fetchStartDate, let sent = t.requestStartDate, let first = t.responseStartDate, let done = t.responseEndDate else {
return
}
let finished = Date()
let serverTiming = response.value(forHTTPHeaderField: "Server-Timing") ?? ""
let server = Self.serverTotal(serverTiming)
let queue = (t.connectStartDate ?? sent).timeIntervalSince(fetched)
let connect = t.connectStartDate.flatMap { start in t.connectEndDate.map { $0.timeIntervalSince(start) } } ?? 0
let network = first.timeIntervalSince(sent) - server
let download = done.timeIntervalSince(first)
let total = done.timeIntervalSince(fetched)
let line = "\(task.originalRequest?.httpMethod ?? "") \(task.originalRequest?.url?.path ?? "")"
Task { @MainActor in
let app = Date().timeIntervalSince(finished)
if total + app < Self.slow {
return
}
log.notice("request slow \(line, privacy: .public) total=\(Self.ms(total + app), privacy: .public) queue=\(Self.ms(queue), privacy: .public) connect=\(Self.ms(connect), privacy: .public) network=\(Self.ms(network), privacy: .public) server=\(Self.ms(server), privacy: .public) download=\(Self.ms(download), privacy: .public) app=\(Self.ms(app), privacy: .public) spans=\(serverTiming, privacy: .public)")
}
}
private static func serverTotal(_ header: String) -> TimeInterval {
for metric in header.split(separator: ",") {
let parts = metric.split(separator: ";").map { $0.trimmingCharacters(in: .whitespaces) }
if parts.first == "total", let dur = parts.dropFirst().first(where: { $0.hasPrefix("dur=") }), let value = Double(dur.dropFirst(4)) {
return value / 1000
}
}
return 0
}
private static func ms(_ seconds: TimeInterval) -> String {
"\(Int((seconds * 1000).rounded()))ms"
}
}