196 lines
4.6 KiB
Go
196 lines
4.6 KiB
Go
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
|
|
}
|