github.com/motemen/ghq@v1.0.3/url.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"net/url"
     6  	"os"
     7  	"path/filepath"
     8  	"regexp"
     9  	"runtime"
    10  	"strings"
    11  
    12  	"github.com/Songmu/gitconfig"
    13  	"github.com/motemen/ghq/logger"
    14  )
    15  
    16  // Convert SCP-like URL to SSH URL(e.g. [user@]host.xz:path/to/repo.git/)
    17  // ref. http://git-scm.com/docs/git-fetch#_git_urls
    18  // (golang hasn't supported Perl-like negative look-behind match)
    19  var (
    20  	hasSchemePattern          = regexp.MustCompile("^[^:]+://")
    21  	scpLikeURLPattern         = regexp.MustCompile("^([^@]+@)?([^:]+):(/?.+)$")
    22  	looksLikeAuthorityPattern = regexp.MustCompile(`[A-Za-z0-9]\.[A-Za-z]+(?::\d{1,5})?$`)
    23  )
    24  
    25  func newURL(ref string, ssh, forceMe bool) (*url.URL, error) {
    26  	// If argURL is a "./foo" or "../bar" form,
    27  	// find repository name trailing after github.com/USER/.
    28  	ref = filepath.ToSlash(ref)
    29  	parts := strings.Split(ref, "/")
    30  	if parts[0] == "." || parts[0] == ".." {
    31  		if wd, err := os.Getwd(); err == nil {
    32  			path := filepath.Clean(filepath.Join(wd, filepath.Join(parts...)))
    33  
    34  			var localRepoRoot string
    35  			roots, err := localRepositoryRoots(true)
    36  			if err != nil {
    37  				return nil, err
    38  			}
    39  			for _, r := range roots {
    40  				p := strings.TrimPrefix(path, r+string(filepath.Separator))
    41  				if p != path && (localRepoRoot == "" || len(p) < len(localRepoRoot)) {
    42  					localRepoRoot = filepath.ToSlash(p)
    43  				}
    44  			}
    45  
    46  			if localRepoRoot != "" {
    47  				// Guess it
    48  				logger.Log("resolved", fmt.Sprintf("relative %q to %q", ref, "https://"+localRepoRoot))
    49  				ref = "https://" + localRepoRoot
    50  			}
    51  		}
    52  	}
    53  
    54  	if !hasSchemePattern.MatchString(ref) {
    55  		if scpLikeURLPattern.MatchString(ref) {
    56  			matched := scpLikeURLPattern.FindStringSubmatch(ref)
    57  			user := matched[1]
    58  			host := matched[2]
    59  			path := matched[3]
    60  			// If the path is a relative path not beginning with a slash like
    61  			// `path/to/repo`, we might convert to like
    62  			// `ssh://user@repo.example.com/~/path/to/repo` using tilde, but
    63  			// since GitHub doesn't support it, we treat relative and absolute
    64  			// paths the same way.
    65  			ref = fmt.Sprintf("ssh://%s%s/%s", user, host, strings.TrimPrefix(path, "/"))
    66  		} else {
    67  			// If ref is like "github.com/motemen/ghq" convert to "https://github.com/motemen/ghq"
    68  			paths := strings.Split(ref, "/")
    69  			if len(paths) > 1 && looksLikeAuthorityPattern.MatchString(paths[0]) {
    70  				ref = "https://" + ref
    71  			}
    72  		}
    73  	}
    74  
    75  	u, err := url.Parse(ref)
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  	if !u.IsAbs() {
    80  		if !strings.Contains(u.Path, "/") {
    81  			u.Path, err = fillUsernameToPath(u.Path, forceMe)
    82  			if err != nil {
    83  				return nil, err
    84  			}
    85  		}
    86  		u.Scheme = "https"
    87  		u.Host = "github.com"
    88  		if u.Path[0] != '/' {
    89  			u.Path = "/" + u.Path
    90  		}
    91  	}
    92  
    93  	if ssh {
    94  		// Assume Git repository if `-p` is given.
    95  		if u, err = convertGitURLHTTPToSSH(u); err != nil {
    96  			return nil, fmt.Errorf("Could not convert URL %q: %w", u, err)
    97  		}
    98  	}
    99  
   100  	return u, nil
   101  }
   102  
   103  func convertGitURLHTTPToSSH(u *url.URL) (*url.URL, error) {
   104  	user := "git"
   105  	if u.User != nil {
   106  		user = u.User.Username()
   107  	}
   108  	sshURL := fmt.Sprintf("ssh://%s@%s%s", user, u.Host, u.Path)
   109  	return u.Parse(sshURL)
   110  }
   111  
   112  func detectUserName() (string, error) {
   113  	user, err := gitconfig.Get("ghq.user")
   114  	if (err != nil && !gitconfig.IsNotFound(err)) || user != "" {
   115  		return user, err
   116  	}
   117  
   118  	user, err = gitconfig.GitHubUser("")
   119  	if (err != nil && !gitconfig.IsNotFound(err)) || user != "" {
   120  		return user, err
   121  	}
   122  
   123  	switch runtime.GOOS {
   124  	case "windows":
   125  		user = os.Getenv("USERNAME")
   126  	default:
   127  		user = os.Getenv("USER")
   128  	}
   129  	if user == "" {
   130  		// Make the error if it does not match any pattern
   131  		return "", fmt.Errorf("failed to detect username. You can set ghq.user to your gitconfig")
   132  	}
   133  	return user, nil
   134  }
   135  
   136  func fillUsernameToPath(path string, forceMe bool) (string, error) {
   137  	if !forceMe {
   138  		completeUser, err := gitconfig.Bool("ghq.completeUser")
   139  		if err != nil && !gitconfig.IsNotFound(err) {
   140  			return path, err
   141  		}
   142  		if err == nil && !completeUser {
   143  			return path + "/" + path, nil
   144  		}
   145  	}
   146  	user, err := detectUserName()
   147  	if err != nil {
   148  		return path, err
   149  	}
   150  	return user + "/" + path, nil
   151  }