nebius/SWE-rebench-V2-PRs huggingface dataset:
Repository: compose-spec/compose-go
Pull request #566 (2024-02-06 13:28:20)

introduce port.name
introduce port.name to document role of a port within a service

laurazard: Maybe we can add a test for this/add it to one of the full parse tests?
ndeloof: added a (dedicated) test - I prefer we avoid making the "full struct" test even more complex :)
ndeloof: > Is the name an optional attribut, should we adapt the portsIndexer

no, this one is not part of the key, this is just metadata - while this probably should be unique, but this is only for user information, so \\_(ツ)_/


Patch:
diff --git a/schema/compose-spec.json b/schema/compose-spec.json
index f40c6ee..552a5e5 100644
--- a/schema/compose-spec.json
+++ b/schema/compose-spec.json
@@ -322,6 +322,7 @@
               {
                 "type": "object",
                 "properties": {
+                  "name": {"type": "string"},
                   "mode": {"type": "string"},
                   "host_ip": {"type": "string"},
                   "target": {"type": "integer"},
diff --git a/types/types.go b/types/types.go
index 0993d76..2bdf0dd 100644
--- a/types/types.go
+++ b/types/types.go
@@ -433,6 +433,7 @@ type ServiceNetworkConfig struct {
 
 // ServicePortConfig is the port configuration for a service
 type ServicePortConfig struct {
+	Name      string `yaml:"name,omitempty" json:"name,omitempty"`
 	Mode      string `yaml:"mode,omitempty" json:"mode,omitempty"`
 	HostIP    string `yaml:"host_ip,omitempty" json:"host_ip,omitempty"`
 	Target    uint32 `yaml:"target,omitempty" json:"target,omitempty"`


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index 525b833..d139ae3 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -3148,3 +3148,30 @@ services:
 	assert.NilError(t, err)
 	assert.Equal(t, p.Name, "test-with-empty-file")
 }
+
+func TestNamedPort(t *testing.T) {
+	yaml := `
+name: test-named-ports
+services:
+  test:
+    image: foo
+    ports:
+      - name: http
+        published: 8080
+        target: 80
+      - name: https
+        published: 8083
+        target: 443
+`
+	p, err := LoadWithContext(context.Background(), types.ConfigDetails{
+		ConfigFiles: []types.ConfigFile{
+			{
+				Content: []byte(yaml),
+			},
+		},
+	})
+	assert.NilError(t, err)
+	ports := p.Services["test"].Ports
+	assert.Equal(t, ports[0].Name, "http")
+	assert.Equal(t, ports[1].Name, "https")
+}
Repository: compose-spec/compose-go
Pull request #569 (2024-02-07 14:36:04)

Rename ServicesBuild -> ServicesWithBuild
Missing rename in previous PR. Only noticed after auto-merge


Patch:
diff --git a/types/project.go b/types/project.go
index c67db1d..e62a2fd 100644
--- a/types/project.go
+++ b/types/project.go
@@ -120,7 +120,7 @@ func (p *Project) ConfigNames() []string {
 	return names
 }
 
-func (p *Project) ServicesBuild() []string {
+func (p *Project) ServicesWithBuild() []string {
 	servicesBuild := p.Services.Filter(func(s ServiceConfig) bool {
 		return s.Build != nil && s.Build.Context != ""
 	})


Test patch:
diff --git a/types/project_test.go b/types/project_test.go
index 39ed1b0..910ec57 100644
--- a/types/project_test.go
+++ b/types/project_test.go
@@ -242,15 +242,15 @@ func TestWithServices(t *testing.T) {
 	assert.DeepEqual(t, seen, []string{"service_1", "service_2", "service_4"})
 }
 
-func TestBuildServices(t *testing.T) {
+func TestServicesWithBuild(t *testing.T) {
 	p := makeProject()
-	assert.DeepEqual(t, []string{}, p.ServicesBuild())
+	assert.DeepEqual(t, []string{}, p.ServicesWithBuild())
 
 	service, err := p.GetService("service_1")
 	assert.NilError(t, err)
 	service.Build = &BuildConfig{}
 	p.Services["service_1"] = service
-	assert.DeepEqual(t, []string{}, p.ServicesBuild())
+	assert.DeepEqual(t, []string{}, p.ServicesWithBuild())
 
 	service.Build = &BuildConfig{
 		Context: ".",
@@ -262,7 +262,7 @@ func TestBuildServices(t *testing.T) {
 		Context: ".",
 	}
 	p.Services["service_2"] = service
-	services := p.ServicesBuild()
+	services := p.ServicesWithBuild()
 	slices.Sort(services)
 	assert.DeepEqual(t, []string{"service_1", "service_2"}, services)
 }
Repository: compose-spec/compose-go
Pull request #580 (2024-02-16 23:54:38)

NetworksByPriority: ensure the sort order is stable
Related to:
- https://github.com/docker/compose/issues/11510
- https://github.com/docker/compose/pull/11429

NetworksByPriority calls sort.Slice() to sort the map of networks attached to a given service by their respective priority. The Priority property being non mandatory, it defaults to 0 when unspecified.

The godoc for sort.Slice() specifies:

> The sort is not guaranteed to be stable: equal elements may be reversed from their original order. For a stable sort, use SliceStable.

SliceStable() won't work either, since NetworksByPriority has to transform the map of networks into a list before calling sort.Slice() -- Go makes no guarantee about the order of items in a map.

The only way to guarantee a stable order is to fall back to lexicographic sorting when two networks have the same priority. It seems to be the best option as the compose-spec doesn't specify how multiple network attachment referencing the same network should be handled.

A regression test has been added.


Patch:
diff --git a/types/types.go b/types/types.go
index 1c5cca7..35c4a4e 100644
--- a/types/types.go
+++ b/types/types.go
@@ -162,6 +162,9 @@ func (s *ServiceConfig) NetworksByPriority() []string {
 		})
 	}
 	sort.Slice(keys, func(i, j int) bool {
+		if keys[i].priority == keys[j].priority {
+			return keys[i].name < keys[j].name
+		}
 		return keys[i].priority > keys[j].priority
 	})
 	var sorted []string


Test patch:
diff --git a/types/types_test.go b/types/types_test.go
index 08df5c8..500f8b6 100644
--- a/types/types_test.go
+++ b/types/types_test.go
@@ -269,6 +269,18 @@ func TestNetworksByPriority(t *testing.T) {
 	assert.DeepEqual(t, s.NetworksByPriority(), []string{"qix", "zot", "bar", "foo"})
 }
 
+func TestNetworksByPriorityWithEqualPriorities(t *testing.T) {
+	s := ServiceConfig{
+		Networks: map[string]*ServiceNetworkConfig{
+			"foo": nil,
+			"bar": nil,
+			"zot": nil,
+			"qix": nil,
+		},
+	}
+	assert.DeepEqual(t, s.NetworksByPriority(), []string{"bar", "foo", "qix", "zot"})
+}
+
 func TestMarshalServiceEntrypoint(t *testing.T) {
 	t.Parallel()
Repository: compose-spec/compose-go
Pull request #581 (2024-02-18 00:26:01)

Fix broken interpolation with JSON (curly braces) default values
Hopefully should be enough to fix a downstream docker compose issue:

```yaml
   environment:
      PARSING_LIMITS: ${PARSING_LIMITS:-{"commits":1}}
```

Now it returns:

```sh
$ export PARSING_LIMITS='{"commits":2}'
$ docker-compose -f docker-compose.yaml config | grep PARSING
PARSING_LIMITS: '{"commits":2}}'
```

Mind an extra `}` at the end of the value.

It prevents from using JSON string as a default value for an environment variable.


Patch:
diff --git a/template/template.go b/template/template.go
index 9367f39..63ae61c 100644
--- a/template/template.go
+++ b/template/template.go
@@ -258,7 +258,7 @@ func getFirstBraceClosingIndex(s string) int {
 				return i
 			}
 		}
-		if strings.HasPrefix(s[i:], "${") {
+		if s[i] == '{' {
 			openVariableBraces++
 			i++
 		}


Test patch:
diff --git a/template/template_test.go b/template/template_test.go
index dbd27de..66fa3c5 100644
--- a/template/template_test.go
+++ b/template/template_test.go
@@ -26,8 +26,9 @@ import (
 )
 
 var defaults = map[string]string{
-	"FOO": "first",
-	"BAR": "",
+	"FOO":  "first",
+	"BAR":  "",
+	"JSON": `{"json":2}`,
 }
 
 func defaultMapping(name string) (string, bool) {
@@ -630,3 +631,19 @@ func TestSubstitutionFunctionChoice(t *testing.T) {
 		})
 	}
 }
+
+func TestNoValueWithCurlyBracesDefault(t *testing.T) {
+	for _, template := range []string{`ok ${missing:-{"json":1}}`, `ok ${missing-{"json":1}}`} {
+		result, err := Substitute(template, defaultMapping)
+		assert.NilError(t, err)
+		assert.Check(t, is.Equal(`ok {"json":1}`, result))
+	}
+}
+
+func TestValueWithCurlyBracesDefault(t *testing.T) {
+	for _, template := range []string{`ok ${JSON:-{"json":1}}`, `ok ${JSON-{"json":1}}`} {
+		result, err := Substitute(template, defaultMapping)
+		assert.NilError(t, err)
+		assert.Check(t, is.Equal(`ok {"json":2}`, result))
+	}
+}
Repository: compose-spec/compose-go
Pull request #594 (2024-03-11 11:06:19)

Add volume subpath option
- related: https://github.com/compose-spec/compose-spec/pull/471

Only supported in the long syntax.


Patch:
diff --git a/schema/compose-spec.json b/schema/compose-spec.json
index e62ca35..f40c6ee 100644
--- a/schema/compose-spec.json
+++ b/schema/compose-spec.json
@@ -389,7 +389,8 @@
                   "volume": {
                     "type": "object",
                     "properties": {
-                      "nocopy": {"type": "boolean"}
+                      "nocopy": {"type": "boolean"},
+                      "subpath": {"type": "string"}
                     },
                     "additionalProperties": false,
                     "patternProperties": {"^x-": {}}
diff --git a/types/types.go b/types/types.go
index 35c4a4e..0993d76 100644
--- a/types/types.go
+++ b/types/types.go
@@ -511,6 +511,9 @@ func (s ServiceVolumeConfig) String() string {
 	if s.Volume != nil && s.Volume.NoCopy {
 		options = append(options, "nocopy")
 	}
+	if s.Volume != nil && s.Volume.Subpath != "" {
+		options = append(options, s.Volume.Subpath)
+	}
 	return fmt.Sprintf("%s:%s:%s", s.Source, s.Target, strings.Join(options, ","))
 }
 
@@ -567,7 +570,8 @@ const (
 
 // ServiceVolumeVolume are options for a service volume of type volume
 type ServiceVolumeVolume struct {
-	NoCopy bool `yaml:"nocopy,omitempty" json:"nocopy,omitempty"`
+	NoCopy  bool   `yaml:"nocopy,omitempty" json:"nocopy,omitempty"`
+	Subpath string `yaml:"subpath,omitempty" json:"subpath,omitempty"`
 
 	Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
 }


Test patch:
diff --git a/format/volume_test.go b/format/volume_test.go
index c4de0b2..3e6dc33 100644
--- a/format/volume_test.go
+++ b/format/volume_test.go
@@ -177,6 +177,14 @@ func TestParseVolumeWithVolumeOptions(t *testing.T) {
 	assert.Check(t, is.DeepEqual(expected, volume))
 }
 
+func TestParseVolumeWithVolumeOptionsSubpath(t *testing.T) {
+	volume, err := ParseVolume("name:/target:nocopy,subpath=etc")
+	assert.NilError(t, err)
+
+	// subpath option is not supported in the short syntax
+	assert.Check(t, is.Equal(volume.Volume.Subpath, ""))
+}
+
 func TestParseVolumeWithReadOnly(t *testing.T) {
 	for _, path := range []string{"./foo", "/home/user"} {
 		volume, err := ParseVolume(path + ":/target:ro")
diff --git a/loader/loader_test.go b/loader/loader_test.go
index b6c613e..525b833 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -1390,6 +1390,23 @@ networks:
 	assert.Check(t, is.DeepEqual(expected, config.Networks))
 }
 
+func TestLoadVolumeSubpath(t *testing.T) {
+	config, err := loadYAML(`
+name: load-volume-subpath
+services:
+  test:
+    image: alpine:latest
+    volumes:
+      - type: volume
+        source: asdf
+        target: /app
+        volume:
+          subpath: etc/settings
+`)
+	assert.Check(t, err)
+	assert.Check(t, is.Equal(config.Services["test"].Volumes[0].Volume.Subpath, "etc/settings"))
+}
+
 func TestLoadExpandedPortFormat(t *testing.T) {
 	config, err := loadYAML(`
 name: load-expanded-port-format
Repository: compose-spec/compose-go
Pull request #597 (2024-03-14 17:10:11)

application (level 7) protocol on ports
see https://github.com/compose-spec/compose-spec/pull/472


Patch:
diff --git a/schema/compose-spec.json b/schema/compose-spec.json
index 552a5e5..3a4ee72 100644
--- a/schema/compose-spec.json
+++ b/schema/compose-spec.json
@@ -327,7 +327,8 @@
                   "host_ip": {"type": "string"},
                   "target": {"type": "integer"},
                   "published": {"type": ["string", "integer"]},
-                  "protocol": {"type": "string"}
+                  "protocol": {"type": "string"},
+                  "app_protocol": {"type": "string"}
                 },
                 "additionalProperties": false,
                 "patternProperties": {"^x-": {}}
diff --git a/types/types.go b/types/types.go
index 2bdf0dd..8144180 100644
--- a/types/types.go
+++ b/types/types.go
@@ -433,12 +433,13 @@ type ServiceNetworkConfig struct {
 
 // ServicePortConfig is the port configuration for a service
 type ServicePortConfig struct {
-	Name      string `yaml:"name,omitempty" json:"name,omitempty"`
-	Mode      string `yaml:"mode,omitempty" json:"mode,omitempty"`
-	HostIP    string `yaml:"host_ip,omitempty" json:"host_ip,omitempty"`
-	Target    uint32 `yaml:"target,omitempty" json:"target,omitempty"`
-	Published string `yaml:"published,omitempty" json:"published,omitempty"`
-	Protocol  string `yaml:"protocol,omitempty" json:"protocol,omitempty"`
+	Name        string `yaml:"name,omitempty" json:"name,omitempty"`
+	Mode        string `yaml:"mode,omitempty" json:"mode,omitempty"`
+	HostIP      string `yaml:"host_ip,omitempty" json:"host_ip,omitempty"`
+	Target      uint32 `yaml:"target,omitempty" json:"target,omitempty"`
+	Published   string `yaml:"published,omitempty" json:"published,omitempty"`
+	Protocol    string `yaml:"protocol,omitempty" json:"protocol,omitempty"`
+	AppProtocol string `yaml:"app_protocol,omitempty" json:"app_protocol,omitempty"`
 
 	Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
 }


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index d139ae3..2032b16 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -3175,3 +3175,32 @@ services:
 	assert.Equal(t, ports[0].Name, "http")
 	assert.Equal(t, ports[1].Name, "https")
 }
+
+func TestAppProtocol(t *testing.T) {
+	yaml := `
+name: test-named-ports
+services:
+  test:
+    image: foo
+    ports:
+      - published: 8080
+        target: 80
+        protocol: tcp
+        app_protocol: http
+      - published: 8083
+        target: 443
+        protocol: tcp
+        app_protocol: https
+`
+	p, err := LoadWithContext(context.Background(), types.ConfigDetails{
+		ConfigFiles: []types.ConfigFile{
+			{
+				Content: []byte(yaml),
+			},
+		},
+	})
+	assert.NilError(t, err)
+	ports := p.Services["test"].Ports
+	assert.Equal(t, ports[0].AppProtocol, "http")
+	assert.Equal(t, ports[1].AppProtocol, "https")
+}
Repository: compose-spec/compose-go
Pull request #608 (2024-03-21 09:33:04)

make defaultPattern public otherwise ExtractVariables isn't usable


Patch:
diff --git a/template/template.go b/template/template.go
index 63ae61c..0d9c17e 100644
--- a/template/template.go
+++ b/template/template.go
@@ -44,7 +44,7 @@ var patternString = fmt.Sprintf(
 	groupInvalid,
 )
 
-var defaultPattern = regexp.MustCompile(patternString)
+var DefaultPattern = regexp.MustCompile(patternString)
 
 // InvalidTemplateError is returned when a variable template is not in a valid
 // format
@@ -121,7 +121,7 @@ func SubstituteWithOptions(template string, mapping Mapping, options ...Option)
 	var returnErr error
 
 	cfg := &Config{
-		pattern:         defaultPattern,
+		pattern:         DefaultPattern,
 		replacementFunc: DefaultReplacementFunc,
 		logging:         true,
 	}
@@ -268,14 +268,14 @@ func getFirstBraceClosingIndex(s string) int {
 
 // Substitute variables in the string with their values
 func Substitute(template string, mapping Mapping) (string, error) {
-	return SubstituteWith(template, mapping, defaultPattern)
+	return SubstituteWith(template, mapping, DefaultPattern)
 }
 
 // ExtractVariables returns a map of all the variables defined in the specified
 // composefile (dict representation) and their default value if any.
 func ExtractVariables(configDict map[string]interface{}, pattern *regexp.Regexp) map[string]Variable {
 	if pattern == nil {
-		pattern = defaultPattern
+		pattern = DefaultPattern
 	}
 	return recurseExtract(configDict, pattern)
 }


Test patch:
diff --git a/template/template_test.go b/template/template_test.go
index 66fa3c5..b1afa21 100644
--- a/template/template_test.go
+++ b/template/template_test.go
@@ -366,15 +366,15 @@ func TestSubstituteWithCustomFunc(t *testing.T) {
 		return value, true, nil
 	}
 
-	result, err := SubstituteWith("ok ${FOO}", defaultMapping, defaultPattern, errIsMissing)
+	result, err := SubstituteWith("ok ${FOO}", defaultMapping, DefaultPattern, errIsMissing)
 	assert.NilError(t, err)
 	assert.Check(t, is.Equal("ok first", result))
 
-	result, err = SubstituteWith("ok ${BAR}", defaultMapping, defaultPattern, errIsMissing)
+	result, err = SubstituteWith("ok ${BAR}", defaultMapping, DefaultPattern, errIsMissing)
 	assert.NilError(t, err)
 	assert.Check(t, is.Equal("ok ", result))
 
-	_, err = SubstituteWith("ok ${NOTHERE}", defaultMapping, defaultPattern, errIsMissing)
+	_, err = SubstituteWith("ok ${NOTHERE}", defaultMapping, DefaultPattern, errIsMissing)
 	assert.Check(t, is.ErrorContains(err, "required variable"))
 }
 
@@ -594,7 +594,7 @@ func TestExtractVariables(t *testing.T) {
 	for _, tc := range testCases {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
-			actual := ExtractVariables(tc.dict, defaultPattern)
+			actual := ExtractVariables(tc.dict, DefaultPattern)
 			assert.Check(t, is.DeepEqual(actual, tc.expected))
 		})
 	}
Repository: compose-spec/compose-go
Pull request #610 (2024-03-27 07:03:58)

merge devices by unique mount path
`devices` must be merged like volumes, target mount path being unique


Patch:
diff --git a/override/uncity.go b/override/uncity.go
index 9cbc45f..057b2f2 100644
--- a/override/uncity.go
+++ b/override/uncity.go
@@ -42,6 +42,7 @@ func init() {
 	unique["services.*.build.labels"] = keyValueIndexer
 	unique["services.*.cap_add"] = keyValueIndexer
 	unique["services.*.cap_drop"] = keyValueIndexer
+	unique["services.*.devices"] = volumeIndexer
 	unique["services.*.configs"] = mountIndexer("")
 	unique["services.*.deploy.labels"] = keyValueIndexer
 	unique["services.*.dns"] = keyValueIndexer


Test patch:
diff --git a/override/merge_devices_test.go b/override/merge_devices_test.go
new file mode 100644
index 0000000..1f9be62
--- /dev/null
+++ b/override/merge_devices_test.go
@@ -0,0 +1,84 @@
+/*
+   Copyright 2020 The Compose Specification Authors.
+
+   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.
+*/
+
+package override
+
+import (
+	"testing"
+)
+
+func Test_mergeYamlDevices(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    devices:
+      - '/dev/sda:/dev/sda'
+      - '/dev/sdb:/dev/sdb'
+      - '/dev/sdc:/dev/sdc'
+      - '/dev/sdd:/dev/sdd'
+`, `
+services:
+  test:
+    devices:
+      - '/dev/sde:/dev/sde'
+      - '/dev/sdf:/dev/sdf'
+      - '/dev/sdg:/dev/sdg'
+      - '/dev/sdh:/dev/sdh'
+`, `
+services:
+  test:
+    image: foo
+    devices:
+      - '/dev/sda:/dev/sda'
+      - '/dev/sdb:/dev/sdb'
+      - '/dev/sdc:/dev/sdc'
+      - '/dev/sdd:/dev/sdd'
+      - '/dev/sde:/dev/sde'
+      - '/dev/sdf:/dev/sdf'
+      - '/dev/sdg:/dev/sdg'
+      - '/dev/sdh:/dev/sdh'
+`)
+}
+
+func Test_mergeYamlDevicesOverride(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    devices:
+      - '/dev/sda:/dev/sda'
+      - '/dev/sdb:/dev/sdb'
+      - '/dev/sdc:/dev/sdc'
+      - '/dev/sdd:/dev/sdd'
+`, `
+services:
+  test:
+    devices:
+      - '/dev/nvme0n1p1:/dev/sda'
+      - '/dev/nvme1n1p1:/dev/sdb'
+      - '/dev/nvme2n1p1:/dev/sdc'
+`, `
+services:
+  test:
+    image: foo
+    devices:
+      - '/dev/nvme0n1p1:/dev/sda'
+      - '/dev/nvme1n1p1:/dev/sdb'
+      - '/dev/nvme2n1p1:/dev/sdc'
+      - '/dev/sdd:/dev/sdd'
+`)
+}
Repository: compose-spec/compose-go
Pull request #619 (2024-04-11 07:05:34)

remove constraint for container_name unicity while loading model
such validation should occur later, as we actually select containers to be created

see https://github.com/docker/compose/issues/11619#issuecomment-2047995758 for context


Patch:
diff --git a/loader/validate.go b/loader/validate.go
index 973e521..cfaf139 100644
--- a/loader/validate.go
+++ b/loader/validate.go
@@ -28,7 +28,6 @@ import (
 
 // checkConsistency validate a compose model is consistent
 func checkConsistency(project *types.Project) error {
-	containerNames := map[string]string{}
 	for _, s := range project.Services {
 		if s.Build == nil && s.Image == "" {
 			return fmt.Errorf("service %q has neither an image nor a build context specified: %w", s.Name, errdefs.ErrInvalid)
@@ -145,13 +144,6 @@ func checkConsistency(project *types.Project) error {
 			}
 		}
 
-		if s.ContainerName != "" {
-			if existing, ok := containerNames[s.ContainerName]; ok {
-				return fmt.Errorf(`"services.%s": container name "%s" is already in use by "services.%s": %w`, s.Name, s.ContainerName, existing, errdefs.ErrInvalid)
-			}
-			containerNames[s.ContainerName] = s.Name
-		}
-
 		if s.GetScale() > 1 && s.ContainerName != "" {
 			attr := "scale"
 			if s.Scale == nil {
diff --git a/types/project.go b/types/project.go
index 780dd91..b86a563 100644
--- a/types/project.go
+++ b/types/project.go
@@ -696,3 +696,17 @@ func (p *Project) WithServicesTransform(fn func(name string, s ServiceConfig) (S
 	}
 	return newProject, eg.Wait()
 }
+
+// CheckContainerNameUnicity validate project doesn't have services declaring the same container_name
+func (p *Project) CheckContainerNameUnicity() error {
+	names := utils.Set[string]{}
+	for name, s := range p.Services {
+		if s.ContainerName != "" {
+			if existing, ok := names[s.ContainerName]; ok {
+				return fmt.Errorf(`services.%s: container name %q is already in use by service %s"`, name, s.ContainerName, existing)
+			}
+			names.Add(s.ContainerName)
+		}
+	}
+	return nil
+}


Test patch:
diff --git a/loader/validate_test.go b/loader/validate_test.go
index d4a2de6..0575a02 100644
--- a/loader/validate_test.go
+++ b/loader/validate_test.go
@@ -262,7 +262,7 @@ func TestValidateDependsOn(t *testing.T) {
 }
 
 func TestValidateContainerName(t *testing.T) {
-	project := types.Project{
+	project := &types.Project{
 		Services: types.Services{
 			"myservice": {
 				Name:          "myservice",
@@ -276,7 +276,7 @@ func TestValidateContainerName(t *testing.T) {
 			},
 		},
 	}
-	err := checkConsistency(&project)
+	err := project.CheckContainerNameUnicity()
 	assert.Assert(t, strings.Contains(err.Error(), `container name "mycontainer" is already in use by`))
 }