// Copyright 2017, 2019 Luke Shumaker // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package main import ( "bytes" "io" "io/ioutil" "os" "os/exec" "path" "strconv" "strings" "git.lukeshu.com/go/libfastimport" "github.com/pkg/errors" "git.parabola.nu/~lukeshu/fastimport-go-utils/fiutil" ) type fullcommit struct { metadata libfastimport.CmdCommit fileactions []libfastimport.Cmd } type Filter struct { fromRef string toPfx string frontend *libfastimport.Frontend backend *libfastimport.Backend refs map[string]string mark int curCommitIn libfastimport.CmdCommit curCommitOut map[string]fullcommit OnCommit func() } func NewFilter(fromRef string, toPfx string) (*Filter, error) { var err error ret := &Filter{ fromRef: fromRef, toPfx: strings.TrimRight(toPfx, "/"), } ret.refs, err = gitRefs() if err != nil { return nil, err } ret.frontend, err = fiutil.GitFastExport( "--use-done-feature", "--no-data", "--", fromRef) if err != nil { return nil, err } ret.backend, err = fiutil.GitFastImport("--done") if err != nil { return nil, err } return ret, nil } func (f *Filter) newmark() int { f.mark++ return f.mark } func (f *Filter) infofile(name string) string { id := strings.Replace(strings.Replace(f.toPfx, "^", "^5E", -1), "/", "^2F", -1) ret := "info/svn2git2aur/" + id + "/" + name _ = os.MkdirAll(path.Dir(ret), 0777) return ret } func (f *Filter) pkgbuild2srcinfo(pkgbuildId string) (string, error) { cachefilename := f.infofile("pkgbuild2srcinfo/" + pkgbuildId) for { // Try to get a cached result b, err := ioutil.ReadFile(cachefilename) if err == nil && len(bytes.TrimSpace(b)) == 40 { return string(bytes.TrimSpace(b)), nil } // Read the PKGBUILD sha1, data, err := f.backend.CatBlob(libfastimport.CmdCatBlob{DataRef: pkgbuildId}) if err != nil { return "", err } if sha1 != pkgbuildId { return "", errors.Errorf("PKGBUILD sha1 mismatch: %q != %q", pkgbuildId, sha1) } // Write the PKGBUILD to a temporary file to pass to makepkg file, err := os.OpenFile(f.infofile("tmp/PKGBUILD"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return "", err } _, err = io.WriteString(file, data) file.Close() if err != nil { return "", err } // Run makepkg on the PKGBUILD to get a .SRCINFO cmd := exec.Command("makepkg", "--printsrcinfo") cmd.Dir = f.infofile("tmp") srcinfoBody, err := cmd.Output() if err != nil { return "", &fiutil.ProcessError{ExitError: err.(*exec.ExitError), Cmd: "makepkg --printsrcinfo"} } // Write the .SRCINFO back in to git mark := f.newmark() err = f.backend.Do(libfastimport.CmdBlob{ Mark: mark, Data: string(srcinfoBody), }) if err != nil { return "", err } // Now get the new ID of the .SRCINFO from git srcinfoId, err := f.backend.GetMark(libfastimport.CmdGetMark{ Mark: mark, }) if err != nil { return "", err } // Write the srcinfoId in to the cache file, err = os.OpenFile(cachefilename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) _, err = io.WriteString(file, srcinfoId) file.Close() if err != nil { return "", err } } } // I don't remember why I wrote this function. I obviously thought I // would need it. It's slow, and doesn't cache, so I figure I meant // for it to be called rarely. Perhaps for resuming incremental // conversion? func gitsvnid2commit(gitsvnid string, searchrange string) (string, error) { cmd := exec.Command("git", "log", "-n1", "--format=%H", "--grep=^git-svn-id: "+gitsvnid+"$", searchrange) out, err := cmd.Output() return strings.TrimSpace(string(out)), err } func (f *Filter) Run() error { for { cmd, err := f.frontend.ReadCmd() if err != nil { return err } switch cmdt := cmd.(type) { case libfastimport.CmdFeature, libfastimport.CmdOption, libfastimport.CmdCheckpoint, libfastimport.CmdProgress, libfastimport.CmdComment: err = f.backend.Do(cmd) if err != nil { return err } case libfastimport.CmdDone: err = f.backend.Do(cmd) if err != nil { return err } cmd, err := f.frontend.ReadCmd() if cmd != nil || err != io.EOF { return errors.Errorf("git fast-export kept going after 'done': cmd=%v err=%v", cmd, err) } return nil case libfastimport.CmdReset: // This should only happen once, at the very // beginning. I think I can ignore it. case libfastimport.CmdCommit: f.curCommitIn = cmdt f.curCommitOut = map[string]fullcommit{} case libfastimport.CmdCommitEnd: for _, fc := range f.curCommitOut { // TODO: I think I might need to issue // a 'reset' command if // f.refs[fc.metadata.Ref] isn't set. // TODO: detect merges and add parents // to fc.metadata.From err = f.backend.Do(fc.metadata) if err != nil { return err } f.refs[fc.metadata.Ref] = ":" + strconv.Itoa(fc.metadata.Mark) for _, fileaction := range fc.fileactions { err = f.backend.Do(fileaction) if err != nil { return err } } } // TODO: synthesize ABS-tree submodule commits f.curCommitIn = libfastimport.CmdCommit{} f.curCommitOut = nil if f.OnCommit != nil { f.OnCommit() } case libfastimport.FileModify: branchname := filename2branchname(string(cmdt.Path)) if branchname == "" { continue } branch_ref := f.toPfx + "/" + branchname from_dataref, from_dataref_ok := f.refs[branch_ref] if from_dataref_ok { file_mode, file_dataref, _, err := f.backend.Ls(libfastimport.CmdLs{DataRef: from_dataref, Path: cmdt.Path}) if err != nil { return err } if file_mode == cmdt.Mode && file_dataref == cmdt.DataRef { // the file hasn't changed; the action is a no-op; ignore it continue } } else { from_dataref = branch_ref + "^0" } if _, ok := f.curCommitOut[branch_ref]; !ok { f.curCommitOut[branch_ref] = fullcommit{metadata: libfastimport.CmdCommit{ Ref: branch_ref, Mark: f.newmark(), Author: f.curCommitIn.Author, Committer: f.curCommitIn.Committer, Msg: f.curCommitIn.Msg, From: from_dataref, Merge: nil, }} } filename := strings.TrimPrefix(string(cmdt.Path), branchname+"/") outcommit := f.curCommitOut[branch_ref] outcommit.fileactions = append(outcommit.fileactions, libfastimport.FileModify{ Mode: cmdt.Mode, Path: libfastimport.Path(filename), DataRef: cmdt.DataRef, }) if filename == "PKGBUILD" { srcinfo, err := f.pkgbuild2srcinfo(cmdt.DataRef) if err != nil { return err } outcommit.fileactions = append(outcommit.fileactions, libfastimport.FileModify{ Mode: cmdt.Mode, Path: libfastimport.Path(".SRCINFO"), DataRef: srcinfo, }) } f.curCommitOut[branch_ref] = outcommit case libfastimport.FileDelete: branchname := filename2branchname(string(cmdt.Path)) if branchname == "" { continue } branch_ref := f.toPfx + "/" + branchname from_dataref, from_dataref_ok := f.refs[branch_ref] if !from_dataref_ok { return errors.Errorf("cannot delete file %q from branch %q, because that branch doesn't exist!", cmdt.Path, branchname) } file_mode, _, _, err := f.backend.Ls(libfastimport.CmdLs{DataRef: from_dataref, Path: cmdt.Path}) if err != nil { return err } if file_mode == 0 { // the file doesn't exist; the action is a no-op; ignore it continue } if _, ok := f.curCommitOut[branch_ref]; !ok { f.curCommitOut[branch_ref] = fullcommit{metadata: libfastimport.CmdCommit{ Ref: branch_ref, Mark: f.newmark(), Author: f.curCommitIn.Author, Committer: f.curCommitIn.Committer, Msg: f.curCommitIn.Msg, From: from_dataref, Merge: nil, }} } filename := strings.TrimPrefix(string(cmdt.Path), branchname+"/") outcommit := f.curCommitOut[branch_ref] outcommit.fileactions = append(outcommit.fileactions, libfastimport.FileDelete{ Path: libfastimport.Path(filename), }) if filename == "PKGBUILD" { outcommit.fileactions = append(outcommit.fileactions, libfastimport.FileDelete{ Path: libfastimport.Path(".SRCINFO"), }) } f.curCommitOut[branch_ref] = outcommit } } return nil }