| 1 |
package convert |
| 2 |
|
| 3 |
import ( |
| 4 |
"fmt" |
| 5 |
"sort" |
| 6 |
"strings" |
| 7 |
|
| 8 |
"github.com/lain/larc/internal/core" |
| 9 |
) |
| 10 |
|
| 11 |
/* Mapping between larc and git data structures. |
| 12 |
* Handles conversion of revision numbers <-> git SHA, |
| 13 |
* flat trees <-> hierarchical trees, etc. */ |
| 14 |
|
| 15 |
// RevisionMapping stores bidirectional mapping between larc revision and git SHA |
| 16 |
type RevisionMapping struct { |
| 17 |
LarcRev int64 |
| 18 |
GitSHA string |
| 19 |
} |
| 20 |
|
| 21 |
// MappingStore tracks mappings during conversion |
| 22 |
type MappingStore struct { |
| 23 |
revToSHA map[int64]string // larc revision -> git SHA |
| 24 |
shaToRev map[string]int64 // git SHA -> larc revision |
| 25 |
blobMap map[string]string // larc blob hash -> git blob SHA (or vice versa) |
| 26 |
} |
| 27 |
|
| 28 |
// NewMappingStore creates a new mapping store |
| 29 |
func NewMappingStore() *MappingStore { |
| 30 |
return &MappingStore{ |
| 31 |
revToSHA: make(map[int64]string), |
| 32 |
shaToRev: make(map[string]int64), |
| 33 |
blobMap: make(map[string]string), |
| 34 |
} |
| 35 |
} |
| 36 |
|
| 37 |
// AddRevisionMapping adds a revision <-> SHA mapping |
| 38 |
func (m *MappingStore) AddRevisionMapping(rev int64, sha string) { |
| 39 |
m.revToSHA[rev] = sha |
| 40 |
m.shaToRev[sha] = rev |
| 41 |
} |
| 42 |
|
| 43 |
// GetGitSHA returns git SHA for larc revision |
| 44 |
func (m *MappingStore) GetGitSHA(rev int64) (string, bool) { |
| 45 |
sha, ok := m.revToSHA[rev] |
| 46 |
return sha, ok |
| 47 |
} |
| 48 |
|
| 49 |
// GetLarcRev returns larc revision for git SHA |
| 50 |
func (m *MappingStore) GetLarcRev(sha string) (int64, bool) { |
| 51 |
rev, ok := m.shaToRev[sha] |
| 52 |
return rev, ok |
| 53 |
} |
| 54 |
|
| 55 |
// AddBlobMapping adds a blob hash mapping |
| 56 |
func (m *MappingStore) AddBlobMapping(larcHash, gitSHA string) { |
| 57 |
m.blobMap[larcHash] = gitSHA |
| 58 |
} |
| 59 |
|
| 60 |
// GetGitBlobSHA returns git blob SHA for larc blob hash |
| 61 |
func (m *MappingStore) GetGitBlobSHA(larcHash string) (string, bool) { |
| 62 |
sha, ok := m.blobMap[larcHash] |
| 63 |
return sha, ok |
| 64 |
} |
| 65 |
|
| 66 |
// TreeNode represents a hierarchical tree node for git |
| 67 |
type TreeNode struct { |
| 68 |
Name string |
| 69 |
Mode uint32 |
| 70 |
IsDir bool |
| 71 |
BlobSHA string // for files |
| 72 |
Children map[string]*TreeNode // for directories |
| 73 |
} |
| 74 |
|
| 75 |
// NewTreeNode creates a new tree node |
| 76 |
func NewTreeNode(name string, isDir bool) *TreeNode { |
| 77 |
node := &TreeNode{ |
| 78 |
Name: name, |
| 79 |
IsDir: isDir, |
| 80 |
} |
| 81 |
if isDir { |
| 82 |
node.Children = make(map[string]*TreeNode) |
| 83 |
node.Mode = 0040000 // git tree mode |
| 84 |
} |
| 85 |
return node |
| 86 |
} |
| 87 |
|
| 88 |
// FlatTreeToHierarchical converts larc flat tree to hierarchical structure for git |
| 89 |
func FlatTreeToHierarchical(entries []core.TreeEntry) *TreeNode { |
| 90 |
root := NewTreeNode("", true) |
| 91 |
|
| 92 |
for _, entry := range entries { |
| 93 |
if entry.Kind == core.EntryKindDir { |
| 94 |
continue // skip directories, we'll create them as needed |
| 95 |
} |
| 96 |
|
| 97 |
parts := strings.Split(entry.Path, "/") |
| 98 |
current := root |
| 99 |
|
| 100 |
/* traverse/create directory path */ |
| 101 |
for i := 0; i < len(parts)-1; i++ { |
| 102 |
dirName := parts[i] |
| 103 |
if child, ok := current.Children[dirName]; ok { |
| 104 |
current = child |
| 105 |
} else { |
| 106 |
newDir := NewTreeNode(dirName, true) |
| 107 |
current.Children[dirName] = newDir |
| 108 |
current = newDir |
| 109 |
} |
| 110 |
} |
| 111 |
|
| 112 |
/* add file entry */ |
| 113 |
fileName := parts[len(parts)-1] |
| 114 |
fileNode := NewTreeNode(fileName, false) |
| 115 |
fileNode.Mode = entry.Mode |
| 116 |
fileNode.BlobSHA = entry.BlobHash // will be remapped later |
| 117 |
current.Children[fileName] = fileNode |
| 118 |
} |
| 119 |
|
| 120 |
return root |
| 121 |
} |
| 122 |
|
| 123 |
// HierarchicalToFlatTree converts git hierarchical tree to larc flat tree |
| 124 |
func HierarchicalToFlatTree(root *TreeNode, prefix string) []core.TreeEntry { |
| 125 |
var entries []core.TreeEntry |
| 126 |
|
| 127 |
/* sort children for consistent ordering */ |
| 128 |
names := make([]string, 0, len(root.Children)) |
| 129 |
for name := range root.Children { |
| 130 |
names = append(names, name) |
| 131 |
} |
| 132 |
sort.Strings(names) |
| 133 |
|
| 134 |
for _, name := range names { |
| 135 |
child := root.Children[name] |
| 136 |
path := name |
| 137 |
if prefix != "" { |
| 138 |
path = prefix + "/" + name |
| 139 |
} |
| 140 |
|
| 141 |
if child.IsDir { |
| 142 |
/* recurse into directory */ |
| 143 |
subEntries := HierarchicalToFlatTree(child, path) |
| 144 |
entries = append(entries, subEntries...) |
| 145 |
} else { |
| 146 |
/* add file entry */ |
| 147 |
entries = append(entries, core.TreeEntry{ |
| 148 |
Path: path, |
| 149 |
Mode: child.Mode, |
| 150 |
BlobHash: child.BlobSHA, |
| 151 |
Kind: core.EntryKindFile, |
| 152 |
}) |
| 153 |
} |
| 154 |
} |
| 155 |
|
| 156 |
return entries |
| 157 |
} |
| 158 |
|
| 159 |
// ConvertBranchName converts larc branch name to git ref name |
| 160 |
func ConvertBranchName(larcBranch string) string { |
| 161 |
/* "port" -> "main" (default branch mapping) */ |
| 162 |
if larcBranch == core.DefaultBranchName { |
| 163 |
return "main" |
| 164 |
} |
| 165 |
return larcBranch |
| 166 |
} |
| 167 |
|
| 168 |
// ConvertRefToBranch converts git ref name to larc branch name |
| 169 |
func ConvertRefToBranch(gitRef string) string { |
| 170 |
/* strip "refs/heads/" prefix if present */ |
| 171 |
ref := strings.TrimPrefix(gitRef, "refs/heads/") |
| 172 |
|
| 173 |
/* "main" or "master" -> "port" */ |
| 174 |
if ref == "main" || ref == "master" { |
| 175 |
return core.DefaultBranchName |
| 176 |
} |
| 177 |
return ref |
| 178 |
} |
| 179 |
|
| 180 |
// FormatRevisionMessage adds larc revision info to git commit message |
| 181 |
func FormatRevisionMessage(message string, rev int64) string { |
| 182 |
return fmt.Sprintf("%s\n\n[larc:r%d]", message, rev) |
| 183 |
} |
| 184 |
|
| 185 |
// ParseRevisionFromMessage extracts larc revision from git commit message |
| 186 |
func ParseRevisionFromMessage(message string) (int64, bool) { |
| 187 |
/* look for [larc:rN] pattern */ |
| 188 |
idx := strings.Index(message, "[larc:r") |
| 189 |
if idx == -1 { |
| 190 |
return 0, false |
| 191 |
} |
| 192 |
|
| 193 |
var rev int64 |
| 194 |
_, err := fmt.Sscanf(message[idx:], "[larc:r%d]", &rev) |
| 195 |
if err != nil { |
| 196 |
return 0, false |
| 197 |
} |
| 198 |
|
| 199 |
return rev, true |
| 200 |
} |
| 201 |
|
| 202 |
// ConvertMode converts larc mode to git mode |
| 203 |
func ConvertMode(mode uint32, kind core.EntryKind) uint32 { |
| 204 |
switch kind { |
| 205 |
case core.EntryKindSymlink: |
| 206 |
return 0120000 // git symlink |
| 207 |
case core.EntryKindFile: |
| 208 |
if mode&0111 != 0 { |
| 209 |
return 0100755 // executable |
| 210 |
} |
| 211 |
return 0100644 // regular file |
| 212 |
default: |
| 213 |
return 0100644 |
| 214 |
} |
| 215 |
} |
| 216 |
|
| 217 |
// ConvertGitMode converts git mode to larc mode |
| 218 |
func ConvertGitMode(gitMode uint32) (uint32, core.EntryKind) { |
| 219 |
switch gitMode { |
| 220 |
case 0120000: |
| 221 |
return 0777, core.EntryKindSymlink |
| 222 |
case 0100755: |
| 223 |
return 0755, core.EntryKindFile |
| 224 |
case 0100644: |
| 225 |
return 0644, core.EntryKindFile |
| 226 |
case 0040000: |
| 227 |
return 0755, core.EntryKindDir |
| 228 |
default: |
| 229 |
return 0644, core.EntryKindFile |
| 230 |
} |
| 231 |
} |
| 232 |
|