github.com/aclements/go-misc@v0.0.0-20240129233631-2f6ede80790c/gover/cache.go (about)

     1  // Copyright 2016 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"io/ioutil"
     9  	"log"
    10  	"os"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strconv"
    14  	"strings"
    15  	"time"
    16  )
    17  
    18  var hashNameRe = regexp.MustCompile(`^[0-9a-f]{7,40}(\+[0-9a-f]{1,10})?$`)
    19  var fullHashRe = regexp.MustCompile("^[0-9a-f]{40}$")
    20  var hashPlusRe = regexp.MustCompile(`^[0-9a-f]{40}(\+[0-9a-f]{10})?$`)
    21  
    22  // resolveName returns the path to the root of the named build and
    23  // whether or not that path exists. It will log an error and exit if
    24  // name is ambiguous. If the path does not exist, the returned path is
    25  // where this build should be saved.
    26  func resolveName(name string) (path string, ok bool) {
    27  	// If the name exactly matches a saved version, return it.
    28  	savePath := filepath.Join(*verDir, name)
    29  	st, err := os.Stat(savePath)
    30  	if err == nil && st.IsDir() {
    31  		return savePath, true
    32  	}
    33  
    34  	// Otherwise, try to resolve it as an unambiguous hash prefix.
    35  	if hashNameRe.MatchString(name) {
    36  		nameParts := strings.SplitN(name, "+", 2)
    37  		builds, err := listBuilds(0)
    38  		if err != nil {
    39  			log.Fatal(err)
    40  		}
    41  
    42  		var fullName string
    43  		for _, b := range builds {
    44  			if !strings.HasPrefix(b.commitHash, nameParts[0]) {
    45  				continue
    46  			}
    47  			if (len(nameParts) == 1) != (b.deltaHash == "") {
    48  				continue
    49  			}
    50  			if len(nameParts) > 1 && !strings.HasPrefix(b.deltaHash, nameParts[1]) {
    51  				continue
    52  			}
    53  
    54  			// We found a match.
    55  			if fullName != "" {
    56  				log.Fatalf("ambiguous name `%s`", name)
    57  			}
    58  			fullName = b.fullName()
    59  		}
    60  		if fullName != "" {
    61  			return filepath.Join(*verDir, fullName), true
    62  		}
    63  	}
    64  
    65  	return savePath, false
    66  }
    67  
    68  type buildInfo struct {
    69  	path       string
    70  	commitHash string
    71  	deltaHash  string
    72  	names      []string
    73  	commit     *commit
    74  }
    75  
    76  func (i buildInfo) fullName() string {
    77  	if i.deltaHash == "" {
    78  		return i.commitHash
    79  	}
    80  	return i.commitHash + "+" + i.deltaHash
    81  }
    82  
    83  func (i buildInfo) shortName() string {
    84  	// TODO: Print more than 7 characters if necessary.
    85  	if i.deltaHash == "" {
    86  		return i.commitHash[:7]
    87  	}
    88  	return i.commitHash[:7] + "+" + i.deltaHash
    89  }
    90  
    91  type listFlags int
    92  
    93  const (
    94  	listNames listFlags = 1 << iota
    95  	listCommit
    96  )
    97  
    98  func listBuilds(flags listFlags) ([]*buildInfo, error) {
    99  	files, err := ioutil.ReadDir(*verDir)
   100  	if os.IsNotExist(err) {
   101  		return nil, nil
   102  	} else if err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	// Collect the saved builds.
   107  	builds := []*buildInfo{}
   108  	var baseMap map[string]*buildInfo
   109  	if flags&listNames != 0 {
   110  		baseMap = make(map[string]*buildInfo)
   111  	}
   112  	for _, file := range files {
   113  		if !file.IsDir() || !hashPlusRe.MatchString(file.Name()) {
   114  			continue
   115  		}
   116  		nameParts := strings.SplitN(file.Name(), "+", 2)
   117  		info := &buildInfo{path: filepath.Join(*verDir, file.Name()), commitHash: nameParts[0]}
   118  		if len(nameParts) > 1 {
   119  			info.deltaHash = nameParts[1]
   120  		}
   121  
   122  		builds = append(builds, info)
   123  		if baseMap != nil {
   124  			baseMap[file.Name()] = info
   125  		}
   126  
   127  		if flags&listCommit != 0 {
   128  			commit, err := ioutil.ReadFile(filepath.Join(*verDir, file.Name(), "commit"))
   129  			if err != nil {
   130  				log.Fatal(err)
   131  			} else {
   132  				info.commit = parseCommit(commit)
   133  			}
   134  		}
   135  	}
   136  
   137  	// Collect the names for each build.
   138  	if flags&listNames != 0 {
   139  		for _, file := range files {
   140  			if file.Mode()&os.ModeType == os.ModeSymlink {
   141  				base, err := os.Readlink(filepath.Join(*verDir, file.Name()))
   142  				if err != nil {
   143  					continue
   144  				}
   145  				if info, ok := baseMap[base]; ok {
   146  					info.names = append(info.names, file.Name())
   147  				}
   148  			}
   149  		}
   150  	}
   151  
   152  	return builds, nil
   153  }
   154  
   155  type commit struct {
   156  	authorDate time.Time
   157  	topLine    string
   158  }
   159  
   160  func parseCommit(obj []byte) *commit {
   161  	out := &commit{}
   162  	lines := strings.Split(string(obj), "\n")
   163  	for i, line := range lines {
   164  		if strings.HasPrefix(line, "author ") {
   165  			fs := strings.Fields(line)
   166  			secs, err := strconv.ParseInt(fs[len(fs)-2], 10, 64)
   167  			if err != nil {
   168  				log.Fatalf("malformed author in commit: %s", err)
   169  			}
   170  			out.authorDate = time.Unix(secs, 0)
   171  		}
   172  		if len(line) == 0 {
   173  			out.topLine = lines[i+1]
   174  			break
   175  		}
   176  	}
   177  	return out
   178  }