Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisd8088 authored Oct 29, 2024
2 parents c609376 + f7bd4fd commit 51dd7eb
Show file tree
Hide file tree
Showing 26 changed files with 531 additions and 194 deletions.
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ jobs:
shell: bash
- run: set GOPATH=%HOME%\go
- run: choco install -y InnoSetup
- run: choco install -y strawberryperl
- run: make man
shell: bash
- run: GOPATH="$HOME/go" PATH="$HOME/go/bin:$PATH" go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ jobs:
shell: bash
- run: set GOPATH=%HOME%\go
- run: choco install -y InnoSetup
- run: choco install -y strawberryperl
- run: choco install -y zip
- run: choco install -y jq
- run: GOPATH="$HOME/go" PATH="$HOME/go/bin:$PATH" go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest
Expand Down
2 changes: 1 addition & 1 deletion commands/command_fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func fetchCommand(cmd *cobra.Command, args []string) {
}

if len(args) > 1 {
refShas := make([]string, len(refs))
refShas := make([]string, 0, len(refs))
for _, ref := range refs {
refShas = append(refShas, ref.Sha)
}
Expand Down
2 changes: 1 addition & 1 deletion commands/command_migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ func getRemoteRefs(l *tasklog.Logger) (map[string][]*git.Ref, error) {
if migrateSkipFetch {
refsForRemote, err = git.CachedRemoteRefs(remote)
} else {
refsForRemote, err = git.RemoteRefs(remote)
refsForRemote, err = git.RemoteRefs(remote, true)
}

if err != nil {
Expand Down
10 changes: 6 additions & 4 deletions commands/command_prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ func pruneTaskGetRetainedWorktree(gitscanner *lfs.GitScanner, fetchconf lfs.Fetc

// Retain other worktree HEADs too
// Working copy, branch & maybe commit is different but repo is shared
allWorktrees, err := git.GetAllWorkTrees(cfg.LocalGitStorageDir())
allWorktrees, err := git.GetAllWorktrees(cfg.LocalGitStorageDir())
if err != nil {
errorChan <- err
return
Expand Down Expand Up @@ -536,9 +536,11 @@ func pruneTaskGetRetainedWorktree(gitscanner *lfs.GitScanner, fetchconf lfs.Fetc
go pruneTaskGetRetainedAtRef(gitscanner, worktree.Ref.Sha, retainChan, errorChan, waitg, sem)
}

// Always scan the index of the worktree
waitg.Add(1)
go pruneTaskGetRetainedIndex(gitscanner, worktree.Ref.Sha, worktree.Dir, retainChan, errorChan, waitg, sem)
if !worktree.Prunable {
// Always scan the index of the worktree if it exists
waitg.Add(1)
go pruneTaskGetRetainedIndex(gitscanner, worktree.Ref.Sha, worktree.Dir, retainChan, errorChan, waitg, sem)
}
}
}

Expand Down
151 changes: 138 additions & 13 deletions git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -902,17 +902,114 @@ func GitCommonDir() (string, error) {
return tools.CanonicalizePath(path, false)
}

// A git worktree (ref + path)
// A git worktree (ref + path + flags)
type Worktree struct {
Ref Ref
Dir string
Ref Ref
Dir string
Prunable bool
}

// GetAllWorkTrees returns the refs that all worktrees are using as HEADs plus the worktree's path.
// This returns all worktrees plus the master working copy, and works even if
// GetAllWorktrees returns the refs that all worktrees are using as HEADs plus the worktree's path.
// This returns all worktrees plus the main working copy, and works even if
// working dir is actually in a worktree right now
// Pass in the git storage dir (parent of 'objects') to work from
func GetAllWorkTrees(storageDir string) ([]*Worktree, error) {
//
// Pass in the git storage dir (parent of 'objects') to work from, in case
// we need to fall back to reading the worktree files directly.
func GetAllWorktrees(storageDir string) ([]*Worktree, error) {
// Versions before 2.7.0 don't support "git-worktree list", and
// those before 2.36.0 don't support the "-z" option, so in these
// cases we fall back to reading the .git/worktrees directory entries
// and then reading the current worktree's HEAD ref. This requires
// the contents of .git/worktrees/*/gitdir files to be absolute paths,
// which is only true for Git versions prior to 2.48.0.
if !IsGitVersionAtLeast("2.36.0") {
return getAllWorktreesFromGitDir(storageDir)
}

cmd, err := gitNoLFS(
"worktree",
"list",
"--porcelain",
"-z", // handle special chars in filenames
)
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to find `git worktree`: %v", err))
}

stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to open output pipe to `git worktree`: %v", err))
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to open error pipe to `git worktree`: %v", err))
}

if err := cmd.Start(); err != nil {
return nil, errors.New(tr.Tr.Get("failed to start `git worktree`: %v", err))
}

scanner := bufio.NewScanner(stdout)
scanner.Split(tools.SplitOnNul)
var dir string
var ref *Ref
var prunable bool
var worktrees []*Worktree
for scanner.Scan() {
line := scanner.Text()

if len(line) == 0 {
if len(dir) > 0 && ref != nil && len(ref.Sha) > 0 {
worktrees = append(worktrees, &Worktree{
Ref: *ref,
Dir: dir,
Prunable: prunable,
})
}
dir = ""
ref = nil
continue
}

parts := strings.SplitN(scanner.Text(), " ", 2)

// We ignore other attributes such as "locked" for now.
switch parts[0] {
case "worktree":
if len(parts) == 2 && len(dir) == 0 {
dir = filepath.Clean(parts[1])
ref = &Ref{Type: RefTypeOther}
prunable = false
}
case "HEAD":
if len(parts) == 2 && ref != nil {
ref.Sha = parts[1]
ref.Name = parts[1]
}
case "branch":
if len(parts) == 2 && ref != nil && len(ref.Sha) > 0 {
ref = ParseRef(parts[1], ref.Sha)
}
case "bare":
// We ignore bare worktrees.
dir = ""
ref = nil
case "prunable":
prunable = true
}
}

// We assume any error output will be short and won't block
// command completion if it isn't drained by a separate goroutine.
msg, _ := io.ReadAll(stderr)
if err := cmd.Wait(); err != nil {
return nil, errors.New(tr.Tr.Get("error in `git worktree`: %v: %s", err, msg))
}

return worktrees, nil
}

func getAllWorktreesFromGitDir(storageDir string) ([]*Worktree, error) {
worktreesdir := filepath.Join(storageDir, "worktrees")
dirf, err := os.Open(worktreesdir)
if err != nil && !os.IsNotExist(err) {
Expand Down Expand Up @@ -947,7 +1044,22 @@ func GetAllWorkTrees(storageDir string) ([]*Worktree, error) {
continue
}

worktrees = append(worktrees, &Worktree{*ref, filepath.Dir(dir)})
// Check if the worktree exists.
dir = filepath.Dir(dir)
var prunable bool
if _, err := os.Stat(dir); err != nil {
if os.IsNotExist(err) {
prunable = true
} else {
tracerx.Printf("Error checking worktree directory %s: %v", dir, err)
}
}

worktrees = append(worktrees, &Worktree{
Ref: *ref,
Dir: dir,
Prunable: prunable,
})
}
}
}
Expand All @@ -961,7 +1073,11 @@ func GetAllWorkTrees(storageDir string) ([]*Worktree, error) {
if err == nil {
dir, err := RootDir()
if err == nil {
worktrees = append(worktrees, &Worktree{*ref, dir})
worktrees = append(worktrees, &Worktree{
Ref: *ref,
Dir: dir,
Prunable: false,
})
} else { // ok if not exists, probably bare repo
tracerx.Printf("Error getting toplevel for main checkout, skipping: %v", err)
}
Expand Down Expand Up @@ -1277,11 +1393,17 @@ func Fetch(remotes ...string) error {
return err
}

// RemoteRefs returns a list of branches & tags for a remote by actually
// accessing the remote via git ls-remote.
func RemoteRefs(remoteName string) ([]*Ref, error) {
// RemoteRefs returns a list of branches and, optionally, tags for a remote
// by actually accessing the remote via git ls-remote.
func RemoteRefs(remoteName string, withTags bool) ([]*Ref, error) {
var ret []*Ref
cmd, err := gitNoLFS("ls-remote", "--heads", "--tags", "-q", remoteName)
args := []string{"ls-remote", "--heads", "-q"}
if withTags {
args = append(args, "--tags")
}
args = append(args, remoteName)

cmd, err := gitNoLFS(args...)
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to find `git ls-remote`: %v", err))
}
Expand All @@ -1302,6 +1424,9 @@ func RemoteRefs(remoteName string) ([]*Ref, error) {

typ := RefTypeRemoteBranch
if ns == "tags" {
if !withTags {
return nil, errors.New(tr.Tr.Get("unexpected tag returned by `git ls-remote --heads`: %s %s", name, sha))
}
typ = RefTypeRemoteTag
}
ret = append(ret, &Ref{name, typ, sha})
Expand Down
80 changes: 68 additions & 12 deletions git/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ func TestResolveEmptyCurrentRef(t *testing.T) {
assert.NotEqual(t, nil, err)
}

func TestWorkTrees(t *testing.T) {
func TestWorktrees(t *testing.T) {
// Only git 2.5+
if !IsGitVersionAtLeast("2.5.0") {
return
Expand All @@ -352,29 +352,42 @@ func TestWorkTrees(t *testing.T) {
inputs := []*test.CommitInput{
{ // 0
Files: []*test.FileInput{
{Filename: "file1.txt", Size: 20},
{Filename: "file1.txt", Size: 10},
},
Tags: []string{"tag1"},
},
{ // 1
Files: []*test.FileInput{
{Filename: "file1.txt", Size: 20},
},
},
{ // 2
NewBranch: "branch2",
Files: []*test.FileInput{
{Filename: "file1.txt", Size: 25},
},
},
{ // 2
{ // 3
NewBranch: "branch3",
ParentBranches: []string{"master"}, // back on master
Files: []*test.FileInput{
{Filename: "file1.txt", Size: 30},
},
},
{ // 3
{ // 4
NewBranch: "branch4",
ParentBranches: []string{"master"}, // back on master
Files: []*test.FileInput{
{Filename: "file1.txt", Size: 40},
},
},
{ // 5
NewBranch: "branch5",
ParentBranches: []string{"master"}, // back on master
Files: []*test.FileInput{
{Filename: "file1.txt", Size: 50},
},
},
}
outputs := repo.AddCommits(inputs)
// Checkout master again otherwise can't create a worktree from branch4 if we're on it here
Expand All @@ -383,35 +396,60 @@ func TestWorkTrees(t *testing.T) {
// We can create worktrees as subfolders for convenience
// Each one is checked out to a different branch
// Note that we *won't* create one for branch3
test.RunGitCommand(t, true, "worktree", "add", "tag1_wt", "tag1")
test.RunGitCommand(t, true, "worktree", "add", "branch2_wt", "branch2")
test.RunGitCommand(t, true, "worktree", "add", "branch4_wt", "branch4")
test.RunGitCommand(t, true, "worktree", "add", "branch5_wt", "branch5")

worktrees, err := GetAllWorkTrees(filepath.Join(repo.Path, ".git"))
assert.Equal(t, nil, err)
os.RemoveAll(filepath.Join(repoDir, "branch5_wt"))

worktrees, err := GetAllWorktrees(filepath.Join(repo.Path, ".git"))
assert.NoError(t, err)
expectedWorktrees := []*Worktree{
{
Ref: Ref{
Name: outputs[0].Sha,
Type: RefTypeOther,
Sha: outputs[0].Sha,
},
Dir: filepath.Join(repoDir, "tag1_wt"),
Prunable: false,
},
{
Ref: Ref{
Name: "master",
Type: RefTypeLocalBranch,
Sha: outputs[0].Sha,
Sha: outputs[1].Sha,
},
Dir: repoDir,
Dir: repoDir,
Prunable: false,
},
{
Ref: Ref{
Name: "branch2",
Type: RefTypeLocalBranch,
Sha: outputs[1].Sha,
Sha: outputs[2].Sha,
},
Dir: filepath.Join(repoDir, "branch2_wt"),
Dir: filepath.Join(repoDir, "branch2_wt"),
Prunable: false,
},
{
Ref: Ref{
Name: "branch4",
Type: RefTypeLocalBranch,
Sha: outputs[3].Sha,
Sha: outputs[4].Sha,
},
Dir: filepath.Join(repoDir, "branch4_wt"),
Dir: filepath.Join(repoDir, "branch4_wt"),
Prunable: false,
},
{
Ref: Ref{
Name: "branch5",
Type: RefTypeLocalBranch,
Sha: outputs[5].Sha,
},
Dir: filepath.Join(repoDir, "branch5_wt"),
Prunable: true,
},
}
// Need to sort for consistent comparison
Expand All @@ -420,6 +458,24 @@ func TestWorkTrees(t *testing.T) {
assert.Equal(t, expectedWorktrees, worktrees, "Worktrees should be correct")
}

func TestWorktreesBareRepo(t *testing.T) {
// Only git 2.5+
if !IsGitVersionAtLeast("2.5.0") {
return
}

repo := test.NewBareRepo(t)
repo.Pushd()
defer func() {
repo.Popd()
repo.Cleanup()
}()

worktrees, err := GetAllWorktrees(repo.Path)
assert.NoError(t, err)
assert.Nil(t, worktrees)
}

func TestVersionCompare(t *testing.T) {
assert.True(t, IsVersionAtLeast("2.6.0", "2.6.0"))
assert.True(t, IsVersionAtLeast("2.6.0", "2.6"))
Expand Down
Loading

0 comments on commit 51dd7eb

Please sign in to comment.