A library for working with Pijul repositories in Go
package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"pijul-go"
	"strings"
	"time"
)

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")
)

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"`
	State     string    `json:"state"`

	mark     int
	exported bool
}

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)
}

func main() {
	flag.Parse()
	if *branch == "" {
		*branch = *channel
	}

	logBytes, err := exec.Command("pijul", "log", "--state", "--repository", *repo, "--output-format=json").Output()
	if err != nil {
		printErrorAndExit("Error running pijul log:", err)
	}

	var changes []change
	err = json.Unmarshal(logBytes, &changes)
	if err != nil {
		printErrorAndExit("Error parsing pijul log output:", err)
	}

	// Reverse the list of changes.
	for i, j := 0, len(changes)-1; i < j; i, j = i+1, j-1 {
		changes[i], changes[j] = changes[j], changes[i]
	}

	stream := new(FastExportStream)
	if *markFile != "" {
		if err := stream.marks.Import(*markFile); err != nil {
			printErrorAndExit("Error loading marks file:", err)
		}
	}
	stream.marks.MarkChanges(changes)

	pristine := pijul.NewGraph()

	for changeIndex, c := range changes {
		changeBytes, err := os.ReadFile(filepath.Join(*repo, ".pijul", "changes", c.Hash[:2], c.Hash[2:]+".change"))
		if err != nil {
			printErrorAndExit(fmt.Sprintf("error loading change %s:", c.Hash), err)
		}
		change, err := pijul.DeserializeChange(changeBytes)
		if err != nil {
			printErrorAndExit(fmt.Sprintf("error deserializing change %s:", c.Hash), err)
		}
		hash, err := pijul.HashFromBase32(c.Hash)
		if err != nil {
			printErrorAndExit("error parsing hash:", err)
		}
		err = pristine.ApplyChange(hash, change)
		if err != nil {
			printErrorAndExit(fmt.Sprintf("error applying change %s:", c.Hash), err)
		}

		if c.exported {
			continue
		}

		var commit Commit
		commit.Mark = c.mark

		if len(c.Authors) > 0 && strings.Contains(c.Authors[0], "<") {
			// Using the author from the log lets use use the result of pijul
			// looking up the identity.
			commit.Committer = c.Authors[0]
		} else if len(change.Authors) > 0 {
			a := change.Authors[0]
			name := a["full_name"]
			if name == "" {
				name = a["name"]
			}
			if name == "" {
				name = a["key"]
			}
			commit.Committer = name + " <" + a["email"] + ">"
		} else {
			commit.Committer = "<>"
		}

		commit.Timestamp = change.Timestamp
		commit.Branch = *branch

		message := change.Message
		if change.Description != "" {
			message += "\n\n" + change.Description
		}
		commit.Message = message

		if changeIndex > 0 {
			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 = true

		root, err := pristine.RootDirectory()
		if err != nil {
			printErrorAndExit("error getting root directory:", err)
		}

		err = addFiles(&commit, stream, root, "")
		if err != nil {
			printErrorAndExit("error outputting files:", err)
		}

		stream.AddCommit(commit)
	}

	if *markFile != "" {
		if err := stream.marks.Export(*markFile); err != nil {
			printErrorAndExit("Error writing marks file:", err)
		}
	}
}

func addFiles(commit *Commit, stream *FastExportStream, dirInode *pijul.Block, pathPrefix string) error {
	entries, err := pijul.ReadDir(dirInode)
	if err != nil {
		return err
	}
	for _, e := range entries {
		if e.IsDirectory {
			err = addFiles(commit, stream, e.Inode, path.Join(pathPrefix, e.Name))
			if err != nil {
				return err
			}
		} else {
			buf := new(bytes.Buffer)
			err = pijul.OutputFile(buf, e.Inode)
			if err != nil {
				return err
			}
			b := stream.AddBlob(buf.Bytes())
			commit.Modifications = append(commit.Modifications, FileModify{Blob: b, Path: path.Join(pathPrefix, e.Name)})
		}
	}
	return nil
}