diff --git a/apps/ci-os/packages/default.nix b/apps/ci-os/packages/default.nix index 1c69fd7..97090cb 100644 --- a/apps/ci-os/packages/default.nix +++ b/apps/ci-os/packages/default.nix @@ -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; }; diff --git a/apps/ci-os/packages/merge-diff/default.nix b/apps/ci-os/packages/merge-diff/default.nix index f137aa4..c1b6dd7 100644 --- a/apps/ci-os/packages/merge-diff/default.nix +++ b/apps/ci-os/packages/merge-diff/default.nix @@ -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 -d " - 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 = [ "." ]; } \ No newline at end of file diff --git a/apps/ci-os/packages/merge-diff/src/go.mod b/apps/ci-os/packages/merge-diff/src/go.mod new file mode 100644 index 0000000..7c2dc90 --- /dev/null +++ b/apps/ci-os/packages/merge-diff/src/go.mod @@ -0,0 +1,3 @@ +module code.252.no/tommy/merge-diff + +go 1.22.6 diff --git a/apps/ci-os/packages/merge-diff/src/main.go b/apps/ci-os/packages/merge-diff/src/main.go new file mode 100644 index 0000000..2f8b3dd --- /dev/null +++ b/apps/ci-os/packages/merge-diff/src/main.go @@ -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 +}