package git import ( "bufio" "context" "fmt" "io" "os" "path/filepath" "strconv" "strings" ) func RenameRefs(repo string, rename func(string) string) error { branches, err := run(repo, "for-each-ref", "--format=%(refname:short)", "refs/heads/byre/") if err != nil { return err } for _, b := range splitLines(branches) { if rename(b) == b { continue } _, err = run(repo, "branch", "-m", b, rename(b)) if err != nil { return err } } snaps, err := run(repo, "for-each-ref", "--format=%(objectname) %(refname)", "refs/byre/tasks/") if err != nil { return err } b := &strings.Builder{} for _, l := range splitLines(snaps) { sha, ref, _ := strings.Cut(l, " ") if rename(ref) == ref { continue } fmt.Fprintf(b, "create %s %s\ndelete %s %s\n", rename(ref), sha, ref, sha) } if b.Len() == 0 { return nil } _, err = runInput(repo, nil, b.String(), "update-ref", "--stdin") return err } func RepairWorktrees(repo string, paths []string) error { if len(paths) == 0 { return nil } _, err := run(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) if err != nil { return err } if code == 1 { return nil } if code != 0 { return fmt.Errorf("git rev-parse %s: exit status %d: %s", NotesRef, code, stderr) } listing, err := run(repo, "ls-tree", "-r", parent) if err != nil { return err } paths := map[string]string{} shas := []string{} for _, l := range splitLines(listing) { meta, path, _ := strings.Cut(l, "\t") fields := strings.Fields(meta) paths[fields[2]] = path shas = append(shas, fields[2]) } if len(shas) == 0 { return nil } contents, err := catBlobs(repo, shas) if err != nil { return err } dir, err := os.MkdirTemp("", "byre-notes-*") if err != nil { return err } defer os.RemoveAll(dir) changed := []string{} files := []string{} for _, sha := range shas { text := rewrite(contents[sha]) if text == contents[sha] { continue } file := filepath.Join(dir, sha) err = os.WriteFile(file, []byte(text), 0o644) if err != nil { return err } changed = append(changed, sha) files = append(files, file) } if len(changed) == 0 { return nil } blobs, err := runInput(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) if err != nil { return err } b := &strings.Builder{} 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") if err != nil { return err } tree, err := runEnv(repo, index, "write-tree") if err != nil { return err } who, err := run(repo, "log", "-1", "--format=%cn%n%ce", parent) if err != nil { return err } name, email, _ := strings.Cut(who, "\n") env := []string{"GIT_AUTHOR_NAME=" + name, "GIT_AUTHOR_EMAIL=" + email, "GIT_COMMITTER_NAME=" + name, "GIT_COMMITTER_EMAIL=" + email} commit, err := runEnv(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) return err } func catBlobs(repo string, shas []string) (map[string]string, error) { out, err := runRaw(repo, strings.Join(shas, "\n")+"\n", "cat-file", "--batch") if err != nil { return nil, err } r := bufio.NewReader(strings.NewReader(out)) contents := map[string]string{} for range shas { header, err := r.ReadString('\n') if err != nil { return nil, err } fields := strings.Fields(header) if len(fields) != 3 { return nil, fmt.Errorf("git cat-file --batch: unexpected header %q", header) } size, err := strconv.Atoi(fields[2]) if err != nil { return nil, err } body := make([]byte, size+1) _, err = io.ReadFull(r, body) if err != nil { return nil, err } contents[fields[0]] = string(body[:size]) } return contents, nil } func splitLines(out string) []string { if out == "" { return []string{} } return strings.Split(out, "\n") } func runRaw(dir, input string, args ...string) (string, error) { out, stderr, code, err := runCodeInput(context.Background(), dir, nil, strings.NewReader(input), args...) if err != nil { return "", err } if code != 0 { return "", fmt.Errorf("git %s: exit status %d: %s", strings.Join(args, " "), code, stderr) } return out, nil }