Browse code

Use git url fragment to specify reference and dir context.

Signed-off-by: David Calavera <david.calavera@gmail.com>

David Calavera authored on 2015/04/25 07:12:45
Showing 6 changed files
... ...
@@ -637,13 +637,36 @@ an [*ADD*](/reference/builder/#add) instruction to reference a file in the
637 637
 context.
638 638
 
639 639
 The `URL` parameter can specify the location of a Git repository;
640
-the repository acts as the build context.  The system recursively clones the repository
640
+the repository acts as the build context. The system recursively clones the repository
641 641
 and its submodules using a `git clone --depth 1 --recursive` command.
642 642
 This command runs in a temporary directory on your local host.
643 643
 After the command succeeds, the directory is sent to the Docker daemon as the context.
644 644
 Local clones give you the ability to access private repositories using
645 645
 local user credentials, VPN's, and so forth.
646 646
 
647
+Git URLs accept context configuration in their fragment section, separated by a colon `:`.
648
+The first part represents the reference that Git will check out, this can be either
649
+a branch, a tag, or a commit SHA. The second part represents a subdirectory
650
+inside the repository that will be used as a build context.
651
+
652
+For example, run this command to use a directory called `docker` in the branch `container`:
653
+
654
+      $ docker build https://github.com/docker/rootfs.git#container:docker
655
+
656
+The following table represents all the valid suffixes with their build contexts:
657
+
658
+Build Syntax Suffix | Commit Used | Build Context Used
659
+--------------------|-------------|-------------------
660
+`myrepo.git` | `refs/heads/master` | `/`
661
+`myrepo.git#mytag` | `refs/tags/mytag` | `/`
662
+`myrepo.git#mybranch` | `refs/heads/mybranch` | `/`
663
+`myrepo.git#abcdef` | `sha1 = abcdef` | `/`
664
+`myrepo.git#:myfolder` | `refs/heads/master` | `/myfolder`
665
+`myrepo.git#master:myfolder` | `refs/heads/master` | `/myfolder`
666
+`myrepo.git#mytag:myfolder` | `refs/tags/mytag` | `/myfolder`
667
+`myrepo.git#mybranch:myfolder` | `refs/heads/mybranch` | `/myfolder`
668
+`myrepo.git#abcdef:myfolder` | `sha1 = abcdef` | `/myfolder`
669
+
647 670
 Instead of specifying a context, you can pass a single Dockerfile in the
648 671
 `URL` or pipe the file in via `STDIN`.  To pipe a Dockerfile from `STDIN`:
649 672
 
... ...
@@ -4221,6 +4221,35 @@ func (s *DockerSuite) TestBuildFromGIT(c *check.C) {
4221 4221
 	}
4222 4222
 }
4223 4223
 
4224
+func (s *DockerSuite) TestBuildFromGITWithContext(c *check.C) {
4225
+	name := "testbuildfromgit"
4226
+	defer deleteImages(name)
4227
+	git, err := fakeGIT("repo", map[string]string{
4228
+		"docker/Dockerfile": `FROM busybox
4229
+					ADD first /first
4230
+					RUN [ -f /first ]
4231
+					MAINTAINER docker`,
4232
+		"docker/first": "test git data",
4233
+	}, true)
4234
+	if err != nil {
4235
+		c.Fatal(err)
4236
+	}
4237
+	defer git.Close()
4238
+
4239
+	u := fmt.Sprintf("%s#master:docker", git.RepoURL)
4240
+	_, err = buildImageFromPath(name, u, true)
4241
+	if err != nil {
4242
+		c.Fatal(err)
4243
+	}
4244
+	res, err := inspectField(name, "Author")
4245
+	if err != nil {
4246
+		c.Fatal(err)
4247
+	}
4248
+	if res != "docker" {
4249
+		c.Fatalf("Maintainer should be docker, got %s", res)
4250
+	}
4251
+}
4252
+
4224 4253
 func (s *DockerSuite) TestBuildCleanupCmdOnEntrypoint(c *check.C) {
4225 4254
 	name := "testbuildcmdcleanuponentrypoint"
4226 4255
 	defer deleteImages(name)
... ...
@@ -1,6 +1,9 @@
1 1
 package urlutil
2 2
 
3
-import "strings"
3
+import (
4
+	"regexp"
5
+	"strings"
6
+)
4 7
 
5 8
 var (
6 9
 	validPrefixes = []string{
... ...
@@ -8,11 +11,13 @@ var (
8 8
 		"github.com/",
9 9
 		"git@",
10 10
 	}
11
+
12
+	urlPathWithFragmentSuffix = regexp.MustCompile(".git(?:#.+)?$")
11 13
 )
12 14
 
13 15
 // IsGitURL returns true if the provided str is a git repository URL.
14 16
 func IsGitURL(str string) bool {
15
-	if IsURL(str) && strings.HasSuffix(str, ".git") {
17
+	if IsURL(str) && urlPathWithFragmentSuffix.MatchString(str) {
16 18
 		return true
17 19
 	}
18 20
 	for _, prefix := range validPrefixes {
... ...
@@ -9,10 +9,15 @@ var (
9 9
 		"git@bitbucket.org:atlassianlabs/atlassian-docker.git",
10 10
 		"https://github.com/docker/docker.git",
11 11
 		"http://github.com/docker/docker.git",
12
+		"http://github.com/docker/docker.git#branch",
13
+		"http://github.com/docker/docker.git#:dir",
12 14
 	}
13 15
 	incompleteGitUrls = []string{
14 16
 		"github.com/docker/docker",
15 17
 	}
18
+	invalidGitUrls = []string{
19
+		"http://github.com/docker/docker.git:#branch",
20
+	}
16 21
 )
17 22
 
18 23
 func TestValidGitTransport(t *testing.T) {
... ...
@@ -35,9 +40,16 @@ func TestIsGIT(t *testing.T) {
35 35
 			t.Fatalf("%q should be detected as valid Git url", url)
36 36
 		}
37 37
 	}
38
+
38 39
 	for _, url := range incompleteGitUrls {
39 40
 		if IsGitURL(url) == false {
40 41
 			t.Fatalf("%q should be detected as valid Git url", url)
41 42
 		}
42 43
 	}
44
+
45
+	for _, url := range invalidGitUrls {
46
+		if IsGitURL(url) == true {
47
+			t.Fatalf("%q should not be detected as valid Git prefix", url)
48
+		}
49
+	}
43 50
 }
... ...
@@ -4,7 +4,10 @@ import (
4 4
 	"fmt"
5 5
 	"io/ioutil"
6 6
 	"net/http"
7
+	"net/url"
8
+	"os"
7 9
 	"os/exec"
10
+	"path/filepath"
8 11
 	"strings"
9 12
 
10 13
 	"github.com/docker/docker/pkg/urlutil"
... ...
@@ -19,20 +22,26 @@ func GitClone(remoteURL string) (string, error) {
19 19
 		return "", err
20 20
 	}
21 21
 
22
-	clone := cloneArgs(remoteURL, root)
22
+	u, err := url.Parse(remoteURL)
23
+	if err != nil {
24
+		return "", err
25
+	}
23 26
 
24
-	if output, err := exec.Command("git", clone...).CombinedOutput(); err != nil {
27
+	fragment := u.Fragment
28
+	clone := cloneArgs(u, root)
29
+
30
+	if output, err := git(clone...); err != nil {
25 31
 		return "", fmt.Errorf("Error trying to use git: %s (%s)", err, output)
26 32
 	}
27 33
 
28
-	return root, nil
34
+	return checkoutGit(fragment, root)
29 35
 }
30 36
 
31
-func cloneArgs(remoteURL, root string) []string {
37
+func cloneArgs(remoteURL *url.URL, root string) []string {
32 38
 	args := []string{"clone", "--recursive"}
33
-	shallow := true
39
+	shallow := len(remoteURL.Fragment) == 0
34 40
 
35
-	if strings.HasPrefix(remoteURL, "http") {
41
+	if shallow && strings.HasPrefix(remoteURL.Scheme, "http") {
36 42
 		res, err := http.Head(fmt.Sprintf("%s/info/refs?service=git-upload-pack", remoteURL))
37 43
 		if err != nil || res.Header.Get("Content-Type") != "application/x-git-upload-pack-advertisement" {
38 44
 			shallow = false
... ...
@@ -43,5 +52,42 @@ func cloneArgs(remoteURL, root string) []string {
43 43
 		args = append(args, "--depth", "1")
44 44
 	}
45 45
 
46
-	return append(args, remoteURL, root)
46
+	if remoteURL.Fragment != "" {
47
+		remoteURL.Fragment = ""
48
+	}
49
+
50
+	return append(args, remoteURL.String(), root)
51
+}
52
+
53
+func checkoutGit(fragment, root string) (string, error) {
54
+	refAndDir := strings.SplitN(fragment, ":", 2)
55
+
56
+	if len(refAndDir[0]) != 0 {
57
+		if output, err := gitWithinDir(root, "checkout", refAndDir[0]); err != nil {
58
+			return "", fmt.Errorf("Error trying to use git: %s (%s)", err, output)
59
+		}
60
+	}
61
+
62
+	if len(refAndDir) > 1 && len(refAndDir[1]) != 0 {
63
+		newCtx := filepath.Join(root, refAndDir[1])
64
+		fi, err := os.Stat(newCtx)
65
+		if err != nil {
66
+			return "", err
67
+		}
68
+		if !fi.IsDir() {
69
+			return "", fmt.Errorf("Error setting git context, not a directory: %s", newCtx)
70
+		}
71
+		root = newCtx
72
+	}
73
+
74
+	return root, nil
75
+}
76
+
77
+func gitWithinDir(dir string, args ...string) ([]byte, error) {
78
+	a := []string{"--work-tree", dir, "--git-dir", filepath.Join(dir, ".git")}
79
+	return git(append(a, args...)...)
80
+}
81
+
82
+func git(args ...string) ([]byte, error) {
83
+	return exec.Command("git", args...).CombinedOutput()
47 84
 }
... ...
@@ -2,9 +2,12 @@ package utils
2 2
 
3 3
 import (
4 4
 	"fmt"
5
+	"io/ioutil"
5 6
 	"net/http"
6 7
 	"net/http/httptest"
7 8
 	"net/url"
9
+	"os"
10
+	"path/filepath"
8 11
 	"reflect"
9 12
 	"testing"
10 13
 )
... ...
@@ -22,7 +25,7 @@ func TestCloneArgsSmartHttp(t *testing.T) {
22 22
 		w.Header().Set("Content-Type", fmt.Sprintf("application/x-%s-advertisement", q))
23 23
 	})
24 24
 
25
-	args := cloneArgs(gitURL, "/tmp")
25
+	args := cloneArgs(serverURL, "/tmp")
26 26
 	exp := []string{"clone", "--recursive", "--depth", "1", gitURL, "/tmp"}
27 27
 	if !reflect.DeepEqual(args, exp) {
28 28
 		t.Fatalf("Expected %v, got %v", exp, args)
... ...
@@ -41,16 +44,132 @@ func TestCloneArgsDumbHttp(t *testing.T) {
41 41
 		w.Header().Set("Content-Type", "text/plain")
42 42
 	})
43 43
 
44
-	args := cloneArgs(gitURL, "/tmp")
44
+	args := cloneArgs(serverURL, "/tmp")
45 45
 	exp := []string{"clone", "--recursive", gitURL, "/tmp"}
46 46
 	if !reflect.DeepEqual(args, exp) {
47 47
 		t.Fatalf("Expected %v, got %v", exp, args)
48 48
 	}
49 49
 }
50
+
50 51
 func TestCloneArgsGit(t *testing.T) {
51
-	args := cloneArgs("git://github.com/docker/docker", "/tmp")
52
+	u, _ := url.Parse("git://github.com/docker/docker")
53
+	args := cloneArgs(u, "/tmp")
52 54
 	exp := []string{"clone", "--recursive", "--depth", "1", "git://github.com/docker/docker", "/tmp"}
53 55
 	if !reflect.DeepEqual(args, exp) {
54 56
 		t.Fatalf("Expected %v, got %v", exp, args)
55 57
 	}
56 58
 }
59
+
60
+func TestCloneArgsStripFragment(t *testing.T) {
61
+	u, _ := url.Parse("git://github.com/docker/docker#test")
62
+	args := cloneArgs(u, "/tmp")
63
+	exp := []string{"clone", "--recursive", "git://github.com/docker/docker", "/tmp"}
64
+	if !reflect.DeepEqual(args, exp) {
65
+		t.Fatalf("Expected %v, got %v", exp, args)
66
+	}
67
+}
68
+
69
+func TestCheckoutGit(t *testing.T) {
70
+	root, err := ioutil.TempDir("", "docker-build-git-checkout")
71
+	if err != nil {
72
+		t.Fatal(err)
73
+	}
74
+	defer os.RemoveAll(root)
75
+
76
+	gitDir := filepath.Join(root, "repo")
77
+	_, err = git("init", gitDir)
78
+	if err != nil {
79
+		t.Fatal(err)
80
+	}
81
+
82
+	if _, err = gitWithinDir(gitDir, "config", "user.email", "test@docker.com"); err != nil {
83
+		t.Fatal(err)
84
+	}
85
+
86
+	if _, err = gitWithinDir(gitDir, "config", "user.name", "Docker test"); err != nil {
87
+		t.Fatal(err)
88
+	}
89
+
90
+	if err = ioutil.WriteFile(filepath.Join(gitDir, "Dockerfile"), []byte("FROM scratch"), 0644); err != nil {
91
+		t.Fatal(err)
92
+	}
93
+
94
+	subDir := filepath.Join(gitDir, "subdir")
95
+	if err = os.Mkdir(subDir, 0755); err != nil {
96
+		t.Fatal(err)
97
+	}
98
+
99
+	if err = ioutil.WriteFile(filepath.Join(subDir, "Dockerfile"), []byte("FROM scratch\nEXPOSE 5000"), 0644); err != nil {
100
+		t.Fatal(err)
101
+	}
102
+
103
+	if _, err = gitWithinDir(gitDir, "add", "-A"); err != nil {
104
+		t.Fatal(err)
105
+	}
106
+
107
+	if _, err = gitWithinDir(gitDir, "commit", "-am", "First commit"); err != nil {
108
+		t.Fatal(err)
109
+	}
110
+
111
+	if _, err = gitWithinDir(gitDir, "checkout", "-b", "test"); err != nil {
112
+		t.Fatal(err)
113
+	}
114
+
115
+	if err = ioutil.WriteFile(filepath.Join(gitDir, "Dockerfile"), []byte("FROM scratch\nEXPOSE 3000"), 0644); err != nil {
116
+		t.Fatal(err)
117
+	}
118
+
119
+	if err = ioutil.WriteFile(filepath.Join(subDir, "Dockerfile"), []byte("FROM busybox\nEXPOSE 5000"), 0644); err != nil {
120
+		t.Fatal(err)
121
+	}
122
+
123
+	if _, err = gitWithinDir(gitDir, "add", "-A"); err != nil {
124
+		t.Fatal(err)
125
+	}
126
+
127
+	if _, err = gitWithinDir(gitDir, "commit", "-am", "Branch commit"); err != nil {
128
+		t.Fatal(err)
129
+	}
130
+
131
+	if _, err = gitWithinDir(gitDir, "checkout", "master"); err != nil {
132
+		t.Fatal(err)
133
+	}
134
+
135
+	cases := []struct {
136
+		frag string
137
+		exp  string
138
+		fail bool
139
+	}{
140
+		{"", "FROM scratch", false},
141
+		{"master", "FROM scratch", false},
142
+		{":subdir", "FROM scratch\nEXPOSE 5000", false},
143
+		{":nosubdir", "", true},   // missing directory error
144
+		{":Dockerfile", "", true}, // not a directory error
145
+		{"master:nosubdir", "", true},
146
+		{"master:subdir", "FROM scratch\nEXPOSE 5000", false},
147
+		{"test", "FROM scratch\nEXPOSE 3000", false},
148
+		{"test:", "FROM scratch\nEXPOSE 3000", false},
149
+		{"test:subdir", "FROM busybox\nEXPOSE 5000", false},
150
+	}
151
+
152
+	for _, c := range cases {
153
+		r, err := checkoutGit(c.frag, gitDir)
154
+
155
+		fail := err != nil
156
+		if fail != c.fail {
157
+			t.Fatalf("Expected %v failure, error was %v\n", c.fail, err)
158
+		}
159
+		if c.fail {
160
+			continue
161
+		}
162
+
163
+		b, err := ioutil.ReadFile(filepath.Join(r, "Dockerfile"))
164
+		if err != nil {
165
+			t.Fatal(err)
166
+		}
167
+
168
+		if string(b) != c.exp {
169
+			t.Fatalf("Expected %v, was %v\n", c.exp, string(b))
170
+		}
171
+	}
172
+}