1 // Frontend: Format changes
3 // Copyright (C) 2021 Simon Ruderich
5 // This program is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with this program. If not, see <http://www.gnu.org/licenses/>.
25 "ruderich.org/simon/safcm"
26 "ruderich.org/simon/safcm/cmd/safcm/config"
29 // NOTE: Be careful when implementing new format* functions. All input from
30 // the remote helper is untrusted and must be either escaped with %q or by
31 // calling EscapeControlCharacters().
39 func (c *Changes) FormatChanges(resp safcm.MsgSyncResp) string {
41 if len(resp.FileChanges) > 0 {
42 changes = append(changes,
43 c.FormatFileChanges(resp.FileChanges))
45 if len(resp.PackageChanges) > 0 {
46 changes = append(changes,
47 c.FormatPackageChanges(resp.PackageChanges))
49 if len(resp.ServiceChanges) > 0 {
50 changes = append(changes,
51 c.FormatServiceChanges(resp.ServiceChanges))
53 if len(resp.CommandChanges) > 0 {
54 changes = append(changes,
55 c.FormatCommandChanges(resp.CommandChanges))
57 if len(changes) == 0 {
58 // Notify user that the host was synced successfully
62 x := strings.Join(changes, "\n")
63 // If quiet is used and only commands without output were executed
64 // then don't prepend a newline so that the whole change output of a
65 // host fits in a single line. This makes the output much more
66 // readable with multiple hosts.
67 if strings.Count(x, "\n") == 1 {
73 func (c *Changes) FormatFileChanges(changes []safcm.FileChange) string {
74 var buf strings.Builder
76 fmt.Fprintf(&buf, "will change %d file(s): (dry-run)\n",
79 fmt.Fprintf(&buf, "changed %d file(s):\n", len(changes))
81 for _, x := range changes {
82 fmt.Fprintf(&buf, "%s:", c.FormatTarget(x.Path))
87 ColorString(c.IsTTY, ColorGreen, "created"),
88 FormatFileType(x.New),
89 FormatFileUserGroup(x.New),
90 FormatFilePerm(x.New),
93 if x.Old.Mode.Type() != x.New.Mode.Type() {
94 info = append(info, fmt.Sprintf("%s -> %s",
95 FormatFileType(x.Old),
96 FormatFileType(x.New),
99 if x.Old.User != x.New.User ||
100 x.Old.Uid != x.New.Uid ||
101 x.Old.Group != x.New.Group ||
102 x.Old.Gid != x.New.Gid {
103 info = append(info, fmt.Sprintf("%s -> %s",
104 FormatFileUserGroup(x.Old),
105 FormatFileUserGroup(x.New),
108 if config.FileModeToFullPerm(x.Old.Mode) !=
109 config.FileModeToFullPerm(x.New.Mode) {
110 info = append(info, fmt.Sprintf("%s -> %s",
111 FormatFilePerm(x.Old),
112 FormatFilePerm(x.New),
117 fmt.Fprint(&buf, " ")
118 fmt.Fprint(&buf, strings.Join(info, ", "))
121 if x.DataDiff != "" {
122 fmt.Fprintf(&buf, "\n%s", c.FormatDiff(x.DataDiff))
124 fmt.Fprintf(&buf, "\n")
129 func FormatFileType(info safcm.FileChangeInfo) string {
130 switch info.Mode.Type() {
131 case 0: // regular file
138 return fmt.Sprintf("invalid type %v", info.Mode.Type())
141 func FormatFileUserGroup(info safcm.FileChangeInfo) string {
142 return fmt.Sprintf("%s(%d) %s(%d)",
143 EscapeControlCharacters(false, info.User), info.Uid,
144 EscapeControlCharacters(false, info.Group), info.Gid)
146 func FormatFilePerm(info safcm.FileChangeInfo) string {
147 return fmt.Sprintf("%#o", config.FileModeToFullPerm(info.Mode))
150 func (c *Changes) FormatPackageChanges(changes []safcm.PackageChange) string {
151 var buf strings.Builder
153 fmt.Fprintf(&buf, "will install %d package(s): (dry-run)\n",
156 fmt.Fprintf(&buf, "installed %d package(s):\n", len(changes))
158 for _, x := range changes {
159 // TODO: indicate if installation failed
160 fmt.Fprintf(&buf, "%s\n", c.FormatTarget(x.Name))
165 func (c *Changes) FormatServiceChanges(changes []safcm.ServiceChange) string {
166 var buf strings.Builder
168 fmt.Fprintf(&buf, "will modify %d service(s): (dry-run)\n",
171 fmt.Fprintf(&buf, "modified %d service(s):\n", len(changes))
173 for _, x := range changes {
176 info = append(info, "started")
179 info = append(info, "enabled")
181 fmt.Fprintf(&buf, "%s: %s\n",
182 c.FormatTarget(x.Name),
183 strings.Join(info, ", "))
188 func (c *Changes) FormatCommandChanges(changes []safcm.CommandChange) string {
191 // Quiet hides all successful, non-trigger commands which produce no
192 // output. This is useful as many commands will be used to enforce a
193 // certain state (e.g. file not-present, `ainsl`, etc.) and are run on
194 // each sync. Displaying them provides not much useful information.
195 // Instead, quiet shows them only when they produce output (e.g.
196 // `ainsl`, `rm -v`) and thus modify the host's state.
199 for _, x := range changes {
200 if x.Trigger == "" &&
208 var buf strings.Builder
210 fmt.Fprintf(&buf, "will execute %d command(s)", len(changes))
212 fmt.Fprintf(&buf, "executed %d command(s)", len(changes))
214 if noOutput > 0 && !c.DryRun {
215 fmt.Fprintf(&buf, ", %d with no output (hidden)", noOutput)
217 if noOutput != len(changes) {
218 fmt.Fprintf(&buf, ":")
221 fmt.Fprintf(&buf, " (dry-run)")
223 fmt.Fprintf(&buf, "\n")
224 for _, x := range changes {
226 x.Trigger == "" && x.Error == "" && x.Output == "" {
230 fmt.Fprintf(&buf, "%s", c.FormatTarget(x.Command))
232 fmt.Fprintf(&buf, ", trigger for %q", x.Trigger)
235 fmt.Fprintf(&buf, ", failed: %q", x.Error)
238 // TODO: truncate very large outputs?
239 x := indentBlock(x.Output, indent)
240 fmt.Fprintf(&buf, ":\n%s",
241 EscapeControlCharacters(c.IsTTY, x))
243 fmt.Fprintf(&buf, "\n")
248 func (c *Changes) FormatTarget(x string) string {
249 x = fmt.Sprintf("%q", x) // escape!
250 return ColorString(c.IsTTY, ColorCyan, x)
253 func (c *Changes) FormatDiff(diff string) string {
256 diff = indentBlock(diff, indent)
257 // Never color diff content as we want to color the whole diff
258 diff = EscapeControlCharacters(false, diff)
264 for _, x := range strings.Split(diff, "\n") {
265 if strings.HasPrefix(x, indent+"+") {
266 x = ColorString(c.IsTTY, ColorGreen, x)
267 } else if strings.HasPrefix(x, indent+"-") {
268 x = ColorString(c.IsTTY, ColorRed, x)
272 return strings.Join(res, "\n")
275 func indentBlock(x string, sep string) string {
276 lines := strings.Split(x, "\n")
277 if lines[len(lines)-1] == "" {
278 lines = lines[:len(lines)-1]
280 lines = append(lines, "\\ No newline at end of file")
283 return sep + strings.Join(lines, "\n"+sep)