refactor(oauth2): move credential merge to oauth2

Description

There is no user-visible change here. Before saving config.yml, glab checks whether another glab process rotated the OAuth token on disk. If so, it keeps the newer token instead of writing back its stale copy. That check landed with !3679 (merged) (the fix for #8390 (closed)), in the general config code.

On !3501 (closed), the reviewer suggested moving it to the OAuth code, since the config code otherwise knows nothing about OAuth. As discussed here !3501 (comment 3553777507)

I moved it, and the behavior is the same as on main:

  • glab adopts the on-disk credentials only when their oauth2_expiry_date is strictly newer.
  • For a host that keeps its token in the keyring, only the newer expiry is copied.
  • A glab auth logout that cleared the credentials in memory is not undone.
  • Expiry dates in the RFC822 format that older glab versions wrote still work.

Some decisions I made:

  1. The OAuth code registers the check by itself when it is part of the binary. A future binary without the OAuth code would silently lose the protection. Today both glab and the docs generator include it (output below). The alternative is one explicit line at glab startup. Do you prefer that?
  2. The suggestion passed the whole config and its directory to the OAuth side. The config code already holds the lock and reads the file, so I kept that there. The OAuth side only gets one host's entry from memory and from disk.
  3. An old comment said a valid disk expiry beats an unreadable in-memory one. The code did the opposite on purpose, for the logout case above. I kept the behavior and fixed the comment.

Closes #8395 (closed)

How has this been tested?

I tested the stale-writer case by hand with real binaries. glab auth login --stdin loads the config, then waits for the token. While it waited, I rewrote config.yml like another glab process would after a token refresh. Then I sent the token. The login went to a local stub server, not a real GitLab instance. Afterwards, the OAuth host looked like this.

Built from main, and the same from this branch:

    gitlab.example.com:
        token: access-NEW
        oauth2_refresh_token: refresh-NEW
        oauth2_expiry_date: 2026-10-10T12:00:00Z

To make sure this goes through the moved code, I built the branch once more without the registration. The stale copy won:

    gitlab.example.com:
        token: access-OLD
        oauth2_refresh_token: refresh-OLD
        oauth2_expiry_date: 2026-10-10T10:00:00Z

I moved the existing merge tests with the code. I added tests for the RFC822 expiry (asked for here !3501 (comment 3553776945)), the expiry comparison, and writing with nothing registered. Without the registration, the 3 tests that expect a merge fail. Checks I ran:

$ go test -count=1 ./internal/config/ ./internal/oauth2/
ok  	gitlab.com/gitlab-org/cli/internal/config	1.668s
ok  	gitlab.com/gitlab-org/cli/internal/oauth2	0.674s
$ for b in glab gen-docs; do printf "%s: " $b; go list -deps ./cmd/$b | grep -c '^gitlab.com/gitlab-org/cli/internal/oauth2$'; done
glab: 1
gen-docs: 1

gofmt -l and go vet printed nothing. I didn't run make test, make lint or -race locally, and I couldn't check Windows. The pipeline covers those.

Screenshots (if appropriate):

None, there is no UI change.

Implemented with AI assistance; I reviewed and tested the changes.

Edited by Hamza Hassanain

Merge request reports

Loading
Loading