1 // "sync" sub-command: 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().
33 func (s *Sync) formatChanges(resp safcm.MsgSyncResp) string {
35 if len(resp.FileChanges) > 0 {
36 changes = append(changes,
37 s.formatFileChanges(resp.FileChanges))
39 if len(resp.PackageChanges) > 0 {
40 changes = append(changes,
41 s.formatPackageChanges(resp.PackageChanges))
43 if len(resp.ServiceChanges) > 0 {
44 changes = append(changes,
45 s.formatServiceChanges(resp.ServiceChanges))
47 if len(resp.CommandChanges) > 0 {
48 changes = append(changes,
49 s.formatCommandChanges(resp.CommandChanges))
51 if len(changes) == 0 {
52 // Notify user that the host was synced successfully
56 x := strings.Join(changes, "\n")
57 // If quiet is used and only commands without output were executed
58 // then don't prepend a newline so that the whole change output of a
59 // host fits in a single line. This makes the output much more
60 // readable with multiple hosts.
61 if strings.Count(x, "\n") == 1 {
67 func (s *Sync) formatFileChanges(changes []safcm.FileChange) string {
68 var buf strings.Builder
70 fmt.Fprintf(&buf, "will change %d file(s): (dry-run)\n",
73 fmt.Fprintf(&buf, "changed %d file(s):\n", len(changes))
75 for _, x := range changes {
76 fmt.Fprintf(&buf, "%s:", s.formatTarget(x.Path))
81 ColorString(s.isTTY, ColorGreen, "created"),
82 formatFileType(x.New),
83 formatFileUserGroup(x.New),
84 formatFilePerm(x.New),
87 if x.Old.Mode.Type() != x.New.Mode.Type() {
88 info = append(info, fmt.Sprintf("%s -> %s",
89 formatFileType(x.Old),
90 formatFileType(x.New),
93 if x.Old.User != x.New.User ||
94 x.Old.Uid != x.New.Uid ||
95 x.Old.Group != x.New.Group ||
96 x.Old.Gid != x.New.Gid {
97 info = append(info, fmt.Sprintf("%s -> %s",
98 formatFileUserGroup(x.Old),
99 formatFileUserGroup(x.New),
102 if config.FileModeToFullPerm(x.Old.Mode) !=
103 config.FileModeToFullPerm(x.New.Mode) {
104 info = append(info, fmt.Sprintf("%s -> %s",
105 formatFilePerm(x.Old),
106 formatFilePerm(x.New),
111 fmt.Fprint(&buf, " ")
112 fmt.Fprint(&buf, strings.Join(info, ", "))
115 if x.DataDiff != "" {
116 fmt.Fprintf(&buf, "\n%s", s.formatDiff(x.DataDiff))
118 fmt.Fprintf(&buf, "\n")
122 func formatFileType(info safcm.FileChangeInfo) string {
123 switch info.Mode.Type() {
124 case 0: // regular file
131 return fmt.Sprintf("invalid type %v", info.Mode.Type())
134 func formatFileUserGroup(info safcm.FileChangeInfo) string {
135 return fmt.Sprintf("%s(%d) %s(%d)",
136 EscapeControlCharacters(false, info.User), info.Uid,
137 EscapeControlCharacters(false, info.Group), info.Gid)
139 func formatFilePerm(info safcm.FileChangeInfo) string {
140 return fmt.Sprintf("%#o", config.FileModeToFullPerm(info.Mode))
143 func (s *Sync) formatPackageChanges(changes []safcm.PackageChange) string {
144 var buf strings.Builder
146 fmt.Fprintf(&buf, "will install %d package(s): (dry-run)\n",
149 fmt.Fprintf(&buf, "installed %d package(s):\n", len(changes))
151 for _, x := range changes {
152 // TODO: indicate if installation failed
153 fmt.Fprintf(&buf, "%s\n", s.formatTarget(x.Name))
158 func (s *Sync) formatServiceChanges(changes []safcm.ServiceChange) string {
159 var buf strings.Builder
161 fmt.Fprintf(&buf, "will modify %d service(s): (dry-run)\n",
164 fmt.Fprintf(&buf, "modified %d service(s):\n", len(changes))
166 for _, x := range changes {
169 info = append(info, "started")
172 info = append(info, "enabled")
174 fmt.Fprintf(&buf, "%s: %s\n",
175 s.formatTarget(x.Name),
176 strings.Join(info, ", "))
181 func (s *Sync) formatCommandChanges(changes []safcm.CommandChange) string {
184 // Quiet hides all successful, non-trigger commands which produce no
185 // output. This is useful as many commands will be used to enforce a
186 // certain state (e.g. file not-present, `ainsl`, etc.) and are run on
187 // each sync. Displaying them provides not much useful information.
188 // Instead, quiet shows them only when they produce output (e.g.
189 // `ainsl`, `rm -v`) and thus modify the host's state.
192 for _, x := range changes {
193 if x.Trigger == "" &&
201 var buf strings.Builder
203 fmt.Fprintf(&buf, "will execute %d command(s)", len(changes))
205 fmt.Fprintf(&buf, "executed %d command(s)", len(changes))
207 if noOutput > 0 && !s.config.DryRun {
208 fmt.Fprintf(&buf, ", %d with no output (hidden)", noOutput)
210 if noOutput != len(changes) {
211 fmt.Fprintf(&buf, ":")
214 fmt.Fprintf(&buf, " (dry-run)")
216 fmt.Fprintf(&buf, "\n")
217 for _, x := range changes {
219 x.Trigger == "" && x.Error == "" && x.Output == "" {
223 fmt.Fprintf(&buf, "%s", s.formatTarget(x.Command))
225 fmt.Fprintf(&buf, ", trigger for %q", x.Trigger)
228 fmt.Fprintf(&buf, ", failed: %q", x.Error)
231 // TODO: truncate very large outputs?
232 x := indentBlock(x.Output, indent)
233 fmt.Fprintf(&buf, ":\n%s",
234 EscapeControlCharacters(s.isTTY, x))
236 fmt.Fprintf(&buf, "\n")
241 func (s *Sync) formatTarget(x string) string {
242 x = fmt.Sprintf("%q", x) // escape!
243 return ColorString(s.isTTY, ColorCyan, x)
246 func (s *Sync) formatDiff(diff string) string {
249 diff = indentBlock(diff, indent)
250 // Never color diff content as we want to color the whole diff
251 diff = EscapeControlCharacters(false, diff)
257 for _, x := range strings.Split(diff, "\n") {
258 if strings.HasPrefix(x, indent+"+") {
259 x = ColorString(s.isTTY, ColorGreen, x)
260 } else if strings.HasPrefix(x, indent+"-") {
261 x = ColorString(s.isTTY, ColorRed, x)
265 return strings.Join(res, "\n")
268 func indentBlock(x string, sep string) string {
269 lines := strings.Split(x, "\n")
270 if lines[len(lines)-1] == "" {
271 lines = lines[:len(lines)-1]
273 lines = append(lines, "\\ No newline at end of file")
276 return sep + strings.Join(lines, "\n"+sep)