X Tutup
package git import ( "bytes" "errors" "fmt" "io" "net/url" "os" "os/exec" "path" "regexp" "runtime" "strings" "github.com/cli/cli/internal/run" "github.com/cli/safeexec" ) // ErrNotOnAnyBranch indicates that the user is in detached HEAD state var ErrNotOnAnyBranch = errors.New("git: not on any branch") // Ref represents a git commit reference type Ref struct { Hash string Name string } // TrackingRef represents a ref for a remote tracking branch type TrackingRef struct { RemoteName string BranchName string } func (r TrackingRef) String() string { return "refs/remotes/" + r.RemoteName + "/" + r.BranchName } // ShowRefs resolves fully-qualified refs to commit hashes func ShowRefs(ref ...string) ([]Ref, error) { args := append([]string{"show-ref", "--verify", "--"}, ref...) showRef, err := GitCommand(args...) if err != nil { return nil, err } output, err := run.PrepareCmd(showRef).Output() var refs []Ref for _, line := range outputLines(output) { parts := strings.SplitN(line, " ", 2) if len(parts) < 2 { continue } refs = append(refs, Ref{ Hash: parts[0], Name: parts[1], }) } return refs, err } // CurrentBranch reads the checked-out branch for the git repository func CurrentBranch() (string, error) { refCmd, err := GitCommand("symbolic-ref", "--quiet", "HEAD") if err != nil { return "", err } stderr := bytes.Buffer{} refCmd.Stderr = &stderr output, err := run.PrepareCmd(refCmd).Output() if err == nil { // Found the branch name return getBranchShortName(output), nil } if stderr.Len() == 0 { // Detached head return "", ErrNotOnAnyBranch } return "", fmt.Errorf("%sgit: %s", stderr.String(), err) } func listRemotes() ([]string, error) { remoteCmd, err := GitCommand("remote", "-v") if err != nil { return nil, err } output, err := run.PrepareCmd(remoteCmd).Output() return outputLines(output), err } func Config(name string) (string, error) { configCmd, err := GitCommand("config", name) if err != nil { return "", err } output, err := run.PrepareCmd(configCmd).Output() if err != nil { return "", fmt.Errorf("unknown config key: %s", name) } return firstLine(output), nil } var GitCommand = func(args ...string) (*exec.Cmd, error) { gitExe, err := safeexec.LookPath("git") if err != nil { programName := "git" if runtime.GOOS == "windows" { programName = "Git for Windows" } return nil, fmt.Errorf("unable to find git executable in PATH; please install %s before retrying", programName) } return exec.Command(gitExe, args...), nil } func UncommittedChangeCount() (int, error) { statusCmd, err := GitCommand("status", "--porcelain") if err != nil { return 0, err } output, err := run.PrepareCmd(statusCmd).Output() if err != nil { return 0, err } lines := strings.Split(string(output), "\n") count := 0 for _, l := range lines { if l != "" { count++ } } return count, nil } type Commit struct { Sha string Title string } func Commits(baseRef, headRef string) ([]*Commit, error) { logCmd, err := GitCommand( "-c", "log.ShowSignature=false", "log", "--pretty=format:%H,%s", "--cherry", fmt.Sprintf("%s...%s", baseRef, headRef)) if err != nil { return nil, err } output, err := run.PrepareCmd(logCmd).Output() if err != nil { return []*Commit{}, err } commits := []*Commit{} sha := 0 title := 1 for _, line := range outputLines(output) { split := strings.SplitN(line, ",", 2) if len(split) != 2 { continue } commits = append(commits, &Commit{ Sha: split[sha], Title: split[title], }) } if len(commits) == 0 { return commits, fmt.Errorf("could not find any commits between %s and %s", baseRef, headRef) } return commits, nil } func lookupCommit(sha, format string) ([]byte, error) { logCmd, err := GitCommand("-c", "log.ShowSignature=false", "show", "-s", "--pretty=format:"+format, sha) if err != nil { return nil, err } return run.PrepareCmd(logCmd).Output() } func LastCommit() (*Commit, error) { output, err := lookupCommit("HEAD", "%H,%s") if err != nil { return nil, err } idx := bytes.IndexByte(output, ',') return &Commit{ Sha: string(output[0:idx]), Title: strings.TrimSpace(string(output[idx+1:])), }, nil } func CommitBody(sha string) (string, error) { output, err := lookupCommit(sha, "%b") return string(output), err } // Push publishes a git ref to a remote and sets up upstream configuration func Push(remote string, ref string, cmdOut, cmdErr io.Writer) error { pushCmd, err := GitCommand("push", "--set-upstream", remote, ref) if err != nil { return err } pushCmd.Stdout = cmdOut pushCmd.Stderr = cmdErr return run.PrepareCmd(pushCmd).Run() } type BranchConfig struct { RemoteName string RemoteURL *url.URL MergeRef string } // ReadBranchConfig parses the `branch.BRANCH.(remote|merge)` part of git config func ReadBranchConfig(branch string) (cfg BranchConfig) { prefix := regexp.QuoteMeta(fmt.Sprintf("branch.%s.", branch)) configCmd, err := GitCommand("config", "--get-regexp", fmt.Sprintf("^%s(remote|merge)$", prefix)) if err != nil { return } output, err := run.PrepareCmd(configCmd).Output() if err != nil { return } for _, line := range outputLines(output) { parts := strings.SplitN(line, " ", 2) if len(parts) < 2 { continue } keys := strings.Split(parts[0], ".") switch keys[len(keys)-1] { case "remote": if strings.Contains(parts[1], ":") { u, err := ParseURL(parts[1]) if err != nil { continue } cfg.RemoteURL = u } else if !isFilesystemPath(parts[1]) { cfg.RemoteName = parts[1] } case "merge": cfg.MergeRef = parts[1] } } return } func DeleteLocalBranch(branch string) error { branchCmd, err := GitCommand("branch", "-D", branch) if err != nil { return err } return run.PrepareCmd(branchCmd).Run() } func HasLocalBranch(branch string) bool { configCmd, err := GitCommand("rev-parse", "--verify", "refs/heads/"+branch) if err != nil { return false } _, err = run.PrepareCmd(configCmd).Output() return err == nil } func CheckoutBranch(branch string) error { configCmd, err := GitCommand("checkout", branch) if err != nil { return err } return run.PrepareCmd(configCmd).Run() } func parseCloneArgs(extraArgs []string) (args []string, target string) { args = extraArgs if len(args) > 0 { if !strings.HasPrefix(args[0], "-") { target, args = args[0], args[1:] } } return } func RunClone(cloneURL string, args []string) (target string, err error) { cloneArgs, target := parseCloneArgs(args) cloneArgs = append(cloneArgs, cloneURL) // If the args contain an explicit target, pass it to clone // otherwise, parse the URL to determine where git cloned it to so we can return it if target != "" { cloneArgs = append(cloneArgs, target) } else { target = path.Base(strings.TrimSuffix(cloneURL, ".git")) } cloneArgs = append([]string{"clone"}, cloneArgs...) cloneCmd, err := GitCommand(cloneArgs...) if err != nil { return "", err } cloneCmd.Stdin = os.Stdin cloneCmd.Stdout = os.Stdout cloneCmd.Stderr = os.Stderr err = run.PrepareCmd(cloneCmd).Run() return } func AddUpstreamRemote(upstreamURL, cloneDir string, branches []string) error { args := []string{"-C", cloneDir, "remote", "add"} for _, branch := range branches { args = append(args, "-t", branch) } args = append(args, "-f", "upstream", upstreamURL) cloneCmd, err := GitCommand(args...) if err != nil { return err } cloneCmd.Stdout = os.Stdout cloneCmd.Stderr = os.Stderr return run.PrepareCmd(cloneCmd).Run() } func isFilesystemPath(p string) bool { return p == "." || strings.HasPrefix(p, "./") || strings.HasPrefix(p, "/") } // ToplevelDir returns the top-level directory path of the current repository func ToplevelDir() (string, error) { showCmd, err := GitCommand("rev-parse", "--show-toplevel") if err != nil { return "", err } output, err := run.PrepareCmd(showCmd).Output() return firstLine(output), err } func outputLines(output []byte) []string { lines := strings.TrimSuffix(string(output), "\n") return strings.Split(lines, "\n") } func firstLine(output []byte) string { if i := bytes.IndexAny(output, "\n"); i >= 0 { return string(output)[0:i] } return string(output) } func getBranchShortName(output []byte) string { branch := firstLine(output) return strings.TrimPrefix(branch, "refs/heads/") }
X Tutup