Skip to content

Commit 3728df1

Browse files
committed
init: git profile
1 parent 38f0444 commit 3728df1

2 files changed

Lines changed: 182 additions & 15 deletions

File tree

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/singhhp1069/git-profile
2+
3+
go 1.25.3

main.go

Lines changed: 179 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
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+
2629
func 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)
8389
func 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+
105120
func 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+
126152
func 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

163189
func 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+
292438
func usage() {
293439
fmt.Println(`git-profile - manage multiple git/GitHub identity profiles
294440
295441
Usage:
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

304462
func 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

Comments
 (0)