1package main
2
3import (
4	"strings"
5
6	"github.com/direnv/direnv/gzenv"
7)
8
9// IgnoredKeys is list of keys we don't want to deal with
10var IgnoredKeys = map[string]bool{
11	// direnv env config
12	"DIRENV_CONFIG": true,
13	"DIRENV_BASH":   true,
14
15	// should only be available inside of the .envrc
16	"DIRENV_IN_ENVRC": true,
17
18	"COMP_WORDBREAKS": true, // Avoids segfaults in bash
19	"PS1":             true, // PS1 should not be exported, fixes problem in bash
20
21	// variables that should change freely
22	"OLDPWD":    true,
23	"PWD":       true,
24	"SHELL":     true,
25	"SHELLOPTS": true,
26	"SHLVL":     true,
27	"_":         true,
28}
29
30// EnvDiff represents the diff between two environments
31type EnvDiff struct {
32	Prev map[string]string `json:"p"`
33	Next map[string]string `json:"n"`
34}
35
36// NewEnvDiff is an empty constructor for EnvDiff
37func NewEnvDiff() *EnvDiff {
38	return &EnvDiff{make(map[string]string), make(map[string]string)}
39}
40
41// BuildEnvDiff analyses the changes between 'e1' and 'e2' and builds an
42// EnvDiff out of it.
43func BuildEnvDiff(e1, e2 Env) *EnvDiff {
44	diff := NewEnvDiff()
45
46	in := func(key string, e Env) bool {
47		_, ok := e[key]
48		return ok
49	}
50
51	for key := range e1 {
52		if IgnoredEnv(key) {
53			continue
54		}
55		if e2[key] != e1[key] || !in(key, e2) {
56			diff.Prev[key] = e1[key]
57		}
58	}
59
60	for key := range e2 {
61		if IgnoredEnv(key) {
62			continue
63		}
64		if e2[key] != e1[key] || !in(key, e1) {
65			diff.Next[key] = e2[key]
66		}
67	}
68
69	return diff
70}
71
72// LoadEnvDiff unmarshalls a gzenv string back into an EnvDiff.
73func LoadEnvDiff(gzenvStr string) (diff *EnvDiff, err error) {
74	diff = new(EnvDiff)
75	err = gzenv.Unmarshal(gzenvStr, diff)
76	return
77}
78
79// Any returns if the diff contains any changes.
80func (diff *EnvDiff) Any() bool {
81	return len(diff.Prev) > 0 || len(diff.Next) > 0
82}
83
84// ToShell applies the env diff as a set of commands that are understood by
85// the target `shell`. The outputted string is then meant to be evaluated in
86// the target shell.
87func (diff *EnvDiff) ToShell(shell Shell) string {
88	e := make(ShellExport)
89
90	for key := range diff.Prev {
91		_, ok := diff.Next[key]
92		if !ok {
93			e.Remove(key)
94		}
95	}
96
97	for key, value := range diff.Next {
98		e.Add(key, value)
99	}
100
101	return shell.Export(e)
102}
103
104// Patch applies the diff to the given env and returns a new env with the
105// changes applied.
106func (diff *EnvDiff) Patch(env Env) (newEnv Env) {
107	newEnv = make(Env)
108
109	for k, v := range env {
110		newEnv[k] = v
111	}
112
113	for key := range diff.Prev {
114		delete(newEnv, key)
115	}
116
117	for key, value := range diff.Next {
118		newEnv[key] = value
119	}
120
121	return newEnv
122}
123
124// Reverse flips the diff so that it applies the other way around.
125func (diff *EnvDiff) Reverse() *EnvDiff {
126	return &EnvDiff{diff.Next, diff.Prev}
127}
128
129// Serialize marshalls the environment diff to the gzenv format.
130func (diff *EnvDiff) Serialize() string {
131	return gzenv.Marshal(diff)
132}
133
134//// Utils
135
136// IgnoredEnv returns true if the key should be ignored in environment diffs.
137func IgnoredEnv(key string) bool {
138	if strings.HasPrefix(key, "__fish") {
139		return true
140	}
141	if strings.HasPrefix(key, "BASH_FUNC_") {
142		return true
143	}
144	_, found := IgnoredKeys[key]
145	return found
146}
147