99 "os"
1010 "os/exec"
1111 "path/filepath"
12+ "sort"
1213 "strings"
1314)
1415
@@ -23,6 +24,8 @@ type Config struct {
2324 Profiles map [string ]Profile `json:"profiles"`
2425}
2526
27+ // ----- Config file handling -----
28+
2629func defaultConfigPath () (string , error ) {
2730 cfgDir , err := os .UserConfigDir ()
2831 if err != nil {
@@ -80,6 +83,9 @@ func saveConfig(cfg *Config, path string) error {
8083 return os .Rename (tmp , path )
8184}
8285
86+ // ----- Git config helpers -----
87+
88+ // scope: "global" or anything else (treated as local)
8389func runGitConfig (scope string , key string , value string ) error {
8490 args := []string {"config" }
8591 if scope == "global" {
@@ -102,6 +108,15 @@ func getGitConfig(key string) (string, error) {
102108 return strings .TrimSpace (string (out )), nil
103109}
104110
111+ func getGitConfigGlobal (key string ) (string , error ) {
112+ cmd := exec .Command ("git" , "config" , "--global" , "--get" , key )
113+ out , err := cmd .Output ()
114+ if err != nil {
115+ return "" , err
116+ }
117+ return strings .TrimSpace (string (out )), nil
118+ }
119+
105120func applyProfile (p Profile , scope string ) error {
106121 if err := runGitConfig (scope , "user.name" , p .GitUser ); err != nil {
107122 return fmt .Errorf ("setting user.name: %w" , err )
@@ -115,14 +130,25 @@ func applyProfile(p Profile, scope string) error {
115130 if err := runGitConfig (scope , "core.sshCommand" , sshCmd ); err != nil {
116131 return fmt .Errorf ("setting core.sshCommand: %w" , err )
117132 }
118- } else if scope == "local " {
133+ } else if scope != "global " {
119134 // Clear repo-specific sshCommand if present
120135 _ = exec .Command ("git" , "config" , "--unset" , "core.sshCommand" ).Run ()
121136 }
122137
123138 return nil
124139}
125140
141+ func gitDir () (string , error ) {
142+ cmd := exec .Command ("git" , "rev-parse" , "--git-dir" )
143+ out , err := cmd .Output ()
144+ if err != nil {
145+ return "" , err
146+ }
147+ return strings .TrimSpace (string (out )), nil
148+ }
149+
150+ // ----- Commands -----
151+
126152func cmdAdd (args []string ) error {
127153 fs := flag .NewFlagSet ("add" , flag .ExitOnError )
128154 id := fs .String ("id" , "" , "Profile ID (e.g. work, personal)" )
@@ -161,6 +187,8 @@ func cmdAdd(args []string) error {
161187}
162188
163189func cmdList (args []string ) error {
190+ _ = args
191+
164192 cfg , _ , err := loadConfig ()
165193 if err != nil {
166194 return err
@@ -171,8 +199,15 @@ func cmdList(args []string) error {
171199 return nil
172200 }
173201
202+ ids := make ([]string , 0 , len (cfg .Profiles ))
203+ for id := range cfg .Profiles {
204+ ids = append (ids , id )
205+ }
206+ sort .Strings (ids )
207+
174208 fmt .Println ("Configured profiles:" )
175- for id , p := range cfg .Profiles {
209+ for _ , id := range ids {
210+ p := cfg .Profiles [id ]
176211 ssh := p .SSHKeyPath
177212 if ssh == "" {
178213 ssh = "(default SSH)"
@@ -240,12 +275,19 @@ func cmdCurrent(args []string) error {
240275 fmt .Println (" core.sshCommand = (default)" )
241276 }
242277
278+ def , errDef := getGitConfig ("gitprofile.default" )
279+ if errDef == nil && def != "" {
280+ fmt .Printf (" gitprofile.default (local) = %s\n " , def )
281+ }
282+ globalDef , errGDef := getGitConfigGlobal ("gitprofile.default" )
283+ if errGDef == nil && globalDef != "" {
284+ fmt .Printf (" gitprofile.default (global) = %s\n " , globalDef )
285+ }
286+
243287 return nil
244288}
245289
246- func cmdChoose (args []string ) error {
247- _ = args
248-
290+ func cmdChoose (_ []string ) error {
249291 cfg , _ , err := loadConfig ()
250292 if err != nil {
251293 return err
@@ -254,13 +296,16 @@ func cmdChoose(args []string) error {
254296 return fmt .Errorf ("no profiles configured; run `git-profile add` first" )
255297 }
256298
257- fmt .Println ("Select profile:" )
258299 ids := make ([]string , 0 , len (cfg .Profiles ))
259- i := 1
260- for id , p := range cfg .Profiles {
261- fmt .Printf (" [%d] %s: %s <%s>\n " , i , id , p .GitUser , p .GitEmail )
300+ for id := range cfg .Profiles {
262301 ids = append (ids , id )
263- i ++
302+ }
303+ sort .Strings (ids )
304+
305+ fmt .Println ("Select profile:" )
306+ for i , id := range ids {
307+ p := cfg .Profiles [id ]
308+ fmt .Printf (" [%d] %s: %s <%s>\n " , i + 1 , id , p .GitUser , p .GitEmail )
264309 }
265310
266311 fmt .Print ("Enter number: " )
@@ -280,7 +325,7 @@ func cmdChoose(args []string) error {
280325 selectedID := ids [choice - 1 ]
281326 p := cfg .Profiles [selectedID ]
282327
283- //alway apply to local repo for choose()
328+ // Always apply to local repo for choose()
284329 if err := applyProfile (p , "local" ); err != nil {
285330 return err
286331 }
@@ -289,16 +334,129 @@ func cmdChoose(args []string) error {
289334 return nil
290335}
291336
337+ // set-default: store default profile in git config
338+ func cmdSetDefault (args []string ) error {
339+ fs := flag .NewFlagSet ("set-default" , flag .ExitOnError )
340+ scopeGlobal := fs .Bool ("global" , false , "Set as global default profile" )
341+ _ = fs .Parse (args )
342+
343+ if fs .NArg () < 1 {
344+ return fmt .Errorf ("usage: git-profile set-default [--global] <profile-id>" )
345+ }
346+ id := fs .Arg (0 )
347+
348+ cfg , _ , err := loadConfig ()
349+ if err != nil {
350+ return err
351+ }
352+
353+ if _ , ok := cfg .Profiles [id ]; ! ok {
354+ return fmt .Errorf ("profile %q not found" , id )
355+ }
356+
357+ scope := "local"
358+ if * scopeGlobal {
359+ scope = "global"
360+ }
361+
362+ if err := runGitConfig (scope , "gitprofile.default" , id ); err != nil {
363+ return err
364+ }
365+
366+ fmt .Printf ("Set %q as %s default profile\n " , id , scope )
367+ return nil
368+ }
369+
370+ // ensure: used by hooks
371+ // 1) If local gitprofile.default exists and matches a profile -> apply it
372+ // 2) Else if global gitprofile.default exists and matches -> apply it
373+ // 3) Else -> interactive choose()
374+ func cmdEnsure (args []string ) error {
375+ _ = args
376+
377+ cfg , _ , err := loadConfig ()
378+ if err != nil {
379+ return err
380+ }
381+ if len (cfg .Profiles ) == 0 {
382+ return fmt .Errorf ("no profiles configured; run `git-profile add` first" )
383+ }
384+
385+ // 1) Local default
386+ if def , err := getGitConfig ("gitprofile.default" ); err == nil && def != "" {
387+ if p , ok := cfg .Profiles [def ]; ok {
388+ return applyProfile (p , "local" )
389+ }
390+ }
391+
392+ // 2) Global default
393+ if gdef , err := getGitConfigGlobal ("gitprofile.default" ); err == nil && gdef != "" {
394+ if p , ok := cfg .Profiles [gdef ]; ok {
395+ return applyProfile (p , "local" )
396+ }
397+ }
398+
399+ // 3) Fallback: interactive
400+ return cmdChoose (nil )
401+ }
402+
403+ // install-hooks: installs prepare-commit-msg & pre-push hooks for this repo
404+ func cmdInstallHooks (args []string ) error {
405+ _ = args
406+
407+ gd , err := gitDir ()
408+ if err != nil {
409+ return fmt .Errorf ("not a git repo? %w" , err )
410+ }
411+
412+ hooksDir := filepath .Join (gd , "hooks" )
413+ if err := os .MkdirAll (hooksDir , 0o755 ); err != nil {
414+ return err
415+ }
416+
417+ hookContent := `#!/bin/sh
418+ # git-profile hook: ensure correct profile before commit/push
419+ git-profile ensure >/dev/null 2>&1 || true
420+ `
421+
422+ hooks := []string {"prepare-commit-msg" , "pre-push" }
423+
424+ for _ , name := range hooks {
425+ path := filepath .Join (hooksDir , name )
426+ if err := os .WriteFile (path , []byte (hookContent ), 0o755 ); err != nil {
427+ return fmt .Errorf ("writing hook %s: %w" , name , err )
428+ }
429+ }
430+
431+ fmt .Printf ("Installed git-profile hooks in %s\n " , hooksDir )
432+ fmt .Println ("From now on, normal `git commit` and `git push` will apply/ask for a profile." )
433+ return nil
434+ }
435+
436+ // ----- Usage / main -----
437+
292438func usage () {
293439 fmt .Println (`git-profile - manage multiple git/GitHub identity profiles
294440
295441Usage:
296- git-profile add --id <id> --name "<User Name>" --email "email@example.com" [--ssh-key /path/to/key]
442+ git-profile add --id <id> --name "<User Name>" --email "email@example.com" [--ssh-key /path/to/key]
297443 git-profile list
298- git-profile use [--global] <id>
444+ git-profile use [--global] <id>
299445 git-profile current
300- git-profile choose (interactive selector; good for wrapping git commit/push)` ,
301- )
446+ git-profile choose
447+ git-profile set-default [--global] <id>
448+ git-profile ensure
449+ git-profile install-hooks
450+
451+ Commands:
452+ add Add a new identity profile
453+ list List configured profiles
454+ use Apply a profile to this repo or globally
455+ current Show current git identity and defaults
456+ choose Interactively choose a profile and apply locally
457+ set-default Set per-repo or global default profile (stored in git config)
458+ ensure Apply repo default, then global default, otherwise prompt (used by hooks)
459+ install-hooks Install hooks so plain 'git commit' and 'git push' call 'git-profile ensure'` )
302460}
303461
304462func main () {
@@ -323,6 +481,12 @@ func main() {
323481 err = cmdCurrent (args )
324482 case "choose" :
325483 err = cmdChoose (args )
484+ case "set-default" :
485+ err = cmdSetDefault (args )
486+ case "ensure" :
487+ err = cmdEnsure (args )
488+ case "install-hooks" :
489+ err = cmdInstallHooks (args )
326490 case "help" , "-h" , "--help" :
327491 usage ()
328492 default :
0 commit comments