github.com/jfrog/jfrog-client-go@v1.40.2/utils/git.go (about) 1 package utils 2 3 import ( 4 "bufio" 5 "bytes" 6 "errors" 7 ioutils "github.com/jfrog/gofrog/io" 8 "os" 9 "os/exec" 10 "path/filepath" 11 "regexp" 12 "strings" 13 14 "github.com/go-git/go-git/v5" 15 "github.com/go-git/go-git/v5/plumbing" 16 "github.com/jfrog/jfrog-client-go/utils/errorutils" 17 "github.com/jfrog/jfrog-client-go/utils/io/fileutils" 18 "github.com/jfrog/jfrog-client-go/utils/log" 19 ) 20 21 type GitManager struct { 22 path string 23 err error 24 revision string 25 url string 26 branch string 27 message string 28 submoduleDotGitPath string 29 } 30 31 func NewGitManager(path string) *GitManager { 32 dotGitPath := filepath.Join(path, ".git") 33 return &GitManager{path: dotGitPath} 34 } 35 36 func (m *GitManager) ExecGit(args ...string) (string, string, error) { 37 var stdout bytes.Buffer 38 var stderr bytes.Buffer 39 cmd := exec.Command("git", args...) 40 cmd.Stdin = nil 41 cmd.Stdout = &stdout 42 cmd.Stderr = &stderr 43 err := cmd.Run() 44 return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), errorutils.CheckError(err) 45 } 46 47 func (m *GitManager) ReadConfig() error { 48 if m.path == "" { 49 return errorutils.CheckErrorf(".git path must be defined") 50 } 51 if !fileutils.IsPathExists(m.path, false) { 52 return errorutils.CheckErrorf(".git path must exist in order to collect vcs details") 53 } 54 55 m.handleSubmoduleIfNeeded() 56 m.readRevisionAndBranch() 57 m.readUrl() 58 if m.revision != "" { 59 m.readMessage() 60 } 61 return m.err 62 } 63 64 // If .git is a file and not a directory, assume it is a git submodule and extract the actual .git directory of the submodule. 65 // The actual .git directory is under the parent project's .git/modules directory. 66 func (m *GitManager) handleSubmoduleIfNeeded() { 67 exists, err := fileutils.IsFileExists(m.path, false) 68 if err != nil { 69 m.err = err 70 return 71 } 72 if !exists { 73 // .git is a directory, continue extracting vcs details. 74 return 75 } 76 // ask git for where the .git directory is directly for submodules and worktrees 77 var stdout bytes.Buffer 78 var stderr bytes.Buffer 79 cmd := exec.Command("git", "rev-parse", "--git-common-dir") 80 cmd.Dir = filepath.Dir(m.path) 81 cmd.Stdin = nil 82 cmd.Stdout = &stdout 83 cmd.Stderr = &stderr 84 err = cmd.Run() 85 if m.err = errors.Join(m.err, err); m.err != nil { 86 return 87 } 88 resolvedGitPath := strings.TrimSpace(stdout.String()) 89 exists, err = fileutils.IsDirExists(resolvedGitPath, false) 90 if m.err = errors.Join(m.err, err); m.err != nil { 91 return 92 } 93 if !exists { 94 m.err = errorutils.CheckErrorf("path found in .git file '" + m.path + "' does not exist: '" + resolvedGitPath + "'") 95 return 96 } 97 m.path = resolvedGitPath 98 } 99 100 func (m *GitManager) GetUrl() string { 101 return m.url 102 } 103 104 func (m *GitManager) GetRevision() string { 105 return m.revision 106 } 107 108 func (m *GitManager) GetBranch() string { 109 return m.branch 110 } 111 112 func (m *GitManager) GetMessage() string { 113 return m.message 114 } 115 116 func (m *GitManager) readUrl() { 117 if m.err != nil { 118 return 119 } 120 dotGitPath := filepath.Join(m.path, "config") 121 file, err := os.Open(dotGitPath) 122 if err != nil { 123 m.err = err 124 return 125 } 126 defer func() { 127 m.err = errors.Join(m.err, errorutils.CheckError(file.Close())) 128 }() 129 130 scanner := bufio.NewScanner(file) 131 var IsNextLineUrl bool 132 var originUrl string 133 for scanner.Scan() { 134 if IsNextLineUrl { 135 text := strings.TrimSpace(scanner.Text()) 136 if strings.HasPrefix(text, "url") { 137 originUrl = strings.TrimSpace(strings.SplitAfter(text, "=")[1]) 138 break 139 } 140 } 141 if scanner.Text() == "[remote \"origin\"]" { 142 IsNextLineUrl = true 143 } 144 } 145 if err := scanner.Err(); err != nil { 146 m.err = errorutils.CheckError(err) 147 return 148 } 149 if !strings.HasSuffix(originUrl, ".git") { 150 originUrl += ".git" 151 } 152 m.url = originUrl 153 154 // Mask url if required 155 matchedResult := regexp.MustCompile(CredentialsInUrlRegexp).FindString(originUrl) 156 if matchedResult == "" { 157 return 158 } 159 m.url = RemoveCredentials(originUrl, matchedResult) 160 } 161 162 func (m *GitManager) getRevisionAndBranchPath() (revision, refUrl string, err error) { 163 dotGitPath := filepath.Join(m.path, "HEAD") 164 file, err := os.Open(dotGitPath) 165 if errorutils.CheckError(err) != nil { 166 return 167 } 168 defer ioutils.Close(file, &err) 169 170 scanner := bufio.NewScanner(file) 171 for scanner.Scan() { 172 text := scanner.Text() 173 if strings.HasPrefix(text, "ref") { 174 refUrl = strings.TrimSpace(strings.SplitAfter(text, ":")[1]) 175 break 176 } 177 revision = text 178 } 179 err = errorutils.CheckError(scanner.Err()) 180 return 181 } 182 183 func (m *GitManager) readRevisionAndBranch() { 184 if m.err != nil { 185 return 186 } 187 // This function will either return the revision or the branch ref: 188 revision, ref, err := m.getRevisionAndBranchPath() 189 if err != nil { 190 m.err = err 191 return 192 } 193 if ref != "" { 194 // Get branch short name (refs/heads/master > master) 195 m.branch = plumbing.ReferenceName(ref).Short() 196 } 197 // If the revision was returned, then we're done: 198 if revision != "" { 199 m.revision = revision 200 return 201 } 202 203 // Else, if found ref try getting revision using it. 204 refPath := filepath.Join(m.path, ref) 205 exists, err := fileutils.IsFileExists(refPath, false) 206 if err != nil { 207 m.err = err 208 return 209 } 210 if exists { 211 m.readRevisionFromRef(refPath) 212 return 213 } 214 // Otherwise, try to find .git/packed-refs and look for the HEAD there 215 m.readRevisionFromPackedRef(ref) 216 } 217 218 func (m *GitManager) readRevisionFromRef(refPath string) { 219 revision := "" 220 file, err := os.Open(refPath) 221 if err != nil { 222 m.err = err 223 return 224 } 225 defer func() { 226 m.err = errors.Join(m.err, errorutils.CheckError(file.Close())) 227 }() 228 229 scanner := bufio.NewScanner(file) 230 for scanner.Scan() { 231 text := scanner.Text() 232 revision = strings.TrimSpace(text) 233 break 234 } 235 if err := scanner.Err(); err != nil { 236 m.err = errorutils.CheckError(err) 237 return 238 } 239 m.revision = revision 240 } 241 242 func (m *GitManager) readRevisionFromPackedRef(ref string) { 243 packedRefPath := filepath.Join(m.path, "packed-refs") 244 exists, err := fileutils.IsFileExists(packedRefPath, false) 245 if err != nil { 246 m.err = err 247 return 248 } 249 if exists { 250 file, err := os.Open(packedRefPath) 251 if err != nil { 252 m.err = err 253 return 254 } 255 defer func() { 256 m.err = errors.Join(m.err, errorutils.CheckError(file.Close())) 257 }() 258 259 scanner := bufio.NewScanner(file) 260 for scanner.Scan() { 261 line := scanner.Text() 262 // Expecting to find the revision (the full extended SHA-1, or a unique leading substring) followed by the ref. 263 if strings.HasSuffix(line, ref) { 264 split := strings.Split(line, " ") 265 if len(split) == 2 { 266 m.revision = split[0] 267 } else { 268 m.err = errors.Join(err, errorutils.CheckErrorf("failed fetching revision for ref :"+ref+" - Unexpected line structure in packed-refs file")) 269 } 270 return 271 } 272 } 273 if err = scanner.Err(); err != nil { 274 m.err = errorutils.CheckError(err) 275 return 276 } 277 } 278 log.Debug("No packed-refs file was found. Assuming git repository is empty") 279 } 280 281 func (m *GitManager) readMessage() { 282 if m.err != nil { 283 return 284 } 285 var err error 286 m.message, err = m.doReadMessage() 287 if err != nil { 288 log.Debug("Latest commit message was not extracted due to", err.Error()) 289 } 290 } 291 292 func (m *GitManager) doReadMessage() (string, error) { 293 path := m.getPathHandleSubmodule() 294 gitRepo, err := git.PlainOpenWithOptions(path, &git.PlainOpenOptions{DetectDotGit: false}) 295 if errorutils.CheckError(err) != nil { 296 return "", err 297 } 298 hash, err := gitRepo.ResolveRevision(plumbing.Revision(m.revision)) 299 if errorutils.CheckError(err) != nil { 300 return "", err 301 } 302 message, err := gitRepo.CommitObject(*hash) 303 if errorutils.CheckError(err) != nil { 304 return "", err 305 } 306 return strings.TrimSpace(message.Message), nil 307 } 308 309 func (m *GitManager) getPathHandleSubmodule() (path string) { 310 if m.submoduleDotGitPath == "" { 311 path = m.path 312 } else { 313 path = m.submoduleDotGitPath 314 } 315 path = strings.TrimSuffix(path, filepath.Join("", ".git")) 316 return 317 }