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)
}
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], "<") {
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
}
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
}