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) formatFileChanges(changes []safcm.FileChange) string {
34 var buf strings.Builder
35 fmt.Fprintf(&buf, "changed %d file(s):", len(changes))
37 fmt.Fprintf(&buf, " (dry-run)")
39 fmt.Fprintf(&buf, "\n")
40 for _, x := range changes {
41 fmt.Fprintf(&buf, "%s:", s.formatTarget(x.Path))
46 ColorString(s.isTTY, ColorGreen, "created"),
47 formatFileType(x.New),
48 formatFileUserGroup(x.New),
49 formatFilePerm(x.New),
52 if x.Old.Mode.Type() != x.New.Mode.Type() {
53 info = append(info, fmt.Sprintf("%s -> %s",
54 formatFileType(x.Old),
55 formatFileType(x.New),
58 if x.Old.User != x.New.User ||
59 x.Old.Uid != x.New.Uid ||
60 x.Old.Group != x.New.Group ||
61 x.Old.Gid != x.New.Gid {
62 info = append(info, fmt.Sprintf("%s -> %s",
63 formatFileUserGroup(x.Old),
64 formatFileUserGroup(x.New),
67 if config.FileModeToFullPerm(x.Old.Mode) !=
68 config.FileModeToFullPerm(x.New.Mode) {
69 info = append(info, fmt.Sprintf("%s -> %s",
70 formatFilePerm(x.Old),
71 formatFilePerm(x.New),
77 fmt.Fprint(&buf, strings.Join(info, ", "))
81 fmt.Fprintf(&buf, "\n%s", s.formatDiff(x.DataDiff))
83 fmt.Fprintf(&buf, "\n")
88 func formatFileType(info safcm.FileChangeInfo) string {
89 switch info.Mode.Type() {
90 case 0: // regular file
97 return fmt.Sprintf("invalid type %v", info.Mode.Type())
100 func formatFileUserGroup(info safcm.FileChangeInfo) string {
101 return fmt.Sprintf("%s(%d) %s(%d)",
102 EscapeControlCharacters(false, info.User), info.Uid,
103 EscapeControlCharacters(false, info.Group), info.Gid)
105 func formatFilePerm(info safcm.FileChangeInfo) string {
106 return fmt.Sprintf("%#o", config.FileModeToFullPerm(info.Mode))
109 func (s *Sync) formatPackageChanges(changes []safcm.PackageChange) string {
110 var buf strings.Builder
111 fmt.Fprintf(&buf, "installed %d package(s):", len(changes))
113 fmt.Fprintf(&buf, " (dry-run)")
115 fmt.Fprintf(&buf, "\n")
116 for _, x := range changes {
117 // TODO: indicate if installation failed
118 fmt.Fprintf(&buf, "%s\n", s.formatTarget(x.Name))
123 func (s *Sync) formatServiceChanges(changes []safcm.ServiceChange) string {
124 var buf strings.Builder
125 fmt.Fprintf(&buf, "modified %d service(s):", len(changes))
127 fmt.Fprintf(&buf, " (dry-run)")
129 fmt.Fprintf(&buf, "\n")
130 for _, x := range changes {
133 info = append(info, "started")
136 info = append(info, "enabled")
138 fmt.Fprintf(&buf, "%s: %s\n",
139 s.formatTarget(x.Name),
140 strings.Join(info, ", "))
145 func (s *Sync) formatCommandChanges(changes []safcm.CommandChange) string {
148 var buf strings.Builder
149 fmt.Fprintf(&buf, "executed %d command(s):", len(changes))
151 fmt.Fprintf(&buf, " (dry-run)")
153 fmt.Fprintf(&buf, "\n")
154 for _, x := range changes {
155 fmt.Fprintf(&buf, "%s", s.formatTarget(x.Command))
157 fmt.Fprintf(&buf, ", trigger for %q", x.Trigger)
160 fmt.Fprintf(&buf, ", failed: %q", x.Error)
163 // TODO: truncate very large outputs?
164 x := indentBlock(x.Output, indent)
165 fmt.Fprintf(&buf, ":\n%s",
166 EscapeControlCharacters(s.isTTY, x))
168 fmt.Fprintf(&buf, "\n")
173 func (s *Sync) formatTarget(x string) string {
174 x = fmt.Sprintf("%q", x) // escape!
175 return ColorString(s.isTTY, ColorCyan, x)
178 func (s *Sync) formatDiff(diff string) string {
181 diff = indentBlock(diff, indent)
182 // Never color diff content as we want to color the whole diff
183 diff = EscapeControlCharacters(false, diff)
189 for _, x := range strings.Split(diff, "\n") {
190 if strings.HasPrefix(x, indent+"+") {
191 x = ColorString(s.isTTY, ColorGreen, x)
192 } else if strings.HasPrefix(x, indent+"-") {
193 x = ColorString(s.isTTY, ColorRed, x)
197 return strings.Join(res, "\n")
200 func indentBlock(x string, sep string) string {
205 lines := strings.Split(x, "\n")
206 if lines[len(lines)-1] == "" {
207 lines = lines[:len(lines)-1]
209 lines = append(lines, "\\ No newline at end of file")
212 return sep + strings.Join(lines, "\n"+sep)