github.com/x-motemen/ghq@v1.6.1/url.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"net/url"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"regexp"
    11  	"runtime"
    12  	"strings"
    13  
    14  	"github.com/Songmu/gitconfig"
    15  	"github.com/x-motemen/ghq/logger"
    16  )
    17  
    18  // Convert SCP-like URL to SSH URL(e.g. [user@]host.xz:path/to/repo.git/)
    19  // ref. http://git-scm.com/docs/git-fetch#_git_urls
    20  // (golang hasn't supported Perl-like negative look-behind match)
    21  var (
    22  	hasSchemePattern          = regexp.MustCompile("^[^:]+://")
    23  	scpLikeURLPattern         = regexp.MustCompile("^([^@]+@)?([^:]+):(/?.+)$")
    24  	looksLikeAuthorityPattern = regexp.MustCompile(`[A-Za-z0-9]\.[A-Za-z]+(?::\d{1,5})?$`)
    25  	codecommitLikeURLPattern  = regexp.MustCompile(`^(codecommit):(?::([a-z][a-z0-9-]+):)?//(?:([^]]+)@)?([\w\.-]+)$`)
    26  )
    27  
    28  func newURL(ref string, ssh, forceMe bool) (*url.URL, error) {
    29  	// If argURL is a "./foo" or "../bar" form,
    30  	// find repository name trailing after github.com/USER/.
    31  	ref = filepath.ToSlash(ref)
    32  	parts := strings.Split(ref, "/")
    33  	if parts[0] == "." || parts[0] == ".." {
    34  		if wd, err := os.Getwd(); err == nil {
    35  			path := filepath.Clean(filepath.Join(wd, filepath.Join(parts...)))
    36  
    37  			var localRepoRoot string
    38  			roots, err := localRepositoryRoots(true)
    39  			if err != nil {
    40  				return nil, err
    41  			}
    42  			for _, r := range roots {
    43  				p := strings.TrimPrefix(path, r+string(filepath.Separator))
    44  				if p != path && (localRepoRoot == "" || len(p) < len(localRepoRoot)) {
    45  					localRepoRoot = filepath.ToSlash(p)
    46  				}
    47  			}
    48  
    49  			if localRepoRoot != "" {
    50  				// Guess it
    51  				logger.Log("resolved", fmt.Sprintf("relative %q to %q", ref, "https://"+localRepoRoot))
    52  				ref = "https://" + localRepoRoot
    53  			}
    54  		}
    55  	}
    56  
    57  	if codecommitLikeURLPattern.MatchString(ref) {
    58  		// SEE ALSO:
    59  		// https://github.com/aws/git-remote-codecommit/blob/master/git_remote_codecommit/__init__.py#L68
    60  		matched := codecommitLikeURLPattern.FindStringSubmatch(ref)
    61  		region := matched[2]
    62  
    63  		if matched[2] == "" {
    64  			// Region detection priority:
    65  			// 1. Explicit specification (codecommit::region://...)
    66  			// 2. Environment variables
    67  			//     a. AWS_REGION (implicit priority)
    68  			//     b. AWS_DEFAULT_REGION
    69  			// 3. AWS CLI profiles
    70  			// SEE ALSO:
    71  			// https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-precedence
    72  			var exists bool
    73  			region, exists = os.LookupEnv("AWS_REGION")
    74  			if !exists {
    75  				region, exists = os.LookupEnv("AWS_DEFAULT_REGION")
    76  			}
    77  
    78  			if !exists {
    79  				var stdout bytes.Buffer
    80  				var stderr bytes.Buffer
    81  
    82  				cmd := exec.Command("aws", "configure", "get", "region")
    83  				cmd.Stdout = &stdout
    84  				cmd.Stderr = &stderr
    85  
    86  				err := cmd.Run()
    87  				if err != nil {
    88  					if stderr.String() == "" {
    89  						fmt.Fprintln(os.Stderr, "You must specify a region. You can also configure your region by running \"aws configure\".")
    90  					} else {
    91  						fmt.Fprint(os.Stderr, stderr.String())
    92  					}
    93  					os.Exit(1)
    94  				}
    95  
    96  				region = strings.TrimSpace(stdout.String())
    97  			}
    98  		}
    99  
   100  		return &url.URL{
   101  			Scheme: matched[1],
   102  			Host:   region,
   103  			User:   url.User(matched[3]),
   104  			Path:   matched[4],
   105  			Opaque: ref,
   106  		}, nil
   107  	}
   108  
   109  	if !hasSchemePattern.MatchString(ref) {
   110  		if scpLikeURLPattern.MatchString(ref) {
   111  			matched := scpLikeURLPattern.FindStringSubmatch(ref)
   112  			user := matched[1]
   113  			host := matched[2]
   114  			path := matched[3]
   115  			// If the path is a relative path not beginning with a slash like
   116  			// `path/to/repo`, we might convert to like
   117  			// `ssh://user@repo.example.com/~/path/to/repo` using tilde, but
   118  			// since GitHub doesn't support it, we treat relative and absolute
   119  			// paths the same way.
   120  			ref = fmt.Sprintf("ssh://%s%s/%s", user, host, strings.TrimPrefix(path, "/"))
   121  		} else {
   122  			// If ref is like "github.com/motemen/ghq" convert to "https://github.com/motemen/ghq"
   123  			paths := strings.Split(ref, "/")
   124  			if len(paths) > 1 && looksLikeAuthorityPattern.MatchString(paths[0]) {
   125  				ref = "https://" + ref
   126  			}
   127  		}
   128  	}
   129  
   130  	u, err := url.Parse(ref)
   131  	if err != nil {
   132  		return nil, err
   133  	}
   134  	if !u.IsAbs() {
   135  		if !strings.Contains(u.Path, "/") {
   136  			u.Path, err = fillUsernameToPath(u.Path, forceMe)
   137  			if err != nil {
   138  				return nil, err
   139  			}
   140  		}
   141  		u.Scheme = "https"
   142  		u.Host = "github.com"
   143  		if u.Path[0] != '/' {
   144  			u.Path = "/" + u.Path
   145  		}
   146  	}
   147  
   148  	if ssh {
   149  		// Assume Git repository if `-p` is given.
   150  		if u, err = convertGitURLHTTPToSSH(u); err != nil {
   151  			return nil, fmt.Errorf("could not convert URL %q: %w", u, err)
   152  		}
   153  	}
   154  
   155  	return u, nil
   156  }
   157  
   158  func convertGitURLHTTPToSSH(u *url.URL) (*url.URL, error) {
   159  	user := "git"
   160  	if u.User != nil {
   161  		user = u.User.Username()
   162  	}
   163  	sshURL := fmt.Sprintf("ssh://%s@%s%s", user, u.Host, u.Path)
   164  	return u.Parse(sshURL)
   165  }
   166  
   167  func detectUserName() (string, error) {
   168  	user, err := gitconfig.Get("ghq.user")
   169  	if (err != nil && !gitconfig.IsNotFound(err)) || user != "" {
   170  		return user, err
   171  	}
   172  
   173  	user, err = gitconfig.GitHubUser("")
   174  	if (err != nil && !gitconfig.IsNotFound(err)) || user != "" {
   175  		return user, err
   176  	}
   177  
   178  	switch runtime.GOOS {
   179  	case "windows":
   180  		user = os.Getenv("USERNAME")
   181  	default:
   182  		user = os.Getenv("USER")
   183  	}
   184  	if user == "" {
   185  		// Make the error if it does not match any pattern
   186  		return "", fmt.Errorf("failed to detect username. You can set ghq.user to your gitconfig")
   187  	}
   188  	return user, nil
   189  }
   190  
   191  func fillUsernameToPath(path string, forceMe bool) (string, error) {
   192  	if !forceMe {
   193  		completeUser, err := gitconfig.Bool("ghq.completeUser")
   194  		if err != nil && !gitconfig.IsNotFound(err) {
   195  			return path, err
   196  		}
   197  		if err == nil && !completeUser {
   198  			return path + "/" + path, nil
   199  		}
   200  	}
   201  	user, err := detectUserName()
   202  	if err != nil {
   203  		return path, err
   204  	}
   205  	return user + "/" + path, nil
   206  }