Jaypore CI

> Jaypore CI: Minimal, Offline, Local CI system.
Log | Files | Refs | README

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 }