github.com/apache/skywalking-eyes@v0.6.0/pkg/header/check.go (about)

     1  // Licensed to the Apache Software Foundation (ASF) under one
     2  // or more contributor license agreements.  See the NOTICE file
     3  // distributed with this work for additional information
     4  // regarding copyright ownership.  The ASF licenses this file
     5  // to you under the Apache License, Version 2.0 (the
     6  // "License"); you may not use this file except in compliance
     7  // with the License.  You may obtain a copy of the License at
     8  //
     9  //     http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing,
    12  // software distributed under the License is distributed on an
    13  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    14  // KIND, either express or implied.  See the License for the
    15  // specific language governing permissions and limitations
    16  // under the License.
    17  
    18  package header
    19  
    20  import (
    21  	"errors"
    22  	"io/fs"
    23  	"net/http"
    24  	"os"
    25  	"path/filepath"
    26  	"regexp"
    27  	"strings"
    28  
    29  	"github.com/apache/skywalking-eyes/internal/logger"
    30  	lcs "github.com/apache/skywalking-eyes/pkg/license"
    31  
    32  	"github.com/bmatcuk/doublestar/v2"
    33  	"github.com/go-git/go-billy/v5/osfs"
    34  	"github.com/go-git/go-git/v5"
    35  	"github.com/go-git/go-git/v5/plumbing/format/gitignore"
    36  	"github.com/go-git/go-git/v5/plumbing/object"
    37  )
    38  
    39  // Check checks the license headers of the specified paths/globs.
    40  func Check(config *ConfigHeader, result *Result) error {
    41  	fileList, err := listFiles(config)
    42  	if err != nil {
    43  		return err
    44  	}
    45  
    46  	for _, file := range fileList {
    47  		if err := CheckFile(file, config, result); err != nil {
    48  			return err
    49  		}
    50  	}
    51  
    52  	return nil
    53  }
    54  
    55  func listFiles(config *ConfigHeader) ([]string, error) {
    56  	var fileList []string
    57  
    58  	repo, err := git.PlainOpen("./")
    59  
    60  	if err != nil { // we're not in a Git workspace, fallback to glob paths
    61  		var localFileList []string
    62  		for _, pattern := range config.Paths {
    63  			if pattern == "." {
    64  				pattern = "./"
    65  			}
    66  			files, err := doublestar.Glob(pattern)
    67  			if err != nil {
    68  				return nil, err
    69  			}
    70  			localFileList = append(localFileList, files...)
    71  		}
    72  
    73  		seen := make(map[string]bool)
    74  		for _, file := range localFileList {
    75  			files, err := walkFile(file, seen)
    76  			if err != nil {
    77  				return nil, err
    78  			}
    79  			fileList = append(fileList, files...)
    80  		}
    81  	} else {
    82  		var candidates []string
    83  
    84  		t, _ := repo.Worktree()
    85  		if t.Excludes == nil {
    86  			t.Excludes = make([]gitignore.Pattern, 0)
    87  		}
    88  		if ignorePattens, err := gitignore.LoadGlobalPatterns(osfs.New("")); err == nil {
    89  			t.Excludes = append(t.Excludes, ignorePattens...)
    90  		}
    91  		if ignorePattens, err := gitignore.LoadSystemPatterns(osfs.New("")); err == nil {
    92  			t.Excludes = append(t.Excludes, ignorePattens...)
    93  		}
    94  		s, _ := t.Status()
    95  		for file := range s {
    96  			candidates = append(candidates, file)
    97  		}
    98  
    99  		head, _ := repo.Head()
   100  		commit, _ := repo.CommitObject(head.Hash())
   101  		tree, err := commit.Tree()
   102  		if err != nil {
   103  			return nil, err
   104  		}
   105  		if err := tree.Files().ForEach(func(file *object.File) error {
   106  			if file == nil {
   107  				return errors.New("file pointer is nil")
   108  			}
   109  			candidates = append(candidates, file.Name)
   110  			return nil
   111  		}); err != nil {
   112  			return nil, err
   113  		}
   114  
   115  		seen := make(map[string]bool)
   116  		for _, file := range candidates {
   117  			if !seen[file] {
   118  				seen[file] = true
   119  				_, err := os.Stat(file)
   120  				if err == nil {
   121  					fileList = append(fileList, file)
   122  				} else if !os.IsNotExist(err) {
   123  					return nil, err
   124  				}
   125  			}
   126  		}
   127  	}
   128  
   129  	return fileList, nil
   130  }
   131  
   132  func walkFile(file string, seen map[string]bool) ([]string, error) {
   133  	var files []string
   134  
   135  	if seen[file] {
   136  		return files, nil
   137  	}
   138  	seen[file] = true
   139  
   140  	if stat, err := os.Stat(file); err == nil {
   141  		switch mode := stat.Mode(); {
   142  		case mode.IsRegular():
   143  			files = append(files, file)
   144  		case mode.IsDir():
   145  			err := filepath.Walk(file, func(path string, info fs.FileInfo, err error) error {
   146  				if path == file {
   147  					// when path is symbolic link file, it causes infinite recursive calls
   148  					return nil
   149  				}
   150  				if seen[path] {
   151  					return nil
   152  				}
   153  				seen[path] = true
   154  				if info.Mode().IsRegular() {
   155  					files = append(files, path)
   156  				}
   157  				return nil
   158  			})
   159  			if err != nil {
   160  				return files, err
   161  			}
   162  		}
   163  	}
   164  
   165  	return files, nil
   166  }
   167  
   168  // CheckFile checks whether the file contains the configured license header.
   169  func CheckFile(file string, config *ConfigHeader, result *Result) error {
   170  	if yes, err := config.ShouldIgnore(file); yes || err != nil {
   171  		result.Ignore(file)
   172  		return err
   173  	}
   174  
   175  	logger.Log.Debugln("Checking file:", file)
   176  
   177  	bs, err := os.ReadFile(file)
   178  	if err != nil {
   179  		return err
   180  	}
   181  	if t := http.DetectContentType(bs); !strings.HasPrefix(t, "text/") {
   182  		logger.Log.Debugln("Ignoring file:", file, "; type:", t)
   183  		return nil
   184  	}
   185  
   186  	content := lcs.NormalizeHeader(string(bs))
   187  	expected, pattern := config.NormalizedLicense(), config.NormalizedPattern()
   188  
   189  	if satisfy(content, config, expected, pattern) {
   190  		result.Succeed(file)
   191  	} else {
   192  		logger.Log.Debugln("Content is:", content)
   193  
   194  		result.Fail(file)
   195  	}
   196  
   197  	return nil
   198  }
   199  
   200  func satisfy(content string, config *ConfigHeader, license string, pattern *regexp.Regexp) bool {
   201  	if index := strings.Index(content, license); strings.TrimSpace(license) != "" && index >= 0 {
   202  		return index < config.LicenseLocationThreshold
   203  	}
   204  
   205  	if pattern == nil {
   206  		return false
   207  	}
   208  	index := pattern.FindStringIndex(content)
   209  
   210  	return len(index) == 2 && index[0] < config.LicenseLocationThreshold
   211  }