Skip to content
Closed
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
34 changes: 34 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Release

on:
push:
tags:
- "v*"

permissions:
contents: write

jobs:
lint:
name: Release
runs-on: ubuntu-latest
steps:
- name: Setup
uses:
actions/setup-go@v4
with:
go-version: 1.21

- name: Checkout
uses: actions/checkout@v4

- name: Build releases
run: |
make releases VERSION=$GITHUB_REF_NAME

- name: Release
uses: softprops/action-gh-release@v1
with:
draft: true
files: |
releases/git-sizer-*
6 changes: 3 additions & 3 deletions docs/BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ Most people can just install a released version of `git-sizer`, [as described in

1. Make sure that you have a recent version of the [Go language toolchain](https://golang.org/doc/install) installed and that you have set `GOPATH`.

2. Get `git-sizer` using `go get`:
2. Get `git-sizer` using `go install`:

go get github.com/github/git-sizer
go install github.com/github/git-sizer@latest

This should fetch and compile the source code and write the executable file to `$GOPATH/bin/`.
This should install the executable file to `$GOPATH/bin/`.

3. Either add `$GOPATH/bin` to your `PATH`, or copy the executable file (`git-sizer` or `git-sizer.exe`) to a directory that is already in your `PATH`.

Expand Down
2 changes: 1 addition & 1 deletion git-sizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func mainImplementation(ctx context.Context, stdout, stderr io.Writer, args []st

// Try to open the repository, but it's not an error yet if this
// fails, because the user might only be asking for `--help`.
repo, repoErr := git.NewRepository(".")
repo, repoErr := git.NewRepositoryFromPath(".")

flags := pflag.NewFlagSet("git-sizer", pflag.ContinueOnError)
flags.Usage = func() {
Expand Down
103 changes: 79 additions & 24 deletions git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
Expand All @@ -15,26 +16,31 @@ type ObjectType string

// Repository represents a Git repository on disk.
type Repository struct {
path string
// gitDir is the path to the `GIT_DIR` for this repository. It
// might be absolute or it might be relative to the current
// directory.
gitDir string

// gitBin is the path of the `git` executable that should be used
// when running commands in this repository.
gitBin string
}

// smartJoin returns the path that can be described as `relPath`
// relative to `path`, given that `path` is either absolute or is
// relative to the current directory.
// smartJoin returns `relPath` if it is an absolute path. If not, it
// assumes that `relPath` is relative to `path`, so it joins them
// together and returns the result. In that case, if `path` itself is
// relative, then the return value is also relative.
func smartJoin(path, relPath string) string {
if filepath.IsAbs(relPath) {
return relPath
}
return filepath.Join(path, relPath)
}

// NewRepository creates a new repository object that can be used for
// running `git` commands within that repository.
func NewRepository(path string) (*Repository, error) {
// NewRepositoryFromGitDir creates a new `Repository` object that can
// be used for running `git` commands, given the value of `GIT_DIR`
// for the repository.
func NewRepositoryFromGitDir(gitDir string) (*Repository, error) {
// Find the `git` executable to be used:
gitBin, err := findGitBin()
if err != nil {
Expand All @@ -43,6 +49,34 @@ func NewRepository(path string) (*Repository, error) {
)
}

repo := Repository{
gitDir: gitDir,
gitBin: gitBin,
}

full, err := repo.IsFull()
if err != nil {
return nil, fmt.Errorf("determining whether the repository is a full clone: %w", err)
}
if !full {
return nil, errors.New("this appears to be a shallow clone; full clone required")
}

return &repo, nil
}

// NewRepositoryFromPath creates a new `Repository` object that can be
// used for running `git` commands within `path`. It does so by asking
// `git` what `GIT_DIR` to use. Git, in turn, bases its decision on
// the path and the environment.
func NewRepositoryFromPath(path string) (*Repository, error) {
gitBin, err := findGitBin()
if err != nil {
return nil, fmt.Errorf(
"could not find 'git' executable (is it in your PATH?): %w", err,
)
}

//nolint:gosec // `gitBin` is chosen carefully, and `path` is the
// path to the repository.
cmd := exec.Command(gitBin, "-C", path, "rev-parse", "--git-dir")
Expand All @@ -63,25 +97,28 @@ func NewRepository(path string) (*Repository, error) {
}
gitDir := smartJoin(path, string(bytes.TrimSpace(out)))

//nolint:gosec // `gitBin` is chosen carefully.
cmd = exec.Command(gitBin, "rev-parse", "--git-path", "shallow")
cmd.Dir = gitDir
out, err = cmd.Output()
return NewRepositoryFromGitDir(gitDir)
}

// IsFull returns `true` iff `repo` appears to be a full clone.
func (repo *Repository) IsFull() (bool, error) {
shallow, err := repo.GitPath("shallow")
if err != nil {
return nil, fmt.Errorf(
"could not run 'git rev-parse --git-path shallow': %w", err,
)
return false, err
}
shallow := smartJoin(gitDir, string(bytes.TrimSpace(out)))

_, err = os.Lstat(shallow)
if err == nil {
return nil, errors.New("this appears to be a shallow clone; full clone required")
return false, nil
}

return &Repository{
path: gitDir,
gitBin: gitBin,
}, nil
if !errors.Is(err, fs.ErrNotExist) {
return false, err
}

// The `shallow` file is absent, which is what we expect
// for a full clone.
return true, nil
}

func (repo *Repository) GitCommand(callerArgs ...string) *exec.Cmd {
Expand All @@ -103,15 +140,33 @@ func (repo *Repository) GitCommand(callerArgs ...string) *exec.Cmd {

cmd.Env = append(
os.Environ(),
"GIT_DIR="+repo.path,
"GIT_DIR="+repo.gitDir,
// Disable grafts when running our commands:
"GIT_GRAFT_FILE="+os.DevNull,
)

return cmd
}

// Path returns the path to `repo`.
func (repo *Repository) Path() string {
return repo.path
// 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
}

// GitPath returns that path of a file within the git repository, by
// calling `git rev-parse --git-path $relPath`. The returned path is
// relative to the current directory.
func (repo *Repository) GitPath(relPath string) (string, error) {
cmd := repo.GitCommand("rev-parse", "--git-path", relPath)
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf(
"running 'git rev-parse --git-path %s': %w", relPath, err,
)
}
// `git rev-parse --git-path` is documented to return the path
// relative to the current directory. Since we haven't changed the
// current directory, we can use it as-is:
return string(bytes.TrimSpace(out)), nil
}
33 changes: 24 additions & 9 deletions git/git_bin.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,41 @@ package git

import (
"path/filepath"
"sync"

"github.com/cli/safeexec"
)

// This variable will be used to memoize the result of `findGitBin()`,
// since its return value only depends on the environment.
var gitBinMemo struct {
once sync.Once

gitBin string
err error
}

// findGitBin finds the `git` binary in PATH that should be used by
// the rest of `git-sizer`. It uses `safeexec` to find the executable,
// because on Windows, `exec.Cmd` looks not only in PATH, but also in
// the current directory. This is a potential risk if the repository
// being scanned is hostile and non-bare because it might possibly
// contain an executable file named `git`.
func findGitBin() (string, error) {
gitBin, err := safeexec.LookPath("git")
if err != nil {
return "", err
}
gitBinMemo.once.Do(func() {
p, err := safeexec.LookPath("git")
if err != nil {
gitBinMemo.err = err
return
}

gitBin, err = filepath.Abs(gitBin)
if err != nil {
return "", err
}
p, err = filepath.Abs(p)
if err != nil {
gitBinMemo.err = err
return
}

return gitBin, nil
gitBinMemo.gitBin = p
})
return gitBinMemo.gitBin, gitBinMemo.err
}
13 changes: 9 additions & 4 deletions git_sizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -273,7 +272,10 @@ func TestRefSelections(t *testing.T) {
args := []string{"--show-refs", "--no-progress", "--json", "--json-version=2"}
args = append(args, p.args...)
cmd := exec.Command(executable, args...)
cmd.Dir = repo.Path
cmd.Env = append(
os.Environ(),
"GIT_DIR="+repo.Path,
)
var stdout bytes.Buffer
cmd.Stdout = &stdout
var stderr bytes.Buffer
Expand Down Expand Up @@ -520,7 +522,10 @@ References (included references marked with '+'):

args := append([]string{"--show-refs", "-v", "--no-progress"}, p.args...)
cmd := exec.Command(executable, args...)
cmd.Dir = repo.Path
cmd.Env = append(
os.Environ(),
"GIT_DIR="+repo.Path,
)
var stdout bytes.Buffer
cmd.Stdout = &stdout
var stderr bytes.Buffer
Expand Down Expand Up @@ -760,7 +765,7 @@ func TestSubmodule(t *testing.T) {

ctx := context.Background()

tmp, err := ioutil.TempDir("", "submodule")
tmp, err := os.MkdirTemp("", "submodule")
require.NoError(t, err, "creating temporary directory")

defer func() {
Expand Down
19 changes: 13 additions & 6 deletions internal/testutils/repoutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
Expand All @@ -21,6 +20,7 @@ import (
// TestRepo represents a git repository used for tests.
type TestRepo struct {
Path string
bare bool
}

// NewTestRepo creates and initializes a test repository in a
Expand All @@ -29,7 +29,7 @@ type TestRepo struct {
func NewTestRepo(t *testing.T, bare bool, pattern string) *TestRepo {
t.Helper()

path, err := ioutil.TempDir("", pattern)
path, err := os.MkdirTemp("", pattern)
require.NoError(t, err)

repo := TestRepo{Path: path}
Expand All @@ -38,6 +38,7 @@ func NewTestRepo(t *testing.T, bare bool, pattern string) *TestRepo {

return &TestRepo{
Path: path,
bare: bare,
}
}

Expand Down Expand Up @@ -73,7 +74,7 @@ func (repo *TestRepo) Remove(t *testing.T) {
func (repo *TestRepo) Clone(t *testing.T, pattern string) *TestRepo {
t.Helper()

path, err := ioutil.TempDir("", pattern)
path, err := os.MkdirTemp("", pattern)
require.NoError(t, err)

err = repo.GitCommand(
Expand All @@ -90,9 +91,15 @@ func (repo *TestRepo) Clone(t *testing.T, pattern string) *TestRepo {
func (repo *TestRepo) Repository(t *testing.T) *git.Repository {
t.Helper()

r, err := git.NewRepository(repo.Path)
require.NoError(t, err)
return r
if repo.bare {
r, err := git.NewRepositoryFromGitDir(repo.Path)
require.NoError(t, err)
return r
} else {
r, err := git.NewRepositoryFromPath(repo.Path)
require.NoError(t, err)
return r
}
}

// localEnvVars is a list of the variable names that should be cleared
Expand Down
6 changes: 3 additions & 3 deletions script/ensure-go-installed.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ if [ -z "$ROOTDIR" ]; then
echo 1>&2 'ensure-go-installed.sh invoked without ROOTDIR set!'
Comment thread
0352397554 marked this conversation as resolved.
fi

# Is go installed, and at least 1.16?
# Is go installed, and at least 1.21?
go_ok() {
set -- $(go version 2>/dev/null |
sed -n 's/.*go\([0-9][0-9]*\)\.\([0-9][0-9]*\).*/\1 \2/p' |
head -n 1)
[ $# -eq 2 ] && [ "$1" -eq 1 ] && [ "$2" -ge 16 ]
[ $# -eq 2 ] && [ "$1" -eq 1 ] && [ "$2" -ge 21 ]
}

# If a local go is installed, use it.
set_up_vendored_go() {
GO_VERSION=go1.16.3
GO_VERSION=go1.21.3
VENDORED_GOROOT="$ROOTDIR/vendor/$GO_VERSION/go"
if [ -x "$VENDORED_GOROOT/bin/go" ]; then
export GOROOT="$VENDORED_GOROOT"
Expand Down
Loading