package convert import ( "fmt" "sort" "strings" "github.com/lain/larc/internal/core" ) /* Mapping between larc and git data structures. * Handles conversion of revision numbers <-> git SHA, * flat trees <-> hierarchical trees, etc. */ // RevisionMapping stores bidirectional mapping between larc revision and git SHA type RevisionMapping struct { LarcRev int64 GitSHA string } // MappingStore tracks mappings during conversion type MappingStore struct { revToSHA map[int64]string // larc revision -> git SHA shaToRev map[string]int64 // git SHA -> larc revision blobMap map[string]string // larc blob hash -> git blob SHA (or vice versa) } // NewMappingStore creates a new mapping store func NewMappingStore() *MappingStore { return &MappingStore{ revToSHA: make(map[int64]string), shaToRev: make(map[string]int64), blobMap: make(map[string]string), } } // AddRevisionMapping adds a revision <-> SHA mapping func (m *MappingStore) AddRevisionMapping(rev int64, sha string) { m.revToSHA[rev] = sha m.shaToRev[sha] = rev } // GetGitSHA returns git SHA for larc revision func (m *MappingStore) GetGitSHA(rev int64) (string, bool) { sha, ok := m.revToSHA[rev] return sha, ok } // GetLarcRev returns larc revision for git SHA func (m *MappingStore) GetLarcRev(sha string) (int64, bool) { rev, ok := m.shaToRev[sha] return rev, ok } // AddBlobMapping adds a blob hash mapping func (m *MappingStore) AddBlobMapping(larcHash, gitSHA string) { m.blobMap[larcHash] = gitSHA } // GetGitBlobSHA returns git blob SHA for larc blob hash func (m *MappingStore) GetGitBlobSHA(larcHash string) (string, bool) { sha, ok := m.blobMap[larcHash] return sha, ok } // TreeNode represents a hierarchical tree node for git type TreeNode struct { Name string Mode uint32 IsDir bool BlobSHA string // for files Children map[string]*TreeNode // for directories } // NewTreeNode creates a new tree node func NewTreeNode(name string, isDir bool) *TreeNode { node := &TreeNode{ Name: name, IsDir: isDir, } if isDir { node.Children = make(map[string]*TreeNode) node.Mode = 0040000 // git tree mode } return node } // FlatTreeToHierarchical converts larc flat tree to hierarchical structure for git func FlatTreeToHierarchical(entries []core.TreeEntry) *TreeNode { root := NewTreeNode("", true) for _, entry := range entries { if entry.Kind == core.EntryKindDir { continue // skip directories, we'll create them as needed } parts := strings.Split(entry.Path, "/") current := root /* traverse/create directory path */ for i := 0; i < len(parts)-1; i++ { dirName := parts[i] if child, ok := current.Children[dirName]; ok { current = child } else { newDir := NewTreeNode(dirName, true) current.Children[dirName] = newDir current = newDir } } /* add file entry */ fileName := parts[len(parts)-1] fileNode := NewTreeNode(fileName, false) fileNode.Mode = entry.Mode fileNode.BlobSHA = entry.BlobHash // will be remapped later current.Children[fileName] = fileNode } return root } // HierarchicalToFlatTree converts git hierarchical tree to larc flat tree func HierarchicalToFlatTree(root *TreeNode, prefix string) []core.TreeEntry { var entries []core.TreeEntry /* sort children for consistent ordering */ names := make([]string, 0, len(root.Children)) for name := range root.Children { names = append(names, name) } sort.Strings(names) for _, name := range names { child := root.Children[name] path := name if prefix != "" { path = prefix + "/" + name } if child.IsDir { /* recurse into directory */ subEntries := HierarchicalToFlatTree(child, path) entries = append(entries, subEntries...) } else { /* add file entry */ entries = append(entries, core.TreeEntry{ Path: path, Mode: child.Mode, BlobHash: child.BlobSHA, Kind: core.EntryKindFile, }) } } return entries } // ConvertBranchName converts larc branch name to git ref name func ConvertBranchName(larcBranch string) string { /* "port" -> "main" (default branch mapping) */ if larcBranch == core.DefaultBranchName { return "main" } return larcBranch } // ConvertRefToBranch converts git ref name to larc branch name func ConvertRefToBranch(gitRef string) string { /* strip "refs/heads/" prefix if present */ ref := strings.TrimPrefix(gitRef, "refs/heads/") /* "main" or "master" -> "port" */ if ref == "main" || ref == "master" { return core.DefaultBranchName } return ref } // FormatRevisionMessage adds larc revision info to git commit message func FormatRevisionMessage(message string, rev int64) string { return fmt.Sprintf("%s\n\n[larc:r%d]", message, rev) } // ParseRevisionFromMessage extracts larc revision from git commit message func ParseRevisionFromMessage(message string) (int64, bool) { /* look for [larc:rN] pattern */ idx := strings.Index(message, "[larc:r") if idx == -1 { return 0, false } var rev int64 _, err := fmt.Sscanf(message[idx:], "[larc:r%d]", &rev) if err != nil { return 0, false } return rev, true } // ConvertMode converts larc mode to git mode func ConvertMode(mode uint32, kind core.EntryKind) uint32 { switch kind { case core.EntryKindSymlink: return 0120000 // git symlink case core.EntryKindFile: if mode&0111 != 0 { return 0100755 // executable } return 0100644 // regular file default: return 0100644 } } // ConvertGitMode converts git mode to larc mode func ConvertGitMode(gitMode uint32) (uint32, core.EntryKind) { switch gitMode { case 0120000: return 0777, core.EntryKindSymlink case 0100755: return 0755, core.EntryKindFile case 0100644: return 0644, core.EntryKindFile case 0040000: return 0755, core.EntryKindDir default: return 0644, core.EntryKindFile } }