this repo has no description
at master 128 lines 3.2 kB view raw
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 15package vcs 16 17import ( 18 "context" 19 "fmt" 20 "path/filepath" 21 "slices" 22 "strconv" 23 "strings" 24 "time" 25) 26 27type gitVCS struct { 28 root string 29} 30 31func 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]. 45func (v gitVCS) Root() string { 46 return v.root 47} 48 49// fixDir adjusts dir according to the semantics described in [VCS.ListFiles]. 50func 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]. 61func (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 slices.Sort(files) 76 return files, nil 77} 78 79// Status implements [VCS.Status]. 80func (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. 114func 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}