Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions gitops/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,57 @@ func (r *Repo) Push(branches []string) {
exec.Mustex(r.Dir, "git", args...)
}

type GitFileChange struct {
Path string
Status string // "A"dded, "M"odified, "D"eleted, etc.
}

func (r *Repo) GetDetailedChanges() ([]GitFileChange, error) {
cmd := oe.Command("git", "status", "--porcelain")
cmd.Dir = r.Dir
output, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("failed to get git status: %w", err)
}

if len(output) == 0 {
return []GitFileChange{}, nil
}

// Split output into lines and process each line
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
changes := make([]GitFileChange, 0, len(lines))

for _, line := range lines {
if len(line) > 3 {
// git status --porcelain output format is "XY filename"
// where X is staged status, Y is unstaged status
statusCode := line[:2]
filename := strings.TrimSpace(line[2:])

// Determine the primary status
var status string
if strings.Contains(statusCode, "D") {
status = "D" // Deleted
} else if strings.Contains(statusCode, "A") {
status = "A" // Added
} else if strings.Contains(statusCode, "M") {
status = "M" // Modified
} else {
status = "M" // Default to modified for other cases
}

changes = append(changes, GitFileChange{
Path: filename,
Status: status,
})
log.Printf("Git change: %s %s", status, filename)
}
}

return changes, nil
}

func (r *Repo) GetModifiedFiles() ([]string, error) {
cmd := oe.Command("git", "status", "--porcelain")
cmd.Dir = r.Dir
Expand Down
171 changes: 160 additions & 11 deletions gitops/git/github_app/github_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,28 +97,62 @@ func CreatePR(from, to, title, body string) error {
return err
}

func CreateCommit(baseBranch string, commitBranch string, gitopsPath string, files []string, prTitle string, prDescription string) {
func CreateCommit(baseBranch string, commitBranch string, gitopsPath string, files []string, deletedFiles []string, prTitle string, prDescription string, branchesNeedingRecreation []string) {
ctx := context.Background()
gh := createGithubClient()

log.Printf("Starting Create Commit: Commit branch: %s\n", commitBranch)
log.Printf("Starting Create Commit: Base branch: %s\n", baseBranch)
log.Printf("GitOps Path: %s\n", gitopsPath)
log.Printf("Modified Files: %v\n", files)
fileEntries, err := getFilesToCommit(gitopsPath, files)

if err != nil {
log.Fatalf("failed to get files to commit: %v", err)
log.Printf("Deleted Files: %v\n", deletedFiles)
log.Printf("Branches needing recreation: %v\n", branchesNeedingRecreation)

// Check if this branch needs recreation due to deletions
needsRecreation := false
branchName := fmt.Sprintf("deploy/%s", commitBranch)
for _, recreateBranch := range branchesNeedingRecreation {
if recreateBranch == branchName {
needsRecreation = true
break
}
}

ref := getRef(ctx, gh, baseBranch, commitBranch)
tree, err := getTree(ctx, gh, ref, fileEntries)
if err != nil {
log.Fatalf("failed to create tree: %v", err)
var ref *github.Reference
var err error

if needsRecreation {
log.Printf("Branch %s needs recreation due to target deletions, force-resetting from %s\n", branchName, baseBranch)
ref, err = forceResetBranch(ctx, gh, baseBranch, branchName)
if err != nil {
log.Fatalf("failed to force reset branch: %v", err)
}

// When recreating, we need to include all current files in the GitOps directory
allFileEntries, err := getAllFilesToCommit(gitopsPath)
if err != nil {
log.Fatalf("failed to get all files to commit: %v", err)
}

tree, err := getTree(ctx, gh, ref, allFileEntries)
if err != nil {
log.Fatalf("failed to create tree: %v", err)
}

pushCommit(ctx, gh, ref, tree, prTitle)
} else {
// Handle incremental changes (adds/modifies/deletes)
ref = getRef(ctx, gh, baseBranch, branchName)

tree, err := getTreeWithChanges(ctx, gh, ref, gitopsPath, files, deletedFiles)
if err != nil {
log.Fatalf("failed to create tree with changes: %v", err)
}

pushCommit(ctx, gh, ref, tree, prTitle)
}

pushCommit(ctx, gh, ref, tree, prTitle)
createPR(ctx, gh, baseBranch, commitBranch, prTitle, prDescription)
createPR(ctx, gh, baseBranch, branchName, prTitle, prDescription)
}

func getFilesToCommit(gitopsPath string, inputPaths []string) ([]FileEntry, error) {
Expand Down Expand Up @@ -227,6 +261,50 @@ func getRef(ctx context.Context, gh *github.Client, baseBranch string, commitBra
return ref
}

func getTreeWithChanges(ctx context.Context, gh *github.Client, ref *github.Reference, gitopsPath string, addedModifiedFiles []string, deletedFiles []string) (tree *github.Tree, err error) {
entries := []*github.TreeEntry{}

// Add/modify files
if len(addedModifiedFiles) > 0 {
fileEntries, err := getFilesToCommit(gitopsPath, addedModifiedFiles)
if err != nil {
return nil, fmt.Errorf("failed to get files to commit: %v", err)
}

for _, file := range fileEntries {
content, err := os.ReadFile(file.FullPath)
if err != nil {
return nil, fmt.Errorf("failed to read file %s: %v", file.FullPath, err)
}
log.Printf("Adding/modifying file %s to tree\n", file.RelativePath)
entries = append(entries, &github.TreeEntry{
Path: github.Ptr(file.RelativePath),
Type: github.Ptr("blob"),
Content: github.Ptr(string(content)),
Mode: github.Ptr("100644"),
})
}
}

// Delete files by setting SHA to nil
for _, deletedFile := range deletedFiles {
log.Printf("Deleting file %s from tree\n", deletedFile)
entries = append(entries, &github.TreeEntry{
Path: github.Ptr(deletedFile),
Mode: github.Ptr("100644"),
Type: github.Ptr("blob"),
SHA: nil, // Setting SHA to nil deletes the file
})
}

if len(entries) == 0 {
return nil, fmt.Errorf("no changes to commit")
}

tree, _, err = gh.Git.CreateTree(ctx, *repoOwner, *repo, *ref.Object.SHA, entries)
return tree, err
}

func getTree(ctx context.Context, gh *github.Client, ref *github.Reference, files []FileEntry) (tree *github.Tree, err error) {
// Create a tree with what to commit.
entries := []*github.TreeEntry{}
Expand All @@ -250,6 +328,77 @@ func getTree(ctx context.Context, gh *github.Client, ref *github.Reference, file
return tree, err
}

func forceResetBranch(ctx context.Context, gh *github.Client, baseBranch string, targetBranch string) (*github.Reference, error) {
// Get the base branch reference
baseRef, _, err := gh.Git.GetRef(ctx, *repoOwner, *repo, "refs/heads/"+baseBranch)
if err != nil {
return nil, fmt.Errorf("failed to get base branch ref: %v", err)
}

// Try to get the existing target branch
targetRef, _, err := gh.Git.GetRef(ctx, *repoOwner, *repo, "refs/heads/"+targetBranch)
if err != nil {
// Branch doesn't exist, create it
log.Printf("Target branch %s doesn't exist, creating it\n", targetBranch)
newRef := &github.Reference{
Ref: github.String("refs/heads/" + targetBranch),
Object: &github.GitObject{SHA: baseRef.Object.SHA},
}
createdRef, _, err := gh.Git.CreateRef(ctx, *repoOwner, *repo, newRef)
if err != nil {
return nil, fmt.Errorf("failed to create branch ref: %v", err)
}
return createdRef, nil
}

// Branch exists, force update it to point to base branch
log.Printf("Force updating branch %s to match %s\n", targetBranch, baseBranch)
targetRef.Object.SHA = baseRef.Object.SHA
updatedRef, _, err := gh.Git.UpdateRef(ctx, *repoOwner, *repo, targetRef, true) // force=true
if err != nil {
return nil, fmt.Errorf("failed to force update branch ref: %v", err)
}

return updatedRef, nil
}

func getAllFilesToCommit(gitopsPath string) ([]FileEntry, error) {
var allFileEntries []FileEntry

// Walk through the entire GitOps directory and get all files
err := filepath.Walk(gitopsPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
// Get path relative to gitopsPath
relPath, err := filepath.Rel(gitopsPath, path)
if err != nil {
return fmt.Errorf("failed to get relative path for %s: %v", path, err)
}
// Skip hidden files and git files
if !strings.HasPrefix(filepath.Base(relPath), ".") {
allFileEntries = append(allFileEntries, FileEntry{
RelativePath: relPath,
FullPath: path,
})
}
}
return nil
})

if err != nil {
return nil, fmt.Errorf("failed to walk GitOps directory: %v", err)
}

if len(allFileEntries) == 0 {
return nil, fmt.Errorf("no files found in GitOps directory %s", gitopsPath)
}

log.Printf("Found %d files in GitOps directory\n", len(allFileEntries))
return allFileEntries, nil
}

func pushCommit(ctx context.Context, gh *github.Client, ref *github.Reference, tree *github.Tree, commitMessage string) {
// Get the parent commit to attach the commit to.
parent, _, err := gh.Repositories.GetCommit(ctx, *repoOwner, *repo, *ref.Object.SHA, nil)
Expand Down
29 changes: 25 additions & 4 deletions gitops/prer/create_gitops_prs.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,10 +309,13 @@ func main() {
var updatedTargets []string
var updatedBranches []string
var modifiedFiles []string
var deletedFiles []string
var branchesNeedingRecreation []string

// Process each release train
for train, targets := range trains {
branch := fmt.Sprintf("deploy/%s%s", train, cfg.DeploymentBranchSuffix)
needsRecreation := false

if !workdir.SwitchToBranch(branch, cfg.PRTargetBranch) {
// Check if branch needs recreation due to deleted targets
Expand All @@ -325,11 +328,16 @@ func main() {
for _, t := range commitmsg.ExtractTargets(msg) {
if !currentTargets[t] {
workdir.RecreateBranch(branch, cfg.PRTargetBranch)
needsRecreation = true
break
}
}
}

if needsRecreation {
branchesNeedingRecreation = append(branchesNeedingRecreation, branch)
}

// Process targets
for _, target := range targets {
bin := bazel.TargetToExecutable(target)
Expand All @@ -339,14 +347,27 @@ func main() {
commitMsg := fmt.Sprintf("GitOps for release branch %s from %s commit %s\n%s",
cfg.ReleaseBranch, cfg.BranchName, cfg.GitCommit, commitmsg.Generate(targets))

files, err := workdir.GetModifiedFiles()
changes, err := workdir.GetDetailedChanges()

if err != nil {
log.Fatalf("failed to get modified files: %v", err)
log.Fatalf("failed to get detailed changes: %v", err)
}

// Separate the changes by type
var addedModifiedFiles []string
var branchDeletedFiles []string
for _, change := range changes {
if change.Status == "D" {
branchDeletedFiles = append(branchDeletedFiles, change.Path)
} else {
addedModifiedFiles = append(addedModifiedFiles, change.Path)
}
}

modifiedFiles = append(modifiedFiles, files...)
modifiedFiles = append(modifiedFiles, addedModifiedFiles...)
deletedFiles = append(deletedFiles, branchDeletedFiles...)
log.Printf("Modified files: %v", modifiedFiles)
log.Printf("Deleted files: %v", deletedFiles)
if workdir.Commit(commitMsg, cfg.GitOpsPath) {
log.Printf("Branch %s has changes, push required", branch)
updatedTargets = append(updatedTargets, targets...)
Expand Down Expand Up @@ -381,7 +402,7 @@ func main() {

switch cfg.GitHost {
case "github_app":
github_app.CreateCommit(cfg.PRTargetBranch, cfg.BranchName, gitopsDir, modifiedFiles, prTitle, prDescription)
github_app.CreateCommit(cfg.PRTargetBranch, cfg.BranchName, gitopsDir, modifiedFiles, deletedFiles, prTitle, prDescription, branchesNeedingRecreation)
return
default:
workdir.Push(updatedBranches)
Expand Down