mirror of https://github.com/docker/buildx.git
				
				
				
			Merge pull request #1477 from crazy-max/git-wsl
build: lookup the right git binary on WSL
This commit is contained in:
		
						commit
						64e4c19971
					
				| 
						 | 
				
			
			@ -594,10 +594,6 @@ func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt Op
 | 
			
		|||
		so.FrontendAttrs["attest:provenance"] = "mode=min,inline-only=true"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for k, v := range getGitAttributes(ctx, opt.Inputs.ContextPath, opt.Inputs.DockerfilePath) {
 | 
			
		||||
		so.FrontendAttrs[k] = v
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// set platforms
 | 
			
		||||
	if len(opt.Platforms) != 0 {
 | 
			
		||||
		pp := make([]string, len(opt.Platforms))
 | 
			
		||||
| 
						 | 
				
			
			@ -852,6 +848,10 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s
 | 
			
		|||
	for k, opt := range opt {
 | 
			
		||||
		multiDriver := len(m[k]) > 1
 | 
			
		||||
		hasMobyDriver := false
 | 
			
		||||
		gitattrs, err := getGitAttributes(ctx, opt.Inputs.ContextPath, opt.Inputs.DockerfilePath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Warn(err)
 | 
			
		||||
		}
 | 
			
		||||
		for i, np := range m[k] {
 | 
			
		||||
			node := nodes[np.driverIndex]
 | 
			
		||||
			if node.Driver.IsMobyDriver() {
 | 
			
		||||
| 
						 | 
				
			
			@ -861,6 +861,9 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s
 | 
			
		|||
			so, release, err := toSolveOpt(ctx, node, multiDriver, opt, np.bopts, configDir, w, func(name string) (io.WriteCloser, func(), error) {
 | 
			
		||||
				return docker.LoadImage(ctx, name, w)
 | 
			
		||||
			})
 | 
			
		||||
			for k, v := range gitattrs {
 | 
			
		||||
				so.FrontendAttrs[k] = v
 | 
			
		||||
			}
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										72
									
								
								build/git.go
								
								
								
								
							
							
						
						
									
										72
									
								
								build/git.go
								
								
								
								
							| 
						 | 
				
			
			@ -3,18 +3,19 @@ package build
 | 
			
		|||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/docker/buildx/util/gitutil"
 | 
			
		||||
	specs "github.com/opencontainers/image-spec/specs-go/v1"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/pkg/errors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const DockerfileLabel = "com.docker.image.source.entrypoint"
 | 
			
		||||
 | 
			
		||||
func getGitAttributes(ctx context.Context, contextPath string, dockerfilePath string) (res map[string]string) {
 | 
			
		||||
func getGitAttributes(ctx context.Context, contextPath string, dockerfilePath string) (res map[string]string, _ error) {
 | 
			
		||||
	res = make(map[string]string)
 | 
			
		||||
	if contextPath == "" {
 | 
			
		||||
		return
 | 
			
		||||
| 
						 | 
				
			
			@ -48,27 +49,50 @@ func getGitAttributes(ctx context.Context, contextPath string, dockerfilePath st
 | 
			
		|||
		wd, _ = filepath.Abs(filepath.Join(cwd, contextPath))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	gitc := gitutil.New(gitutil.WithContext(ctx), gitutil.WithWorkingDir(wd))
 | 
			
		||||
	if !gitc.IsInsideWorkTree() {
 | 
			
		||||
		logrus.Warnf("Unable to determine Git information")
 | 
			
		||||
	gitc, err := gitutil.New(gitutil.WithContext(ctx), gitutil.WithWorkingDir(wd))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if st, err := os.Stat(path.Join(wd, ".git")); err == nil && st.IsDir() {
 | 
			
		||||
			return res, errors.New("git was not found in the system. Current commit information was not captured by the build")
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var resRevision, resSource, resDockerfilePath string
 | 
			
		||||
	if !gitc.IsInsideWorkTree() {
 | 
			
		||||
		if st, err := os.Stat(path.Join(wd, ".git")); err == nil && st.IsDir() {
 | 
			
		||||
			return res, errors.New("failed to read current commit information with git rev-parse --is-inside-work-tree")
 | 
			
		||||
		}
 | 
			
		||||
		return res, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if sha, err := gitc.FullCommit(); err == nil && sha != "" {
 | 
			
		||||
		resRevision = sha
 | 
			
		||||
	if sha, err := gitc.FullCommit(); err != nil {
 | 
			
		||||
		return res, errors.Wrapf(err, "failed to get git commit")
 | 
			
		||||
	} else if sha != "" {
 | 
			
		||||
		if gitc.IsDirty() {
 | 
			
		||||
			resRevision += "-dirty"
 | 
			
		||||
			sha += "-dirty"
 | 
			
		||||
		}
 | 
			
		||||
		if setGitLabels {
 | 
			
		||||
			res["label:"+specs.AnnotationRevision] = sha
 | 
			
		||||
		}
 | 
			
		||||
		if setGitInfo {
 | 
			
		||||
			res["vcs:revision"] = sha
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if rurl, err := gitc.RemoteURL(); err == nil && rurl != "" {
 | 
			
		||||
		resSource = rurl
 | 
			
		||||
	if rurl, err := gitc.RemoteURL(); err != nil {
 | 
			
		||||
		return res, errors.Wrapf(err, "failed to get git remote url")
 | 
			
		||||
	} else if rurl != "" {
 | 
			
		||||
		if setGitLabels {
 | 
			
		||||
			res["label:"+specs.AnnotationSource] = rurl
 | 
			
		||||
		}
 | 
			
		||||
		if setGitInfo {
 | 
			
		||||
			res["vcs:source"] = rurl
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if setGitLabels {
 | 
			
		||||
		if root, err := gitc.RootDir(); err == nil && root != "" {
 | 
			
		||||
		if root, err := gitc.RootDir(); err != nil {
 | 
			
		||||
			return res, errors.Wrapf(err, "failed to get git root dir")
 | 
			
		||||
		} else if root != "" {
 | 
			
		||||
			if dockerfilePath == "" {
 | 
			
		||||
				dockerfilePath = filepath.Join(wd, "Dockerfile")
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			@ -78,32 +102,10 @@ func getGitAttributes(ctx context.Context, contextPath string, dockerfilePath st
 | 
			
		|||
			}
 | 
			
		||||
			dockerfilePath, _ = filepath.Rel(root, dockerfilePath)
 | 
			
		||||
			if !strings.HasPrefix(dockerfilePath, "..") {
 | 
			
		||||
				resDockerfilePath = dockerfilePath
 | 
			
		||||
				res["label:"+DockerfileLabel] = dockerfilePath
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if resSource != "" {
 | 
			
		||||
		if setGitLabels {
 | 
			
		||||
			res["label:"+specs.AnnotationSource] = resSource
 | 
			
		||||
		}
 | 
			
		||||
		if setGitInfo {
 | 
			
		||||
			res["vcs:source"] = resSource
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if resRevision != "" {
 | 
			
		||||
		if setGitLabels {
 | 
			
		||||
			res["label:"+specs.AnnotationRevision] = resRevision
 | 
			
		||||
		}
 | 
			
		||||
		if setGitInfo {
 | 
			
		||||
			res["vcs:revision"] = resRevision
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if resDockerfilePath != "" {
 | 
			
		||||
		if setGitLabels {
 | 
			
		||||
			res["label:"+DockerfileLabel] = resDockerfilePath
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@ package build
 | 
			
		|||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
| 
						 | 
				
			
			@ -10,21 +11,42 @@ import (
 | 
			
		|||
	"github.com/docker/buildx/util/gitutil"
 | 
			
		||||
	specs "github.com/opencontainers/image-spec/specs-go/v1"
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func setupTest(tb testing.TB) {
 | 
			
		||||
	gitutil.Mktmp(tb)
 | 
			
		||||
	gitutil.GitInit(tb)
 | 
			
		||||
 | 
			
		||||
	c, err := gitutil.New()
 | 
			
		||||
	require.NoError(tb, err)
 | 
			
		||||
	gitutil.GitInit(c, tb)
 | 
			
		||||
 | 
			
		||||
	df := []byte("FROM alpine:latest\n")
 | 
			
		||||
	assert.NoError(tb, os.WriteFile("Dockerfile", df, 0644))
 | 
			
		||||
	gitutil.GitAdd(tb, "Dockerfile")
 | 
			
		||||
	gitutil.GitCommit(tb, "initial commit")
 | 
			
		||||
 | 
			
		||||
	gitutil.GitAdd(c, tb, "Dockerfile")
 | 
			
		||||
	gitutil.GitCommit(c, tb, "initial commit")
 | 
			
		||||
	gitutil.GitSetRemote(c, tb, "git@github.com:docker/buildx.git")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetGitAttributesNotGitRepo(t *testing.T) {
 | 
			
		||||
	_, err := getGitAttributes(context.Background(), t.TempDir(), "Dockerfile")
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetGitAttributesBadGitRepo(t *testing.T) {
 | 
			
		||||
	tmp := t.TempDir()
 | 
			
		||||
	require.NoError(t, os.MkdirAll(path.Join(tmp, ".git"), 0755))
 | 
			
		||||
 | 
			
		||||
	_, err := getGitAttributes(context.Background(), tmp, "Dockerfile")
 | 
			
		||||
	assert.Error(t, err)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetGitAttributesNoContext(t *testing.T) {
 | 
			
		||||
	setupTest(t)
 | 
			
		||||
 | 
			
		||||
	gitattrs := getGitAttributes(context.Background(), "", "Dockerfile")
 | 
			
		||||
	gitattrs, err := getGitAttributes(context.Background(), "", "Dockerfile")
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Empty(t, gitattrs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -41,6 +63,7 @@ func TestGetGitAttributes(t *testing.T) {
 | 
			
		|||
			envGitInfo:   "",
 | 
			
		||||
			expected: []string{
 | 
			
		||||
				"vcs:revision",
 | 
			
		||||
				"vcs:source",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
| 
						 | 
				
			
			@ -55,6 +78,7 @@ func TestGetGitAttributes(t *testing.T) {
 | 
			
		|||
			envGitInfo:   "true",
 | 
			
		||||
			expected: []string{
 | 
			
		||||
				"vcs:revision",
 | 
			
		||||
				"vcs:source",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
| 
						 | 
				
			
			@ -64,6 +88,7 @@ func TestGetGitAttributes(t *testing.T) {
 | 
			
		|||
			expected: []string{
 | 
			
		||||
				"label:" + DockerfileLabel,
 | 
			
		||||
				"label:" + specs.AnnotationRevision,
 | 
			
		||||
				"label:" + specs.AnnotationSource,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
| 
						 | 
				
			
			@ -73,7 +98,9 @@ func TestGetGitAttributes(t *testing.T) {
 | 
			
		|||
			expected: []string{
 | 
			
		||||
				"label:" + DockerfileLabel,
 | 
			
		||||
				"label:" + specs.AnnotationRevision,
 | 
			
		||||
				"label:" + specs.AnnotationSource,
 | 
			
		||||
				"vcs:revision",
 | 
			
		||||
				"vcs:source",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -87,52 +114,42 @@ func TestGetGitAttributes(t *testing.T) {
 | 
			
		|||
			if tt.envGitInfo != "" {
 | 
			
		||||
				t.Setenv("BUILDX_GIT_INFO", tt.envGitInfo)
 | 
			
		||||
			}
 | 
			
		||||
			gitattrs := getGitAttributes(context.Background(), ".", "Dockerfile")
 | 
			
		||||
			gitattrs, err := getGitAttributes(context.Background(), ".", "Dockerfile")
 | 
			
		||||
			require.NoError(t, err)
 | 
			
		||||
			for _, e := range tt.expected {
 | 
			
		||||
				assert.Contains(t, gitattrs, e)
 | 
			
		||||
				assert.NotEmpty(t, gitattrs[e])
 | 
			
		||||
				if e == "label:"+DockerfileLabel {
 | 
			
		||||
					assert.Equal(t, "Dockerfile", gitattrs[e])
 | 
			
		||||
				} else if e == "label:"+specs.AnnotationSource || e == "vcs:source" {
 | 
			
		||||
					assert.Equal(t, "git@github.com:docker/buildx.git", gitattrs[e])
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetGitAttributesWithRemote(t *testing.T) {
 | 
			
		||||
	setupTest(t)
 | 
			
		||||
	gitutil.GitSetRemote(t, "git@github.com:docker/buildx.git")
 | 
			
		||||
 | 
			
		||||
	t.Setenv("BUILDX_GIT_LABELS", "true")
 | 
			
		||||
	gitattrs := getGitAttributes(context.Background(), ".", "Dockerfile")
 | 
			
		||||
	assert.Equal(t, 5, len(gitattrs))
 | 
			
		||||
	assert.Contains(t, gitattrs, "label:"+DockerfileLabel)
 | 
			
		||||
	assert.Equal(t, "Dockerfile", gitattrs["label:"+DockerfileLabel])
 | 
			
		||||
	assert.Contains(t, gitattrs, "label:"+specs.AnnotationRevision)
 | 
			
		||||
	assert.NotEmpty(t, gitattrs["label:"+specs.AnnotationRevision])
 | 
			
		||||
	assert.Contains(t, gitattrs, "label:"+specs.AnnotationSource)
 | 
			
		||||
	assert.Equal(t, "git@github.com:docker/buildx.git", gitattrs["label:"+specs.AnnotationSource])
 | 
			
		||||
	assert.Contains(t, gitattrs, "vcs:revision")
 | 
			
		||||
	assert.NotEmpty(t, gitattrs["vcs:revision"])
 | 
			
		||||
	assert.Contains(t, gitattrs, "vcs:source")
 | 
			
		||||
	assert.Equal(t, "git@github.com:docker/buildx.git", gitattrs["vcs:source"])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetGitAttributesDirty(t *testing.T) {
 | 
			
		||||
	setupTest(t)
 | 
			
		||||
 | 
			
		||||
	// make a change to test dirty flag
 | 
			
		||||
	df := []byte("FROM alpine:edge\n")
 | 
			
		||||
	assert.NoError(t, os.Mkdir("dir", 0755))
 | 
			
		||||
	assert.NoError(t, os.WriteFile(filepath.Join("dir", "Dockerfile"), df, 0644))
 | 
			
		||||
	require.NoError(t, os.Mkdir("dir", 0755))
 | 
			
		||||
	require.NoError(t, os.WriteFile(filepath.Join("dir", "Dockerfile"), df, 0644))
 | 
			
		||||
 | 
			
		||||
	t.Setenv("BUILDX_GIT_LABELS", "true")
 | 
			
		||||
	gitattrs := getGitAttributes(context.Background(), ".", "Dockerfile")
 | 
			
		||||
	assert.Equal(t, 3, len(gitattrs))
 | 
			
		||||
	gitattrs, _ := getGitAttributes(context.Background(), ".", "Dockerfile")
 | 
			
		||||
	assert.Equal(t, 5, len(gitattrs))
 | 
			
		||||
 | 
			
		||||
	assert.Contains(t, gitattrs, "label:"+DockerfileLabel)
 | 
			
		||||
	assert.Equal(t, "Dockerfile", gitattrs["label:"+DockerfileLabel])
 | 
			
		||||
	assert.Contains(t, gitattrs, "label:"+specs.AnnotationSource)
 | 
			
		||||
	assert.Equal(t, "git@github.com:docker/buildx.git", gitattrs["label:"+specs.AnnotationSource])
 | 
			
		||||
	assert.Contains(t, gitattrs, "label:"+specs.AnnotationRevision)
 | 
			
		||||
	assert.True(t, strings.HasSuffix(gitattrs["label:"+specs.AnnotationRevision], "-dirty"))
 | 
			
		||||
 | 
			
		||||
	assert.Contains(t, gitattrs, "vcs:source")
 | 
			
		||||
	assert.Equal(t, "git@github.com:docker/buildx.git", gitattrs["vcs:source"])
 | 
			
		||||
	assert.Contains(t, gitattrs, "vcs:revision")
 | 
			
		||||
	assert.True(t, strings.HasSuffix(gitattrs["vcs:revision"], "-dirty"))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										1
									
								
								go.mod
								
								
								
								
							| 
						 | 
				
			
			@ -17,6 +17,7 @@ require (
 | 
			
		|||
	github.com/hashicorp/go-cty-funcs v0.0.0-20200930094925-2721b1e36840
 | 
			
		||||
	github.com/hashicorp/hcl/v2 v2.8.2
 | 
			
		||||
	github.com/moby/buildkit v0.11.0-rc1.0.20221213193744-862b22d7e7cf
 | 
			
		||||
	github.com/moby/sys/mountinfo v0.6.2
 | 
			
		||||
	github.com/morikuni/aec v1.0.0
 | 
			
		||||
	github.com/opencontainers/go-digest v1.0.0
 | 
			
		||||
	github.com/opencontainers/image-spec v1.0.3-0.20220303224323-02efb9a75ee1
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										2
									
								
								go.sum
								
								
								
								
							| 
						 | 
				
			
			@ -411,6 +411,7 @@ github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8
 | 
			
		|||
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
 | 
			
		||||
github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
 | 
			
		||||
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
 | 
			
		||||
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
 | 
			
		||||
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
 | 
			
		||||
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
 | 
			
		||||
github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI=
 | 
			
		||||
| 
						 | 
				
			
			@ -780,6 +781,7 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc
 | 
			
		|||
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
 | 
			
		||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,42 @@
 | 
			
		|||
//go:build !windows
 | 
			
		||||
// +build !windows
 | 
			
		||||
 | 
			
		||||
package gitutil
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	"github.com/moby/sys/mountinfo"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func gitPath(wd string) (string, error) {
 | 
			
		||||
	// On WSL2 we need to check if the current working directory is mounted on
 | 
			
		||||
	// a Windows drive and if so, we need to use the Windows git executable.
 | 
			
		||||
	if os.Getenv("WSL_DISTRO_NAME") != "" && wd != "" {
 | 
			
		||||
		// ensure any symlinks are resolved
 | 
			
		||||
		wdPath, err := filepath.EvalSymlinks(wd)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		mi, err := mountinfo.GetMounts(mountinfo.ParentsFilter(wdPath))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		// find the longest mount point
 | 
			
		||||
		var idx, maxlen int
 | 
			
		||||
		for i := range mi {
 | 
			
		||||
			if len(mi[i].Mountpoint) > maxlen {
 | 
			
		||||
				maxlen = len(mi[i].Mountpoint)
 | 
			
		||||
				idx = i
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if mi[idx].FSType == "9p" {
 | 
			
		||||
			if p, err := exec.LookPath("git.exe"); err == nil {
 | 
			
		||||
				return p, nil
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return exec.LookPath("git")
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
package gitutil
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os/exec"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func gitPath(wd string) (string, error) {
 | 
			
		||||
	return exec.LookPath("git.exe")
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -11,8 +11,9 @@ import (
 | 
			
		|||
 | 
			
		||||
// Git represents an active git object
 | 
			
		||||
type Git struct {
 | 
			
		||||
	ctx context.Context
 | 
			
		||||
	wd  string
 | 
			
		||||
	ctx     context.Context
 | 
			
		||||
	wd      string
 | 
			
		||||
	gitpath string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Option provides a variadic option for configuring the git client.
 | 
			
		||||
| 
						 | 
				
			
			@ -33,14 +34,22 @@ func WithWorkingDir(wd string) Option {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// New initializes a new git client
 | 
			
		||||
func New(opts ...Option) *Git {
 | 
			
		||||
func New(opts ...Option) (*Git, error) {
 | 
			
		||||
	var err error
 | 
			
		||||
	c := &Git{
 | 
			
		||||
		ctx: context.Background(),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, opt := range opts {
 | 
			
		||||
		opt(c)
 | 
			
		||||
	}
 | 
			
		||||
	return c
 | 
			
		||||
 | 
			
		||||
	c.gitpath, err = gitPath(c.wd)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, errors.New("git not found in PATH")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return c, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Git) IsInsideWorkTree() bool {
 | 
			
		||||
| 
						 | 
				
			
			@ -89,16 +98,12 @@ func (c *Git) Tag() (string, error) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func (c *Git) run(args ...string) (string, error) {
 | 
			
		||||
	if _, err := exec.LookPath("git"); err != nil {
 | 
			
		||||
		return "", errors.New("git not present in PATH")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var extraArgs = []string{
 | 
			
		||||
		"-c", "log.showSignature=false",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	args = append(extraArgs, args...)
 | 
			
		||||
	cmd := exec.Command("git", args...)
 | 
			
		||||
	cmd := exec.CommandContext(c.ctx, c.gitpath, args...)
 | 
			
		||||
	if c.wd != "" {
 | 
			
		||||
		cmd.Dir = c.wd
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,9 @@ import (
 | 
			
		|||
)
 | 
			
		||||
 | 
			
		||||
func TestGit(t *testing.T) {
 | 
			
		||||
	c := New()
 | 
			
		||||
	c, err := New()
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	out, err := c.run("status")
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
	require.NotEmpty(t, out)
 | 
			
		||||
| 
						 | 
				
			
			@ -20,10 +22,12 @@ func TestGit(t *testing.T) {
 | 
			
		|||
 | 
			
		||||
func TestGitFullCommit(t *testing.T) {
 | 
			
		||||
	Mktmp(t)
 | 
			
		||||
	GitInit(t)
 | 
			
		||||
	GitCommit(t, "bar")
 | 
			
		||||
	c, err := New()
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	GitInit(c, t)
 | 
			
		||||
	GitCommit(c, t, "bar")
 | 
			
		||||
 | 
			
		||||
	c := New()
 | 
			
		||||
	out, err := c.FullCommit()
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
	require.Equal(t, 40, len(out))
 | 
			
		||||
| 
						 | 
				
			
			@ -31,10 +35,12 @@ func TestGitFullCommit(t *testing.T) {
 | 
			
		|||
 | 
			
		||||
func TestGitShortCommit(t *testing.T) {
 | 
			
		||||
	Mktmp(t)
 | 
			
		||||
	GitInit(t)
 | 
			
		||||
	GitCommit(t, "bar")
 | 
			
		||||
	c, err := New()
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	GitInit(c, t)
 | 
			
		||||
	GitCommit(c, t, "bar")
 | 
			
		||||
 | 
			
		||||
	c := New()
 | 
			
		||||
	out, err := c.ShortCommit()
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
	require.Equal(t, 7, len(out))
 | 
			
		||||
| 
						 | 
				
			
			@ -42,13 +48,15 @@ func TestGitShortCommit(t *testing.T) {
 | 
			
		|||
 | 
			
		||||
func TestGitTagsPointsAt(t *testing.T) {
 | 
			
		||||
	Mktmp(t)
 | 
			
		||||
	GitInit(t)
 | 
			
		||||
	GitCommit(t, "bar")
 | 
			
		||||
	GitTag(t, "v0.8.0")
 | 
			
		||||
	GitCommit(t, "foo")
 | 
			
		||||
	GitTag(t, "v0.9.0")
 | 
			
		||||
	c, err := New()
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	GitInit(c, t)
 | 
			
		||||
	GitCommit(c, t, "bar")
 | 
			
		||||
	GitTag(c, t, "v0.8.0")
 | 
			
		||||
	GitCommit(c, t, "foo")
 | 
			
		||||
	GitTag(c, t, "v0.9.0")
 | 
			
		||||
 | 
			
		||||
	c := New()
 | 
			
		||||
	out, err := c.clean(c.run("tag", "--points-at", "HEAD", "--sort", "-version:creatordate"))
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
	require.Equal(t, "v0.9.0", out)
 | 
			
		||||
| 
						 | 
				
			
			@ -56,13 +64,15 @@ func TestGitTagsPointsAt(t *testing.T) {
 | 
			
		|||
 | 
			
		||||
func TestGitDescribeTags(t *testing.T) {
 | 
			
		||||
	Mktmp(t)
 | 
			
		||||
	GitInit(t)
 | 
			
		||||
	GitCommit(t, "bar")
 | 
			
		||||
	GitTag(t, "v0.8.0")
 | 
			
		||||
	GitCommit(t, "foo")
 | 
			
		||||
	GitTag(t, "v0.9.0")
 | 
			
		||||
	c, err := New()
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	GitInit(c, t)
 | 
			
		||||
	GitCommit(c, t, "bar")
 | 
			
		||||
	GitTag(c, t, "v0.8.0")
 | 
			
		||||
	GitCommit(c, t, "foo")
 | 
			
		||||
	GitTag(c, t, "v0.9.0")
 | 
			
		||||
 | 
			
		||||
	c := New()
 | 
			
		||||
	out, err := c.clean(c.run("describe", "--tags", "--abbrev=0"))
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
	require.Equal(t, "v0.9.0", out)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,46 +7,46 @@ import (
 | 
			
		|||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GitInit(tb testing.TB) {
 | 
			
		||||
func GitInit(c *Git, tb testing.TB) {
 | 
			
		||||
	tb.Helper()
 | 
			
		||||
	out, err := fakeGit("init")
 | 
			
		||||
	out, err := fakeGit(c, "init")
 | 
			
		||||
	require.NoError(tb, err)
 | 
			
		||||
	require.Contains(tb, out, "Initialized empty Git repository")
 | 
			
		||||
	require.NoError(tb, err)
 | 
			
		||||
	GitCheckoutBranch(tb, "main")
 | 
			
		||||
	_, _ = fakeGit("branch", "-D", "master")
 | 
			
		||||
	GitCheckoutBranch(c, tb, "main")
 | 
			
		||||
	_, _ = fakeGit(c, "branch", "-D", "master")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GitCommit(tb testing.TB, msg string) {
 | 
			
		||||
func GitCommit(c *Git, tb testing.TB, msg string) {
 | 
			
		||||
	tb.Helper()
 | 
			
		||||
	out, err := fakeGit("commit", "--allow-empty", "-m", msg)
 | 
			
		||||
	out, err := fakeGit(c, "commit", "--allow-empty", "-m", msg)
 | 
			
		||||
	require.NoError(tb, err)
 | 
			
		||||
	require.Contains(tb, out, "main", msg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GitTag(tb testing.TB, tag string) {
 | 
			
		||||
func GitTag(c *Git, tb testing.TB, tag string) {
 | 
			
		||||
	tb.Helper()
 | 
			
		||||
	out, err := fakeGit("tag", tag)
 | 
			
		||||
	out, err := fakeGit(c, "tag", tag)
 | 
			
		||||
	require.NoError(tb, err)
 | 
			
		||||
	require.Empty(tb, out)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GitCheckoutBranch(tb testing.TB, name string) {
 | 
			
		||||
func GitCheckoutBranch(c *Git, tb testing.TB, name string) {
 | 
			
		||||
	tb.Helper()
 | 
			
		||||
	out, err := fakeGit("checkout", "-b", name)
 | 
			
		||||
	out, err := fakeGit(c, "checkout", "-b", name)
 | 
			
		||||
	require.NoError(tb, err)
 | 
			
		||||
	require.Empty(tb, out)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GitAdd(tb testing.TB, file string) {
 | 
			
		||||
func GitAdd(c *Git, tb testing.TB, file string) {
 | 
			
		||||
	tb.Helper()
 | 
			
		||||
	_, err := fakeGit("add", file)
 | 
			
		||||
	_, err := fakeGit(c, "add", file)
 | 
			
		||||
	require.NoError(tb, err)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GitSetRemote(tb testing.TB, url string) {
 | 
			
		||||
func GitSetRemote(c *Git, tb testing.TB, url string) {
 | 
			
		||||
	tb.Helper()
 | 
			
		||||
	_, err := fakeGit("remote", "add", "origin", url)
 | 
			
		||||
	_, err := fakeGit(c, "remote", "add", "origin", url)
 | 
			
		||||
	require.NoError(tb, err)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -62,7 +62,7 @@ func Mktmp(tb testing.TB) string {
 | 
			
		|||
	return folder
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func fakeGit(args ...string) (string, error) {
 | 
			
		||||
func fakeGit(c *Git, args ...string) (string, error) {
 | 
			
		||||
	allArgs := []string{
 | 
			
		||||
		"-c", "user.name=buildx",
 | 
			
		||||
		"-c", "user.email=buildx@docker.com",
 | 
			
		||||
| 
						 | 
				
			
			@ -71,6 +71,5 @@ func fakeGit(args ...string) (string, error) {
 | 
			
		|||
		"-c", "log.showSignature=false",
 | 
			
		||||
	}
 | 
			
		||||
	allArgs = append(allArgs, args...)
 | 
			
		||||
	c := New()
 | 
			
		||||
	return c.clean(c.run(allArgs...))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,202 @@
 | 
			
		|||
 | 
			
		||||
                                 Apache License
 | 
			
		||||
                           Version 2.0, January 2004
 | 
			
		||||
                        http://www.apache.org/licenses/
 | 
			
		||||
 | 
			
		||||
   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
 | 
			
		||||
 | 
			
		||||
   1. Definitions.
 | 
			
		||||
 | 
			
		||||
      "License" shall mean the terms and conditions for use, reproduction,
 | 
			
		||||
      and distribution as defined by Sections 1 through 9 of this document.
 | 
			
		||||
 | 
			
		||||
      "Licensor" shall mean the copyright owner or entity authorized by
 | 
			
		||||
      the copyright owner that is granting the License.
 | 
			
		||||
 | 
			
		||||
      "Legal Entity" shall mean the union of the acting entity and all
 | 
			
		||||
      other entities that control, are controlled by, or are under common
 | 
			
		||||
      control with that entity. For the purposes of this definition,
 | 
			
		||||
      "control" means (i) the power, direct or indirect, to cause the
 | 
			
		||||
      direction or management of such entity, whether by contract or
 | 
			
		||||
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
 | 
			
		||||
      outstanding shares, or (iii) beneficial ownership of such entity.
 | 
			
		||||
 | 
			
		||||
      "You" (or "Your") shall mean an individual or Legal Entity
 | 
			
		||||
      exercising permissions granted by this License.
 | 
			
		||||
 | 
			
		||||
      "Source" form shall mean the preferred form for making modifications,
 | 
			
		||||
      including but not limited to software source code, documentation
 | 
			
		||||
      source, and configuration files.
 | 
			
		||||
 | 
			
		||||
      "Object" form shall mean any form resulting from mechanical
 | 
			
		||||
      transformation or translation of a Source form, including but
 | 
			
		||||
      not limited to compiled object code, generated documentation,
 | 
			
		||||
      and conversions to other media types.
 | 
			
		||||
 | 
			
		||||
      "Work" shall mean the work of authorship, whether in Source or
 | 
			
		||||
      Object form, made available under the License, as indicated by a
 | 
			
		||||
      copyright notice that is included in or attached to the work
 | 
			
		||||
      (an example is provided in the Appendix below).
 | 
			
		||||
 | 
			
		||||
      "Derivative Works" shall mean any work, whether in Source or Object
 | 
			
		||||
      form, that is based on (or derived from) the Work and for which the
 | 
			
		||||
      editorial revisions, annotations, elaborations, or other modifications
 | 
			
		||||
      represent, as a whole, an original work of authorship. For the purposes
 | 
			
		||||
      of this License, Derivative Works shall not include works that remain
 | 
			
		||||
      separable from, or merely link (or bind by name) to the interfaces of,
 | 
			
		||||
      the Work and Derivative Works thereof.
 | 
			
		||||
 | 
			
		||||
      "Contribution" shall mean any work of authorship, including
 | 
			
		||||
      the original version of the Work and any modifications or additions
 | 
			
		||||
      to that Work or Derivative Works thereof, that is intentionally
 | 
			
		||||
      submitted to Licensor for inclusion in the Work by the copyright owner
 | 
			
		||||
      or by an individual or Legal Entity authorized to submit on behalf of
 | 
			
		||||
      the copyright owner. For the purposes of this definition, "submitted"
 | 
			
		||||
      means any form of electronic, verbal, or written communication sent
 | 
			
		||||
      to the Licensor or its representatives, including but not limited to
 | 
			
		||||
      communication on electronic mailing lists, source code control systems,
 | 
			
		||||
      and issue tracking systems that are managed by, or on behalf of, the
 | 
			
		||||
      Licensor for the purpose of discussing and improving the Work, but
 | 
			
		||||
      excluding communication that is conspicuously marked or otherwise
 | 
			
		||||
      designated in writing by the copyright owner as "Not a Contribution."
 | 
			
		||||
 | 
			
		||||
      "Contributor" shall mean Licensor and any individual or Legal Entity
 | 
			
		||||
      on behalf of whom a Contribution has been received by Licensor and
 | 
			
		||||
      subsequently incorporated within the Work.
 | 
			
		||||
 | 
			
		||||
   2. Grant of Copyright License. Subject to the terms and conditions of
 | 
			
		||||
      this License, each Contributor hereby grants to You a perpetual,
 | 
			
		||||
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 | 
			
		||||
      copyright license to reproduce, prepare Derivative Works of,
 | 
			
		||||
      publicly display, publicly perform, sublicense, and distribute the
 | 
			
		||||
      Work and such Derivative Works in Source or Object form.
 | 
			
		||||
 | 
			
		||||
   3. Grant of Patent License. Subject to the terms and conditions of
 | 
			
		||||
      this License, each Contributor hereby grants to You a perpetual,
 | 
			
		||||
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 | 
			
		||||
      (except as stated in this section) patent license to make, have made,
 | 
			
		||||
      use, offer to sell, sell, import, and otherwise transfer the Work,
 | 
			
		||||
      where such license applies only to those patent claims licensable
 | 
			
		||||
      by such Contributor that are necessarily infringed by their
 | 
			
		||||
      Contribution(s) alone or by combination of their Contribution(s)
 | 
			
		||||
      with the Work to which such Contribution(s) was submitted. If You
 | 
			
		||||
      institute patent litigation against any entity (including a
 | 
			
		||||
      cross-claim or counterclaim in a lawsuit) alleging that the Work
 | 
			
		||||
      or a Contribution incorporated within the Work constitutes direct
 | 
			
		||||
      or contributory patent infringement, then any patent licenses
 | 
			
		||||
      granted to You under this License for that Work shall terminate
 | 
			
		||||
      as of the date such litigation is filed.
 | 
			
		||||
 | 
			
		||||
   4. Redistribution. You may reproduce and distribute copies of the
 | 
			
		||||
      Work or Derivative Works thereof in any medium, with or without
 | 
			
		||||
      modifications, and in Source or Object form, provided that You
 | 
			
		||||
      meet the following conditions:
 | 
			
		||||
 | 
			
		||||
      (a) You must give any other recipients of the Work or
 | 
			
		||||
          Derivative Works a copy of this License; and
 | 
			
		||||
 | 
			
		||||
      (b) You must cause any modified files to carry prominent notices
 | 
			
		||||
          stating that You changed the files; and
 | 
			
		||||
 | 
			
		||||
      (c) You must retain, in the Source form of any Derivative Works
 | 
			
		||||
          that You distribute, all copyright, patent, trademark, and
 | 
			
		||||
          attribution notices from the Source form of the Work,
 | 
			
		||||
          excluding those notices that do not pertain to any part of
 | 
			
		||||
          the Derivative Works; and
 | 
			
		||||
 | 
			
		||||
      (d) If the Work includes a "NOTICE" text file as part of its
 | 
			
		||||
          distribution, then any Derivative Works that You distribute must
 | 
			
		||||
          include a readable copy of the attribution notices contained
 | 
			
		||||
          within such NOTICE file, excluding those notices that do not
 | 
			
		||||
          pertain to any part of the Derivative Works, in at least one
 | 
			
		||||
          of the following places: within a NOTICE text file distributed
 | 
			
		||||
          as part of the Derivative Works; within the Source form or
 | 
			
		||||
          documentation, if provided along with the Derivative Works; or,
 | 
			
		||||
          within a display generated by the Derivative Works, if and
 | 
			
		||||
          wherever such third-party notices normally appear. The contents
 | 
			
		||||
          of the NOTICE file are for informational purposes only and
 | 
			
		||||
          do not modify the License. You may add Your own attribution
 | 
			
		||||
          notices within Derivative Works that You distribute, alongside
 | 
			
		||||
          or as an addendum to the NOTICE text from the Work, provided
 | 
			
		||||
          that such additional attribution notices cannot be construed
 | 
			
		||||
          as modifying the License.
 | 
			
		||||
 | 
			
		||||
      You may add Your own copyright statement to Your modifications and
 | 
			
		||||
      may provide additional or different license terms and conditions
 | 
			
		||||
      for use, reproduction, or distribution of Your modifications, or
 | 
			
		||||
      for any such Derivative Works as a whole, provided Your use,
 | 
			
		||||
      reproduction, and distribution of the Work otherwise complies with
 | 
			
		||||
      the conditions stated in this License.
 | 
			
		||||
 | 
			
		||||
   5. Submission of Contributions. Unless You explicitly state otherwise,
 | 
			
		||||
      any Contribution intentionally submitted for inclusion in the Work
 | 
			
		||||
      by You to the Licensor shall be under the terms and conditions of
 | 
			
		||||
      this License, without any additional terms or conditions.
 | 
			
		||||
      Notwithstanding the above, nothing herein shall supersede or modify
 | 
			
		||||
      the terms of any separate license agreement you may have executed
 | 
			
		||||
      with Licensor regarding such Contributions.
 | 
			
		||||
 | 
			
		||||
   6. Trademarks. This License does not grant permission to use the trade
 | 
			
		||||
      names, trademarks, service marks, or product names of the Licensor,
 | 
			
		||||
      except as required for reasonable and customary use in describing the
 | 
			
		||||
      origin of the Work and reproducing the content of the NOTICE file.
 | 
			
		||||
 | 
			
		||||
   7. Disclaimer of Warranty. Unless required by applicable law or
 | 
			
		||||
      agreed to in writing, Licensor provides the Work (and each
 | 
			
		||||
      Contributor provides its Contributions) on an "AS IS" BASIS,
 | 
			
		||||
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 | 
			
		||||
      implied, including, without limitation, any warranties or conditions
 | 
			
		||||
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
 | 
			
		||||
      PARTICULAR PURPOSE. You are solely responsible for determining the
 | 
			
		||||
      appropriateness of using or redistributing the Work and assume any
 | 
			
		||||
      risks associated with Your exercise of permissions under this License.
 | 
			
		||||
 | 
			
		||||
   8. Limitation of Liability. In no event and under no legal theory,
 | 
			
		||||
      whether in tort (including negligence), contract, or otherwise,
 | 
			
		||||
      unless required by applicable law (such as deliberate and grossly
 | 
			
		||||
      negligent acts) or agreed to in writing, shall any Contributor be
 | 
			
		||||
      liable to You for damages, including any direct, indirect, special,
 | 
			
		||||
      incidental, or consequential damages of any character arising as a
 | 
			
		||||
      result of this License or out of the use or inability to use the
 | 
			
		||||
      Work (including but not limited to damages for loss of goodwill,
 | 
			
		||||
      work stoppage, computer failure or malfunction, or any and all
 | 
			
		||||
      other commercial damages or losses), even if such Contributor
 | 
			
		||||
      has been advised of the possibility of such damages.
 | 
			
		||||
 | 
			
		||||
   9. Accepting Warranty or Additional Liability. While redistributing
 | 
			
		||||
      the Work or Derivative Works thereof, You may choose to offer,
 | 
			
		||||
      and charge a fee for, acceptance of support, warranty, indemnity,
 | 
			
		||||
      or other liability obligations and/or rights consistent with this
 | 
			
		||||
      License. However, in accepting such obligations, You may act only
 | 
			
		||||
      on Your own behalf and on Your sole responsibility, not on behalf
 | 
			
		||||
      of any other Contributor, and only if You agree to indemnify,
 | 
			
		||||
      defend, and hold each Contributor harmless for any liability
 | 
			
		||||
      incurred by, or claims asserted against, such Contributor by reason
 | 
			
		||||
      of your accepting any such warranty or additional liability.
 | 
			
		||||
 | 
			
		||||
   END OF TERMS AND CONDITIONS
 | 
			
		||||
 | 
			
		||||
   APPENDIX: How to apply the Apache License to your work.
 | 
			
		||||
 | 
			
		||||
      To apply the Apache License to your work, attach the following
 | 
			
		||||
      boilerplate notice, with the fields enclosed by brackets "[]"
 | 
			
		||||
      replaced with your own identifying information. (Don't include
 | 
			
		||||
      the brackets!)  The text should be enclosed in the appropriate
 | 
			
		||||
      comment syntax for the file format. We also recommend that a
 | 
			
		||||
      file or class name and description of purpose be included on the
 | 
			
		||||
      same "printed page" as the copyright notice for easier
 | 
			
		||||
      identification within third-party archives.
 | 
			
		||||
 | 
			
		||||
   Copyright [yyyy] [name of copyright owner]
 | 
			
		||||
 | 
			
		||||
   Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
   you may not use this file except in compliance with the License.
 | 
			
		||||
   You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
       http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
   Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
   distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
   See the License for the specific language governing permissions and
 | 
			
		||||
   limitations under the License.
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,44 @@
 | 
			
		|||
// Package mountinfo provides a set of functions to retrieve information about OS mounts.
 | 
			
		||||
//
 | 
			
		||||
// Currently it supports Linux. For historical reasons, there is also some support for FreeBSD and OpenBSD,
 | 
			
		||||
// and a shallow implementation for Windows, but in general this is Linux-only package, so
 | 
			
		||||
// the rest of the document only applies to Linux, unless explicitly specified otherwise.
 | 
			
		||||
//
 | 
			
		||||
// In Linux, information about mounts seen by the current process is available from
 | 
			
		||||
// /proc/self/mountinfo. Note that due to mount namespaces, different processes can
 | 
			
		||||
// see different mounts. A per-process mountinfo table is available from /proc/<PID>/mountinfo,
 | 
			
		||||
// where <PID> is a numerical process identifier.
 | 
			
		||||
//
 | 
			
		||||
// In general, /proc is not a very efficient interface, and mountinfo is not an exception.
 | 
			
		||||
// For example, there is no way to get information about a specific mount point (i.e. it
 | 
			
		||||
// is all-or-nothing). This package tries to hide the /proc ineffectiveness by using
 | 
			
		||||
// parse filters while reading mountinfo. A filter can skip some entries, or stop
 | 
			
		||||
// processing the rest of the file once the needed information is found.
 | 
			
		||||
//
 | 
			
		||||
// For mountinfo filters that accept path as an argument, the path must be absolute,
 | 
			
		||||
// having all symlinks resolved, and being cleaned (i.e. no extra slashes or dots).
 | 
			
		||||
// One way to achieve all of the above is to employ filepath.Abs followed by
 | 
			
		||||
// filepath.EvalSymlinks (the latter calls filepath.Clean on the result so
 | 
			
		||||
// there is no need to explicitly call filepath.Clean).
 | 
			
		||||
//
 | 
			
		||||
// NOTE that in many cases there is no need to consult mountinfo at all. Here are some
 | 
			
		||||
// of the cases where mountinfo should not be parsed:
 | 
			
		||||
//
 | 
			
		||||
// 1. Before performing a mount. Usually, this is not needed, but if required (say to
 | 
			
		||||
// prevent over-mounts), to check whether a directory is mounted, call os.Lstat
 | 
			
		||||
// on it and its parent directory, and compare their st.Sys().(*syscall.Stat_t).Dev
 | 
			
		||||
// fields -- if they differ, then the directory is the mount point. NOTE this does
 | 
			
		||||
// not work for bind mounts. Optionally, the filesystem type can also be checked
 | 
			
		||||
// by calling unix.Statfs and checking the Type field (i.e. filesystem type).
 | 
			
		||||
//
 | 
			
		||||
// 2. After performing a mount. If there is no error returned, the mount succeeded;
 | 
			
		||||
// checking the mount table for a new mount is redundant and expensive.
 | 
			
		||||
//
 | 
			
		||||
// 3. Before performing an unmount. It is more efficient to do an unmount and ignore
 | 
			
		||||
// a specific error (EINVAL) which tells the directory is not mounted.
 | 
			
		||||
//
 | 
			
		||||
// 4. After performing an unmount. If there is no error returned, the unmount succeeded.
 | 
			
		||||
//
 | 
			
		||||
// 5. To find the mount point root of a specific directory. You can perform os.Stat()
 | 
			
		||||
// on the directory and traverse up until the Dev field of a parent directory differs.
 | 
			
		||||
package mountinfo
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,101 @@
 | 
			
		|||
package mountinfo
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/sys/unix"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// MountedFast is a method of detecting a mount point without reading
 | 
			
		||||
// mountinfo from procfs. A caller can only trust the result if no error
 | 
			
		||||
// and sure == true are returned. Otherwise, other methods (e.g. parsing
 | 
			
		||||
// /proc/mounts) have to be used. If unsure, use Mounted instead (which
 | 
			
		||||
// uses MountedFast, but falls back to parsing mountinfo if needed).
 | 
			
		||||
//
 | 
			
		||||
// If a non-existent path is specified, an appropriate error is returned.
 | 
			
		||||
// In case the caller is not interested in this particular error, it should
 | 
			
		||||
// be handled separately using e.g. errors.Is(err, fs.ErrNotExist).
 | 
			
		||||
//
 | 
			
		||||
// This function is only available on Linux. When available (since kernel
 | 
			
		||||
// v5.6), openat2(2) syscall is used to reliably detect all mounts. Otherwise,
 | 
			
		||||
// the implementation falls back to using stat(2), which can reliably detect
 | 
			
		||||
// normal (but not bind) mounts.
 | 
			
		||||
func MountedFast(path string) (mounted, sure bool, err error) {
 | 
			
		||||
	// Root is always mounted.
 | 
			
		||||
	if path == string(os.PathSeparator) {
 | 
			
		||||
		return true, true, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	path, err = normalizePath(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, false, err
 | 
			
		||||
	}
 | 
			
		||||
	mounted, sure, err = mountedFast(path)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// mountedByOpenat2 is a method of detecting a mount that works for all kinds
 | 
			
		||||
// of mounts (incl. bind mounts), but requires a recent (v5.6+) linux kernel.
 | 
			
		||||
func mountedByOpenat2(path string) (bool, error) {
 | 
			
		||||
	dir, last := filepath.Split(path)
 | 
			
		||||
 | 
			
		||||
	dirfd, err := unix.Openat2(unix.AT_FDCWD, dir, &unix.OpenHow{
 | 
			
		||||
		Flags: unix.O_PATH | unix.O_CLOEXEC,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, &os.PathError{Op: "openat2", Path: dir, Err: err}
 | 
			
		||||
	}
 | 
			
		||||
	fd, err := unix.Openat2(dirfd, last, &unix.OpenHow{
 | 
			
		||||
		Flags:   unix.O_PATH | unix.O_CLOEXEC | unix.O_NOFOLLOW,
 | 
			
		||||
		Resolve: unix.RESOLVE_NO_XDEV,
 | 
			
		||||
	})
 | 
			
		||||
	_ = unix.Close(dirfd)
 | 
			
		||||
	switch err { //nolint:errorlint // unix errors are bare
 | 
			
		||||
	case nil: // definitely not a mount
 | 
			
		||||
		_ = unix.Close(fd)
 | 
			
		||||
		return false, nil
 | 
			
		||||
	case unix.EXDEV: // definitely a mount
 | 
			
		||||
		return true, nil
 | 
			
		||||
	}
 | 
			
		||||
	// not sure
 | 
			
		||||
	return false, &os.PathError{Op: "openat2", Path: path, Err: err}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// mountedFast is similar to MountedFast, except it expects a normalized path.
 | 
			
		||||
func mountedFast(path string) (mounted, sure bool, err error) {
 | 
			
		||||
	// Root is always mounted.
 | 
			
		||||
	if path == string(os.PathSeparator) {
 | 
			
		||||
		return true, true, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Try a fast path, using openat2() with RESOLVE_NO_XDEV.
 | 
			
		||||
	mounted, err = mountedByOpenat2(path)
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		return mounted, true, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Another fast path: compare st.st_dev fields.
 | 
			
		||||
	mounted, err = mountedByStat(path)
 | 
			
		||||
	// This does not work for bind mounts, so false negative
 | 
			
		||||
	// is possible, therefore only trust if return is true.
 | 
			
		||||
	if mounted && err == nil {
 | 
			
		||||
		return true, true, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func mounted(path string) (bool, error) {
 | 
			
		||||
	path, err := normalizePath(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
	mounted, sure, err := mountedFast(path)
 | 
			
		||||
	if sure && err == nil {
 | 
			
		||||
		return mounted, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Fallback to parsing mountinfo.
 | 
			
		||||
	return mountedByMountinfo(path)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,53 @@
 | 
			
		|||
//go:build linux || freebsd || openbsd || darwin
 | 
			
		||||
// +build linux freebsd openbsd darwin
 | 
			
		||||
 | 
			
		||||
package mountinfo
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/sys/unix"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func mountedByStat(path string) (bool, error) {
 | 
			
		||||
	var st unix.Stat_t
 | 
			
		||||
 | 
			
		||||
	if err := unix.Lstat(path, &st); err != nil {
 | 
			
		||||
		return false, &os.PathError{Op: "stat", Path: path, Err: err}
 | 
			
		||||
	}
 | 
			
		||||
	dev := st.Dev
 | 
			
		||||
	parent := filepath.Dir(path)
 | 
			
		||||
	if err := unix.Lstat(parent, &st); err != nil {
 | 
			
		||||
		return false, &os.PathError{Op: "stat", Path: parent, Err: err}
 | 
			
		||||
	}
 | 
			
		||||
	if dev != st.Dev {
 | 
			
		||||
		// Device differs from that of parent,
 | 
			
		||||
		// so definitely a mount point.
 | 
			
		||||
		return true, nil
 | 
			
		||||
	}
 | 
			
		||||
	// NB: this does not detect bind mounts on Linux.
 | 
			
		||||
	return false, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func normalizePath(path string) (realPath string, err error) {
 | 
			
		||||
	if realPath, err = filepath.Abs(path); err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	if realPath, err = filepath.EvalSymlinks(realPath); err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	if _, err := os.Stat(realPath); err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return realPath, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func mountedByMountinfo(path string) (bool, error) {
 | 
			
		||||
	entries, err := GetMounts(SingleEntryFilter(path))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return len(entries) > 0, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,67 @@
 | 
			
		|||
package mountinfo
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetMounts retrieves a list of mounts for the current running process,
 | 
			
		||||
// with an optional filter applied (use nil for no filter).
 | 
			
		||||
func GetMounts(f FilterFunc) ([]*Info, error) {
 | 
			
		||||
	return parseMountTable(f)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Mounted determines if a specified path is a mount point. In case of any
 | 
			
		||||
// error, false (and an error) is returned.
 | 
			
		||||
//
 | 
			
		||||
// If a non-existent path is specified, an appropriate error is returned.
 | 
			
		||||
// In case the caller is not interested in this particular error, it should
 | 
			
		||||
// be handled separately using e.g. errors.Is(err, fs.ErrNotExist).
 | 
			
		||||
func Mounted(path string) (bool, error) {
 | 
			
		||||
	// root is always mounted
 | 
			
		||||
	if path == string(os.PathSeparator) {
 | 
			
		||||
		return true, nil
 | 
			
		||||
	}
 | 
			
		||||
	return mounted(path)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Info reveals information about a particular mounted filesystem. This
 | 
			
		||||
// struct is populated from the content in the /proc/<pid>/mountinfo file.
 | 
			
		||||
type Info struct {
 | 
			
		||||
	// ID is a unique identifier of the mount (may be reused after umount).
 | 
			
		||||
	ID int
 | 
			
		||||
 | 
			
		||||
	// Parent is the ID of the parent mount (or of self for the root
 | 
			
		||||
	// of this mount namespace's mount tree).
 | 
			
		||||
	Parent int
 | 
			
		||||
 | 
			
		||||
	// Major and Minor are the major and the minor components of the Dev
 | 
			
		||||
	// field of unix.Stat_t structure returned by unix.*Stat calls for
 | 
			
		||||
	// files on this filesystem.
 | 
			
		||||
	Major, Minor int
 | 
			
		||||
 | 
			
		||||
	// Root is the pathname of the directory in the filesystem which forms
 | 
			
		||||
	// the root of this mount.
 | 
			
		||||
	Root string
 | 
			
		||||
 | 
			
		||||
	// Mountpoint is the pathname of the mount point relative to the
 | 
			
		||||
	// process's root directory.
 | 
			
		||||
	Mountpoint string
 | 
			
		||||
 | 
			
		||||
	// Options is a comma-separated list of mount options.
 | 
			
		||||
	Options string
 | 
			
		||||
 | 
			
		||||
	// Optional are zero or more fields of the form "tag[:value]",
 | 
			
		||||
	// separated by a space.  Currently, the possible optional fields are
 | 
			
		||||
	// "shared", "master", "propagate_from", and "unbindable". For more
 | 
			
		||||
	// information, see mount_namespaces(7) Linux man page.
 | 
			
		||||
	Optional string
 | 
			
		||||
 | 
			
		||||
	// FSType is the filesystem type in the form "type[.subtype]".
 | 
			
		||||
	FSType string
 | 
			
		||||
 | 
			
		||||
	// Source is filesystem-specific information, or "none".
 | 
			
		||||
	Source string
 | 
			
		||||
 | 
			
		||||
	// VFSOptions is a comma-separated list of superblock options.
 | 
			
		||||
	VFSOptions string
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,56 @@
 | 
			
		|||
//go:build freebsd || openbsd || darwin
 | 
			
		||||
// +build freebsd openbsd darwin
 | 
			
		||||
 | 
			
		||||
package mountinfo
 | 
			
		||||
 | 
			
		||||
import "golang.org/x/sys/unix"
 | 
			
		||||
 | 
			
		||||
// parseMountTable returns information about mounted filesystems
 | 
			
		||||
func parseMountTable(filter FilterFunc) ([]*Info, error) {
 | 
			
		||||
	count, err := unix.Getfsstat(nil, unix.MNT_WAIT)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	entries := make([]unix.Statfs_t, count)
 | 
			
		||||
	_, err = unix.Getfsstat(entries, unix.MNT_WAIT)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var out []*Info
 | 
			
		||||
	for _, entry := range entries {
 | 
			
		||||
		var skip, stop bool
 | 
			
		||||
		mountinfo := getMountinfo(&entry)
 | 
			
		||||
 | 
			
		||||
		if filter != nil {
 | 
			
		||||
			// filter out entries we're not interested in
 | 
			
		||||
			skip, stop = filter(mountinfo)
 | 
			
		||||
			if skip {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		out = append(out, mountinfo)
 | 
			
		||||
		if stop {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return out, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func mounted(path string) (bool, error) {
 | 
			
		||||
	path, err := normalizePath(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
	// Fast path: compare st.st_dev fields.
 | 
			
		||||
	// This should always work for FreeBSD and OpenBSD.
 | 
			
		||||
	mounted, err := mountedByStat(path)
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		return mounted, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Fallback to parsing mountinfo
 | 
			
		||||
	return mountedByMountinfo(path)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,63 @@
 | 
			
		|||
package mountinfo
 | 
			
		||||
 | 
			
		||||
import "strings"
 | 
			
		||||
 | 
			
		||||
// FilterFunc is a type defining a callback function for GetMount(),
 | 
			
		||||
// used to filter out mountinfo entries we're not interested in,
 | 
			
		||||
// and/or stop further processing if we found what we wanted.
 | 
			
		||||
//
 | 
			
		||||
// It takes a pointer to the Info struct (fully populated with all available
 | 
			
		||||
// fields on the GOOS platform), and returns two booleans:
 | 
			
		||||
//
 | 
			
		||||
// skip: true if the entry should be skipped;
 | 
			
		||||
//
 | 
			
		||||
// stop: true if parsing should be stopped after the entry.
 | 
			
		||||
type FilterFunc func(*Info) (skip, stop bool)
 | 
			
		||||
 | 
			
		||||
// PrefixFilter discards all entries whose mount points do not start with, or
 | 
			
		||||
// are equal to the path specified in prefix. The prefix path must be absolute,
 | 
			
		||||
// have all symlinks resolved, and cleaned (i.e. no extra slashes or dots).
 | 
			
		||||
//
 | 
			
		||||
// PrefixFilter treats prefix as a path, not a partial prefix, which means that
 | 
			
		||||
// given "/foo", "/foo/bar" and "/foobar" entries, PrefixFilter("/foo") returns
 | 
			
		||||
// "/foo" and "/foo/bar", and discards "/foobar".
 | 
			
		||||
func PrefixFilter(prefix string) FilterFunc {
 | 
			
		||||
	return func(m *Info) (bool, bool) {
 | 
			
		||||
		skip := !strings.HasPrefix(m.Mountpoint+"/", prefix+"/")
 | 
			
		||||
		return skip, false
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SingleEntryFilter looks for a specific entry.
 | 
			
		||||
func SingleEntryFilter(mp string) FilterFunc {
 | 
			
		||||
	return func(m *Info) (bool, bool) {
 | 
			
		||||
		if m.Mountpoint == mp {
 | 
			
		||||
			return false, true // don't skip, stop now
 | 
			
		||||
		}
 | 
			
		||||
		return true, false // skip, keep going
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ParentsFilter returns all entries whose mount points
 | 
			
		||||
// can be parents of a path specified, discarding others.
 | 
			
		||||
//
 | 
			
		||||
// For example, given /var/lib/docker/something, entries
 | 
			
		||||
// like /var/lib/docker, /var and / are returned.
 | 
			
		||||
func ParentsFilter(path string) FilterFunc {
 | 
			
		||||
	return func(m *Info) (bool, bool) {
 | 
			
		||||
		skip := !strings.HasPrefix(path, m.Mountpoint)
 | 
			
		||||
		return skip, false
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FSTypeFilter returns all entries that match provided fstype(s).
 | 
			
		||||
func FSTypeFilter(fstype ...string) FilterFunc {
 | 
			
		||||
	return func(m *Info) (bool, bool) {
 | 
			
		||||
		for _, t := range fstype {
 | 
			
		||||
			if m.FSType == t {
 | 
			
		||||
				return false, false // don't skip, keep going
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return true, false // skip, keep going
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
//go:build freebsd || darwin
 | 
			
		||||
// +build freebsd darwin
 | 
			
		||||
 | 
			
		||||
package mountinfo
 | 
			
		||||
 | 
			
		||||
import "golang.org/x/sys/unix"
 | 
			
		||||
 | 
			
		||||
func getMountinfo(entry *unix.Statfs_t) *Info {
 | 
			
		||||
	return &Info{
 | 
			
		||||
		Mountpoint: unix.ByteSliceToString(entry.Mntonname[:]),
 | 
			
		||||
		FSType:     unix.ByteSliceToString(entry.Fstypename[:]),
 | 
			
		||||
		Source:     unix.ByteSliceToString(entry.Mntfromname[:]),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,214 @@
 | 
			
		|||
package mountinfo
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetMountsFromReader retrieves a list of mounts from the
 | 
			
		||||
// reader provided, with an optional filter applied (use nil
 | 
			
		||||
// for no filter). This can be useful in tests or benchmarks
 | 
			
		||||
// that provide fake mountinfo data, or when a source other
 | 
			
		||||
// than /proc/self/mountinfo needs to be read from.
 | 
			
		||||
//
 | 
			
		||||
// This function is Linux-specific.
 | 
			
		||||
func GetMountsFromReader(r io.Reader, filter FilterFunc) ([]*Info, error) {
 | 
			
		||||
	s := bufio.NewScanner(r)
 | 
			
		||||
	out := []*Info{}
 | 
			
		||||
	for s.Scan() {
 | 
			
		||||
		var err error
 | 
			
		||||
 | 
			
		||||
		/*
 | 
			
		||||
		   See http://man7.org/linux/man-pages/man5/proc.5.html
 | 
			
		||||
 | 
			
		||||
		   36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
 | 
			
		||||
		   (1)(2)(3)   (4)   (5)      (6)      (7)   (8) (9)   (10)         (11)
 | 
			
		||||
 | 
			
		||||
		   (1) mount ID:  unique identifier of the mount (may be reused after umount)
 | 
			
		||||
		   (2) parent ID:  ID of parent (or of self for the top of the mount tree)
 | 
			
		||||
		   (3) major:minor:  value of st_dev for files on filesystem
 | 
			
		||||
		   (4) root:  root of the mount within the filesystem
 | 
			
		||||
		   (5) mount point:  mount point relative to the process's root
 | 
			
		||||
		   (6) mount options:  per mount options
 | 
			
		||||
		   (7) optional fields:  zero or more fields of the form "tag[:value]"
 | 
			
		||||
		   (8) separator:  marks the end of the optional fields
 | 
			
		||||
		   (9) filesystem type:  name of filesystem of the form "type[.subtype]"
 | 
			
		||||
		   (10) mount source:  filesystem specific information or "none"
 | 
			
		||||
		   (11) super options:  per super block options
 | 
			
		||||
 | 
			
		||||
		   In other words, we have:
 | 
			
		||||
		    * 6 mandatory fields	(1)..(6)
 | 
			
		||||
		    * 0 or more optional fields	(7)
 | 
			
		||||
		    * a separator field		(8)
 | 
			
		||||
		    * 3 mandatory fields	(9)..(11)
 | 
			
		||||
		*/
 | 
			
		||||
 | 
			
		||||
		text := s.Text()
 | 
			
		||||
		fields := strings.Split(text, " ")
 | 
			
		||||
		numFields := len(fields)
 | 
			
		||||
		if numFields < 10 {
 | 
			
		||||
			// should be at least 10 fields
 | 
			
		||||
			return nil, fmt.Errorf("parsing '%s' failed: not enough fields (%d)", text, numFields)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// separator field
 | 
			
		||||
		sepIdx := numFields - 4
 | 
			
		||||
		// In Linux <= 3.9 mounting a cifs with spaces in a share
 | 
			
		||||
		// name (like "//srv/My Docs") _may_ end up having a space
 | 
			
		||||
		// in the last field of mountinfo (like "unc=//serv/My Docs").
 | 
			
		||||
		// Since kernel 3.10-rc1, cifs option "unc=" is ignored,
 | 
			
		||||
		// so spaces should not appear.
 | 
			
		||||
		//
 | 
			
		||||
		// Check for a separator, and work around the spaces bug
 | 
			
		||||
		for fields[sepIdx] != "-" {
 | 
			
		||||
			sepIdx--
 | 
			
		||||
			if sepIdx == 5 {
 | 
			
		||||
				return nil, fmt.Errorf("parsing '%s' failed: missing - separator", text)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		p := &Info{}
 | 
			
		||||
 | 
			
		||||
		p.Mountpoint, err = unescape(fields[4])
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("parsing '%s' failed: mount point: %w", fields[4], err)
 | 
			
		||||
		}
 | 
			
		||||
		p.FSType, err = unescape(fields[sepIdx+1])
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("parsing '%s' failed: fstype: %w", fields[sepIdx+1], err)
 | 
			
		||||
		}
 | 
			
		||||
		p.Source, err = unescape(fields[sepIdx+2])
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("parsing '%s' failed: source: %w", fields[sepIdx+2], err)
 | 
			
		||||
		}
 | 
			
		||||
		p.VFSOptions = fields[sepIdx+3]
 | 
			
		||||
 | 
			
		||||
		// ignore any numbers parsing errors, as there should not be any
 | 
			
		||||
		p.ID, _ = strconv.Atoi(fields[0])
 | 
			
		||||
		p.Parent, _ = strconv.Atoi(fields[1])
 | 
			
		||||
		mm := strings.SplitN(fields[2], ":", 3)
 | 
			
		||||
		if len(mm) != 2 {
 | 
			
		||||
			return nil, fmt.Errorf("parsing '%s' failed: unexpected major:minor pair %s", text, mm)
 | 
			
		||||
		}
 | 
			
		||||
		p.Major, _ = strconv.Atoi(mm[0])
 | 
			
		||||
		p.Minor, _ = strconv.Atoi(mm[1])
 | 
			
		||||
 | 
			
		||||
		p.Root, err = unescape(fields[3])
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("parsing '%s' failed: root: %w", fields[3], err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		p.Options = fields[5]
 | 
			
		||||
 | 
			
		||||
		// zero or more optional fields
 | 
			
		||||
		p.Optional = strings.Join(fields[6:sepIdx], " ")
 | 
			
		||||
 | 
			
		||||
		// Run the filter after parsing all fields.
 | 
			
		||||
		var skip, stop bool
 | 
			
		||||
		if filter != nil {
 | 
			
		||||
			skip, stop = filter(p)
 | 
			
		||||
			if skip {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		out = append(out, p)
 | 
			
		||||
		if stop {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if err := s.Err(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return out, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseMountTable(filter FilterFunc) ([]*Info, error) {
 | 
			
		||||
	f, err := os.Open("/proc/self/mountinfo")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer f.Close()
 | 
			
		||||
 | 
			
		||||
	return GetMountsFromReader(f, filter)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PidMountInfo retrieves the list of mounts from a given process' mount
 | 
			
		||||
// namespace. Unless there is a need to get mounts from a mount namespace
 | 
			
		||||
// different from that of a calling process, use GetMounts.
 | 
			
		||||
//
 | 
			
		||||
// This function is Linux-specific.
 | 
			
		||||
//
 | 
			
		||||
// Deprecated: this will be removed before v1; use GetMountsFromReader with
 | 
			
		||||
// opened /proc/<pid>/mountinfo as an argument instead.
 | 
			
		||||
func PidMountInfo(pid int) ([]*Info, error) {
 | 
			
		||||
	f, err := os.Open(fmt.Sprintf("/proc/%d/mountinfo", pid))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer f.Close()
 | 
			
		||||
 | 
			
		||||
	return GetMountsFromReader(f, nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// A few specific characters in mountinfo path entries (root and mountpoint)
 | 
			
		||||
// are escaped using a backslash followed by a character's ascii code in octal.
 | 
			
		||||
//
 | 
			
		||||
//   space              -- as \040
 | 
			
		||||
//   tab (aka \t)       -- as \011
 | 
			
		||||
//   newline (aka \n)   -- as \012
 | 
			
		||||
//   backslash (aka \\) -- as \134
 | 
			
		||||
//
 | 
			
		||||
// This function converts path from mountinfo back, i.e. it unescapes the above sequences.
 | 
			
		||||
func unescape(path string) (string, error) {
 | 
			
		||||
	// try to avoid copying
 | 
			
		||||
	if strings.IndexByte(path, '\\') == -1 {
 | 
			
		||||
		return path, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// The following code is UTF-8 transparent as it only looks for some
 | 
			
		||||
	// specific characters (backslash and 0..7) with values < utf8.RuneSelf,
 | 
			
		||||
	// and everything else is passed through as is.
 | 
			
		||||
	buf := make([]byte, len(path))
 | 
			
		||||
	bufLen := 0
 | 
			
		||||
	for i := 0; i < len(path); i++ {
 | 
			
		||||
		if path[i] != '\\' {
 | 
			
		||||
			buf[bufLen] = path[i]
 | 
			
		||||
			bufLen++
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		s := path[i:]
 | 
			
		||||
		if len(s) < 4 {
 | 
			
		||||
			// too short
 | 
			
		||||
			return "", fmt.Errorf("bad escape sequence %q: too short", s)
 | 
			
		||||
		}
 | 
			
		||||
		c := s[1]
 | 
			
		||||
		switch c {
 | 
			
		||||
		case '0', '1', '2', '3', '4', '5', '6', '7':
 | 
			
		||||
			v := c - '0'
 | 
			
		||||
			for j := 2; j < 4; j++ { // one digit already; two more
 | 
			
		||||
				if s[j] < '0' || s[j] > '7' {
 | 
			
		||||
					return "", fmt.Errorf("bad escape sequence %q: not a digit", s[:3])
 | 
			
		||||
				}
 | 
			
		||||
				x := s[j] - '0'
 | 
			
		||||
				v = (v << 3) | x
 | 
			
		||||
			}
 | 
			
		||||
			if v > 255 {
 | 
			
		||||
				return "", fmt.Errorf("bad escape sequence %q: out of range" + s[:3])
 | 
			
		||||
			}
 | 
			
		||||
			buf[bufLen] = v
 | 
			
		||||
			bufLen++
 | 
			
		||||
			i += 3
 | 
			
		||||
			continue
 | 
			
		||||
		default:
 | 
			
		||||
			return "", fmt.Errorf("bad escape sequence %q: not a digit" + s[:3])
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return string(buf[:bufLen]), nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
package mountinfo
 | 
			
		||||
 | 
			
		||||
import "golang.org/x/sys/unix"
 | 
			
		||||
 | 
			
		||||
func getMountinfo(entry *unix.Statfs_t) *Info {
 | 
			
		||||
	return &Info{
 | 
			
		||||
		Mountpoint: unix.ByteSliceToString(entry.F_mntonname[:]),
 | 
			
		||||
		FSType:     unix.ByteSliceToString(entry.F_fstypename[:]),
 | 
			
		||||
		Source:     unix.ByteSliceToString(entry.F_mntfromname[:]),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
//go:build !windows && !linux && !freebsd && !openbsd && !darwin
 | 
			
		||||
// +build !windows,!linux,!freebsd,!openbsd,!darwin
 | 
			
		||||
 | 
			
		||||
package mountinfo
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"runtime"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var errNotImplemented = fmt.Errorf("not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
 | 
			
		||||
 | 
			
		||||
func parseMountTable(_ FilterFunc) ([]*Info, error) {
 | 
			
		||||
	return nil, errNotImplemented
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func mounted(path string) (bool, error) {
 | 
			
		||||
	return false, errNotImplemented
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
package mountinfo
 | 
			
		||||
 | 
			
		||||
func parseMountTable(_ FilterFunc) ([]*Info, error) {
 | 
			
		||||
	// Do NOT return an error!
 | 
			
		||||
	return nil, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func mounted(_ string) (bool, error) {
 | 
			
		||||
	return false, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -510,6 +510,9 @@ github.com/moby/patternmatcher
 | 
			
		|||
## explicit; go 1.13
 | 
			
		||||
github.com/moby/spdystream
 | 
			
		||||
github.com/moby/spdystream/spdy
 | 
			
		||||
# github.com/moby/sys/mountinfo v0.6.2
 | 
			
		||||
## explicit; go 1.16
 | 
			
		||||
github.com/moby/sys/mountinfo
 | 
			
		||||
# github.com/moby/sys/sequential v0.5.0
 | 
			
		||||
## explicit; go 1.17
 | 
			
		||||
github.com/moby/sys/sequential
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue