on main help sign in
feat(config): migrate deprecated co_authored_by

The old co_authored_by boolean is now deprecated in favor of trailer_style enum. When only the old setting is present, it silently migrates (true → co-authored-by, false → none). When trailer_style is set, co_authored_by is ignored. The deprecated field remains in the schema marked with deprecated: true so editors can warn users. Assisted-by: Claude Sonnet 4.5 via Crush

commit27093ea42e1fe96bb99d84b62b68978d8e3156c2 authorAmolith <amolith@secluded.site> dateWed 5 Nov 2025, 08:49 (1 year ago) parent425d873
4 files changed+118
internal/config/attribution_migration_test.go+95 internal/config/config.go+11 internal/config/load.go+7 schema.json+5
internal/config/attribution_migration_test.gonew file+95
@@ -0,0 +1,95 @@
1+package config
2+
3+import (
4+ "io"
5+ "strings"
6+ "testing"
7+
8+ "github.com/stretchr/testify/require"
9+)
10+
11+func TestAttributionMigration(t *testing.T) {
12+ t.Parallel()
13+
14+ tests := []struct {
15+ name string
16+ configJSON string
17+ expectedTrailer TrailerStyle
18+ expectedGenerate bool
19+ }{
20+ {
21+ name: "old setting co_authored_by=true migrates to co-authored-by",
22+ configJSON: `{
23+ "options": {
24+ "attribution": {
25+ "co_authored_by": true,
26+ "generated_with": false
27+ }
28+ }
29+ }`,
30+ expectedTrailer: TrailerStyleCoAuthoredBy,
31+ expectedGenerate: false,
32+ },
33+ {
34+ name: "old setting co_authored_by=false migrates to none",
35+ configJSON: `{
36+ "options": {
37+ "attribution": {
38+ "co_authored_by": false,
39+ "generated_with": true
40+ }
41+ }
42+ }`,
43+ expectedTrailer: TrailerStyleNone,
44+ expectedGenerate: true,
45+ },
46+ {
47+ name: "new setting takes precedence over old setting",
48+ configJSON: `{
49+ "options": {
50+ "attribution": {
51+ "trailer_style": "assisted-by",
52+ "co_authored_by": true,
53+ "generated_with": false
54+ }
55+ }
56+ }`,
57+ expectedTrailer: TrailerStyleAssistedBy,
58+ expectedGenerate: false,
59+ },
60+ {
61+ name: "default when neither setting present",
62+ configJSON: `{
63+ "options": {
64+ "attribution": {
65+ "generated_with": true
66+ }
67+ }
68+ }`,
69+ expectedTrailer: TrailerStyleCoAuthoredBy,
70+ expectedGenerate: true,
71+ },
72+ {
73+ name: "default when attribution is null",
74+ configJSON: `{
75+ "options": {}
76+ }`,
77+ expectedTrailer: TrailerStyleCoAuthoredBy,
78+ expectedGenerate: true,
79+ },
80+ }
81+
82+ for _, tt := range tests {
83+ t.Run(tt.name, func(t *testing.T) {
84+ t.Parallel()
85+
86+ cfg, err := loadFromReaders([]io.Reader{strings.NewReader(tt.configJSON)})
87+ require.NoError(t, err)
88+
89+ cfg.setDefaults(t.TempDir(), "")
90+
91+ require.Equal(t, tt.expectedTrailer, cfg.Options.Attribution.TrailerStyle)
92+ require.Equal(t, tt.expectedGenerate, cfg.Options.Attribution.GeneratedWith)
93+ })
94+ }
95+}
internal/config/config.go+11
@@ -14,6 +14,7 @@ import (
1414 "github.com/charmbracelet/catwalk/pkg/catwalk"
1515 "github.com/charmbracelet/crush/internal/csync"
1616 "github.com/charmbracelet/crush/internal/env"
17+ "github.com/invopop/jsonschema"
1718 "github.com/tidwall/sjson"
1819)
1920
@@ -176,9 +177,19 @@ const (
176177
177178type Attribution struct {
178179 TrailerStyle TrailerStyle `json:"trailer_style,omitempty" jsonschema:"description=Style of attribution trailer to add to commits,enum=none,enum=co-authored-by,enum=assisted-by,default=co-authored-by"`
180+ CoAuthoredBy *bool `json:"co_authored_by,omitempty" jsonschema:"description=Deprecated: use trailer_style instead"`
179181 GeneratedWith bool `json:"generated_with,omitempty" jsonschema:"description=Add Generated with Crush line to commit messages and issues and PRs,default=true"`
180182}
181183
184+// JSONSchemaExtend marks the co_authored_by field as deprecated in the schema.
185+func (Attribution) JSONSchemaExtend(schema *jsonschema.Schema) {
186+ if schema.Properties != nil {
187+ if prop, ok := schema.Properties.Get("co_authored_by"); ok {
188+ prop.Deprecated = true
189+ }
190+ }
191+}
192+
182193type Options struct {
183194 ContextPaths []string `json:"context_paths,omitempty" jsonschema:"description=Paths to files containing context information for the AI,example=.cursorrules,example=CRUSH.md"`
184195 TUI *TUIOptions `json:"tui,omitempty" jsonschema:"description=Terminal user interface options"`
internal/config/load.go+7
@@ -356,6 +356,13 @@ func (c *Config) setDefaults(workingDir, dataDir string) {
356356 TrailerStyle: TrailerStyleCoAuthoredBy,
357357 GeneratedWith: true,
358358 }
359+ } else if c.Options.Attribution.TrailerStyle == "" {
360+ // Migrate deprecated co_authored_by or apply default
361+ if c.Options.Attribution.CoAuthoredBy != nil && !*c.Options.Attribution.CoAuthoredBy {
362+ c.Options.Attribution.TrailerStyle = TrailerStyleNone
363+ } else {
364+ c.Options.Attribution.TrailerStyle = TrailerStyleCoAuthoredBy
365+ }
359366 }
360367}
361368
schema.json+5
@@ -15,6 +15,11 @@
1515 "description": "Style of attribution trailer to add to commits",
1616 "default": "co-authored-by"
1717 },
18+ "co_authored_by": {
19+ "type": "boolean",
20+ "description": "Deprecated: use trailer_style instead",
21+ "deprecated": true
22+ },
1823 "generated_with": {
1924 "type": "boolean",
2025 "description": "Add Generated with Crush line to commit messages and issues and PRs",