− package main
−
− import (
− "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 int
− exported 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 []change
− err = 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 Commit
− commit.Mark = c.mark
−
− author := 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 string
− s := 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 = author
−
− commit.Timestamp = c.Timestamp
− commit.Branch = *branch
−
− message := c.Message
− if c.Description != "" {
− message += "\n\n" + c.Description
− }
− commit.Message = message
−
− if 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 = true
−
− listing, 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"