github.com/blend/go-sdk@v1.20220411.3/cmd/copyright/main.go (about) 1 /* 2 3 Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package main 9 10 import ( 11 "bufio" 12 "context" 13 "flag" 14 "fmt" 15 "os" 16 "strings" 17 "time" 18 19 "github.com/blend/go-sdk/ansi" 20 "github.com/blend/go-sdk/copyright" 21 ) 22 23 var ( 24 flagFallbackNoticeTemplate string 25 flagExtensionNoticeTemplates flagStrings 26 flagNoticeBodyTemplate string 27 flagCompany string 28 flagYear int 29 flagLicense string 30 31 flagRestrictions string 32 flagRestrictionsOpenSource bool 33 flagRestrictionsInternal bool 34 35 flagVerify bool 36 flagInject bool 37 flagRemove bool 38 39 flagExcludes flagStrings 40 flagExcludesFrom flagStrings 41 flagExcludeDefaults bool 42 flagIncludeFiles flagStrings 43 44 flagExitFirst bool 45 flagQuiet bool 46 flagVerbose bool 47 flagDebug bool 48 flagShowDiff bool 49 ) 50 51 func init() { 52 flag.BoolVar(&flagQuiet, "quiet", false, "If all output should be suppressed") 53 flag.BoolVar(&flagVerbose, "verbose", false, "If verbose output should be shown") 54 flag.BoolVar(&flagDebug, "debug", false, "If debug output should be shown") 55 flag.BoolVar(&flagShowDiff, "show-diff", false, "If the text diff in verification output should be shown") 56 57 flag.BoolVar(&flagExitFirst, "exit-first", false, "If the program should exit on the first verification error") 58 59 flag.StringVar(&flagNoticeBodyTemplate, "notice-body-template", copyright.DefaultNoticeBodyTemplate, "The notice body template; will try as a file path first, then used as a literal value. This is the template to inject into the filetype specific template for each file.") 60 flag.StringVar(&flagFallbackNoticeTemplate, "fallback-notice-template", "", "The fallback notice template; will try as a file path first, then used as a literal value. This is the full notice (i.e. filetype specific) to use if there is no built-in notice template for the filetype.") 61 62 flag.StringVar(&flagCompany, "company", "", "The company name to use in templates as {{ .Company }}") 63 flag.IntVar(&flagYear, "year", time.Now().UTC().Year(), "The year to use in templates as {{ .Year }}") 64 flag.StringVar(&flagLicense, "license", copyright.DefaultOpenSourceLicense, "The license to use in templates as {{ .License }}") 65 flag.StringVar(&flagRestrictions, "restrictions", copyright.DefaultRestrictionsInternal, "The restriction template to compile and insert in the notice body template as {{ .Restrictions }}") 66 flag.BoolVar(&flagRestrictionsOpenSource, "restrictions-open-source", false, fmt.Sprintf("The restrictions should be the open source defaults (i.e. %q)", copyright.DefaultRestrictionsOpenSource)) 67 flag.BoolVar(&flagRestrictionsInternal, "restrictions-internal", false, fmt.Sprintf("The restrictions should be the internal defaults (i.e. %q)", copyright.DefaultRestrictionsInternal)) 68 69 flag.BoolVar(&flagVerify, "verify", false, "If we should validate notices are present (exclusive with -inject and -remove) (this is the default)") 70 flag.BoolVar(&flagInject, "inject", false, "If we should inject the notice (exclusive with -verify and -remove)") 71 flag.BoolVar(&flagRemove, "remove", false, "If we should remove the notice (exclusive with -verify and -inject)") 72 73 flag.Var(&flagExtensionNoticeTemplates, "ext", "Extension specific notice template overrides overrides; should be in the form -ext=js=js_template.txt, can be multiple") 74 75 flag.BoolVar(&flagExcludeDefaults, "exclude-defaults", true, "If we should add the exclude defaults (e.g. node_modules etc.)") 76 flag.Var(&flagExcludes, "exclude", "Files or directories to exclude via glob match, can be multiple") 77 flag.Var(&flagExcludesFrom, "excludes-from", "A file to read for globs to exclude (e.g. .gitignore), can be multiple") 78 flag.Var(&flagIncludeFiles, "include-file", "Files to include via glob match, can be multiple") 79 80 oldUsage := flag.Usage 81 flag.Usage = func() { 82 fmt.Fprint(flag.CommandLine.Output(), `blend source code copyright management cli 83 84 > copyright [--inject|--verify|--remove] [ROOT(s)...] 85 86 Verify, inject or remove copyright notices from files in a given tree. 87 88 By default, this tool verifies that copyright notices are present with no flags provided. 89 90 Headers are treated exactly; do not edit the headers once they've been injected. 91 92 To verify headers: 93 94 > copyright 95 - OR - 96 > copyright --verify 97 - OR - 98 > copyright --verify ./critical 99 - OR - 100 > copyright --verify ./critical/foo.py 101 102 To inject headers: 103 104 > copyright --inject 105 - OR - 106 > copyright --inject ./critical/foo.py 107 108 - NOTE: you can run "--inject" multiple times; it will only add the header if it is not present. 109 110 To remove headers: 111 112 > copyright --remove 113 114 If you have an old version of the header in your files, and you want to migrate to an updated version: 115 116 - Save the existing header to a file, "notice.txt", including any newlines between the notice and code 117 - Remove existing notices: 118 > copyright --remove -ext=py=notice.txt 119 - Then inject the new notice: 120 > copyright --inject --include-file="*.py" 121 - You should now have the new notice in your files, and "--inject" will honor it 122 123 `, 124 ) 125 oldUsage() 126 } 127 128 flag.Parse() 129 } 130 131 func main() { 132 ctx := context.Background() 133 134 if flagNoticeBodyTemplate == "" { 135 fmt.Fprintln(os.Stderr, "--notice provided is an empty string; cannot continue") 136 os.Exit(1) 137 } 138 139 var roots []string 140 if args := flag.Args(); len(args) > 0 { 141 roots = args[:] 142 } else { 143 roots = []string{"."} 144 } 145 146 if flagExcludeDefaults { 147 flagExcludes = append(flagExcludes, flagStrings(copyright.DefaultExcludes)...) 148 } 149 for _, excludesFrom := range flagExcludesFrom { 150 excludes, err := readExcludesFile(excludesFrom) 151 if err != nil { 152 fmt.Fprintln(os.Stderr, err.Error()) 153 os.Exit(1) 154 } 155 flagExcludes = append(flagExcludes, excludes...) 156 } 157 158 if len(flagIncludeFiles) == 0 { 159 flagIncludeFiles = flagStrings(copyright.DefaultIncludeFiles) 160 } 161 162 var restrictions string 163 if flagRestrictionsOpenSource { 164 restrictions = copyright.DefaultRestrictionsOpenSource 165 } else if flagRestrictionsInternal { 166 restrictions = copyright.DefaultRestrictionsInternal 167 } else { 168 restrictions = flagRestrictions 169 } 170 171 extensionNoticeTemplates := copyright.DefaultExtensionNoticeTemplates 172 for _, extValue := range flagExtensionNoticeTemplates { 173 ext, noticeTemplate, err := parseExtensionNoticeBodyTemplate(extValue) 174 if err != nil { 175 fmt.Fprintln(os.Stderr, err.Error()) 176 os.Exit(1) 177 } 178 extensionNoticeTemplates[ext] = noticeTemplate 179 } 180 181 engine := copyright.Copyright{ 182 Config: copyright.Config{ 183 FallbackNoticeTemplate: tryReadFile(flagFallbackNoticeTemplate), 184 NoticeBodyTemplate: tryReadFile(flagNoticeBodyTemplate), 185 Company: flagCompany, 186 Restrictions: restrictions, 187 Year: flagYear, 188 License: flagLicense, 189 ExtensionNoticeTemplates: extensionNoticeTemplates, 190 Excludes: flagExcludes, 191 IncludeFiles: flagIncludeFiles, 192 ExitFirst: &flagExitFirst, 193 Quiet: &flagQuiet, 194 Verbose: &flagVerbose, 195 Debug: &flagDebug, 196 ShowDiff: &flagShowDiff, 197 }, 198 } 199 200 var actions []func(context.Context, string) error 201 var actionLabels []string 202 203 if flagRemove { 204 actions = append(actions, engine.Remove) 205 actionLabels = append(actionLabels, "remove") 206 } 207 if flagInject { 208 actions = append(actions, engine.Inject) 209 actionLabels = append(actionLabels, "inject") 210 } 211 if flagVerify { 212 actions = append(actions, engine.Verify) 213 actionLabels = append(actionLabels, "verify") 214 } 215 if len(actions) == 0 { 216 actions = append(actions, engine.Verify) 217 actionLabels = append(actionLabels, "verify") 218 } 219 220 for index, action := range actions { 221 didFail := false 222 actionLabel := actionLabels[index] 223 224 for _, root := range roots { 225 maybeFail(ctx, action, root, &didFail) 226 } 227 228 if didFail { 229 if !flagQuiet { 230 fmt.Printf("copyright %s %s!\nuse `copyright --inject` to add missing notices\n", actionLabel, ansi.Red("failed")) 231 } 232 os.Exit(1) 233 } 234 if !flagQuiet { 235 fmt.Printf("copyright %s %s!\n", actionLabel, ansi.Green("ok")) 236 } 237 } 238 } 239 240 type flagStrings []string 241 242 func (fs flagStrings) String() string { 243 return strings.Join(fs, ", ") 244 } 245 246 func (fs *flagStrings) Set(flagValue string) error { 247 if flagValue == "" { 248 return fmt.Errorf("invalid flag value; is empty") 249 } 250 *fs = append(*fs, flagValue) 251 return nil 252 } 253 254 func tryReadFile(path string) string { 255 contents, err := os.ReadFile(path) 256 if err != nil { 257 return path 258 } 259 return strings.TrimSpace(string(contents)) 260 } 261 262 func readExcludesFile(path string) ([]string, error) { 263 f, err := os.Open(path) 264 if err != nil { 265 return nil, err 266 } 267 defer f.Close() 268 var output []string 269 scanner := bufio.NewScanner(f) 270 for scanner.Scan() { 271 output = append(output, strings.TrimSpace(scanner.Text())) 272 } 273 return output, nil 274 } 275 276 func parseExtensionNoticeBodyTemplate(extensionNoticeBodyTemplate string) (extension, noticeBodyTemplate string, err error) { 277 parts := strings.SplitN(extensionNoticeBodyTemplate, "=", 2) 278 if len(parts) < 2 { 279 err = fmt.Errorf("invalid `-ext` value; %s", extensionNoticeBodyTemplate) 280 return 281 } 282 extension = parts[0] 283 if !strings.HasPrefix(extension, ".") { 284 extension = "." + extension 285 } 286 noticeBodyTemplate = tryReadFile(parts[1]) 287 return 288 } 289 290 func maybeFail(ctx context.Context, action func(context.Context, string) error, root string, didFail *bool) { 291 err := action(ctx, root) 292 if err != nil { 293 if err == copyright.ErrFailure { 294 *didFail = true 295 return 296 } 297 fmt.Fprintf(os.Stderr, "%+v\n", err) 298 os.Exit(1) 299 } 300 }