nebius/SWE-rebench-V2-PRs huggingface dataset:
Repository: compose-spec/compose-go
Pull request #405 (2023-05-16 07:20:06)

fix nested var expansion
The previously used pattern `(.*}|[^}]*)` basically means "_either a nested value, identified by a closing brace, or a plain text value with no brace_". This is pretty complicated way to say we need to take anything that comes next, and recursively interpolate value.

closes https://github.com/compose-spec/compose-go/issues/401


Patch:
diff --git a/loader/volume.go b/loader/volume.go
index dd83414..f1e66cd 100644
--- a/loader/volume.go
+++ b/loader/volume.go
@@ -176,8 +176,5 @@ func isFilePath(source string) bool {
 	}
 
 	first, nextIndex := utf8.DecodeRuneInString(source)
-	if len(source) <= nextIndex {
-		return false
-	}
 	return isWindowsDrive([]rune{first}, rune(source[nextIndex]))
 }
diff --git a/template/template.go b/template/template.go
index f9b52f6..3d548e0 100644
--- a/template/template.go
+++ b/template/template.go
@@ -28,7 +28,7 @@ import (
 var delimiter = "\\$"
 var substitutionNamed = "[_a-z][_a-z0-9]*"
 
-var substitutionBraced = "[_a-z][_a-z0-9]*(?::?[-+?](.*}|[^}]*))?"
+var substitutionBraced = "[_a-z][_a-z0-9]*(?::?[-+?](.*))?"
 
 var patternString = fmt.Sprintf(
 	"%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?:(?P<braced>%s)}|(?P<invalid>)))",


Test patch:
diff --git a/interpolation/interpolation_test.go b/interpolation/interpolation_test.go
index 4c6526b..29d83ef 100644
--- a/interpolation/interpolation_test.go
+++ b/interpolation/interpolation_test.go
@@ -119,6 +119,11 @@ func TestValidUnexistentInterpolation(t *testing.T) {
 		{test: "{{{ ${BAR} }}}", expected: "{{{  }}}"},
 		{test: "${FOO:?baz} }}}", errMsg: "baz"},
 		{test: "${FOO?baz} }}}", errMsg: "baz"},
+		// nested variables
+		{test: "${FOO:-${BAR:-${ZOT:-qix}}}", expected: "qix"},
+		{test: "${FOO:-${BAR:-x}_test_${BAR:-y}}", expected: "x_test_y"},
+		{test: "${FOO:-${BAR:-x}_test}", expected: "x_test"},
+		{test: "${FOO:-${BAR:-${ZOT:-x}}_test}", expected: "x_test"},
 	}
 
 	getServiceConfig := func(val string) map[string]interface{} {
diff --git a/loader/loader_test.go b/loader/loader_test.go
index 1ae80b0..cf27efc 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -2354,34 +2354,3 @@ func TestInvalidProjectNameType(t *testing.T) {
 	assert.Error(t, err, "validating filename0.yml: name must be a string")
 	assert.Assert(t, is.Nil(p))
 }
-
-func TestNumericIDs(t *testing.T) {
-	p, err := loadYAML(`
-name: 'test-numeric-ids'
-services:
-  foo:
-    image: busybox
-    volumes:
-      - 0:/foo
-
-volumes:
-  '0': {}
-`)
-	assert.NilError(t, err)
-	assert.DeepEqual(t, p.Services, types.Services{
-		{
-			Name:        "foo",
-			Image:       "busybox",
-			Environment: types.MappingWithEquals{},
-			Scale:       1,
-			Volumes: []types.ServiceVolumeConfig{
-				{
-					Type:   types.VolumeTypeVolume,
-					Source: "0",
-					Target: "/foo",
-					Volume: &types.ServiceVolumeVolume{},
-				},
-			},
-		},
-	})
-}
Repository: compose-spec/compose-go
Pull request #421 (2023-06-23 17:47:22)

improve dotenv error messages
- print the variable name that failed to parse
- print the name of the file where the syntax error appeared

nicks: the validation failure doesn't seem related to my pr -- not sure what's going on.
nicks: ah, i see, it's waiting on https://github.com/compose-spec/compose-go/pull/420 to be merged

Patch:
diff --git a/dotenv/parser.go b/dotenv/parser.go
index bbcd318..6c03801 100644
--- a/dotenv/parser.go
+++ b/dotenv/parser.go
@@ -123,8 +123,8 @@ loop:
 			}
 
 			return "", "", inherited, fmt.Errorf(
-				`line %d: unexpected character %q in variable name`,
-				p.line, string(rune))
+				`line %d: unexpected character %q in variable name %q`,
+				p.line, string(rune), strings.Split(src, "\n")[0])
 		}
 	}
 
diff --git a/types/project.go b/types/project.go
index 896bcd6..bf5b8a7 100644
--- a/types/project.go
+++ b/types/project.go
@@ -507,7 +507,7 @@ func (p Project) ResolveServicesEnvironment(discardEnvFiles bool) error {
 
 			fileVars, err := dotenv.ParseWithLookup(bytes.NewBuffer(b), resolve)
 			if err != nil {
-				return err
+				return errors.Wrapf(err, "failed to read %s", envFile)
 			}
 			environment.OverrideBy(Mapping(fileVars).ToMappingWithEquals())
 		}


Test patch:
diff --git a/dotenv/parser_test.go b/dotenv/parser_test.go
index ce580f0..340ea3f 100644
--- a/dotenv/parser_test.go
+++ b/dotenv/parser_test.go
@@ -32,3 +32,9 @@ func TestParseBytes(t *testing.T) {
 		assert.Equal(t, value, out[key])
 	}
 }
+
+func TestParseVariable(t *testing.T) {
+	err := newParser().parse("%!(EXTRA string)=foo", map[string]string{}, nil)
+	assert.Error(t, err, "line 1: unexpected character \"%\" in variable name \"%!(EXTRA string)=foo\"")
+
+}
Repository: compose-spec/compose-go
Pull request #425 (2023-06-28 21:39:28)

loader: path resolution consistency
Move the project working directory absolute path resolution to `ResolveRelativePaths` and remove redundant local volume (a rarely used way of creating bind mounts) absolute path resolution from the normalization method.


Patch:
diff --git a/loader/normalize.go b/loader/normalize.go
index df7812a..58863b5 100644
--- a/loader/normalize.go
+++ b/loader/normalize.go
@@ -18,7 +18,6 @@ package loader
 
 import (
 	"fmt"
-	"path/filepath"
 	"strings"
 
 	"github.com/compose-spec/compose-go/errdefs"
@@ -29,13 +28,6 @@ import (
 
 // Normalize compose project by moving deprecated attributes to their canonical position and injecting implicit defaults
 func Normalize(project *types.Project) error {
-	// TODO(milas): this really belongs in ResolveRelativePaths
-	absWorkingDir, err := filepath.Abs(project.WorkingDir)
-	if err != nil {
-		return err
-	}
-	project.WorkingDir = absWorkingDir
-
 	if project.Networks == nil {
 		project.Networks = make(map[string]types.NetworkConfig)
 	}
@@ -134,14 +126,6 @@ func Normalize(project *types.Project) error {
 		project.Services[i] = s
 	}
 
-	for name, config := range project.Volumes {
-		if config.Driver == "local" && config.DriverOpts["o"] == "bind" {
-			// This is actually a bind mount
-			config.DriverOpts["device"] = absPath(project.WorkingDir, config.DriverOpts["device"])
-			project.Volumes[name] = config
-		}
-	}
-
 	setNameFromKey(project)
 
 	return nil
diff --git a/loader/paths.go b/loader/paths.go
index 8314485..85b687d 100644
--- a/loader/paths.go
+++ b/loader/paths.go
@@ -26,6 +26,12 @@ import (
 
 // ResolveRelativePaths resolves relative paths based on project WorkingDirectory
 func ResolveRelativePaths(project *types.Project) error {
+	absWorkingDir, err := filepath.Abs(project.WorkingDir)
+	if err != nil {
+		return err
+	}
+	project.WorkingDir = absWorkingDir
+
 	absComposeFiles, err := absComposeFiles(project.ComposeFiles)
 	if err != nil {
 		return err


Test patch:
diff --git a/loader/merge_test.go b/loader/merge_test.go
index 7ab03ab..06c2e08 100644
--- a/loader/merge_test.go
+++ b/loader/merge_test.go
@@ -233,6 +233,7 @@ func loadTestProject(configDetails types.ConfigDetails) (*types.Project, error)
 	return Load(configDetails, func(options *Options) {
 		options.SkipNormalization = true
 		options.SkipConsistencyCheck = true
+		options.ResolvePaths = false
 	})
 }
 
diff --git a/loader/normalize_test.go b/loader/normalize_test.go
index db8eca3..e290f14 100644
--- a/loader/normalize_test.go
+++ b/loader/normalize_test.go
@@ -18,7 +18,6 @@ package loader
 
 import (
 	"os"
-	"path/filepath"
 	"testing"
 
 	"github.com/compose-spec/compose-go/types"
@@ -103,7 +102,6 @@ func TestNormalizeVolumes(t *testing.T) {
 		},
 	}
 
-	absCwd, _ := filepath.Abs(".")
 	expected := types.Project{
 		Name:     "myProject",
 		Networks: types.Networks{"default": {Name: "myProject_default"}},
@@ -117,7 +115,6 @@ func TestNormalizeVolumes(t *testing.T) {
 				Name: "CustomName",
 			},
 		},
-		WorkingDir: absCwd,
 	}
 	err := Normalize(&project)
 	assert.NilError(t, err)
Repository: compose-spec/compose-go
Pull request #427 (2023-07-03 15:46:50)

merge expose and keep order
`Expose` entries must be unique and order MUST be preserved so that config hash is reproducible

closes https://github.com/docker/compose/issues/10765
https://docker.atlassian.net/browse/ENV-242


Patch:
diff --git a/loader/merge.go b/loader/merge.go
index c6b25f5..3c4848e 100644
--- a/loader/merge.go
+++ b/loader/merge.go
@@ -150,13 +150,12 @@ func unique(slice []string) []string {
 		return nil
 	}
 	uniqMap := make(map[string]struct{})
+	var uniqSlice []string
 	for _, v := range slice {
-		uniqMap[v] = struct{}{}
-	}
-
-	uniqSlice := make([]string, 0, len(uniqMap))
-	for v := range uniqMap {
-		uniqSlice = append(uniqSlice, v)
+		if _, ok := uniqMap[v]; !ok {
+			uniqSlice = append(uniqSlice, v)
+			uniqMap[v] = struct{}{}
+		}
 	}
 	return uniqSlice
 }


Test patch:
diff --git a/loader/merge_test.go b/loader/merge_test.go
index fefb36f..7ab03ab 100644
--- a/loader/merge_test.go
+++ b/loader/merge_test.go
@@ -1433,3 +1433,56 @@ services:
 		Environment: map[string]string{consts.ComposeProjectName: "test"},
 	}, config)
 }
+
+func TestMergeExpose(t *testing.T) {
+	base := `
+name: test
+services:
+  foo:
+    image: foo
+    expose:
+      - "8080"
+      - "8081"
+      - "8082"
+      - "8083"
+      - "8084"
+`
+	override := `
+services:
+  foo:
+    image: foo
+    expose:
+      - "8090"
+      - "8091"
+      - "8082"
+      - "8081"
+`
+	configDetails := types.ConfigDetails{
+		Environment: map[string]string{},
+		ConfigFiles: []types.ConfigFile{
+			{Filename: "base.yml", Content: []byte(base)},
+			{Filename: "override.yml", Content: []byte(override)},
+		},
+	}
+	config, err := loadTestProject(configDetails)
+	assert.NilError(t, err)
+	assert.DeepEqual(t, &types.Project{
+		Name:       "test",
+		WorkingDir: "",
+		Services: []types.ServiceConfig{
+			{
+				Name:        "foo",
+				Image:       "foo",
+				Environment: types.MappingWithEquals{},
+				Expose:      types.StringOrNumberList{"8080", "8081", "8082", "8083", "8084", "8090", "8091"},
+				Scale:       1,
+			},
+		},
+		Networks:    types.Networks{},
+		Volumes:     types.Volumes{},
+		Secrets:     types.Secrets{},
+		Configs:     types.Configs{},
+		Extensions:  types.Extensions{},
+		Environment: map[string]string{consts.ComposeProjectName: "test"},
+	}, config)
+}
Repository: compose-spec/compose-go
Pull request #441 (2023-08-03 14:04:42)

use include.env_file to resolve variables in included compose.yaml file
`loadInclude` has to define a dedicated `interp.Options` to use local environment map for variable interplation

closes https://github.com/docker/compose/issues/10842
closes https://github.com/compose-spec/compose-go/issues/440


Patch:
diff --git a/loader/include.go b/loader/include.go
index 5bc86b7..026dcf9 100644
--- a/loader/include.go
+++ b/loader/include.go
@@ -22,6 +22,7 @@ import (
 	"path/filepath"
 
 	"github.com/compose-spec/compose-go/dotenv"
+	interp "github.com/compose-spec/compose-go/interpolation"
 	"github.com/compose-spec/compose-go/types"
 	"github.com/pkg/errors"
 )
@@ -74,11 +75,17 @@ func loadInclude(ctx context.Context, configDetails types.ConfigDetails, model *
 			return nil, err
 		}
 
-		imported, err := load(ctx, types.ConfigDetails{
+		config := types.ConfigDetails{
 			WorkingDir:  r.ProjectDirectory,
 			ConfigFiles: types.ToConfigFiles(r.Path),
 			Environment: env,
-		}, loadOptions, loaded)
+		}
+		loadOptions.Interpolate = &interp.Options{
+			Substitute:      options.Interpolate.Substitute,
+			LookupValue:     config.LookupEnv,
+			TypeCastMapping: options.Interpolate.TypeCastMapping,
+		}
+		imported, err := load(ctx, config, loadOptions, loaded)
 		if err != nil {
 			return nil, err
 		}


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index 697ef8d..317b89b 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -2528,7 +2528,7 @@ services:
 		},
 		{
 			Name:          "imported",
-			ContainerName: "imported",
+			ContainerName: "extends", // as defined by ./testdata/subdir/extra.env
 			Environment:   types.MappingWithEquals{"SOURCE": strPtr("extends")},
 			EnvFile: types.StringList{
 				filepath.Join(workingDir, "testdata", "subdir", "extra.env"),
Repository: compose-spec/compose-go
Pull request #445 (2023-08-17 10:52:00)

ability to convert a mapping (back) to KEY=VALUE strings
this would allow to use Mapping from os.Environ and back as demonstrated on https://github.com/docker/compose/pull/10811/files#diff-5eb9d3607857efe53a0fb84cf97f6ef564834e72c264005796416bb7936471e2R176


Patch:
diff --git a/types/types.go b/types/types.go
index a9a515f..517d9a4 100644
--- a/types/types.go
+++ b/types/types.go
@@ -491,6 +491,16 @@ func NewMapping(values []string) Mapping {
 	return mapping
 }
 
+// convert values into a set of KEY=VALUE strings
+func (m Mapping) Values() []string {
+	values := make([]string, 0, len(m))
+	for k, v := range m {
+		values = append(values, fmt.Sprintf("%s=%s", k, v))
+	}
+	sort.Strings(values)
+	return values
+}
+
 // ToMappingWithEquals converts Mapping into a MappingWithEquals with pointer references
 func (m Mapping) ToMappingWithEquals() MappingWithEquals {
 	mapping := MappingWithEquals{}


Test patch:
diff --git a/types/types_test.go b/types/types_test.go
index e4e7a13..495b8fe 100644
--- a/types/types_test.go
+++ b/types/types_test.go
@@ -368,3 +368,14 @@ func equalTrimSpace(x interface{}, y interface{}) is.Comparison {
 	}
 	return is.DeepEqual(trim(x), trim(y))
 }
+
+func TestMappingValues(t *testing.T) {
+	values := []string{"BAR=QIX", "FOO=BAR", "QIX=ZOT"}
+	mapping := NewMapping(values)
+	assert.DeepEqual(t, mapping, Mapping{
+		"FOO": "BAR",
+		"BAR": "QIX",
+		"QIX": "ZOT",
+	})
+	assert.DeepEqual(t, mapping.Values(), values)
+}
Repository: compose-spec/compose-go
Pull request #454 (2023-09-04 07:57:35)

keep depends_on config for services explicitly selected
As we select services A,B,C with dependencies declared, with `IgnoreDependencies` option, we should drop dependencies _but_ those between selected services
this fixes https://github.com/docker/compose/issues/10959


Patch:
diff --git a/types/project.go b/types/project.go
index 3d773c5..713b207 100644
--- a/types/project.go
+++ b/types/project.go
@@ -423,7 +423,14 @@ func (p *Project) ForServices(names []string, options ...DependencyOption) error
 		if _, ok := set[s.Name]; ok {
 			for _, option := range options {
 				if option == IgnoreDependencies {
-					s.DependsOn = nil
+					// remove all dependencies but those implied by explicitly selected services
+					dependencies := s.DependsOn
+					for d := range dependencies {
+						if _, ok := set[d]; !ok {
+							delete(dependencies, d)
+						}
+					}
+					s.DependsOn = dependencies
 				}
 			}
 			enabled = append(enabled, s)


Test patch:
diff --git a/types/project_test.go b/types/project_test.go
index d475e84..8bf7e89 100644
--- a/types/project_test.go
+++ b/types/project_test.go
@@ -115,6 +115,28 @@ func Test_ForServices(t *testing.T) {
 	assert.Equal(t, p.DisabledServices[4].Name, "service_5")
 }
 
+func Test_ForServicesIgnoreDependencies(t *testing.T) {
+	p := makeProject()
+	err := p.ForServices([]string{"service_2"}, IgnoreDependencies)
+	assert.NilError(t, err)
+
+	assert.Equal(t, len(p.DisabledServices), 5)
+	service, err := p.GetService("service_2")
+	assert.NilError(t, err)
+	assert.Equal(t, len(service.DependsOn), 0)
+
+	p = makeProject()
+	err = p.ForServices([]string{"service_2", "service_3"}, IgnoreDependencies)
+	assert.NilError(t, err)
+
+	assert.Equal(t, len(p.DisabledServices), 4)
+	service, err = p.GetService("service_3")
+	assert.NilError(t, err)
+	assert.Equal(t, len(service.DependsOn), 1)
+	_, dependsOn := service.DependsOn["service_2"]
+	assert.Check(t, dependsOn)
+}
+
 func Test_ForServicesCycle(t *testing.T) {
 	p := makeProject()
 	p.Services[0].Links = []string{"service_2"}
Repository: compose-spec/compose-go
Pull request #455 (2023-09-04 12:51:57)

apply reset on extended services
postProcessor (used to collect `!reset` tags) need to be applied not just on 1+N config files after merge, but also on first compose.yaml file when this one uses `extends` to load another model and override service declaration.

closes https://github.com/docker/compose/issues/10962


Patch:
diff --git a/loader/loader.go b/loader/loader.go
index 205dcd6..62549ce 100644
--- a/loader/loader.go
+++ b/loader/loader.go
@@ -255,7 +255,6 @@ func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options,
 	loaded = append(loaded, mainFile)
 
 	includeRefs := make(map[string][]types.IncludeConfig)
-	first := true
 	for _, file := range configDetails.ConfigFiles {
 		var postProcessor PostProcessor
 		configDict := file.Config
@@ -285,22 +284,21 @@ func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options,
 				}
 			}
 
-			if first {
-				first = false
+			if model == nil {
 				model = cfg
-				return nil
-			}
-			merged, err := merge([]*types.Config{model, cfg})
-			if err != nil {
-				return err
+			} else {
+				merged, err := merge([]*types.Config{model, cfg})
+				if err != nil {
+					return err
+				}
+				model = merged
 			}
 			if postProcessor != nil {
-				err = postProcessor.Apply(merged)
+				err = postProcessor.Apply(model)
 				if err != nil {
 					return err
 				}
 			}
-			model = merged
 			return nil
 		}
 


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index 07e6baf..35478ed 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -422,6 +422,27 @@ services:
 	assert.Equal(t, svcB.Build.Context, bDir)
 }
 
+func TestLoadExtendsWihReset(t *testing.T) {
+	actual, err := loadYAML(`
+name: load-extends
+services:
+  foo:
+    extends:
+      service: bar
+    volumes: !reset []
+  bar:
+    image: alpine
+    command: echo
+    volumes:
+       - .:/src
+`)
+	assert.NilError(t, err)
+	assert.Check(t, is.Len(actual.Services, 2))
+	foo, err := actual.GetService("foo")
+	assert.NilError(t, err)
+	assert.Check(t, len(foo.Volumes) == 0)
+}
+
 func TestLoadCredentialSpec(t *testing.T) {
 	actual, err := loadYAML(`
 name: load-credential-spec
Repository: compose-spec/compose-go
Pull request #462 (2023-09-21 14:23:07)

dotenv: fix an unintended buffer copy
before this change: the test allocates ~10MB
after this change: the test allocates ~300KB

fixes https://github.com/docker/compose/issues/10951

ndeloof: nice catch, thanks for digging into this issue

Patch:
diff --git a/dotenv/parser.go b/dotenv/parser.go
index aec72a8..11b6d02 100644
--- a/dotenv/parser.go
+++ b/dotenv/parser.go
@@ -98,7 +98,11 @@ func (p *parser) locateKeyName(src string) (string, string, bool, error) {
 	var key string
 	var inherited bool
 	// trim "export" and space at beginning
-	src = strings.TrimLeftFunc(exportRegex.ReplaceAllString(src, ""), isSpace)
+	if exportRegex.MatchString(src) {
+		// we use a `strings.trim` to preserve the pointer to the same underlying memory.
+		// a regexp replace would copy the string.
+		src = strings.TrimLeftFunc(strings.TrimPrefix(src, "export"), isSpace)
+	}
 
 	// locate key name end and validate it in single loop
 	offset := 0


Test patch:
diff --git a/dotenv/parser_test.go b/dotenv/parser_test.go
index 340ea3f..54580e2 100644
--- a/dotenv/parser_test.go
+++ b/dotenv/parser_test.go
@@ -1,6 +1,9 @@
 package dotenv
 
 import (
+	"fmt"
+	"runtime"
+	"strings"
 	"testing"
 
 	"gotest.tools/v3/assert"
@@ -38,3 +41,24 @@ func TestParseVariable(t *testing.T) {
 	assert.Error(t, err, "line 1: unexpected character \"%\" in variable name \"%!(EXTRA string)=foo\"")
 
 }
+
+func TestMemoryExplosion(t *testing.T) {
+	p := newParser()
+	var startMemStats runtime.MemStats
+	var endMemStats runtime.MemStats
+	runtime.ReadMemStats(&startMemStats)
+
+	size := 1000
+	input := []string{}
+	for i := 0; i < size; i++ {
+		input = append(input, fmt.Sprintf("KEY%d=VALUE%d", i, i))
+	}
+	out := map[string]string{}
+	err := p.parse(strings.Join(input, "\n"), out, nil)
+	assert.NilError(t, err)
+	assert.Equal(t, size, len(out))
+	runtime.ReadMemStats(&endMemStats)
+	assert.Assert(t, endMemStats.Alloc-startMemStats.Alloc < uint64(size)*1000, /* assume 1K per line */
+		"memory usage should be linear with input size. Memory grew by: %d",
+		endMemStats.Alloc-startMemStats.Alloc)
+}
Repository: compose-spec/compose-go
Pull request #469 (2023-10-12 13:11:08)

add restart action to watch attribute in the develop section
Implementation of https://github.com/compose-spec/compose-spec/pull/423


Patch:
diff --git a/schema/compose-spec.json b/schema/compose-spec.json
index ba80eec..d4bff38 100644
--- a/schema/compose-spec.json
+++ b/schema/compose-spec.json
@@ -475,7 +475,7 @@
             "properties": {
               "ignore": {"type": "array", "items": {"type": "string"}},
               "path": {"type": "string"},
-              "action": {"type": "string", "enum": ["rebuild", "sync"]},
+              "action": {"type": "string", "enum": ["rebuild", "sync", "sync+restart"]},
               "target": {"type": "string"}
             }
           },
diff --git a/types/develop.go b/types/develop.go
index fb58060..5fc1071 100644
--- a/types/develop.go
+++ b/types/develop.go
@@ -23,8 +23,9 @@ type DevelopConfig struct {
 type WatchAction string
 
 const (
-	WatchActionSync    WatchAction = "sync"
-	WatchActionRebuild WatchAction = "rebuild"
+	WatchActionSync        WatchAction = "sync"
+	WatchActionRebuild     WatchAction = "rebuild"
+	WatchActionSyncRestart WatchAction = "sync+restart"
 )
 
 type Trigger struct {


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index a3f4a17..ade6f34 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -2842,6 +2842,14 @@ services:
         # rebuild image and recreate service
         - path: ./backend/src
           action: rebuild
+  proxy:
+    image: example/proxy
+    build: ./proxy
+    develop:
+      watch: 
+        # rebuild image and recreate service
+        - path: ./proxy/proxy.conf
+          action: sync+restart
 `, nil), func(options *Options) {
 		options.ResolvePaths = false
 	})
@@ -2868,4 +2876,14 @@ services:
 			},
 		},
 	})
+	proxy, err := project.GetService("proxy")
+	assert.NilError(t, err)
+	assert.DeepEqual(t, *proxy.Develop, types.DevelopConfig{
+		Watch: []types.Trigger{
+			{
+				Path:   "./proxy/proxy.conf",
+				Action: types.WatchActionSyncRestart,
+			},
+		},
+	})
 }
Repository: compose-spec/compose-go
Pull request #475 (2023-10-27 08:19:53)

label can be set with empty value


Patch:
diff --git a/types/labels.go b/types/labels.go
index f30699b..000476b 100644
--- a/types/labels.go
+++ b/types/labels.go
@@ -66,10 +66,7 @@ func (l *Labels) DecodeMapstructure(value interface{}) error {
 	case []interface{}:
 		labels := make(map[string]string, len(v))
 		for _, s := range v {
-			k, e, ok := strings.Cut(fmt.Sprint(s), "=")
-			if !ok {
-				return fmt.Errorf("invalid label %q", v)
-			}
+			k, e, _ := strings.Cut(fmt.Sprint(s), "=")
 			labels[k] = labelValue(e)
 		}
 		*l = labels


Test patch:
diff --git a/types/labels_test.go b/types/labels_test.go
new file mode 100644
index 0000000..9a4bf5c
--- /dev/null
+++ b/types/labels_test.go
@@ -0,0 +1,34 @@
+/*
+   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 types
+
+import (
+	"testing"
+
+	"gotest.tools/v3/assert"
+)
+
+func TestDecodeLabel(t *testing.T) {
+	l := Labels{}
+	err := l.DecodeMapstructure([]any{
+		"a=b",
+		"c",
+	})
+	assert.NilError(t, err)
+	assert.Equal(t, l["a"], "b")
+	assert.Equal(t, l["c"], "")
+}