]> ruderich.org/simon Gitweb - safcm/safcm.git/blob - cmd/safcm/sync_changes.go
46349116a602d1fb7e389b7b089116aabbbbd418
[safcm/safcm.git] / cmd / safcm / sync_changes.go
1 // "sync" sub-command: format changes
2
3 // Copyright (C) 2021  Simon Ruderich
4 //
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.
9 //
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.
14 //
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/>.
17
18 package main
19
20 import (
21         "fmt"
22         "io/fs"
23         "strings"
24
25         "ruderich.org/simon/safcm"
26         "ruderich.org/simon/safcm/cmd/safcm/config"
27 )
28
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().
32
33 func (s *Sync) formatFileChanges(changes []safcm.FileChange) string {
34         var buf strings.Builder
35         fmt.Fprintf(&buf, "changed %d file(s):", len(changes))
36         if s.config.DryRun {
37                 fmt.Fprintf(&buf, " (dry-run)")
38         }
39         fmt.Fprintf(&buf, "\n")
40         for _, x := range changes {
41                 fmt.Fprintf(&buf, "%s:", s.formatTarget(x.Path))
42
43                 var info []string
44                 if x.Created {
45                         info = append(info,
46                                 ColorString(s.isTTY, ColorGreen, "created"),
47                                 formatFileType(x.New),
48                                 formatFileUserGroup(x.New),
49                                 formatFilePerm(x.New),
50                         )
51                 } else {
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),
56                                 ))
57                         }
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),
65                                 ))
66                         }
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),
72                                 ))
73                         }
74                 }
75                 if len(info) > 0 {
76                         fmt.Fprint(&buf, " ")
77                         fmt.Fprint(&buf, strings.Join(info, ", "))
78                 }
79
80                 if x.DataDiff != "" {
81                         fmt.Fprintf(&buf, "\n%s", s.formatDiff(x.DataDiff))
82                 }
83                 fmt.Fprintf(&buf, "\n")
84         }
85
86         return buf.String()
87 }
88 func formatFileType(info safcm.FileChangeInfo) string {
89         switch info.Mode.Type() {
90         case 0: // regular file
91                 return "file"
92         case fs.ModeSymlink:
93                 return "symlink"
94         case fs.ModeDir:
95                 return "dir"
96         default:
97                 return fmt.Sprintf("invalid type %v", info.Mode.Type())
98         }
99 }
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)
104 }
105 func formatFilePerm(info safcm.FileChangeInfo) string {
106         return fmt.Sprintf("%#o", config.FileModeToFullPerm(info.Mode))
107 }
108
109 func (s *Sync) formatPackageChanges(changes []safcm.PackageChange) string {
110         var buf strings.Builder
111         fmt.Fprintf(&buf, "installed %d package(s):", len(changes))
112         if s.config.DryRun {
113                 fmt.Fprintf(&buf, " (dry-run)")
114         }
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))
119         }
120         return buf.String()
121 }
122
123 func (s *Sync) formatServiceChanges(changes []safcm.ServiceChange) string {
124         var buf strings.Builder
125         fmt.Fprintf(&buf, "modified %d service(s):", len(changes))
126         if s.config.DryRun {
127                 fmt.Fprintf(&buf, " (dry-run)")
128         }
129         fmt.Fprintf(&buf, "\n")
130         for _, x := range changes {
131                 var info []string
132                 if x.Started {
133                         info = append(info, "started")
134                 }
135                 if x.Enabled {
136                         info = append(info, "enabled")
137                 }
138                 fmt.Fprintf(&buf, "%s: %s\n",
139                         s.formatTarget(x.Name),
140                         strings.Join(info, ", "))
141         }
142         return buf.String()
143 }
144
145 func (s *Sync) formatCommandChanges(changes []safcm.CommandChange) string {
146         const indent = "   > "
147
148         // Quiet hides all successful, non-trigger commands which produce no
149         // output. This is useful as many commands will be used to enforce a
150         // certain state (e.g. file not-present, `ainsl`, etc.) and are run on
151         // each sync. Displaying them provides not much useful information.
152         // Instead, quiet shows them only when they produce output (e.g.
153         // `ainsl`, `rm -v`) and thus modify the host's state.
154         var noOutput int
155         if s.config.Quiet && !s.config.DryRun {
156                 for _, x := range changes {
157                         if x.Trigger == "" &&
158                                 x.Error == "" &&
159                                 x.Output == "" {
160                                 noOutput++
161                         }
162                 }
163         }
164
165         var buf strings.Builder
166         fmt.Fprintf(&buf, "executed %d command(s)", len(changes))
167         if noOutput > 0 {
168                 fmt.Fprintf(&buf, ", %d with no output", noOutput)
169         }
170         if noOutput != len(changes) {
171                 fmt.Fprintf(&buf, ":")
172         }
173         if s.config.DryRun {
174                 fmt.Fprintf(&buf, " (dry-run)")
175         }
176         fmt.Fprintf(&buf, "\n")
177         for _, x := range changes {
178                 if noOutput > 0 &&
179                         x.Trigger == "" && x.Error == "" && x.Output == "" {
180                         continue
181                 }
182
183                 fmt.Fprintf(&buf, "%s", s.formatTarget(x.Command))
184                 if x.Trigger != "" {
185                         fmt.Fprintf(&buf, ", trigger for %q", x.Trigger)
186                 }
187                 if x.Error != "" {
188                         fmt.Fprintf(&buf, ", failed: %q", x.Error)
189                 }
190                 if x.Output != "" {
191                         // TODO: truncate very large outputs?
192                         x := indentBlock(x.Output, indent)
193                         fmt.Fprintf(&buf, ":\n%s",
194                                 EscapeControlCharacters(s.isTTY, x))
195                 }
196                 fmt.Fprintf(&buf, "\n")
197         }
198         return buf.String()
199 }
200
201 func (s *Sync) formatTarget(x string) string {
202         x = fmt.Sprintf("%q", x) // escape!
203         return ColorString(s.isTTY, ColorCyan, x)
204 }
205
206 func (s *Sync) formatDiff(diff string) string {
207         const indent = "   "
208
209         diff = indentBlock(diff, indent)
210         // Never color diff content as we want to color the whole diff
211         diff = EscapeControlCharacters(false, diff)
212         if !s.isTTY {
213                 return diff
214         }
215
216         var res []string
217         for _, x := range strings.Split(diff, "\n") {
218                 if strings.HasPrefix(x, indent+"+") {
219                         x = ColorString(s.isTTY, ColorGreen, x)
220                 } else if strings.HasPrefix(x, indent+"-") {
221                         x = ColorString(s.isTTY, ColorRed, x)
222                 }
223                 res = append(res, x)
224         }
225         return strings.Join(res, "\n")
226 }
227
228 func indentBlock(x string, sep string) string {
229         if x == "" {
230                 return ""
231         }
232
233         lines := strings.Split(x, "\n")
234         if lines[len(lines)-1] == "" {
235                 lines = lines[:len(lines)-1]
236         } else {
237                 lines = append(lines, "\\ No newline at end of file")
238         }
239
240         return sep + strings.Join(lines, "\n"+sep)
241 }