git.go (3651B)
1 package jci 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "os/exec" 8 "path/filepath" 9 "strings" 10 ) 11 12 // git runs a git command and returns stdout 13 func git(args ...string) (string, error) { 14 cmd := exec.Command("git", args...) 15 var stdout, stderr bytes.Buffer 16 cmd.Stdout = &stdout 17 cmd.Stderr = &stderr 18 if err := cmd.Run(); err != nil { 19 return "", fmt.Errorf("git %s: %v\n%s", strings.Join(args, " "), err, stderr.String()) 20 } 21 return strings.TrimSpace(stdout.String()), nil 22 } 23 24 // GetCurrentCommit returns the current HEAD commit hash 25 func GetCurrentCommit() (string, error) { 26 return git("rev-parse", "HEAD") 27 } 28 29 // GetRepoRoot returns the root directory of the git repository 30 func GetRepoRoot() (string, error) { 31 return git("rev-parse", "--show-toplevel") 32 } 33 34 // RefExists checks if a ref exists 35 func RefExists(ref string) bool { 36 _, err := git("rev-parse", "--verify", ref) 37 return err == nil 38 } 39 40 // StoreTree stores a directory as a tree object and creates a commit under refs/jci/<commit> 41 func StoreTree(dir string, commit string, message string) error { 42 repoRoot, err := GetRepoRoot() 43 if err != nil { 44 return err 45 } 46 47 tmpIndex := repoRoot + "/.git/jci-index" 48 defer exec.Command("rm", "-f", tmpIndex).Run() 49 50 // We need to use git hash-object and mktree to build a tree 51 // from files outside the repo 52 treeID, err := hashDir(dir, repoRoot, tmpIndex) 53 if err != nil { 54 return fmt.Errorf("failed to hash directory: %w", err) 55 } 56 57 // Create commit from tree 58 commitTreeCmd := exec.Command("git", "commit-tree", treeID, "-m", message) 59 commitTreeCmd.Dir = repoRoot 60 commitOut, err := commitTreeCmd.Output() 61 if err != nil { 62 return fmt.Errorf("git commit-tree: %v", err) 63 } 64 commitID := strings.TrimSpace(string(commitOut)) 65 66 // Update ref 67 ref := "refs/jci/" + commit 68 if _, err := git("update-ref", ref, commitID); err != nil { 69 return fmt.Errorf("git update-ref: %v", err) 70 } 71 72 return nil 73 } 74 75 // hashDir recursively hashes a directory and returns its tree ID 76 func hashDir(dir string, repoRoot string, tmpIndex string) (string, error) { 77 entries, err := os.ReadDir(dir) 78 if err != nil { 79 return "", err 80 } 81 82 var treeEntries []string 83 84 for _, entry := range entries { 85 path := filepath.Join(dir, entry.Name()) 86 87 if entry.IsDir() { 88 // Recursively hash subdirectory 89 subTreeID, err := hashDir(path, repoRoot, tmpIndex) 90 if err != nil { 91 return "", err 92 } 93 treeEntries = append(treeEntries, fmt.Sprintf("040000 tree %s\t%s", subTreeID, entry.Name())) 94 } else { 95 // Hash file 96 cmd := exec.Command("git", "hash-object", "-w", path) 97 cmd.Dir = repoRoot 98 out, err := cmd.Output() 99 if err != nil { 100 return "", fmt.Errorf("hash-object %s: %v", path, err) 101 } 102 blobID := strings.TrimSpace(string(out)) 103 104 // Get file mode 105 info, err := entry.Info() 106 if err != nil { 107 return "", err 108 } 109 mode := "100644" 110 if info.Mode()&0111 != 0 { 111 mode = "100755" 112 } 113 treeEntries = append(treeEntries, fmt.Sprintf("%s blob %s\t%s", mode, blobID, entry.Name())) 114 } 115 } 116 117 // Create tree from entries 118 treeInput := strings.Join(treeEntries, "\n") 119 if treeInput != "" { 120 treeInput += "\n" 121 } 122 123 cmd := exec.Command("git", "mktree") 124 cmd.Dir = repoRoot 125 cmd.Stdin = strings.NewReader(treeInput) 126 out, err := cmd.Output() 127 if err != nil { 128 return "", fmt.Errorf("mktree: %v (input: %q)", err, treeInput) 129 } 130 131 return strings.TrimSpace(string(out)), nil 132 } 133 134 // ListJCIRefs returns all refs under refs/jci/ 135 func ListJCIRefs() ([]string, error) { 136 out, err := git("for-each-ref", "--format=%(refname:short)", "refs/jci/") 137 if err != nil { 138 return nil, err 139 } 140 if out == "" { 141 return nil, nil 142 } 143 return strings.Split(out, "\n"), nil 144 }