refactor(merge-diff): rewrite bash into go to better handle complexity of multi-parent pull requests
This commit is contained in:
parent
7b9be1d1b3
commit
1103d6b4ab
4 changed files with 194 additions and 128 deletions
|
@ -1,12 +1,12 @@
|
|||
{ pkgs, lib, gitSvPkg, ... }:
|
||||
|
||||
let
|
||||
fluxLocal = import ./flux-local { inherit pkgs lib; };
|
||||
fluxLocal = import ./flux-local { inherit lib pkgs; };
|
||||
in
|
||||
{
|
||||
flux-local = fluxLocal;
|
||||
flux-diff = import ./flux-diff { inherit pkgs lib fluxLocal; };
|
||||
merge-diff = import ./merge-diff { inherit pkgs lib gitSvPkg; };
|
||||
flux-diff = import ./flux-diff { inherit lib pkgs fluxLocal; };
|
||||
merge-diff = import ./merge-diff { inherit lib pkgs gitSvPkg; };
|
||||
forgejo-label = import ./forgejo-label { inherit lib pkgs; };
|
||||
forgejo-comment = import ./forgejo-comment { inherit lib pkgs; };
|
||||
forgejo-release = import ./forgejo-release { inherit lib pkgs; };
|
||||
|
|
|
@ -1,131 +1,15 @@
|
|||
{ pkgs, lib, gitSvPkg, ... }:
|
||||
{ lib, pkgs, gitSvPkg }:
|
||||
|
||||
with pkgs;
|
||||
pkgs.buildGoModule rec {
|
||||
pname = "merge-diff";
|
||||
version = "2.0.0";
|
||||
|
||||
writeShellApplication rec {
|
||||
name = "merge-diff";
|
||||
buildInputs = with pkgs; [ gitFull jq coreutils gnugrep gawk gitSvPkg ];
|
||||
|
||||
runtimeInputs = [ gitFull jq coreutils gnugrep gawk gitSvPkg ];
|
||||
src = ./src;
|
||||
|
||||
# Define the shell script
|
||||
text = ''
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Vendor dependencies for reproducibility
|
||||
vendorHash = null;
|
||||
|
||||
# Function to display usage
|
||||
usage() {
|
||||
echo "Usage: $0 -p <path_pattern> -d <directory depth>"
|
||||
echo " -p, --pattern Path pattern to filter changed files (can be specified multiple times)"
|
||||
echo " -d, --depth Depth of changeset (default is 2)"
|
||||
echo " -h, --help Display this help message"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse command-line arguments
|
||||
PATTERNS=()
|
||||
DEPTH=2
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-p|--pattern)
|
||||
PATTERNS+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
-d|--depth)
|
||||
DEPTH="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ ''${#PATTERNS[@]} -eq 0 ]; then
|
||||
echo "Error: At least one path pattern must be specified."
|
||||
usage
|
||||
fi
|
||||
|
||||
# Get the current commit SHA
|
||||
CURRENT_SHA=$(git rev-parse HEAD)
|
||||
|
||||
# Get parent commits
|
||||
PARENT_COUNT=$(git rev-list --parents -n 1 "$CURRENT_SHA" | wc -w)
|
||||
|
||||
if [ "$PARENT_COUNT" -gt 2 ]; then
|
||||
echo "This is a merge commit with $((PARENT_COUNT - 1)) parents."
|
||||
# For simplicity, comparing with the first parent
|
||||
PARENT_SHA=$(git rev-parse HEAD^1)
|
||||
elif [ "$PARENT_COUNT" -eq 2 ]; then
|
||||
# Regular commit with one parent
|
||||
PARENT_SHA=$(git rev-parse HEAD^1)
|
||||
else
|
||||
echo "Single commit with no parents. Listing all changes."
|
||||
PARENT_SHA=HEAD~1
|
||||
fi
|
||||
|
||||
# List changed files between parent and current commit
|
||||
CHANGED_FILES=$(git diff --name-only "$PARENT_SHA" "$CURRENT_SHA")
|
||||
|
||||
#echo "Changed files:"
|
||||
#echo "$CHANGED_FILES"
|
||||
|
||||
# Initialize an empty array for filtered changes
|
||||
FILTERED_CHANGES=()
|
||||
|
||||
# Iterate over each pattern and filter changed files
|
||||
for pattern in "''${PATTERNS[@]}"; do
|
||||
while IFS= read -r file; do
|
||||
if [[ "$file" =~ $pattern ]]; then
|
||||
file=$(echo "$file"| cut -d/ -f1-"$DEPTH")
|
||||
FILTERED_CHANGES+=("$file")
|
||||
fi
|
||||
done <<< "$CHANGED_FILES"
|
||||
done
|
||||
|
||||
# Remove duplicates
|
||||
UNIQUE_FILTERED_CHANGES=$(printf "%s\n" "''${FILTERED_CHANGES[@]}" | sort -u)
|
||||
|
||||
#echo "Filtered changed files:"
|
||||
#echo "$UNIQUE_FILTERED_CHANGES"
|
||||
|
||||
|
||||
# Determine semantic version bump using git-sv
|
||||
# Ensure that git-sv is available
|
||||
if ! command -v git-sv &> /dev/null; then
|
||||
echo "Error: git-sv is not installed or not in PATH."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the semantic version bump
|
||||
VERSION_BUMP=$(git-sv next-version)
|
||||
|
||||
# Validate the version bump
|
||||
#if [[ "$VERSION_BUMP" =~ ^(patch|minor|major)$ ]]; then
|
||||
# echo "Version bump: $VERSION_BUMP"
|
||||
#else
|
||||
# echo "Error: Unexpected version bump value: $VERSION_BUMP"
|
||||
# exit 1
|
||||
#fi
|
||||
|
||||
# Convert to JSON
|
||||
RESULTS=$(jq -n \
|
||||
--argjson filteredChanges "$(printf '%s\n' "$UNIQUE_FILTERED_CHANGES" | jq -R -s -c 'split("\n") | map(select(length > 0))')" \
|
||||
--arg versionBump "$VERSION_BUMP" \
|
||||
'{filteredChanges: $filteredChanges, versionBump: $versionBump}')
|
||||
|
||||
# Output results
|
||||
echo "$RESULTS"
|
||||
'';
|
||||
|
||||
# Metadata for the package
|
||||
meta = with lib; {
|
||||
homepage = "https://code.252.no/tommy/containers";
|
||||
description = "Identify Merge Commit and List Changed Files with Flexible Filtering and Semantic Versioning";
|
||||
license = licenses.mit;
|
||||
maintainers = with maintainers; [ "tommy-skaug" ];
|
||||
};
|
||||
subPackages = [ "." ];
|
||||
}
|
3
apps/ci-os/packages/merge-diff/src/go.mod
Normal file
3
apps/ci-os/packages/merge-diff/src/go.mod
Normal file
|
@ -0,0 +1,3 @@
|
|||
module code.252.no/tommy/merge-diff
|
||||
|
||||
go 1.22.6
|
179
apps/ci-os/packages/merge-diff/src/main.go
Normal file
179
apps/ci-os/packages/merge-diff/src/main.go
Normal file
|
@ -0,0 +1,179 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
FilteredChanges []string `json:"filteredChanges"`
|
||||
NextVersion string `json:"nextVersion"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Parse command-line arguments
|
||||
var patterns patternList
|
||||
var depth int
|
||||
|
||||
flag.Var(&patterns, "p", "Path pattern to filter changed files (can be specified multiple times)")
|
||||
flag.Var(&patterns, "pattern", "Path pattern to filter changed files (can be specified multiple times)")
|
||||
flag.IntVar(&depth, "d", 2, "Depth of changeset (default is 2)")
|
||||
flag.IntVar(&depth, "depth", 2, "Depth of changeset (default is 2)")
|
||||
help := flag.Bool("h", false, "Display this help message")
|
||||
helpLong := flag.Bool("help", false, "Display this help message")
|
||||
flag.Parse()
|
||||
|
||||
if *help || *helpLong {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(patterns) == 0 {
|
||||
fmt.Println("Error: At least one path pattern must be specified.")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get the current commit SHA
|
||||
currentSHA := getCurrentSHA()
|
||||
|
||||
// Get parent commits
|
||||
parentSHAs := getParentSHAs(currentSHA)
|
||||
|
||||
if len(parentSHAs) == 0 {
|
||||
fmt.Println("Single commit with no parents. Listing all changes.")
|
||||
parentSHAs = append(parentSHAs, currentSHA+"~1")
|
||||
}
|
||||
|
||||
// Collect all changed files from all parents
|
||||
changedFiles := make(map[string]struct{})
|
||||
for _, parentSHA := range parentSHAs {
|
||||
files := getChangedFiles(parentSHA, currentSHA)
|
||||
for _, file := range files {
|
||||
changedFiles[file] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter and process changed files
|
||||
filteredChanges := filterAndProcessFiles(changedFiles, patterns, depth)
|
||||
|
||||
// Get the next semantic version using git-sv
|
||||
nextVersion := getNextVersion()
|
||||
|
||||
// Prepare the result
|
||||
result := Result{
|
||||
FilteredChanges: uniqueStrings(filteredChanges),
|
||||
NextVersion: nextVersion,
|
||||
}
|
||||
|
||||
// Output results as JSON
|
||||
output, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
log.Fatalf("Error marshaling JSON: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(output))
|
||||
}
|
||||
|
||||
// Custom flag type for multiple patterns
|
||||
type patternList []string
|
||||
|
||||
func (p *patternList) String() string {
|
||||
return strings.Join(*p, ", ")
|
||||
}
|
||||
|
||||
func (p *patternList) Set(value string) error {
|
||||
*p = append(*p, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func getCurrentSHA() string {
|
||||
output, err := exec.Command("git", "rev-parse", "HEAD").Output()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting current SHA: %v", err)
|
||||
}
|
||||
return strings.TrimSpace(string(output))
|
||||
}
|
||||
|
||||
func getParentSHAs(commitSHA string) []string {
|
||||
output, err := exec.Command("git", "rev-list", "--parents", "-n", "1", commitSHA).Output()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting parent SHAs: %v", err)
|
||||
}
|
||||
parts := strings.Fields(string(output))
|
||||
if len(parts) > 1 {
|
||||
return parts[1:] // Exclude the current commit SHA
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func getChangedFiles(parentSHA, currentSHA string) []string {
|
||||
output, err := exec.Command("git", "diff", "--name-only", parentSHA, currentSHA).Output()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting changed files: %v", err)
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
if len(lines) == 1 && lines[0] == "" {
|
||||
return []string{}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func filterAndProcessFiles(changedFiles map[string]struct{}, patterns []string, depth int) []string {
|
||||
var filtered []string
|
||||
for file := range changedFiles {
|
||||
for _, pattern := range patterns {
|
||||
matched, err := regexp.MatchString(pattern, file)
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid regex pattern: %v", err)
|
||||
}
|
||||
if matched {
|
||||
processedFile := processFilePath(file, depth)
|
||||
filtered = append(filtered, processedFile)
|
||||
break // Stop checking other patterns for this file
|
||||
}
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func processFilePath(filePath string, depth int) string {
|
||||
parts := strings.Split(filePath, string(os.PathSeparator))
|
||||
if len(parts) > depth {
|
||||
parts = parts[:depth]
|
||||
}
|
||||
return filepath.Join(parts...)
|
||||
}
|
||||
|
||||
func uniqueStrings(input []string) []string {
|
||||
uniqueMap := make(map[string]struct{})
|
||||
for _, item := range input {
|
||||
uniqueMap[item] = struct{}{}
|
||||
}
|
||||
var result []string
|
||||
for item := range uniqueMap {
|
||||
result = append(result, item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func getNextVersion() string {
|
||||
// Ensure that git-sv is available
|
||||
if _, err := exec.LookPath("git-sv"); err != nil {
|
||||
log.Fatalf("Error: git-sv is not installed or not in PATH.")
|
||||
}
|
||||
output, err := exec.Command("git-sv", "next-version").Output()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting next version: %v", err)
|
||||
}
|
||||
nextVersion := strings.TrimSpace(string(output))
|
||||
return nextVersion
|
||||
}
|
Loading…
Reference in a new issue