Repository navigation
Conversation
GitHub Desktop now allows multiple signed-in accounts per endpoint, so repositories carry an explicit account assignment (endpoint + login) which is used for all API and HTTPS Git credentials. - Add a nullable login to the repository model with a one-time migration assigning existing repositories to their endpoint's account - Choose the account in Repository Settings; prompt on explicit operations when unassigned or when the assigned account is signed out, with targeted reauthentication for the missing login - Ask whether to keep or clear assignments on explicit sign-out while preserving them when accounts are removed automatically - Resolve trampoline credentials from the repository at the operation's path using a cached copy of the repositories store, with a token-free fallbackAccount for clones and tutorial pushes - Route CI, pull request, issue, mention, and notification API requests through the repository's account - Probe account access when cloning or adding repositories - Scope GitHub repository metadata per account to isolate permissions, protected branches, and cached pull requests - Clear the assignment when a remote moves to another endpoint Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in short-lived token support (#22917, #22937, #22938, #22939) and adapts it to multiple accounts per endpoint: - Credential sessions are keyed per account (endpoint + login) instead of per endpoint, so renewal, invalidation and storage writes are isolated. - AccountsStore lookups, retirement and refresh match the exact account. - The invalidated token dialog names the signed-out login and offers a targeted sign-in for that account. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Fetch, pull and push no longer require an account before running Git. The credential helper already prompts for an account when Git requests HTTPS credentials during a user-initiated operation, so SSH remotes and public HTTPS repositories work without an account, and background fetches run again for unassigned repositories or signed-out assigned accounts. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
When an unassigned repository needs an account and no account is signed in for its endpoint, show the regular sign-in prompt for that endpoint instead of an account choice with nothing to choose from. The signed-in account is assigned to the repository. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The pickers looked up the selected account by endpoint only, so choosing a second account on the same host silently used the first. Accounts are now matched by endpoint and login, and a clone from the GitHub tabs is pinned to the chosen account with no fallback to other accounts on the endpoint. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A repository whose remote was removed keeps its account assignment. Opening Publish for it now starts on that account's tab with the account selected. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Repositories on the GitHub tabs of the clone dialog are listed by the selected account, so probing that account for access is redundant. Worse, a transient probe failure left the cloned repository unassigned. The selected account is now assigned directly and the repository's metadata is fetched on the next refresh. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The account picker and the remote URL each rendered their own dialog content, and each received the dialog's minimum height, leaving large gaps. Both now share one content area, with the account below the remote and its help text styled as a description. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The credential helper normalized the requested URL to an API endpoint before matching the repository's assigned account or the fallback account, which discarded the scheme and port. A plain HTTP request to the same host would receive the account's token. Require the request's origin to match the account's web origin, as before repository assignments. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A locked or failing keychain at startup made accounts drop out of AccountsStore.getAll(), so the one-time migration permanently wrote null for their repositories. Migrate from the endpoint and login of every saved account instead. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adding a local repository that no account can access matched its remote against the first signed-in account on the host, leaving an orphaned GitHub repository record scoped to that login. Use an anonymous match instead, shared with repositoryWithRefreshedGitHubRepository. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Saving a new account and remote URL together passed the stale repository to setRemoteURL, so the GitHub association refresh bailed because the login had changed. Return the updated repository from updateRepositoryAccount and use it for the remote update. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adding, cloning and publishing set the GitHub repository and then the account in two steps. The first step scoped the record to the new repository's empty login, leaving an orphaned copy behind. Let setGitHubRepository assign the login in the same transaction. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Repository state and GitStores are keyed by the repository hash, which changes when the account is reassigned and again when the new account's GitHub metadata is refreshed. Both reset the in-progress commit message. Carry the repository state and the GitStore draft over to the new identity in those two places. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sessions were keyed by endpoint, so a second account on the same endpoint shared the first account's websocket, and signing out either account took the shared session offline for both. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Drop getAccountForEndpoint, upsertGitHubRepositoryLight, promptForGitHubSignIn and its SignIn popup fields, and redundant dynamic imports in AppStore. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Resolve conflicts with refreshable OAuth tokens, credential leases and session-bound account snapshots: - CredentialSessions keys sessions by account (endpoint and login) while keeping development's session binding, leases and expiry overrides. - The credential trampoline leases a fresh token for the repository's assigned (or fallback) account. It only sends credentials when the request's origin (scheme, host and port) exactly matches the repository's endpoint, rather than comparing an https API URL rebuilt from the request. The two are equivalent for real accounts, whose endpoints are always https, and the exact match also works with the test servers development's credential tests use. - Sign-in now saves the account before completing, so reauthentication checks the expected login before saving and AppStore no longer tracks account persistence. - The token expiration test dialog selects accounts by account key. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Credential origin isolation and account-selection behavior have unresolved correctness and security issues.
5 open findings
Preserve Enterprise Server ports in allowed origin checks · New Distinguish probe failures from inaccessible repositories · New Allow URL-tab clones to probe alternate accounts after 404s · New Clear organization selection when switching GitHub accounts · New Re-probe newly signed-in accounts before accepting them · New
What changed in this PR
Adds explicit repository account assignments to support multiple accounts on the same GitHub endpoint.
Changes:
- Scopes credentials and repository metadata by account.
- Adds account selection and targeted sign-in/sign-out flows.
- Updates account-aware consumers, regression tests, and documentation.
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Check repository access with fetchRepositoryIfAccessible, which only treats a 404 as no access and throws on any other failure. Previously a timeout or server error for one account could silently assign another account, or clone a repository without credentials. Also check an account signed in from the account chooser before using it, rather than rejecting it because it wasn't among the accounts checked up front. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A SAML 403, network error or 5xx for one account no longer aborts adding a local repository or cloning a URL. The account is left out (not treated as lacking access) and the remaining accounts are used. Adding a repository also no longer half-adds it when choosing an account fails. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Clone Again used to discover an account as if the URL was new, possibly cloning with a different account while keeping the old assignment. It now uses the assigned account (prompting if it's signed out) and never changes the assignment. Unassigned repositories behave as before. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Switching a repository Alice -> Bob -> Alice skipped carrying state over because Alice's identity still had state from before, bringing back her old draft. Always carry the state of the identity being left over. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The API has no repository for a wiki, so probing owner/repo.wiki always 404'd and a private wiki got no account. Check access to the wiki's repository instead and assign the account without API metadata. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Refreshing a repository probed every unrecognized remote host with isGitHubHost on each selection, so GitLab or Gitea repositories made a request (up to 2s) every time. Only known GitHub hosts are associated during refresh now. A repository on an unrecognized GitHub Enterprise Server, or with a GitHub remote added outside Desktop, is associated when Git first asks for its credentials, so it still gets the sign-in or account prompt. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The account picker only resolved its prompt through onComplete. If the picker was closed another way (or couldn't be shown because one was already open) the prompt never settled and every later prompt queued behind it. The prompt now resolves without an account once its popup leaves the stack. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A manual refresh resolves the account first, which may prompt. If the repository's assignment changed in the meantime the refresh would use the previous account's credentials for a repository now assigned to another. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adding the new remote can associate the repository anonymously before publishing records the account. setGitHubRepository then saw a stale repository and left the newer anonymous association in place, so the published repository stayed unassigned. Publish now passes the current repository. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
With several accounts per endpoint, signing in without an expected login silently reused whichever session was active in the browser, making it hard to add a second account. The authorization URL now includes prompt=select_account in that case. Targeted reauthentication still passes the expected login instead. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signing in to an endpoint that already has an account adds another account now, so nothing enters the ExistingAccountWarning step anymore. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The accounts store emits whenever a token is refreshed or account details are updated. Clearing every cached status then made all subscribers flash to no status and refetch. The cache is now only cleared when an account is signed in or out. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
@niik probably worth updating the branch (stack) now, since there are some conflicts with |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Remotes were matched to accounts by hostname only, so an HTTPS remote on a GitHub Enterprise Server's custom port never matched its account (and could match an account for the same host on the default port). Deriving the endpoint from an SSH remote's host also lost the web port, and kept an ssh:// port as if it were one. HTTPS remotes now match an endpoint's host including its port, SSH remotes match its hostname, and refresh resolves a remote's endpoint from the current association and signed-in accounts before deriving one. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…tity Publishing assigns the chosen account before pushing so Git uses its credentials, but that changes the repository's hash and left the push reading empty state (no remote), reopening the Publish dialog instead of pushing. Carry the state over and refresh the new identity before pushing. Also carry over in-memory state (such as commit message drafts) when a refresh associates or clears a repository's GitHub repository, and when signing out clears account assignments. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
sergiou87
left a comment
There was a problem hiding this comment.
Copilot found another bug and I was able to reproduce it. It's quite nasty but not sure how often users will run into this. Happy to leave this for another PR if you want!
Changing a repository's account (or its GitHub metadata) changed its hash, which was the key of the repository state and GitStore caches. Operations already running kept the previous identity, so e.g. a fetch running when the account changed cleared its in-progress flag on state nobody was looking at, leaving the repository busy. Both caches are now keyed by the repository id and path, so there's a single state and GitStore per working directory regardless of account. GitStores adopt the latest repository snapshot so later operations use the current account, and results loaded with a previous account (pull requests, branch protection) are discarded. Repository store updates are never emitted with data older than an update a caller already got back, and renaming a worktree is refused while an operation is running. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Updating a repository's missing flag, gitDir or path returned a repository derived from the caller's snapshot, which may predate an account change, and adopting it rolled the GitStore back to the previous account. They now return the persisted repository. A repository adopted before its GitStore exists is also remembered, so a store created later from an older snapshot uses it. Renaming the current worktree now also stops fetches, commits and commit message generation from starting until the rename has finished, since their state is moved to the new path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
sergiou87
left a comment
There was a problem hiding this comment.
All good now, thanks for those fixes!!
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>



Description
GitHub Desktop has supported one account per endpoint. This lets several accounts be signed in to the same endpoint, and makes the choice of account for each repository explicit. Every repository now stores an assigned account login, which, together with its GitHub repository's endpoint, decides which credentials are used for API calls and HTTPS Git operations.
Repositorygains a nullablelogin.accountLogin), so permissions, pull request metadata and branch protections from one identity never leak into another.createCredentialHelperTrampolineHandlernow takes the repositories store, which it mirrors the same way it already mirrors accounts.docs/technical/oauth.md.Known follow-ups
These are deferred and tracked separately:
Screenshots
TBD
Release notes
Notes: no-notes