diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f4427e8..e0d3b51 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,8 +11,15 @@ Please note that this project is released with a [Contributor Code of Conduct][c ## Submitting a pull request 1. [Fork][fork] and clone the repository -2. Configure and install the dependencies: `script/bootstrap` -3. Make sure the tests pass on your machine: `make test` + + + +2. Configure and install the dependencies + - On Unix-like machines: `script/bootstrap` + - On Windows: `script/bootstrap.ps1` (requires PowerShell 7+) +3. Make sure the tests pass on your machine + - On Unix-like machines: `make test` + - On Windows machines: `make -f Makefile.win test` (because there's a different Makefile when building on Windows) 4. Create a new branch: `git checkout -b my-branch-name` 5. Make your change, add tests, and make sure the tests still pass 6. Push to your fork and [submit a pull request][pr] diff --git a/Makefile.win b/Makefile.win new file mode 100644 index 0000000..b772251 --- /dev/null +++ b/Makefile.win @@ -0,0 +1,57 @@ +# Windows-specific Makefile for git-sizer designed for PowerShell + +PACKAGE := github.com/github/git-sizer +GO111MODULES := 1 + +# Use the project's go wrapper script via the -File parameter to avoid loading your profile +GOSCRIPT := $(CURDIR)/script/go.ps1 +GO := pwsh.exe -NoProfile -ExecutionPolicy Bypass -File $(GOSCRIPT) + +# Get the build version from git using try/catch instead of "||" +BUILD_VERSION := $(shell pwsh.exe -NoProfile -ExecutionPolicy Bypass -Command "try { git describe --tags --always --dirty 2>$$null } catch { Write-Output 'unknown' }") +LDFLAGS := -X github.com/github/git-sizer/main.BuildVersion=$(BUILD_VERSION) +GOFLAGS := -mod=readonly + +ifdef USE_ISATTY +GOFLAGS := $(GOFLAGS) --tags isatty +endif + +# Find all Go source files +GO_SRC_FILES := $(shell powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-ChildItem -Path . -Filter *.go -Recurse | Select-Object -ExpandProperty FullName") + +# Define common PowerShell command +PWSH := @powershell -NoProfile -ExecutionPolicy Bypass -Command + +# Default target +all: bin/git-sizer.exe + +# Main binary target - depend on all Go source files +bin/git-sizer.exe: $(GO_SRC_FILES) + $(PWSH) "if (-not (Test-Path bin)) { New-Item -ItemType Directory -Path bin | Out-Null }" + $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -a -o .\bin\git-sizer.exe . + +# Test target - explicitly run the build first to ensure binary is up to date +test: + @$(MAKE) -f Makefile.win bin/git-sizer.exe + @$(MAKE) -f Makefile.win gotest + +# Run go tests +gotest: + $(GO) test -timeout 60s $(GOFLAGS) -ldflags "$(LDFLAGS)" ./... + +# Clean up builds +clean: + $(PWSH) "if (Test-Path bin) { Remove-Item -Recurse -Force bin }" + +# Help target +help: + $(PWSH) "Write-Host 'Windows Makefile for git-sizer' -ForegroundColor Cyan" + $(PWSH) "Write-Host ''" + $(PWSH) "Write-Host 'Targets:' -ForegroundColor Green" + $(PWSH) "Write-Host ' all - Build git-sizer (default)'" + $(PWSH) "Write-Host ' test - Run tests'" + $(PWSH) "Write-Host ' clean - Clean build artifacts'" + $(PWSH) "Write-Host ''" + $(PWSH) "Write-Host 'Example usage:' -ForegroundColor Green" + $(PWSH) "Write-Host ' make -f Makefile.win'" + $(PWSH) "Write-Host ' make -f Makefile.win test'" diff --git a/git-sizer.go b/git-sizer.go index 1ef9812..fe5876b 100644 --- a/git-sizer.go +++ b/git-sizer.go @@ -331,6 +331,19 @@ func mainImplementation(ctx context.Context, stdout, stderr io.Writer, args []st return fmt.Errorf("error scanning repository: %w", err) } + // Calculate the actual size of the .git directory. + gitDir, err := repo.GitDir() + if err != nil { + return fmt.Errorf("error getting Git directory path: %w", err) + } + + gitDirSize, err := sizes.CalculateGitDirSize(gitDir) + if err != nil { + return fmt.Errorf("error calculating Git directory size: %w", err) + } + + historySize.GitDirSize = gitDirSize + if jsonOutput { var j []byte var err error diff --git a/git/git.go b/git/git.go index 096ce81..5fc8f34 100644 --- a/git/git.go +++ b/git/git.go @@ -150,8 +150,11 @@ func (repo *Repository) GitCommand(callerArgs ...string) *exec.Cmd { // GitDir returns the path to `repo`'s `GIT_DIR`. It might be absolute // or it might be relative to the current directory. -func (repo *Repository) GitDir() string { - return repo.gitDir +func (repo *Repository) GitDir() (string, error) { + if repo.gitDir == "" { + return "", errors.New("gitDir is not set") + } + return repo.gitDir, nil } // GitPath returns that path of a file within the git repository, by diff --git a/script/bootstrap.ps1 b/script/bootstrap.ps1 new file mode 100644 index 0000000..7f0413a --- /dev/null +++ b/script/bootstrap.ps1 @@ -0,0 +1,18 @@ +#!/usr/bin/env pwsh + +# Exit immediately if any command fails +$ErrorActionPreference = "Stop" + +# Change directory to the parent directory of the script +Set-Location -Path (Split-Path -Parent $PSCommandPath | Split-Path -Parent) + +# Set ROOTDIR environment variable to the current directory +$env:ROOTDIR = (Get-Location).Path + +# Check if the operating system is macOS +if ($IsMacOS) { + brew bundle +} + +# Source the ensure-go-installed.ps1 script +. ./script/ensure-go-installed.ps1 \ No newline at end of file diff --git a/script/ensure-go-installed.ps1 b/script/ensure-go-installed.ps1 new file mode 100644 index 0000000..3d653c1 --- /dev/null +++ b/script/ensure-go-installed.ps1 @@ -0,0 +1,64 @@ +# This script is meant to be sourced with ROOTDIR set. + +if (-not $env:ROOTDIR) { + $env:ROOTDIR = (Resolve-Path (Join-Path $scriptDir "..")).Path +} + +# Function to check if Go is installed and at least version 1.21 +function GoOk { + $goVersionOutput = & go version 2>$null + if ($goVersionOutput) { + $goVersion = $goVersionOutput -match 'go(\d+)\.(\d+)' | Out-Null + $majorVersion = [int]$Matches[1] + $minorVersion = [int]$Matches[2] + return ($majorVersion -eq 1 -and $minorVersion -ge 21) + } + return $false +} + +# Function to set up a local Go installation if available +function SetUpVendoredGo { + $GO_VERSION = "go1.23.7" + $VENDORED_GOROOT = Join-Path -Path $env:ROOTDIR -ChildPath "vendor/$GO_VERSION/go" + if (Test-Path -Path "$VENDORED_GOROOT/bin/go") { + $env:GOROOT = $VENDORED_GOROOT + $env:PATH = "$env:GOROOT/bin;$env:PATH" + } +} + +# Function to check if Make is installed and install it if needed +function EnsureMakeInstalled { + $makeInstalled = $null -ne (Get-Command "make" -ErrorAction SilentlyContinue) + if (-not $makeInstalled) { + #Write-Host "Installing Make using winget..." + winget install --no-upgrade --nowarn -e --id GnuWin32.Make + if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne 0x8A150061) { + Write-Error "Failed to install Make. Please install it manually. Exit code: $LASTEXITCODE" + } + # Refresh PATH to include the newly installed Make + $env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH", "User") + } + + # Add GnuWin32 bin directory directly to the PATH + $gnuWin32Path = "C:\Program Files (x86)\GnuWin32\bin" + if (Test-Path -Path $gnuWin32Path) { + $env:PATH = "$gnuWin32Path;$env:PATH" + } else { + Write-Host "Couldn't find GnuWin32 bin directory at the expected location." + # Also refresh PATH from environment variables as a fallback + $env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH", "User") + } +} + +SetUpVendoredGo + +if (-not (GoOk)) { + & ./script/install-vendored-go >$null + if ($LASTEXITCODE -ne 0) { + exit 1 + } + SetUpVendoredGo +} + +# Ensure Make is installed +EnsureMakeInstalled diff --git a/script/go.ps1 b/script/go.ps1 new file mode 100644 index 0000000..e2314b8 --- /dev/null +++ b/script/go.ps1 @@ -0,0 +1,21 @@ +# Ensure that script errors stop execution +$ErrorActionPreference = "Stop" + +# Determine the root directory of the project. +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$ROOTDIR = (Resolve-Path (Join-Path $scriptDir "..")).Path + +# Source the ensure-go-installed functionality. +# (This assumes you have a corresponding PowerShell version of ensure-go-installed. +# If not, you could call the bash version via bash.exe if available.) +$ensureScript = Join-Path $ROOTDIR "script\ensure-go-installed.ps1" +if (Test-Path $ensureScript) { + . $ensureScript +} else { + Write-Error "Unable to locate '$ensureScript'. Please provide a PowerShell version of ensure-go-installed." +} + +# Execute the actual 'go' command with passed arguments. +# This re-invokes the Go tool in PATH. +$goExe = "go" +& $goExe @args \ No newline at end of file diff --git a/sizes/dirsize.go b/sizes/dirsize.go new file mode 100644 index 0000000..eb9eb99 --- /dev/null +++ b/sizes/dirsize.go @@ -0,0 +1,31 @@ +package sizes + +import ( + "os" + "path/filepath" + + "github.com/github/git-sizer/counts" +) + +// CalculateGitDirSize returns the total size in bytes of the .git directory. +func CalculateGitDirSize(gitDir string) (counts.Count64, error) { + var totalSize counts.Count64 + + err := filepath.Walk(gitDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + // Only skip errors for files we cannot access. + if os.IsNotExist(err) || os.IsPermission(err) { + return nil + } + return err + } + + // Only count files, not directories. + if !info.IsDir() { + totalSize.Increment(counts.Count64(info.Size())) + } + return nil + }) + + return totalSize, err +} diff --git a/sizes/output.go b/sizes/output.go index 933cc05..0538cb7 100644 --- a/sizes/output.go +++ b/sizes/output.go @@ -279,10 +279,10 @@ func (t *Threshold) Type() string { // A `pflag.Value` that can be used as a boolean option that sets a // `Threshold` variable to a fixed value. For example, // -// pflag.Var( -// sizes.NewThresholdFlagValue(&threshold, 30), -// "critical", "only report critical statistics", -// ) +// pflag.Var( +// sizes.NewThresholdFlagValue(&threshold, 30), +// "critical", "only report critical statistics", +// ) // // adds a `--critical` flag that sets `threshold` to 30. type thresholdFlagValue struct { @@ -492,7 +492,7 @@ func (s *HistorySize) contents(refGroups []RefGroup) tableContents { return S( "", S( - "Overall repository size", + "Repository statistics", S( "Commits", I("uniqueCommitCount", "Count", @@ -521,11 +521,18 @@ func (s *HistorySize) contents(refGroups []RefGroup) tableContents { I("uniqueBlobCount", "Count", "The total number of distinct blob objects", nil, s.UniqueBlobCount, metric, "", 1.5e6), - I("uniqueBlobSize", "Total size", + I("uniqueBlobSize", "Uncompressed total size", "The total size of all distinct blob objects", nil, s.UniqueBlobSize, binary, "B", 10e9), ), + S( + "On-disk size", + I("gitDirSize", "Compressed total size", + "The actual on-disk size of the .git directory", + nil, s.GitDirSize, binary, "B", 1e9), + ), + S( "Annotated tags", I("uniqueTagCount", "Count", diff --git a/sizes/sizes.go b/sizes/sizes.go index b3de0bc..73834fd 100644 --- a/sizes/sizes.go +++ b/sizes/sizes.go @@ -210,6 +210,9 @@ type HistorySize struct { // The tree with the maximum expanded submodule count. MaxExpandedSubmoduleCountTree *Path `json:"max_expanded_submodule_count_tree,omitempty"` + + // The actual size of the .git directory on disk. + GitDirSize counts.Count64 `json:"git_dir_size"` } // Convenience function: forget `*path` if it is non-nil and overwrite