mirror of
https://github.com/vee1e/gittuf.git
synced 2026-09-01 18:27:06 +00:00
Worktree resolution chopped ".git" off the resolved GIT_DIR path, which only works for the standard <repo>/.git layout. This breaks repositories created with --separate-git-dir, misclassifies linked worktrees as bare (so file restoration was silently skipped), and misclassifies bare repositories named foo.git as non-bare. Linked worktrees could not even be loaded because ensureNoCompatObjectFormat expects a config file that only exists in the repository's common Git directory. - add Repository.GetWorktree() resolving via $GIT_DIR/gitdir, core.worktree, load-time discovery, then the standard layout; validated so stale or malformed records fall through cleanly - determine bareness via rev-parse --is-bare-repository instead of the GIT_DIR name, exposed through the ErrNoWorktree sentinel - read configuration from the common directory via $GIT_DIR/commondir - route Status(), RestoreWorktree(), and post-propagation restore through GetWorktree() Fixes #1006 Signed-off-by: lakshit verma <vermalucky2004@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, err := r.GetWorktree()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to check status of repository: %w", err)
|
|
}
|
|
|
|
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
|
|
}
|