Allow checking out any ref in gitutils

Also changes so that shallow fetch is performed
even when a specific ref is specified.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi 2017-04-10 17:11:28 -07:00
parent fa54c94b9d
commit 493a6c3d41
3 changed files with 65 additions and 53 deletions

View file

@ -73,17 +73,18 @@ pre-packaged tarball contexts and plain text files.
### Git repositories
When the `URL` parameter points to the location of a Git repository, the
repository acts as the build context. The system recursively clones the
repository and its submodules using a `git clone --depth 1 --recursive`
command. This command runs in a temporary directory on your local host. After
the command succeeds, the directory is sent to the Docker daemon as the
context. Local clones give you the ability to access private repositories using
local user credentials, VPN's, and so forth.
repository acts as the build context. The system recursively fetches the
repository and its submodules. The commit history is not preserved. A
repository is first pulled into a temporary directory on your local host. After
the that succeeds, the directory is sent to the Docker daemon as the context.
Local copy gives you the ability to access private repositories using local
user credentials, VPN's, and so forth.
Git URLs accept context configuration in their fragment section, separated by a
colon `:`. The first part represents the reference that Git will check out,
this can be either a branch, a tag, or a commit SHA. The second part represents
a subdirectory inside the repository that will be used as a build context.
this can be either a branch, a tag, or a remote reference. The second part
represents a subdirectory inside the repository that will be used as a build
context.
For example, run this command to use a directory called `docker` in the branch
`container`:
@ -100,12 +101,11 @@ Build Syntax Suffix | Commit Used | Build Context Used
`myrepo.git` | `refs/heads/master` | `/`
`myrepo.git#mytag` | `refs/tags/mytag` | `/`
`myrepo.git#mybranch` | `refs/heads/mybranch` | `/`
`myrepo.git#abcdef` | `sha1 = abcdef` | `/`
`myrepo.git#pull/42/head` | `refs/pull/42/head` | `/`
`myrepo.git#:myfolder` | `refs/heads/master` | `/myfolder`
`myrepo.git#master:myfolder` | `refs/heads/master` | `/myfolder`
`myrepo.git#mytag:myfolder` | `refs/tags/mytag` | `/myfolder`
`myrepo.git#mybranch:myfolder` | `refs/heads/mybranch` | `/myfolder`
`myrepo.git#abcdef:myfolder` | `sha1 = abcdef` | `/myfolder`
### Tarball contexts

View file

@ -12,6 +12,7 @@ import (
"github.com/docker/docker/pkg/symlink"
"github.com/docker/docker/pkg/urlutil"
"github.com/pkg/errors"
)
// Clone clones a repository into a newly created directory which
@ -30,21 +31,45 @@ func Clone(remoteURL string) (string, error) {
return "", err
}
fragment := u.Fragment
clone := cloneArgs(u, root)
if output, err := git(clone...); err != nil {
return "", fmt.Errorf("Error trying to use git: %s (%s)", err, output)
if out, err := gitWithinDir(root, "init"); err != nil {
return "", errors.Wrapf(err, "failed to init repo at %s: %s", root, out)
}
return checkoutGit(fragment, root)
ref, subdir := getRefAndSubdir(u.Fragment)
fetch := fetchArgs(u, ref)
u.Fragment = ""
// Add origin remote for compatibility with previous implementation that
// used "git clone" and also to make sure local refs are created for branches
if out, err := gitWithinDir(root, "remote", "add", "origin", u.String()); err != nil {
return "", errors.Wrapf(err, "failed add origin repo at %s: %s", u.String(), out)
}
if output, err := gitWithinDir(root, fetch...); err != nil {
return "", errors.Wrapf(err, "error fetching: %s", output)
}
return checkoutGit(root, ref, subdir)
}
func cloneArgs(remoteURL *url.URL, root string) []string {
args := []string{"clone", "--recursive"}
shallow := len(remoteURL.Fragment) == 0
func getRefAndSubdir(fragment string) (ref string, subdir string) {
refAndDir := strings.SplitN(fragment, ":", 2)
ref = "master"
if len(refAndDir[0]) != 0 {
ref = refAndDir[0]
}
if len(refAndDir) > 1 && len(refAndDir[1]) != 0 {
subdir = refAndDir[1]
}
return
}
if shallow && strings.HasPrefix(remoteURL.Scheme, "http") {
func fetchArgs(remoteURL *url.URL, ref string) []string {
args := []string{"fetch", "--recurse-submodules=yes"}
shallow := true
if strings.HasPrefix(remoteURL.Scheme, "http") {
res, err := http.Head(fmt.Sprintf("%s/info/refs?service=git-upload-pack", remoteURL))
if err != nil || res.Header.Get("Content-Type") != "application/x-git-upload-pack-advertisement" {
shallow = false
@ -55,26 +80,23 @@ func cloneArgs(remoteURL *url.URL, root string) []string {
args = append(args, "--depth", "1")
}
if remoteURL.Fragment != "" {
remoteURL.Fragment = ""
}
return append(args, remoteURL.String(), root)
return append(args, "origin", ref)
}
func checkoutGit(fragment, root string) (string, error) {
refAndDir := strings.SplitN(fragment, ":", 2)
if len(refAndDir[0]) != 0 {
if output, err := gitWithinDir(root, "checkout", refAndDir[0]); err != nil {
return "", fmt.Errorf("Error trying to use git: %s (%s)", err, output)
func checkoutGit(root, ref, subdir string) (string, error) {
// Try checking out by ref name first. This will work on branches and sets
// .git/HEAD to the current branch name
if output, err := gitWithinDir(root, "checkout", ref); err != nil {
// If checking out by branch name fails check out the last fetched ref
if _, err2 := gitWithinDir(root, "checkout", "FETCH_HEAD"); err2 != nil {
return "", errors.Wrapf(err, "error checking out %s: %s", ref, output)
}
}
if len(refAndDir) > 1 && len(refAndDir[1]) != 0 {
newCtx, err := symlink.FollowSymlinkInScope(filepath.Join(root, refAndDir[1]), root)
if subdir != "" {
newCtx, err := symlink.FollowSymlinkInScope(filepath.Join(root, subdir), root)
if err != nil {
return "", fmt.Errorf("Error setting git context, %q not within git root: %s", refAndDir[1], err)
return "", errors.Wrapf(err, "error setting git context, %q not within git root", subdir)
}
fi, err := os.Stat(newCtx)
@ -82,7 +104,7 @@ func checkoutGit(fragment, root string) (string, error) {
return "", err
}
if !fi.IsDir() {
return "", fmt.Errorf("Error setting git context, not a directory: %s", newCtx)
return "", errors.Errorf("error setting git context, not a directory: %s", newCtx)
}
root = newCtx
}

View file

@ -20,15 +20,14 @@ func TestCloneArgsSmartHttp(t *testing.T) {
serverURL, _ := url.Parse(server.URL)
serverURL.Path = "/repo.git"
gitURL := serverURL.String()
mux.HandleFunc("/repo.git/info/refs", func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query().Get("service")
w.Header().Set("Content-Type", fmt.Sprintf("application/x-%s-advertisement", q))
})
args := cloneArgs(serverURL, "/tmp")
exp := []string{"clone", "--recursive", "--depth", "1", gitURL, "/tmp"}
args := fetchArgs(serverURL, "master")
exp := []string{"fetch", "--recurse-submodules=yes", "--depth", "1", "origin", "master"}
if !reflect.DeepEqual(args, exp) {
t.Fatalf("Expected %v, got %v", exp, args)
}
@ -40,14 +39,13 @@ func TestCloneArgsDumbHttp(t *testing.T) {
serverURL, _ := url.Parse(server.URL)
serverURL.Path = "/repo.git"
gitURL := serverURL.String()
mux.HandleFunc("/repo.git/info/refs", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
})
args := cloneArgs(serverURL, "/tmp")
exp := []string{"clone", "--recursive", gitURL, "/tmp"}
args := fetchArgs(serverURL, "master")
exp := []string{"fetch", "--recurse-submodules=yes", "origin", "master"}
if !reflect.DeepEqual(args, exp) {
t.Fatalf("Expected %v, got %v", exp, args)
}
@ -55,17 +53,8 @@ func TestCloneArgsDumbHttp(t *testing.T) {
func TestCloneArgsGit(t *testing.T) {
u, _ := url.Parse("git://github.com/docker/docker")
args := cloneArgs(u, "/tmp")
exp := []string{"clone", "--recursive", "--depth", "1", "git://github.com/docker/docker", "/tmp"}
if !reflect.DeepEqual(args, exp) {
t.Fatalf("Expected %v, got %v", exp, args)
}
}
func TestCloneArgsStripFragment(t *testing.T) {
u, _ := url.Parse("git://github.com/docker/docker#test")
args := cloneArgs(u, "/tmp")
exp := []string{"clone", "--recursive", "git://github.com/docker/docker", "/tmp"}
args := fetchArgs(u, "master")
exp := []string{"fetch", "--recurse-submodules=yes", "--depth", "1", "origin", "master"}
if !reflect.DeepEqual(args, exp) {
t.Fatalf("Expected %v, got %v", exp, args)
}
@ -198,7 +187,8 @@ func TestCheckoutGit(t *testing.T) {
}
for _, c := range cases {
r, err := checkoutGit(c.frag, gitDir)
ref, subdir := getRefAndSubdir(c.frag)
r, err := checkoutGit(gitDir, ref, subdir)
fail := err != nil
if fail != c.fail {