@@ -11,6 +11,7 @@ import (
1111	"time" 
1212
1313	"github.com/adrg/xdg" 
14+ 	"github.com/jesseduffield/generics/orderedset" 
1415	"github.com/jesseduffield/lazygit/pkg/utils" 
1516	"github.com/jesseduffield/lazygit/pkg/utils/yaml_utils" 
1617	"github.com/samber/lo" 
@@ -96,7 +97,7 @@ func NewAppConfig(
9697		configFiles  =  []* ConfigFile {configFile }
9798	}
9899
99- 	userConfig , err  :=  loadUserConfigWithDefaults (configFiles )
100+ 	userConfig , err  :=  loadUserConfigWithDefaults (configFiles ,  false )
100101	if  err  !=  nil  {
101102		return  nil , err 
102103	}
@@ -145,11 +146,11 @@ func findOrCreateConfigDir() (string, error) {
145146	return  folder , os .MkdirAll (folder , 0o755 )
146147}
147148
148- func  loadUserConfigWithDefaults (configFiles  []* ConfigFile ) (* UserConfig , error ) {
149- 	return  loadUserConfig (configFiles , GetDefaultConfig ())
149+ func  loadUserConfigWithDefaults (configFiles  []* ConfigFile ,  isGuiInitialized   bool ) (* UserConfig , error ) {
150+ 	return  loadUserConfig (configFiles , GetDefaultConfig (),  isGuiInitialized )
150151}
151152
152- func  loadUserConfig (configFiles  []* ConfigFile , base  * UserConfig ) (* UserConfig , error ) {
153+ func  loadUserConfig (configFiles  []* ConfigFile , base  * UserConfig ,  isGuiInitialized   bool ) (* UserConfig , error ) {
153154	for  _ , configFile  :=  range  configFiles  {
154155		path  :=  configFile .Path 
155156		statInfo , err  :=  os .Stat (path )
@@ -194,7 +195,7 @@ func loadUserConfig(configFiles []*ConfigFile, base *UserConfig) (*UserConfig, e
194195			return  nil , err 
195196		}
196197
197- 		content , err  =  migrateUserConfig (path , content )
198+ 		content , err  =  migrateUserConfig (path , content ,  isGuiInitialized )
198199		if  err  !=  nil  {
199200			return  nil , err 
200201		}
@@ -215,41 +216,64 @@ func loadUserConfig(configFiles []*ConfigFile, base *UserConfig) (*UserConfig, e
215216	return  base , nil 
216217}
217218
219+ type  ChangesSet  =  orderedset.OrderedSet [string ]
220+ 
221+ func  NewChangesSet () * ChangesSet  {
222+ 	return  orderedset .New [string ]()
223+ }
224+ 
218225// Do any backward-compatibility migrations of things that have changed in the 
219226// config over time; examples are renaming a key to a better name, moving a key 
220227// from one container to another, or changing the type of a key (e.g. from bool 
221228// to an enum). 
222- func  migrateUserConfig (path  string , content  []byte ) ([]byte , error ) {
223- 	changedContent , err  :=  computeMigratedConfig (path , content )
229+ func  migrateUserConfig (path  string , content  []byte , isGuiInitialized  bool ) ([]byte , error ) {
230+ 	changes  :=  NewChangesSet ()
231+ 
232+ 	changedContent , didChange , err  :=  computeMigratedConfig (path , content , changes )
224233	if  err  !=  nil  {
225234		return  nil , err 
226235	}
227236
228- 	// Write config back if changed 
229- 	if  string (changedContent ) !=  string (content ) {
230- 		fmt .Println ("Provided user config is deprecated but auto-fixable. Attempting to write fixed version back to file..." )
231- 		if  err  :=  os .WriteFile (path , changedContent , 0o644 ); err  !=  nil  {
232- 			return  nil , fmt .Errorf ("While attempting to write back fixed user config to %s, an error occurred: %s" , path , err )
233- 		}
234- 		fmt .Printf ("Success. New config written to %s\n " , path )
235- 		return  changedContent , nil 
237+ 	// Nothing to do if config didn't change 
238+ 	if  ! didChange  {
239+ 		return  content , nil 
236240	}
237241
238- 	return  content , nil 
242+ 	changesText  :=  "The following changes were made:\n \n " 
243+ 	changesText  +=  strings .Join (lo .Map (changes .ToSliceFromOldest (), func (change  string , _  int ) string  {
244+ 		return  fmt .Sprintf ("- %s\n " , change )
245+ 	}), "" )
246+ 
247+ 	// Write config back 
248+ 	if  ! isGuiInitialized  {
249+ 		fmt .Printf ("The user config file %s must be migrated. Attempting to do this automatically.\n " , path )
250+ 		fmt .Println (changesText )
251+ 	}
252+ 	if  err  :=  os .WriteFile (path , changedContent , 0o644 ); err  !=  nil  {
253+ 		errorMsg  :=  fmt .Sprintf ("While attempting to write back migrated user config to %s, an error occurred: %s" , path , err )
254+ 		if  isGuiInitialized  {
255+ 			errorMsg  +=  "\n \n "  +  changesText 
256+ 		}
257+ 		return  nil , errors .New (errorMsg )
258+ 	}
259+ 	if  ! isGuiInitialized  {
260+ 		fmt .Printf ("Config file saved successfully to %s\n " , path )
261+ 	}
262+ 	return  changedContent , nil 
239263}
240264
241265// A pure function helper for testing purposes 
242- func  computeMigratedConfig (path  string , content  []byte ) ([]byte , error ) {
266+ func  computeMigratedConfig (path  string , content  []byte ,  changes   * ChangesSet ) ([]byte ,  bool , error ) {
243267	var  err  error 
244268	var  rootNode  yaml.Node 
245269	err  =  yaml .Unmarshal (content , & rootNode )
246270	if  err  !=  nil  {
247- 		return  nil , fmt .Errorf ("failed to parse YAML: %w" , err )
271+ 		return  nil , false ,  fmt .Errorf ("failed to parse YAML: %w" , err )
248272	}
249273	var  originalCopy  yaml.Node 
250274	err  =  yaml .Unmarshal (content , & originalCopy )
251275	if  err  !=  nil  {
252- 		return  nil , fmt .Errorf ("failed to parse YAML, but only the second time!?!? How did that happen: %w" , err )
276+ 		return  nil , false ,  fmt .Errorf ("failed to parse YAML, but only the second time!?!? How did that happen: %w" , err )
253277	}
254278
255279	pathsToReplace  :=  []struct  {
@@ -262,60 +286,64 @@ func computeMigratedConfig(path string, content []byte) ([]byte, error) {
262286	}
263287
264288	for  _ , pathToReplace  :=  range  pathsToReplace  {
265- 		err  :=  yaml_utils .RenameYamlKey (& rootNode , pathToReplace .oldPath , pathToReplace .newName )
289+ 		err ,  didReplace  :=  yaml_utils .RenameYamlKey (& rootNode , pathToReplace .oldPath , pathToReplace .newName )
266290		if  err  !=  nil  {
267- 			return  nil , fmt .Errorf ("Couldn't migrate config file at `%s` for key %s: %s" , path , strings .Join (pathToReplace .oldPath , "." ), err )
291+ 			return  nil , false , fmt .Errorf ("Couldn't migrate config file at `%s` for key %s: %s" , path , strings .Join (pathToReplace .oldPath , "." ), err )
292+ 		}
293+ 		if  didReplace  {
294+ 			changes .Add (fmt .Sprintf ("Renamed '%s' to '%s'" , strings .Join (pathToReplace .oldPath , "." ), pathToReplace .newName ))
268295		}
269296	}
270297
271- 	err  =  changeNullKeybindingsToDisabled (& rootNode )
298+ 	err  =  changeNullKeybindingsToDisabled (& rootNode ,  changes )
272299	if  err  !=  nil  {
273- 		return  nil , fmt .Errorf ("Couldn't migrate config file at `%s`: %s" , path , err )
300+ 		return  nil , false ,  fmt .Errorf ("Couldn't migrate config file at `%s`: %s" , path , err )
274301	}
275302
276- 	err  =  changeElementToSequence (& rootNode , []string {"git" , "commitPrefix" })
303+ 	err  =  changeElementToSequence (& rootNode , []string {"git" , "commitPrefix" },  changes )
277304	if  err  !=  nil  {
278- 		return  nil , fmt .Errorf ("Couldn't migrate config file at `%s`: %s" , path , err )
305+ 		return  nil , false ,  fmt .Errorf ("Couldn't migrate config file at `%s`: %s" , path , err )
279306	}
280307
281- 	err  =  changeCommitPrefixesMap (& rootNode )
308+ 	err  =  changeCommitPrefixesMap (& rootNode ,  changes )
282309	if  err  !=  nil  {
283- 		return  nil , fmt .Errorf ("Couldn't migrate config file at `%s`: %s" , path , err )
310+ 		return  nil , false ,  fmt .Errorf ("Couldn't migrate config file at `%s`: %s" , path , err )
284311	}
285312
286- 	err  =  changeCustomCommandStreamAndOutputToOutputEnum (& rootNode )
313+ 	err  =  changeCustomCommandStreamAndOutputToOutputEnum (& rootNode ,  changes )
287314	if  err  !=  nil  {
288- 		return  nil , fmt .Errorf ("Couldn't migrate config file at `%s`: %s" , path , err )
315+ 		return  nil , false ,  fmt .Errorf ("Couldn't migrate config file at `%s`: %s" , path , err )
289316	}
290317
291- 	err  =  migrateAllBranchesLogCmd (& rootNode )
318+ 	err  =  migrateAllBranchesLogCmd (& rootNode ,  changes )
292319	if  err  !=  nil  {
293- 		return  nil , fmt .Errorf ("Couldn't migrate config file at `%s`: %s" , path , err )
320+ 		return  nil , false ,  fmt .Errorf ("Couldn't migrate config file at `%s`: %s" , path , err )
294321	}
295322
296323	// Add more migrations here... 
297324
298- 	if  ! reflect .DeepEqual (rootNode , originalCopy ) {
299- 		newContent , err  :=  yaml_utils .YamlMarshal (& rootNode )
300- 		if  err  !=  nil  {
301- 			return  nil , fmt .Errorf ("Failed to remarsal!\n  %w" , err )
302- 		}
303- 		return  newContent , nil 
304- 	} else  {
305- 		return  content , nil 
325+ 	if  reflect .DeepEqual (rootNode , originalCopy ) {
326+ 		return  nil , false , nil 
306327	}
328+ 
329+ 	newContent , err  :=  yaml_utils .YamlMarshal (& rootNode )
330+ 	if  err  !=  nil  {
331+ 		return  nil , false , fmt .Errorf ("Failed to remarsal!\n  %w" , err )
332+ 	}
333+ 	return  newContent , true , nil 
307334}
308335
309- func  changeNullKeybindingsToDisabled (rootNode  * yaml.Node ) error  {
336+ func  changeNullKeybindingsToDisabled (rootNode  * yaml.Node ,  changes   * ChangesSet ) error  {
310337	return  yaml_utils .Walk (rootNode , func (node  * yaml.Node , path  string ) {
311338		if  strings .HasPrefix (path , "keybinding." ) &&  node .Kind  ==  yaml .ScalarNode  &&  node .Tag  ==  "!!null"  {
312339			node .Value  =  "<disabled>" 
313340			node .Tag  =  "!!str" 
341+ 			changes .Add (fmt .Sprintf ("Changed 'null' to '<disabled>' for keybinding '%s'" , path ))
314342		}
315343	})
316344}
317345
318- func  changeElementToSequence (rootNode  * yaml.Node , path  []string ) error  {
346+ func  changeElementToSequence (rootNode  * yaml.Node , path  []string ,  changes   * ChangesSet ) error  {
319347	return  yaml_utils .TransformNode (rootNode , path , func (node  * yaml.Node ) error  {
320348		if  node .Kind  ==  yaml .MappingNode  {
321349			nodeContentCopy  :=  node .Content 
@@ -327,13 +355,15 @@ func changeElementToSequence(rootNode *yaml.Node, path []string) error {
327355				Content : nodeContentCopy ,
328356			}}
329357
358+ 			changes .Add (fmt .Sprintf ("Changed '%s' to an array of strings" , strings .Join (path , "." )))
359+ 
330360			return  nil 
331361		}
332362		return  nil 
333363	})
334364}
335365
336- func  changeCommitPrefixesMap (rootNode  * yaml.Node ) error  {
366+ func  changeCommitPrefixesMap (rootNode  * yaml.Node ,  changes   * ChangesSet ) error  {
337367	return  yaml_utils .TransformNode (rootNode , []string {"git" , "commitPrefixes" }, func (prefixesNode  * yaml.Node ) error  {
338368		if  prefixesNode .Kind  ==  yaml .MappingNode  {
339369			for  _ , contentNode  :=  range  prefixesNode .Content  {
@@ -346,14 +376,15 @@ func changeCommitPrefixesMap(rootNode *yaml.Node) error {
346376						Kind :    yaml .MappingNode ,
347377						Content : nodeContentCopy ,
348378					}}
379+ 					changes .Add ("Changed 'git.commitPrefixes' elements to arrays of strings" )
349380				}
350381			}
351382		}
352383		return  nil 
353384	})
354385}
355386
356- func  changeCustomCommandStreamAndOutputToOutputEnum (rootNode  * yaml.Node ) error  {
387+ func  changeCustomCommandStreamAndOutputToOutputEnum (rootNode  * yaml.Node ,  changes   * ChangesSet ) error  {
357388	return  yaml_utils .Walk (rootNode , func (node  * yaml.Node , path  string ) {
358389		// We are being lazy here and rely on the fact that the only mapping 
359390		// nodes in the tree under customCommands are actual custom commands. If 
@@ -364,16 +395,25 @@ func changeCustomCommandStreamAndOutputToOutputEnum(rootNode *yaml.Node) error {
364395			if  streamKey , streamValue  :=  yaml_utils .RemoveKey (node , "subprocess" ); streamKey  !=  nil  {
365396				if  streamValue .Kind  ==  yaml .ScalarNode  &&  streamValue .Value  ==  "true"  {
366397					output  =  "terminal" 
398+ 					changes .Add ("Changed 'subprocess: true' to 'output: terminal' in custom command" )
399+ 				} else  {
400+ 					changes .Add ("Deleted redundant 'subprocess: false' in custom command" )
367401				}
368402			}
369403			if  streamKey , streamValue  :=  yaml_utils .RemoveKey (node , "stream" ); streamKey  !=  nil  {
370404				if  streamValue .Kind  ==  yaml .ScalarNode  &&  streamValue .Value  ==  "true"  &&  output  ==  ""  {
371405					output  =  "log" 
406+ 					changes .Add ("Changed 'stream: true' to 'output: log' in custom command" )
407+ 				} else  {
408+ 					changes .Add (fmt .Sprintf ("Deleted redundant 'stream: %v' property in custom command" , streamValue .Value ))
372409				}
373410			}
374411			if  streamKey , streamValue  :=  yaml_utils .RemoveKey (node , "showOutput" ); streamKey  !=  nil  {
375412				if  streamValue .Kind  ==  yaml .ScalarNode  &&  streamValue .Value  ==  "true"  &&  output  ==  ""  {
413+ 					changes .Add ("Changed 'showOutput: true' to 'output: popup' in custom command" )
376414					output  =  "popup" 
415+ 				} else  {
416+ 					changes .Add (fmt .Sprintf ("Deleted redundant 'showOutput: %v' property in custom command" , streamValue .Value ))
377417				}
378418			}
379419			if  output  !=  ""  {
@@ -397,7 +437,7 @@ func changeCustomCommandStreamAndOutputToOutputEnum(rootNode *yaml.Node) error {
397437// a single element at `allBranchesLogCmd` and the sequence at `allBranchesLogCmds`. 
398438// Some users have explicitly set `allBranchesLogCmd` to be an empty string in order 
399439// to remove it, so in that case we just delete the element, and add nothing to the list 
400- func  migrateAllBranchesLogCmd (rootNode  * yaml.Node ) error  {
440+ func  migrateAllBranchesLogCmd (rootNode  * yaml.Node ,  changes   * ChangesSet ) error  {
401441	return  yaml_utils .TransformNode (rootNode , []string {"git" }, func (gitNode  * yaml.Node ) error  {
402442		cmdKeyNode , cmdValueNode  :=  yaml_utils .LookupKey (gitNode , "allBranchesLogCmd" )
403443		// Nothing to do if they do not have the deprecated item 
@@ -406,6 +446,7 @@ func migrateAllBranchesLogCmd(rootNode *yaml.Node) error {
406446		}
407447
408448		cmdsKeyNode , cmdsValueNode  :=  yaml_utils .LookupKey (gitNode , "allBranchesLogCmds" )
449+ 		var  change  string 
409450		if  cmdsKeyNode  ==  nil  {
410451			// Create empty sequence node and attach it onto the root git node 
411452			// We will later populate it with the individual allBranchesLogCmd record 
@@ -415,17 +456,24 @@ func migrateAllBranchesLogCmd(rootNode *yaml.Node) error {
415456				cmdsKeyNode ,
416457				cmdsValueNode ,
417458			)
418- 		} else  if  cmdsValueNode .Kind  !=  yaml .SequenceNode  {
419- 			return  errors .New ("You should have an allBranchesLogCmds defined as a sequence!" )
459+ 			change  =  "Created git.allBranchesLogCmds array containing value of git.allBranchesLogCmd" 
460+ 		} else  {
461+ 			if  cmdsValueNode .Kind  !=  yaml .SequenceNode  {
462+ 				return  errors .New ("You should have an allBranchesLogCmds defined as a sequence!" )
463+ 			}
464+ 
465+ 			change  =  "Prepended git.allBranchesLogCmd value to git.allBranchesLogCmds array" 
420466		}
421467
422468		if  cmdValueNode .Value  !=  ""  {
423469			// Prepending the individual element to make it show up first in the list, which was prior behavior 
424470			cmdsValueNode .Content  =  utils .Prepend (cmdsValueNode .Content , & yaml.Node {Kind : yaml .ScalarNode , Value : cmdValueNode .Value })
471+ 			changes .Add (change )
425472		}
426473
427474		// Clear out the existing allBranchesLogCmd, now that we have migrated it into the list 
428475		_ , _  =  yaml_utils .RemoveKey (gitNode , "allBranchesLogCmd" )
476+ 		changes .Add ("Removed obsolete git.allBranchesLogCmd" )
429477
430478		return  nil 
431479	})
@@ -471,7 +519,7 @@ func (c *AppConfig) GetUserConfigDir() string {
471519
472520func  (c  * AppConfig ) ReloadUserConfigForRepo (repoConfigFiles  []* ConfigFile ) error  {
473521	configFiles  :=  append (c .globalUserConfigFiles , repoConfigFiles ... )
474- 	userConfig , err  :=  loadUserConfigWithDefaults (configFiles )
522+ 	userConfig , err  :=  loadUserConfigWithDefaults (configFiles ,  true )
475523	if  err  !=  nil  {
476524		return  err 
477525	}
@@ -496,7 +544,7 @@ func (c *AppConfig) ReloadChangedUserConfigFiles() (error, bool) {
496544		return  nil , false 
497545	}
498546
499- 	userConfig , err  :=  loadUserConfigWithDefaults (c .userConfigFiles )
547+ 	userConfig , err  :=  loadUserConfigWithDefaults (c .userConfigFiles ,  true )
500548	if  err  !=  nil  {
501549		return  err , false 
502550	}
0 commit comments