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  }