-
Notifications
You must be signed in to change notification settings - Fork 354
fix: gitter falls back to reclone too often #5890
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
f2cab9c
Don't immediately reclone when it's 429 or host issue
Ly-Joey b574beb
move error-related logic and test to its own file
Ly-Joey 227f547
Make context cancel case clearer, some wordings
Ly-Joey 2844ceb
Update go/cmd/gitter/errors.go
Ly-Joey b789eb0
Address (PR) comments and add (code) comments
Ly-Joey 511732f
Only log sinceLastAccess when it exist.
Ly-Joey da62147
naming was kinda messy
Ly-Joey File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "regexp" | ||
| "strconv" | ||
| "strings" | ||
| ) | ||
|
|
||
| // regex to extract HTTP status code from git error output. | ||
| var httpStatusRegex = regexp.MustCompile(`(?:The requested URL returned error:\s*|HTTP\s+|http_code\s*=\s*|remote:\s*)(\d{3}\b)`) | ||
|
|
||
| // extractHTTPStatusCode extracts a 3-digit HTTP status code from git or libcurl stderr output if present. | ||
| func extractHTTPStatusCode(err error) int { | ||
| if err == nil { | ||
| return 0 | ||
| } | ||
| m := httpStatusRegex.FindStringSubmatch(err.Error()) | ||
| if len(m) >= 2 { | ||
| code, _ := strconv.Atoi(m[1]) | ||
| if code >= 100 && code <= 599 { | ||
| return code | ||
| } | ||
| } | ||
|
|
||
| return 0 | ||
| } | ||
|
|
||
| // errContainsAny returns true if err's lowercase error message contains any of the given substrings (case-insensitively). | ||
| func errContainsAny(err error, substrs ...string) bool { | ||
| if err == nil { | ||
| return false | ||
| } | ||
| s := strings.ToLower(err.Error()) | ||
| for _, sub := range substrs { | ||
| if strings.Contains(s, strings.ToLower(sub)) { | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| return false | ||
| } | ||
|
|
||
| // errContainsAll returns true if err's lowercase error message contains all of the given substrings (case-insensitively). | ||
| func errContainsAll(err error, substrs ...string) bool { | ||
| if err == nil || len(substrs) == 0 { | ||
| return false | ||
| } | ||
| s := strings.ToLower(err.Error()) | ||
| for _, sub := range substrs { | ||
| if !strings.Contains(s, strings.ToLower(sub)) { | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| return true | ||
| } | ||
|
|
||
| // isIndexLockError checks if an error indicates a stale index.lock file left behind by an interrupted git process. | ||
| func isIndexLockError(err error) bool { | ||
| return errContainsAll(err, "index.lock", "file exists") | ||
| } | ||
|
|
||
| // isRefConflictError checks if an error was caused by conflicting local and remote branch or tag references. | ||
| func isRefConflictError(err error) bool { | ||
| // conflicting ref names (e.g. branch vs directory name) | ||
| return errContainsAny(err, "refname conflict") || | ||
| // stale local tracking branches that conflict with remote | ||
| errContainsAll(err, "some local refs could not be updated", "try running 'git remote prune origin'") | ||
| } | ||
|
|
||
| // isRateLimitError checks if an error indicates rate limiting (HTTP 429) or upstream load shedding by the git host. | ||
| func isRateLimitError(err error) bool { | ||
| if extractHTTPStatusCode(err) == 429 { | ||
| return true | ||
| } | ||
|
|
||
| return errContainsAny(err, | ||
| // gitlab rate limit message | ||
| "unable to handle this request due to load", | ||
| // generic rate limiting | ||
| "too many requests", | ||
| // github secondary rate limits | ||
| "secondary rate limit", | ||
| ) | ||
| } | ||
|
|
||
| // isRemoteHostError checks if an error indicates an upstream server error (HTTP 5xx), transport failure, | ||
| // network timeout, TLS error, or remote server resource exhaustion (e.g. OOM during pack generation). | ||
| func isRemoteHostError(err error) bool { | ||
| code := extractHTTPStatusCode(err) | ||
| if code >= 500 { | ||
| return true | ||
| } | ||
|
|
||
| return errContainsAny(err, | ||
| // connection failures, drops, resets | ||
| "could not connect to server", | ||
| "failed to connect to", | ||
| "connection reset by peer", | ||
| "recv failure", | ||
| // network timeouts | ||
| "connection timed out", | ||
| // server closed connection prematurely | ||
| "empty reply from server", | ||
| // tls / ssl negotiation failures | ||
| "tls connect error", | ||
| "ssl routines", | ||
| // git smart http rpc failures | ||
| "rpc failed", | ||
| // interrupted or truncated pack transfers (e.g. remote OOM) | ||
| "early eof", | ||
| "fetch-pack: invalid index-pack output", | ||
| // remote host unreachable | ||
| "is not responding", | ||
| // dns failures | ||
| "could not resolve host", | ||
| "temporary failure in name resolution", | ||
| ) | ||
| } | ||
|
|
||
| // isAuthError checks if an error is due to missing git credentials or authentication failure. | ||
| func isAuthError(err error) bool { | ||
|
Ly-Joey marked this conversation as resolved.
|
||
| if extractHTTPStatusCode(err) == 401 { | ||
| return true | ||
| } | ||
|
|
||
| return errContainsAny(err, | ||
| "could not read username", | ||
| "authentication failed", | ||
| ) | ||
| } | ||
|
|
||
| // isForbiddenError checks if access to the remote repository was denied (HTTP 403). | ||
| func isForbiddenError(err error) bool { | ||
| if extractHTTPStatusCode(err) == 403 { | ||
| return true | ||
| } | ||
|
|
||
| return errContainsAny(err, | ||
| "forbidden", | ||
| ) | ||
| } | ||
|
|
||
| // isNotFoundError returns true if the requested repository does not exist (HTTP 404 or repository not found). | ||
| func isNotFoundError(err error) bool { | ||
| if extractHTTPStatusCode(err) == 404 { | ||
| return true | ||
| } | ||
|
|
||
| return errContainsAll(err, "repository", "not found") | ||
| } | ||
|
|
||
| // isRefNotFoundError returns true if the requested branch, tag, or commit hash cannot be resolved. | ||
| func isRefNotFoundError(err error) bool { | ||
| return errContainsAny(err, | ||
| "not found or invalid", | ||
| "failed to resolve target ref", | ||
| "failed to run git rev-parse", | ||
| "ref cannot be empty", | ||
| ) | ||
| } | ||
|
|
||
| // isFileNotFoundError returns true if a file path does not exist at the requested commit. | ||
| func isFileNotFoundError(err error) bool { | ||
| return errContainsAny(err, | ||
| "git cat-file failed", | ||
| "invalid object name", | ||
| "does not exist in", | ||
| ) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "errors" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestIsIndexLockError(t *testing.T) { | ||
| tests := []struct { | ||
| err error | ||
| expected bool | ||
| }{ | ||
| {errors.New("fatal: Unable to create '/path/to/repo.git/index.lock': File exists"), true}, | ||
| {errors.New("some other error"), false}, | ||
| {nil, false}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| if result := isIndexLockError(tt.err); result != tt.expected { | ||
| t.Errorf("isIndexLockError(%v) = %v, expected %v", tt.err, result, tt.expected) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestIsRefConflictError(t *testing.T) { | ||
| tests := []struct { | ||
| err error | ||
| expected bool | ||
| }{ | ||
| {errors.New("error: some local refs could not be updated; try running 'git remote prune origin' to remove any old, conflicting branches"), true}, | ||
| {errors.New("error: fetching ref refs/remotes/some-ref-name failed: refname conflict"), true}, | ||
| {errors.New("some other error"), false}, | ||
| {nil, false}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| if result := isRefConflictError(tt.err); result != tt.expected { | ||
| t.Errorf("isRefConflictError(%v) = %v, expected %v", tt.err, result, tt.expected) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestExtractHTTPStatusCode(t *testing.T) { | ||
| tests := []struct { | ||
| err error | ||
| expected int | ||
| }{ | ||
| {errors.New("fatal: unable to access 'https://gitlab.example.com/repo': The requested URL returned error: 429"), 429}, | ||
| {errors.New("fatal: unable to access 'https://github.com/repo': The requested URL returned error: 403"), 403}, | ||
| {errors.New("fatal: unable to access 'https://github.com/repo': The requested URL returned error: 404"), 404}, | ||
| {errors.New("error: RPC failed; HTTP 500 curl 22 The requested URL returned error: 500"), 500}, | ||
| {errors.New("fatal: unable to access 'https://gitlab.example.com/repo': The requested URL returned error: 502"), 502}, | ||
| {errors.New("fatal: repository not found"), 0}, | ||
| {nil, 0}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| if result := extractHTTPStatusCode(tt.err); result != tt.expected { | ||
| t.Errorf("extractHTTPStatusCode(%v) = %v, expected %v", tt.err, result, tt.expected) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestIsRateLimitError(t *testing.T) { | ||
| tests := []struct { | ||
| err error | ||
| expected bool | ||
| }{ | ||
| {errors.New("fatal: unable to access 'https://gitlab.example.com/repo': The requested URL returned error: 429"), true}, | ||
| {errors.New("fatal: remote error: GitLab is currently unable to handle this request due to load (ID a2b5c730282383b9-ORD)."), true}, //nolint:revive // Testing exact error message from remote git host | ||
| {errors.New("fatal: You have exceeded a secondary rate limit"), true}, | ||
| {errors.New("fatal: unable to access 'https://github.com/repo': The requested URL returned error: 403"), false}, | ||
| {nil, false}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| if result := isRateLimitError(tt.err); result != tt.expected { | ||
| t.Errorf("isRateLimitError(%v) = %v, expected %v", tt.err, result, tt.expected) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestIsRemoteHostError(t *testing.T) { | ||
| tests := []struct { | ||
| err error | ||
| expected bool | ||
| }{ | ||
| {errors.New("fatal: unable to access 'https://git.example.com/repo': The requested URL returned error: 500"), true}, | ||
| {errors.New("fatal: unable to access 'https://gitlab.example.com/repo': The requested URL returned error: 502"), true}, | ||
| {errors.New("fatal: unable to access 'https://git.example.com/repo/': Could not connect to server"), true}, | ||
| {errors.New("fatal: unable to access 'https://git.example.com/repo/': Connection reset by peer"), true}, | ||
| {errors.New("fatal: early EOF\nfatal: fetch-pack: invalid index-pack output"), true}, | ||
| {errors.New("fatal: unable to access 'https://github.com/repo': The requested URL returned error: 403"), false}, | ||
| {nil, false}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| if result := isRemoteHostError(tt.err); result != tt.expected { | ||
| t.Errorf("isRemoteHostError(%v) = %v, expected %v", tt.err, result, tt.expected) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestIsAuthError(t *testing.T) { | ||
| tests := []struct { | ||
| err error | ||
| expected bool | ||
| }{ | ||
| {errors.New("fatal: could not read Username for 'https://github.com': terminal prompts disabled"), true}, | ||
| {errors.New("fatal: Authentication failed for 'https://github.com/example/repo.git/'"), true}, | ||
| {errors.New("fatal: unable to access 'https://github.com/example/repo/': The requested URL returned error: 403"), false}, | ||
| {nil, false}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| if result := isAuthError(tt.err); result != tt.expected { | ||
| t.Errorf("isAuthError(%v) = %v, expected %v", tt.err, result, tt.expected) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestIsForbiddenError(t *testing.T) { | ||
| tests := []struct { | ||
| err error | ||
| expected bool | ||
| }{ | ||
| {errors.New("fatal: unable to access 'https://github.com/example/repo/': The requested URL returned error: 403"), true}, | ||
| {errors.New("remote: 403 Forbidden"), true}, | ||
| {errors.New("fatal: unable to access 'https://gitlab.example.com/repo': The requested URL returned error: 429"), false}, | ||
| {nil, false}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| if result := isForbiddenError(tt.err); result != tt.expected { | ||
| t.Errorf("isForbiddenError(%v) = %v, expected %v", tt.err, result, tt.expected) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestIsNotFoundError(t *testing.T) { | ||
| tests := []struct { | ||
| err error | ||
| expected bool | ||
| }{ | ||
| {errors.New("remote: Repository not found"), true}, | ||
| {errors.New("fatal: unable to access 'https://github.com/example/repo/': The requested URL returned error: 404"), true}, | ||
| {errors.New("fatal: unable to access 'https://github.com/example/repo/': The requested URL returned error: 403"), false}, | ||
| {nil, false}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| if result := isNotFoundError(tt.err); result != tt.expected { | ||
| t.Errorf("isNotFoundError(%v) = %v, expected %v", tt.err, result, tt.expected) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestIsRefNotFoundError(t *testing.T) { | ||
| tests := []struct { | ||
| err error | ||
| expected bool | ||
| }{ | ||
| {errors.New("ref cannot be empty"), true}, | ||
| {errors.New("failed to resolve target ref 'non-existent-branch'"), true}, | ||
| {errors.New("git cat-file failed: fatal: not found or invalid"), true}, | ||
| {errors.New("fatal: Authentication failed"), false}, | ||
| {nil, false}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| if result := isRefNotFoundError(tt.err); result != tt.expected { | ||
| t.Errorf("isRefNotFoundError(%v) = %v, expected %v", tt.err, result, tt.expected) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestIsFileNotFoundError(t *testing.T) { | ||
| tests := []struct { | ||
| err error | ||
| expected bool | ||
| }{ | ||
| {errors.New("git cat-file failed: fatal: Not a valid object name 1234abcd"), true}, | ||
| {errors.New("file does not exist in commit"), true}, | ||
| {errors.New("fatal: Authentication failed"), false}, | ||
| {nil, false}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| if result := isFileNotFoundError(tt.err); result != tt.expected { | ||
| t.Errorf("isFileNotFoundError(%v) = %v, expected %v", tt.err, result, tt.expected) | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.