I thought I did this earlier, but apparently I didn't.
P6D65XL7ENXQO32YQSG6I655EBBUKF2XSIQTVCLPEWFR4S5KLPVAC RGK2IUMOM7CWPSAHLJBLIRJ7Y6HZPHLTLJ7ADTWVBMI5ZJNBGY2AC R7WB2ZZZMN4DUC7PGWRKW3QNWJQF4P6XDZIYLTBV4INJDHSBPZQQC YANQZYFXQ7CJ4ZDGVBORVBAE2I6XSUH55TDBY3I2Z2LYI4U4O53QC Y7VFVY6EMR7FMVCTPLBIYVRI2DJTOSUK7LSRRRIODT3G57FR7Z4QC RTQQLOCOBMMY5RQ6QI7TYKILLSSW5YNVPA33NL4MHM27QXMOCKPQC RFMRCLJXZIUTMQOGKVN2YYYJC2HHDJP6NS6K7P6PGIZ3S6QF77SAC 534I6MRXZVDW5W5OXRBCSTJ2WFZ4ECL6UQCIUNB46LSH3IJXJWDAC KZ4XMKSPLDYKDWCDZXTIJXZPXUZPMGYSZAIIJTQQSYDZVWOZVI5AC OLMQ7EVU4GITQMACZRI7VEGXG7GMZ4ZVKU4BNQUAJS3GEIBEOJMAC 2J4YY37DAVTCJJGWZFQ7PL4NBGW2LTBDA4I5CSY7SFPBG76AJI5QC YBAXM44PROCHZWNCOAFGQ3P25FN45KEICIRGC3PKAUIF3HF6PX7AC K23EJ6EJBFYC73TX5MMV7ZMI2GXVQJT6OQCGV56MXPQN4BLOQZ5AC P2B4ZSO5YWG73KTDPO7KX5JNRNFDKF3UZEOJNGN22JRMAWQXFCSQC 5ETDKF5FO7AE7G6ZGP5WAMMY6VQZT2KQ75UZ3OD2WM24Q2JZPAOQC EGKSUIOBLW2DUXFNMXXVQ2NDLE7YQE27YHNE2R6YSC2K2ELG6WJAC NXJB3UTCVQZWUD5DKP7QOV2Y6LNS4AHZBC3SOKGLUKUISSB4QBZAC package mainimport ("fmt""time")func formatTime(t time.Time) string {return fmt.Sprintf("%d %s", t.Unix(), t.Format("-0700"))}type change struct {Hash string `json:"hash"`Authors []string `json:"authors"`Timestamp time.Time `json:"timestamp"`Message string `json:"message"`Description string `json:"description"`mark intexported bool}func main() {flag.Parse()// Make a clone of the repository to work with, so that we don't mess up the original.tempDir, err := os.MkdirTemp("", "")if err != nil {printErrorAndExit("Error creating temporary directory:", err)}tempRepo := filepath.Join(tempDir, "repo")_, err = exec.Command("pijul", "clone", "--channel", *channel, *repo, tempRepo).Output()if err != nil {printErrorAndExit("Error cloning the repository:", err)}if err := os.Chdir(tempRepo); err != nil {printErrorAndExit("Error changing to the clone of the repository:", err)}logBytes, err := exec.Command("pijul", "log", "--description", "--output-format=json").Output()if err != nil {printErrorAndExit("Error running pijul log:", err)}var changes []changeerr = json.Unmarshal(logBytes, &changes)if err != nil {printErrorAndExit("Error parsing pijul log output:", err)}// If the first change is empty, skip it.if len(changes[len(changes)-1].Authors) == 0 {changes = changes[:len(changes)-1]}stream := new(FastExportStream)if *markFile != "" {if err := stream.marks.Import(*markFile); err != nil {printErrorAndExit("Error loading marks file:", err)}}stream.marks.MarkChanges(changes)for changeIndex, c := range changes {if c.exported {break}if changeIndex > 0 {if _, err := exec.Command("pijul", "unrecord", "--reset", changes[changeIndex-1].Hash).Output(); err != nil {printErrorAndExit("Error unrecording change "+changes[changeIndex-1].Hash+":", err)}}var commit Commitcommit.Mark = c.markauthor := c.Authors[0]author = strings.Replace(author, "() <", "<", 1)if !strings.Contains(author, "<") {author += " <>"// Since the email address is missing from the log,// we'll try to find it in the output of pijul change.changeInfo, err := exec.Command("pijul", "change", c.Hash).Output()if err != nil {printErrorAndExit("Error from pijul change:", err)}if i := bytes.Index(changeInfo, authorsHeader); i != -1 {var name, email strings := bufio.NewScanner(bytes.NewReader(changeInfo[i+len(authorsHeader):]))for s.Scan() {line := s.Text()if line == "" {break}if strings.HasPrefix(line, "email =") {email = unquote(strings.TrimPrefix(line, "email ="))} else if strings.HasPrefix(line, "name =") {name = unquote(strings.TrimPrefix(line, "name ="))}}if name != "" && email != "" {author = name + " <" + email + ">"}}}commit.Committer = authorcommit.Timestamp = c.Timestampcommit.Branch = *branchmessage := c.Messageif c.Description != "" {message += "\n\n" + c.Description}commit.Message = messageif changeIndex < len(changes)-1 {commit.From = changes[changeIndex+1].mark}// To specify the content for the commit, we remove everything// and then add back in all the files that are present after the change.commit.DeleteAll = truelisting, err := exec.Command("pijul", "list").Output()if err != nil {printErrorAndExit("Error from pijul list:", err)}for _, f := range strings.Split(string(listing), "\n") {if f == "" {continue}info, err := os.Stat(f)if err != nil {printErrorAndExit("Error from Stat:", err)}if info.IsDir() {continue}data, err := os.ReadFile(f)if err != nil {printErrorAndExit("Error reading "+f+":", err)}b := stream.AddBlob(data)commit.Modifications = append(commit.Modifications, FileModify{Blob: b, Path: f})}stream.AddCommit(commit)}stream.ReverseCommits()if err := stream.WriteTo(os.Stdout); err != nil {printErrorAndExit("Error writing output stream:", err)}if err := os.RemoveAll(tempDir); err != nil {printErrorAndExit("Error removing temporary directory:", err)}if *markFile != "" {if err := stream.marks.Export(*markFile); err != nil {printErrorAndExit("Error writing marks file:", err)}}}if *branch == "" {*branch = *channel}func printErrorAndExit(description string, err error) {msg := err.Error()if err, ok := err.(*exec.ExitError); ok && len(err.Stderr) > 0 {msg = string(err.Stderr)}fmt.Fprintln(os.Stderr, description, msg)os.Exit(2)}// unquote tries to unquote various types of quoted strings, and returns the// result. If none of the formats works, it returns s unchanged except for// trimming off whitespace.func unquote(s string) string {s = strings.TrimSpace(s)switch {case strings.HasPrefix(s, "'''") && strings.HasSuffix(s, "'''"):return strings.TrimPrefix(strings.TrimSuffix(s, "'''"), "'''")case strings.HasPrefix(s, "'") && strings.HasSuffix(s, "'"):return strings.TrimPrefix(strings.TrimSuffix(s, "'"), "'")case strings.HasPrefix(s, "\"") && strings.HasSuffix(s, "\""):unquoted, err := strconv.Unquote(s)if err == nil {return unquoted}}return s}var authorsHeader = []byte("\n[[authors]]\n")var (repo = flag.String("repo", ".", "path of the repository to export")channel = flag.String("channel", "main", "which channel to export")branch = flag.String("branch", "", "destination branch in Git (default is the same as channel)")markFile = flag.String("marks", "", "path to file to store persistent marks"))"os""os/exec""path/filepath""strconv""strings""encoding/json""flag""bufio""bytes"
package maintype Marks struct {marks intchanges []markedChange}func (m *Marks) Next() int {m.marks++return m.marks}func (m *Marks) MarkChanges(changes []change) {for i := range changes {if mark, ok := hashToMark[changes[i].Hash]; ok {changes[i].mark = markchanges[i].exported = true} else {changes[i].mark = m.Next()m.changes = append(m.changes, markedChange{mark: changes[i].mark,hash: changes[i].Hash,})}}}// Import loads a marks file analogous to those used by git fast-export's// --import-marks and --export-marks switches.func (m *Marks) Import(filename string) error {f, err := os.Open(filename)if err != nil {return err}defer f.Close()br := bufio.NewReader(f)for {var mc markedChangen, err := fmt.Fscanf(br, ":%d %s\n", &mc.mark, &mc.hash)if err == io.ErrUnexpectedEOF && n == 0 {return nil}if err != nil {return err}m.changes = append(m.changes, mc)}}func (m *Marks) Export(filename string) error {f, err := os.Create(filename)if err != nil {return err}for _, c := range m.changes {fmt.Fprintf(f, ":%d %s\n", c.mark, c.hash)}return f.Close()}// Make a lookup table for existing marks, and make sure we don't// reuse them.hashToMark := make(map[string]int)for _, mc := range m.changes {hashToMark[mc.hash] = mc.markif mc.mark > m.marks {m.marks = mc.mark}}import ("bufio""fmt""io""os")type markedChange struct {mark inthash string}
package mainimport ("fmt""io""time")type FileModify struct {Blob intPath string}func (f FileModify) WriteTo(w io.Writer) error {_, err := fmt.Fprintf(w, "M 644 :%d %s\n", f.Blob, f.Path)return err}type Commit struct {Mark intCommitter stringTimestamp time.TimeMessage stringDeleteAll boolModifications []FileModify}func (c Commit) WriteTo(w io.Writer) error {if _, err := fmt.Fprintln(w, "commit refs/heads/"+c.Branch); err != nil {return err}if c.Mark != 0 {if _, err := fmt.Fprintf(w, "mark :%d\n", c.Mark); err != nil {return err}}if _, err := fmt.Fprintln(w, "committer", c.Committer, formatTime(c.Timestamp)); err != nil {return err}if _, err := fmt.Fprintln(w, "data", len(c.Message)); err != nil {return err}if _, err := fmt.Fprintln(w, c.Message); err != nil {return err}if c.DeleteAll {if _, err := fmt.Fprintln(w, "deleteall"); err != nil {return err}}for _, m := range c.Modifications {if err := m.WriteTo(w); err != nil {return err}if c.From != 0 {if _, err := fmt.Fprintf(w, "from :%d\n", c.From); err != nil {return err}}}if _, err := fmt.Fprintln(w); err != nil {return err}return nil}// AddBlob adds a blob to the stream and returns its mark.func (f *FastExportStream) AddBlob(data []byte) int {hash := sha512.Sum512(data)if mark, ok := f.blobIndex[hash]; ok {return mark}b := Blob{Mark: f.marks.Next(),Data: data,}f.Blobs = append(f.Blobs, b)return b.Markif f.blobIndex == nil {f.blobIndex = make(map[[64]byte]int)}f.blobIndex[hash] = b.Mark}}func (f *FastExportStream) WriteTo(w io.Writer) error {for _, b := range f.Blobs {if err := b.WriteTo(w); err != nil {return err}}for _, c := range f.Commits {if err := c.WriteTo(w); err != nil {return err}}}return nil}type Blob struct {Mark intData []byte}func (b Blob) WriteTo(w io.Writer) error {if _, err := fmt.Fprintln(w, "blob"); err != nil {return err}if b.Mark != 0 {if _, err := fmt.Fprintf(w, "mark :%d\n", b.Mark); err != nil {return err}}if _, err := fmt.Fprintln(w, "data", len(b.Data)); err != nil {return err}if _, err := w.Write(b.Data); err != nil {return err}if _, err := fmt.Fprintln(w); err != nil {return err}return nil}// A FastExportStream is an in-memory representation of a fast-export stream.type FastExportStream struct {Commits []CommitBlobs []Blobmarks MarksblobIndex map[[64]byte]int}func (f *FastExportStream) AddCommit(c Commit) {if c.Mark == 0 {c.Mark = f.marks.Next()}f.Commits = append(f.Commits, c)}func (f *FastExportStream) ReverseCommits() {for i, j := 0, len(f.Commits)-1; j > i; i, j = i+1, j-1 {f.Commits[i], f.Commits[j] = f.Commits[j], f.Commits[i]From intBranch string"crypto/sha512"