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 }