mirror of
https://github.com/vee1e/gittuf.git
synced 2026-09-01 10:18:18 +00:00
LoadRepository did os.Chdir(repositoryPath) to run 'git rev-parse --git-dir', and Status/RestoreWorktree/tree-restore did the same to run worktree-relative commands. os.Chdir is process-global; concurrent LoadRepository calls for different paths could each resolve the wrong gitDirPath. Adds executor.withDir(dir) which sets cmd.Dir. LoadRepository now runs 'git rev-parse --absolute-git-dir' with cmd.Dir set to the repository path (and EvalSymlinks the result to match prior behaviour). The worktree commands use withDir(worktree). TestLoadRepositoryConcurrent runs 20 pairs of LoadRepository for two distinct repos in parallel under -race and asserts each gets its own gitDirPath. Signed-off-by: Andrew Nesbitt <andrewnez@gmail.com>
157 lines
3.9 KiB
Go
157 lines
3.9 KiB
Go
// Copyright The gittuf Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package gitinterface
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// See https://git-scm.com/docs/git-status#_porcelain_format_version_1.
|
|
|
|
var (
|
|
ErrInvalidStatusCodeLength = errors.New("status code string must be of length 1")
|
|
ErrInvalidStatusCode = errors.New("status code string is unrecognized")
|
|
)
|
|
|
|
type StatusCode uint
|
|
|
|
const (
|
|
StatusCodeUnmodified StatusCode = iota + 1 // we use 0 as error code
|
|
StatusCodeModified
|
|
StatusCodeTypeChanged
|
|
StatusCodeAdded
|
|
StatusCodeDeleted
|
|
StatusCodeRenamed
|
|
StatusCodeCopied
|
|
StatusCodeUpdatedUnmerged
|
|
StatusCodeUntracked
|
|
StatusCodeIgnored
|
|
)
|
|
|
|
func (s StatusCode) String() string {
|
|
switch s {
|
|
case StatusCodeUnmodified:
|
|
return " " // is this actually a space or empty string?
|
|
case StatusCodeModified:
|
|
return "M"
|
|
case StatusCodeTypeChanged:
|
|
return "T"
|
|
case StatusCodeAdded:
|
|
return "A"
|
|
case StatusCodeDeleted:
|
|
return "D"
|
|
case StatusCodeRenamed:
|
|
return "R"
|
|
case StatusCodeCopied:
|
|
return "C"
|
|
case StatusCodeUpdatedUnmerged:
|
|
return "U"
|
|
case StatusCodeUntracked:
|
|
return "?"
|
|
case StatusCodeIgnored:
|
|
return "!"
|
|
default:
|
|
return "invalid-code"
|
|
}
|
|
}
|
|
|
|
func NewStatusCodeFromByte(s byte) (StatusCode, error) {
|
|
switch s {
|
|
case ' ':
|
|
return StatusCodeUnmodified, nil
|
|
case 'M':
|
|
return StatusCodeModified, nil
|
|
case 'T':
|
|
return StatusCodeTypeChanged, nil
|
|
case 'A':
|
|
return StatusCodeAdded, nil
|
|
case 'D':
|
|
return StatusCodeDeleted, nil
|
|
case 'R':
|
|
return StatusCodeRenamed, nil
|
|
case 'C':
|
|
return StatusCodeCopied, nil
|
|
case 'U':
|
|
return StatusCodeUpdatedUnmerged, nil
|
|
case '?':
|
|
return StatusCodeUntracked, nil
|
|
case '!':
|
|
return StatusCodeIgnored, nil
|
|
default:
|
|
return 0, ErrInvalidStatusCode
|
|
}
|
|
}
|
|
|
|
type FileStatus struct {
|
|
X StatusCode
|
|
Y StatusCode
|
|
}
|
|
|
|
func (f *FileStatus) Untracked() bool {
|
|
return f.X == StatusCodeUntracked || f.Y == StatusCodeUntracked
|
|
}
|
|
|
|
func (r *Repository) Status() (map[string]FileStatus, error) {
|
|
worktree := r.gitDirPath
|
|
if !r.IsBare() {
|
|
worktree = strings.TrimSuffix(worktree, ".git") // TODO: this doesn't support detached git dir
|
|
}
|
|
|
|
output, err := r.executor("status", "--porcelain=1", "-z", "--untracked-files=all", "--ignored").withDir(worktree).executeString()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to check status of repository: %w", err)
|
|
}
|
|
|
|
statuses := map[string]FileStatus{}
|
|
|
|
// `git status --porcelain=1 -z` emits NUL-separated tokens.
|
|
// For rename/copy records, the source path is emitted as an additional
|
|
// token after the main status token.
|
|
tokens := strings.Split(output, string('\000'))
|
|
for i := 0; i < len(tokens); i++ {
|
|
token := tokens[i]
|
|
if len(token) == 0 {
|
|
continue
|
|
}
|
|
|
|
// first two characters are status codes, find the corresponding
|
|
// statuses
|
|
xb := token[0]
|
|
yb := token[1]
|
|
// Note: we identify the status after inspecting the path so we can
|
|
// provide better error messages
|
|
|
|
// then, we have a single space followed by the path, ignore space and
|
|
// read in the rest as the filepath
|
|
filePath := strings.TrimSpace(token[2:])
|
|
|
|
xStatus, err := NewStatusCodeFromByte(xb)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse status code '%c' for path '%s': %w", xb, filePath, err)
|
|
}
|
|
|
|
yStatus, err := NewStatusCodeFromByte(yb)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse status code '%c' for path '%s': %w", yb, filePath, err)
|
|
}
|
|
|
|
status := FileStatus{X: xStatus, Y: yStatus}
|
|
|
|
statuses[filePath] = status
|
|
|
|
// After splitting on NUL, rename/copy records have an additional token
|
|
// for the source path immediately after the main token.
|
|
if xStatus == StatusCodeRenamed || xStatus == StatusCodeCopied ||
|
|
yStatus == StatusCodeRenamed || yStatus == StatusCodeCopied {
|
|
if i+1 >= len(tokens) || len(tokens[i+1]) == 0 {
|
|
return nil, fmt.Errorf("unable to parse rename/copy status for path '%s': missing source path", filePath)
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
|
|
return statuses, nil
|
|
}
|