Log slow API requests with server spans and client timing
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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...)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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, " ", "-")
|
||||
}
|
||||
@@ -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()),
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user