Skip to content
Merged
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
28 changes: 28 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: Go

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.24'

- name: Build
run: go build -v ./...

- name: Test
run: go test -v ./...
49 changes: 49 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: release

on:
push:
# run only against tags
tags:
- "*"

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: git fetch --force --tags
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ">=1.22.0"
cache: true

- run: go generate -v ./...
- run: go vet -v ./...
- run: go test -v ./...

# https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63
- run: CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o diffs-cli_${{ github.ref_name }}_darwin_amd64 .
- run: CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o diffs-cli_${{ github.ref_name }}_darwin_arm64 .
- run: CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -o diffs-cli_${{ github.ref_name }}_linux_386 .
- run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o diffs-cli_${{ github.ref_name }}_linux_amd64 .
- run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o diffs-cli_${{ github.ref_name }}_linux_arm64 .

# create checksums.txt
- run: shasum -a 256 diffs-cli_* > checksums.txt

- name: Create a Release in a GitHub Action
uses: softprops/action-gh-release@v2
with:
files: |
diffs-cli_${{ github.ref_name }}_darwin_amd64
diffs-cli_${{ github.ref_name }}_darwin_arm64
diffs-cli_${{ github.ref_name }}_linux_386
diffs-cli_${{ github.ref_name }}_linux_amd64
diffs-cli_${{ github.ref_name }}_linux_arm64
checksums.txt
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*.dll
*.so
*.dylib
diffs-cli

# Test binary, built with `go test -c`
*.test
Expand Down
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
# diffs-cli
Diffs CLI

A tiny Go program that shows git diffs in the current directory and all git repositories in subdirectories.

## Usage

```bash
# Build
go build -o diffs-cli .

# Run (default port 8080, current directory)
./diffs-cli

# Run on custom port
./diffs-cli -port 9000

# Scan a different directory
./diffs-cli -C /path/to/workspace
```

Then open http://localhost:8080 (or your custom port) in a browser to view the diffs.
25 changes: 25 additions & 0 deletions diffs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
Comment thread
alexec marked this conversation as resolved.
<html>
<head>
<meta charset="UTF-8">
<title>Diffs</title>
</head>
<body>
<h1>Git Diffs</h1>
<pre id="diff">Loading diffs...</pre>
<script>
fetch('/', {
headers: {
'Accept': 'text/x-diff'
}
})
.then(response => response.text())
.then(data => {
document.getElementById('diff').textContent = data || 'No changes';
})
.catch(error => {
document.getElementById('diff').textContent = 'Error loading diffs: ' + error;
});
</script>
</body>
</html>
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/kitproj/diffs-cli

go 1.24.4
90 changes: 90 additions & 0 deletions handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package main

import (
"context"
_ "embed"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)

//go:embed diffs.html
var diffsHTML []byte

func diffsHandler(w http.ResponseWriter, r *http.Request) {
accept := r.Header.Get("Accept")

if strings.Contains(accept, "text/x-diff") {
serveDiffsText(w, r)
} else {
serveDiffsHTML(w, r)
}
}

func serveDiffsHTML(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write(diffsHTML)
}

func findGitRepos(root string) ([]string, error) {
var repos []string

err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}

if info.IsDir() && info.Name() == ".git" {
repoPath := filepath.Dir(path)
repos = append(repos, repoPath)
return filepath.SkipDir
}

return nil
})

return repos, err
}

func serveDiffsText(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()

writer := &maxSizeWriter{Writer: w, maxSize: 5 * 1024 * 1024}

repos, err := findGitRepos(".")
if err != nil {
http.Error(w, "Failed to find git repositories: "+err.Error(), http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "text/x-diff; charset=utf-8")

for _, repoPath := range repos {
relPath, _ := filepath.Rel(".", repoPath)
if relPath == "." {
relPath = ""
}

repoName := relPath
if repoName == "" {
repoName = filepath.Base(repoPath)
}

cmd := exec.CommandContext(ctx, "bash", "-c", `
git diff --src-prefix=a/`+repoName+`/ --dst-prefix=b/`+repoName+`/ HEAD
git ls-files --others --exclude-standard | while IFS= read -r file; do
if [ -n "$file" ]; then
git diff --no-index --src-prefix=a/`+repoName+`/ --dst-prefix=b/`+repoName+`/ /dev/null "$file" 2>/dev/null || true
fi
done
`)
cmd.Dir = repoPath
cmd.Stdout = writer
cmd.Stderr = writer
_ = cmd.Run()
}
}
Loading
Loading