cuelang.org/go@v0.10.1/internal/vcs/git.go (about) 1 // Copyright 2024 CUE Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package vcs 16 17 import ( 18 "context" 19 "fmt" 20 "path/filepath" 21 "sort" 22 "strconv" 23 "strings" 24 "time" 25 ) 26 27 type gitVCS struct { 28 root string 29 } 30 31 func newGitVCS(dir string) (VCS, error) { 32 root := findRoot(dir, ".git") 33 if root == "" { 34 return nil, &vcsNotFoundError{ 35 kind: "git", 36 dir: dir, 37 } 38 } 39 return gitVCS{ 40 root: root, 41 }, nil 42 } 43 44 // Root implements [VCS.Root]. 45 func (v gitVCS) Root() string { 46 return v.root 47 } 48 49 // fixDir adjusts dir according to the semantics described in [VCS.ListFiles]. 50 func fixDir(v VCS, dir string) string { 51 if dir == "" { 52 return v.Root() 53 } 54 if !filepath.IsAbs(dir) { 55 return filepath.Join(v.Root(), dir) 56 } 57 return dir 58 } 59 60 // ListFiles implements [VCS.ListFiles]. 61 func (v gitVCS) ListFiles(ctx context.Context, dir string, paths ...string) ([]string, error) { 62 dir = fixDir(v, dir) 63 64 // TODO should we use --recurse-submodules? 65 gitargs := append([]string{"ls-files", "-z", "--"}, paths...) 66 out, err := runCmd(ctx, dir, "git", gitargs...) 67 if err != nil { 68 return nil, err 69 } 70 out = strings.TrimSuffix(out, "\x00") 71 if out == "" { 72 return nil, nil 73 } 74 files := strings.Split(out, "\x00") 75 sort.Strings(files) 76 return files, nil 77 } 78 79 // Status implements [VCS.Status]. 80 func (v gitVCS) Status(ctx context.Context, paths ...string) (Status, error) { 81 gitargs := append([]string{"status", "--porcelain", "--"}, paths...) 82 out, err := runCmd(ctx, v.root, "git", gitargs...) 83 if err != nil { 84 return Status{}, err 85 } 86 uncommitted := len(out) > 0 87 88 // "git status" works for empty repositories, but "git log" does not. 89 // Assume there are no commits in the repo when "git log" fails with 90 // uncommitted files and skip tagging revision / committime. 91 var rev string 92 var commitTime time.Time 93 out, err = runCmd(ctx, v.root, "git", 94 "-c", "log.showsignature=false", 95 "log", "-1", "--format=%H:%ct", 96 ) 97 if err != nil && !uncommitted { 98 return Status{}, err 99 } 100 if err == nil { 101 rev, commitTime, err = parseRevTime(out) 102 if err != nil { 103 return Status{}, err 104 } 105 } 106 return Status{ 107 Revision: rev, 108 CommitTime: commitTime, 109 Uncommitted: uncommitted, 110 }, nil 111 } 112 113 // parseRevTime parses commit details in "revision:seconds" format. 114 func parseRevTime(out string) (string, time.Time, error) { 115 buf := strings.TrimSpace(out) 116 117 rev, t, _ := strings.Cut(buf, ":") 118 if rev == "" { 119 return "", time.Time{}, fmt.Errorf("unrecognized VCS tool output %q", out) 120 } 121 122 secs, err := strconv.ParseInt(t, 10, 64) 123 if err != nil { 124 return "", time.Time{}, fmt.Errorf("unrecognized VCS tool output: %v", err) 125 } 126 127 return rev, time.Unix(secs, 0), nil 128 }