mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
chore: add gitutils package to inspect local git repo
This commit is contained in:
@@ -0,0 +1,91 @@
|
|||||||
|
package gitutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GitState contains raw information about the Git repository state.
|
||||||
|
type GitState struct {
|
||||||
|
// IsDirty indicates whether there are uncommitted changes.
|
||||||
|
IsDirty bool
|
||||||
|
// IsRepo indicates whether the directory is a Git repository.
|
||||||
|
IsRepo bool
|
||||||
|
// SHA is the full SHA-1 (40 characters) of the current commit.
|
||||||
|
SHA string
|
||||||
|
// Time is the current commit datetime.
|
||||||
|
Time time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectGitState inspects the Git repo state from the specified directory.
|
||||||
|
// If the directory is not a Git repository, returns GitState with IsRepo=false.
|
||||||
|
func InspectGitState(dir string) (GitState, error) {
|
||||||
|
state := GitState{
|
||||||
|
IsRepo: isGitRepo(dir),
|
||||||
|
}
|
||||||
|
|
||||||
|
if !state.IsRepo {
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current commit SHA.
|
||||||
|
sha, err := gitCommand(dir, "rev-parse", "HEAD")
|
||||||
|
if err != nil {
|
||||||
|
return state, fmt.Errorf("get current commit SHA: %w", err)
|
||||||
|
}
|
||||||
|
state.SHA = strings.TrimSpace(sha)
|
||||||
|
|
||||||
|
// Get the current commit timestamp.
|
||||||
|
timestamp, err := gitCommand(dir, "log", "-1", "--format=%ct")
|
||||||
|
if err != nil {
|
||||||
|
return state, fmt.Errorf("get current commit timestamp: %w", err)
|
||||||
|
}
|
||||||
|
seconds, err := strconv.ParseInt(strings.TrimSpace(timestamp), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return state, fmt.Errorf("parse current commit timestamp: %w", err)
|
||||||
|
}
|
||||||
|
state.Time = time.Unix(seconds, 0)
|
||||||
|
|
||||||
|
// Check for uncommitted changes.
|
||||||
|
status, err := gitCommand(dir, "status", "--porcelain")
|
||||||
|
if err != nil {
|
||||||
|
return state, fmt.Errorf("check git status: %w", err)
|
||||||
|
}
|
||||||
|
state.IsDirty = strings.TrimSpace(status) != ""
|
||||||
|
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isGitRepo checks if the directory is a Git repository.
|
||||||
|
func isGitRepo(dir string) bool {
|
||||||
|
_, err := gitCommand(dir, "rev-parse", "--git-dir")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// gitCommand runs a git command in the specified directory.
|
||||||
|
func gitCommand(dir string, args ...string) (string, error) {
|
||||||
|
cmd := exec.Command("git", args...)
|
||||||
|
cmd.Dir = dir
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
return "", fmt.Errorf("git command failed: %s", exitErr.Stderr)
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(output), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShortSHA returns a truncated SHA with the specified length.
|
||||||
|
// If length is negative, zero, or greater than the SHA length, it returns the full SHA.
|
||||||
|
// Returns empty string if SHA is empty.
|
||||||
|
func (s *GitState) ShortSHA(length int) string {
|
||||||
|
if length <= 0 || length > len(s.SHA) {
|
||||||
|
length = len(s.SHA)
|
||||||
|
}
|
||||||
|
return s.SHA[:length]
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package gitutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInspectGitState_NotARepo(t *testing.T) {
|
||||||
|
// Create a temporary directory that's not a git repo.
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
state, err := InspectGitState(tmpDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// If tmpDir happens to be in a git repo (e.g., parent directory), skip this test.
|
||||||
|
// This is common when running tests from within the project itself.
|
||||||
|
if state.IsRepo {
|
||||||
|
t.Skip("tmpDir is within a git repository, skipping test")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.False(t, state.IsRepo)
|
||||||
|
assert.Empty(t, state.SHA)
|
||||||
|
assert.Empty(t, state.ShortSHA(7))
|
||||||
|
assert.True(t, state.Time.IsZero()) // Should have zero time if not a repo.
|
||||||
|
assert.False(t, state.IsDirty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInspectGitState_CleanRepo(t *testing.T) {
|
||||||
|
// Create a temporary git repo.
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
initGitRepo(t, tmpDir)
|
||||||
|
|
||||||
|
createAndCommitFile(t, tmpDir, "test.txt", "test content")
|
||||||
|
|
||||||
|
state, err := InspectGitState(tmpDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.True(t, state.IsRepo)
|
||||||
|
assert.Len(t, state.SHA, 40)
|
||||||
|
assert.Equal(t, state.ShortSHA(7), state.SHA[:7])
|
||||||
|
assert.False(t, state.Time.IsZero())
|
||||||
|
assert.False(t, state.IsDirty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInspectGitState_DirtyRepo(t *testing.T) {
|
||||||
|
// Create a temporary git repo.
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
initGitRepo(t, tmpDir)
|
||||||
|
|
||||||
|
createAndCommitFile(t, tmpDir, "test.txt", "test content")
|
||||||
|
|
||||||
|
// Modify the file to make the repo dirty.
|
||||||
|
err := os.WriteFile(filepath.Join(tmpDir, "test.txt"), []byte("modified content"), 0o644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
state, err := InspectGitState(tmpDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.True(t, state.IsRepo)
|
||||||
|
assert.True(t, state.IsDirty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInspectGitState_UntrackedFiles(t *testing.T) {
|
||||||
|
// Create a temporary git repo.
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
initGitRepo(t, tmpDir)
|
||||||
|
|
||||||
|
createAndCommitFile(t, tmpDir, "test.txt", "test content")
|
||||||
|
|
||||||
|
// Add an untracked file.
|
||||||
|
err := os.WriteFile(filepath.Join(tmpDir, "untracked.txt"), []byte("untracked"), 0o644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
state, err := InspectGitState(tmpDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.True(t, state.IsRepo)
|
||||||
|
assert.True(t, state.IsDirty) // Untracked files make the repo dirty.
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGitState_ShortSHA(t *testing.T) {
|
||||||
|
state := &GitState{
|
||||||
|
SHA: "1234567890abcdef1234567890abcdef12345678",
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
length int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"short 7", 7, "1234567"},
|
||||||
|
{"short 10", 10, "1234567890"},
|
||||||
|
{"full SHA", 40, "1234567890abcdef1234567890abcdef12345678"},
|
||||||
|
{"longer than SHA", 50, "1234567890abcdef1234567890abcdef12345678"},
|
||||||
|
{"negative", -42, "1234567890abcdef1234567890abcdef12345678"},
|
||||||
|
{"zero", 0, "1234567890abcdef1234567890abcdef12345678"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := state.ShortSHA(tt.length)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGitState_ShortSHA_Empty(t *testing.T) {
|
||||||
|
state := &GitState{
|
||||||
|
SHA: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
result := state.ShortSHA(7)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions.
|
||||||
|
|
||||||
|
func initGitRepo(t *testing.T, dir string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Initialize git repo.
|
||||||
|
runGitCommand(t, dir, "init")
|
||||||
|
runGitCommand(t, dir, "config", "user.email", "test@example.com")
|
||||||
|
runGitCommand(t, dir, "config", "user.name", "Test User")
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAndCommitFile(t *testing.T, dir, filename, content string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Create file.
|
||||||
|
path := filepath.Join(dir, filename)
|
||||||
|
err := os.WriteFile(path, []byte(content), 0o644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Commit file.
|
||||||
|
runGitCommand(t, dir, "add", filename)
|
||||||
|
runGitCommand(t, dir, "commit", "-m", "Add "+filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGitCommand(t *testing.T, dir string, args ...string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
cmd := exec.Command("git", args...)
|
||||||
|
cmd.Dir = dir
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
require.NoError(t, err, "git command failed: %s", output)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user