Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,24 @@ git-profile import work "$(cat work.json)"

## Config file

By default, Git Profile stores profiles in:
By default, Git Profile looks for profiles in this order:

1. `$XDG_CONFIG_HOME/git-profile/config.json` (or `~/.config/git-profile/config.json`)
2. `~/.gitprofile` (legacy path and array-based format)

New installs create the XDG config file in the map-based format.
Existing `~/.gitprofile` files stay in the legacy array-based format and are not rewritten in place.

To copy a legacy config to the XDG path in map format:

```bash
~/.gitprofile
git-profile migrate
```

You can override it with:
The legacy file is kept. After migration, the XDG file takes priority.
Use `--force` to overwrite an existing XDG config file.

You can override the path with:

```bash
git-profile --config /path/to/file add work user.email work@example.com
Expand Down
24 changes: 4 additions & 20 deletions cmd/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (

"github.com/spf13/cobra"

"github.com/dotzero/git-profile/internal/config"
"github.com/dotzero/git-profile/internal/ui"
)

Expand Down Expand Up @@ -112,22 +111,17 @@ func profileUpdateEntries(
}

if entries, ok := cfg.Lookup(profile); ok {
values := entriesToMap(entries)
formData.UserName = values[userNameKey]
formData.UserEmail = values[userEmailKey]
formData.UserSigningKey = values[userSigningKeyKey]
formData.UserName = entries[userNameKey]
formData.UserEmail = entries[userEmailKey]
formData.UserSigningKey = entries[userSigningKeyKey]
}

result, err := editProfileFields(formData, cmd.InOrStdin(), cmd.OutOrStdout())
if err != nil {
return err
}

currentValues := map[string]string{}
if entries, ok := cfg.Lookup(result.Profile); ok {
currentValues = entriesToMap(entries)
}

currentValues, _ := cfg.Lookup(result.Profile)
changed := 0

values := map[string]string{
Expand Down Expand Up @@ -165,13 +159,3 @@ func profileUpdateEntries(

return nil
}

func entriesToMap(entries []config.Entry) map[string]string {
values := make(map[string]string, len(entries))

for _, entry := range entries {
values[entry.Key] = entry.Value
}

return values
}
16 changes: 8 additions & 8 deletions cmd/add_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ func TestAddInteractiveStoresOnlyChangedFilledValues(t *testing.T) {
is := is.New(t)

cfg := &storageMock{
LookupFunc: func(name string) ([]config.Entry, bool) {
return []config.Entry{
{Key: userNameKey, Value: "Jane Doe"},
{Key: userEmailKey, Value: "old@example.com"},
LookupFunc: func(name string) (config.Entry, bool) {
return config.Entry{
userNameKey: "Jane Doe",
userEmailKey: "old@example.com",
}, true
},
SaveFunc: func(filename string) error {
Expand Down Expand Up @@ -105,10 +105,10 @@ func TestAddInteractiveSkipsSaveWhenNothingChanged(t *testing.T) {
is := is.New(t)

cfg := &storageMock{
LookupFunc: func(name string) ([]config.Entry, bool) {
return []config.Entry{
{Key: userNameKey, Value: "Jane Doe"},
{Key: userEmailKey, Value: "work@example.com"},
LookupFunc: func(name string) (config.Entry, bool) {
return config.Entry{
userNameKey: "Jane Doe",
userEmailKey: "work@example.com",
}, true
},
SaveFunc: func(filename string) error {
Expand Down
8 changes: 4 additions & 4 deletions cmd/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ func TestExport(t *testing.T) {
is := is.New(t)

cfg := &storageMock{
LookupFunc: func(name string) ([]config.Entry, bool) {
return []config.Entry{
{Key: "user.email", Value: "work@example.com"},
LookupFunc: func(name string) (config.Entry, bool) {
return config.Entry{
"user.email": "work@example.com",
}, true
},
}
Expand All @@ -29,5 +29,5 @@ func TestExport(t *testing.T) {
err := cmd.Execute()

is.NoErr(err)
is.Equal(trim(b.String()), `[{"key":"user.email","value":"work@example.com"}]`)
is.Equal(trim(b.String()), `{"user.email":"work@example.com"}`)
}
24 changes: 18 additions & 6 deletions cmd/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/spf13/cobra"

"github.com/dotzero/git-profile/internal/config"
"github.com/dotzero/git-profile/internal/oldconfig"
"github.com/dotzero/git-profile/internal/ui"
)

Expand All @@ -18,21 +19,32 @@ func Import(cfg storage) *cobra.Command {
Short: "Import a profile",
Long: "Import a profile from JSON.",
Args: cobra.ExactArgs(2),
Example: `git-profile import my-profile '[{"key":"user.email","value":"work@example.com"}]'`,
Example: `git-profile import my-profile '{"user.email":"work@example.com"}'`,
Run: func(cmd *cobra.Command, args []string) {
profile := args[0]
filename, _ := cmd.Flags().GetString("config")

var entries []config.Entry
var entries config.Entry

err := json.Unmarshal([]byte(args[1]), &entries)
if err != nil {
ui.PrintErrln(cmd, ui.ErrorStyle, "Unable to decode profile values: %s", err)
os.Exit(1)
// Try to unmarshal as old format (array)
var oldEntries []oldconfig.OldEntry

errOld := json.Unmarshal([]byte(args[1]), &oldEntries)
if errOld != nil {
ui.PrintErrln(cmd, ui.ErrorStyle, "Unable to decode profile values: %s", err)
os.Exit(1)
}

entries = make(config.Entry)
for _, entry := range oldEntries {
entries[entry.Key] = entry.Value
}
}

for _, entry := range entries {
cfg.Store(profile, entry.Key, entry.Value)
for key, val := range entries {
cfg.Store(profile, key, val)
}

err = cfg.Save(filename)
Expand Down
68 changes: 54 additions & 14 deletions cmd/import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,63 @@ import (
)

func TestImport(t *testing.T) {
is := is.New(t)
t.Run("map format", func(t *testing.T) {
is := is.New(t)

cfg := &storageMock{
SaveFunc: func(filename string) error {
return nil
},
StoreFunc: func(profile string, key string, value string) {},
}
stored := make(map[string]string)
cfg := &storageMock{
SaveFunc: func(filename string) error {
return nil
},
StoreFunc: func(profile string, key string, value string) {
is.Equal(profile, "profile")

var b bytes.Buffer
stored[key] = value
},
}

cmd := Import(cfg)
var b bytes.Buffer

cmd.SetOut(&b)
cmd.SetArgs([]string{"profile", `[{"key":"user.email","value":"work@example.com"}]`})
err := cmd.Execute()
cmd := Import(cfg)
cmd.SetOut(&b)
cmd.SetArgs([]string{"profile", `{"user.email": "work@example.com", "core.autocrlf": "input"}`})
err := cmd.Execute()

is.NoErr(err)
is.Equal(trim(b.String()), "Successfully imported `profile` profile.")
is.NoErr(err)
is.Equal(trim(b.String()), "Successfully imported `profile` profile.")
is.Equal(stored, map[string]string{
"user.email": "work@example.com",
"core.autocrlf": "input",
})
})

t.Run("array format", func(t *testing.T) {
is := is.New(t)

stored := make(map[string]string)
cfg := &storageMock{
SaveFunc: func(filename string) error {
return nil
},
StoreFunc: func(profile string, key string, value string) {
is.Equal(profile, "profile")

stored[key] = value
},
}

var b bytes.Buffer

cmd := Import(cfg)
cmd.SetOut(&b)
cmd.SetArgs([]string{"profile", `[{"key": "user.email", "value": "work@example.com"}, {"key": "core.autocrlf", "value": "input"}]`})
err := cmd.Execute()

is.NoErr(err)
is.Equal(trim(b.String()), "Successfully imported `profile` profile.")
is.Equal(stored, map[string]string{
"user.email": "work@example.com",
"core.autocrlf": "input",
})
})
}
2 changes: 1 addition & 1 deletion cmd/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

type storage interface {
Len() int
Lookup(name string) ([]config.Entry, bool)
Lookup(name string) (config.Entry, bool)
Names() []string
Delete(profile string, value string) bool
DeleteProfile(profile string) bool
Expand Down
14 changes: 12 additions & 2 deletions cmd/list.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cmd

import (
"sort"

"github.com/spf13/cobra"

"github.com/dotzero/git-profile/internal/ui"
Expand All @@ -23,8 +25,16 @@ func List(cfg storage, v vcs) *cobra.Command {
ui.Println(cmd, ui.SuccessStyle, "- %s:", name)

profile, _ := cfg.Lookup(name)
for _, entry := range profile {
ui.Println(cmd, ui.DefaultStyle, " %s: %s", entry.Key, entry.Value)

keys := make([]string, 0, len(profile))
for key := range profile {
keys = append(keys, key)
}

sort.Strings(keys)

for _, key := range keys {
ui.Println(cmd, ui.DefaultStyle, " %s: %s", key, profile[key])
}
}
},
Expand Down
9 changes: 5 additions & 4 deletions cmd/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ func TestList(t *testing.T) {
NamesFunc: func() []string {
return []string{"home"}
},
LookupFunc: func(name string) ([]config.Entry, bool) {
return []config.Entry{
{Key: "user.email", Value: "work@example.com"},
LookupFunc: func(name string) (config.Entry, bool) {
return config.Entry{
"user.email": "work@example.com",
"core.autocrlf": "input",
}, true
},
}
Expand All @@ -43,5 +44,5 @@ func TestList(t *testing.T) {
err := cmd.Execute()

is.NoErr(err)
is.Equal(trim(b.String()), "Available profiles:\n- home:\n user.email: work@example.com")
is.Equal(trim(b.String()), "Available profiles:\n- home:\n core.autocrlf: input\n user.email: work@example.com")
}
44 changes: 44 additions & 0 deletions cmd/migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package cmd

import (
"os"

"github.com/spf13/cobra"

"github.com/dotzero/git-profile/internal/config"
"github.com/dotzero/git-profile/internal/ui"
)

// Migrate returns `migrate` command
func Migrate() *cobra.Command {
var force bool

cmd := &cobra.Command{
Use: "migrate",
Short: "Migrate ~/.gitprofile to the XDG config path",
Long: multiline(
"Copy ~/.gitprofile to $XDG_CONFIG_HOME/git-profile/config.json.",
"The destination uses the map-based format.",
"The legacy file is left in place. Once the XDG file exists, it takes priority.",
),
Example: multiline(
`git-profile migrate`,
`git-profile migrate --force`,
),
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, _ []string) {
src, dst, err := config.Migrate(force)
if err != nil {
ui.PrintErrln(cmd, ui.ErrorStyle, "%s", err)
os.Exit(1)
}

ui.Println(cmd, ui.SuccessStyle, "Migrated `%s` to `%s`.", src, dst)
ui.Println(cmd, ui.DefaultStyle, "Legacy file kept at `%s`.", src)
},
}

cmd.Flags().BoolVar(&force, "force", false, "overwrite an existing XDG config file")

return cmd
}
Loading