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  }