Skip to content

Commit 2165415

Browse files
committed
ensureSharedHistory fix
1 parent d9ec019 commit 2165415

2 files changed

Lines changed: 40 additions & 29 deletions

File tree

packages/beachball/src/git/ensureSharedHistory.ts

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ import { BeachballError } from '../types/BeachballError';
66

77
/**
88
* Ensure that adequate history is available to check for changes between HEAD and `options.branch`.
9-
* Otherwise attempting to get changes will fail with an error "no merge base".
10-
* (This is mostly an issue with CI builds that use shallow clones.)
9+
* Otherwise attempting to get changes will fail with an error "no merge base". (This is mostly an
10+
* issue with CI builds that use shallow clones and may not have a tracking branch configured for
11+
* the remote target branch.)
12+
*
13+
* Since repo may be very large, it's important to avoid fetching the entire history or extra refs.
14+
* If the repo is shallow and no common commit is found after a normal fetch, this function will
15+
* iteratively deepen the history (100 commits at a time by default) and check for a common commit
16+
* each round. If still not found, it will unshallow the clone.
1117
*
1218
* Throws an error if history is inadequate and cannot be fixed.
1319
*/
@@ -33,26 +39,13 @@ export function ensureSharedHistory(
3339
}
3440

3541
if (!remote) {
36-
// If the remote isn't specified, even if fetching is allowed it will be unclear what to
37-
// compare against
42+
// If the remote isn't specified, even if fetching is allowed, it will be unclear what to
43+
// compare against, so throw an error.
3844
throw new BeachballError(
3945
`Target branch "${branch}" doesn't exist locally, and a remote name wasn't specified and couldn't be inferred. ` +
4046
'Please set "repository" in your root package.json or include a remote in the beachball "branch" setting.'
4147
);
4248
}
43-
44-
// If fetching the requested branch isn't already (probably) configured, add it to the list so
45-
// it can be fetched in the next step. Otherwise the ref <remote>/<remoteBranch> won't exist locally.
46-
const fetchConfig = git(['config', '--get-all', `remote.${remote}.fetch`], { cwd }).stdout.trim();
47-
if (!fetchConfig.includes(`${remote}/*`) && !fetchConfig.includes(branch)) {
48-
console.log(`Adding branch "${remoteBranch}" to fetch config for remote "${remote}"`);
49-
const result = git(['remote', 'set-branches', '--add', remote, remoteBranch], { cwd });
50-
if (!result.success) {
51-
throw new BeachballError(
52-
`Failed to add branch "${remoteBranch}" to fetch config for remote "${remote}":\n${result.stderr}`
53-
);
54-
}
55-
}
5649
}
5750

5851
if (fetch) {
@@ -89,6 +82,8 @@ export function ensureSharedHistory(
8982

9083
// Try fetching more history
9184
isConnected = deepenHistory({ remote, remoteBranch, branch, depth, cwd, verbose });
85+
} else {
86+
// Repo isn't shallow, so potentially we just need to add a ref to be connected
9287
}
9388

9489
if (!isConnected) {

packages/beachball/src/git/fetch.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@ import { getGitEnv } from './gitAsync';
33

44
type GitFetchParams = {
55
cwd: string;
6-
/** Remote to fetch from. If not specified, fetches all remotes. */
7-
remote?: string;
8-
/** Branch to fetch. This will be ignored if `remote` is not also specified. */
9-
branch?: string;
6+
/**
7+
* Remote to fetch from. This should almost always be set but might be an empty string
8+
* if the repo somehow has no remotes configured.
9+
*/
10+
remote: string;
11+
/**
12+
* Branch to fetch. It will be converted to a full refspec for fetching:
13+
* e.g. `branch: 'main', remote: 'origin'` will be converted to `+main:refs/remotes/origin/main`.
14+
*/
15+
branch: string;
1016
/** Set depth to this number of commits (mutually exclusive with `deepen` and `unshallow`) */
1117
depth?: number;
1218
/** Deepen a shallow clone by this number of commits (mutually exclusive with `depth` and `unshallow`) */
@@ -20,6 +26,9 @@ type GitFetchParams = {
2026
* Wrapper for `git fetch`. If `verbose` is true, log the command before starting, and display output
2127
* on stdout (except in tests). In tests with `verbose`, the output will be logged all together to
2228
* `console.log` when the command finishes (for easier mocking/capturing).
29+
*
30+
* This converts `remote` and `branch` into a fully qualified refspec, so it doesn't matter if
31+
* the remote branch is tracked or not in the local repository.
2332
*/
2433
export function gitFetch(params: GitFetchParams): GitProcessOutput & { errorMessage?: string } {
2534
const { remote, branch, depth, deepen, unshallow, cwd, verbose } = params;
@@ -31,22 +40,29 @@ export function gitFetch(params: GitFetchParams): GitProcessOutput & { errorMess
3140

3241
const extraArgs = depth ? [`--depth=${depth}`] : deepen ? [`--deepen=${deepen}`] : unshallow ? ['--unshallow'] : [];
3342

34-
let description = remote
35-
? `Fetching ${branch ? `branch "${branch}" from ` : ''}remote "${remote}"`
36-
: 'Fetching all remotes';
43+
// Be specific with the ref being fetched, so we don't have to worry about tracking configs.
44+
// In git fetch <remote> +<src>:<dst>...
45+
// - The + means allow non-fast-forward updates (in case the remote was force pushed).
46+
// - <src> refs/heads/${branch} is resolved against the remote's advertised refs. The fully
47+
// qualified ref is unambiguous, whereas bare branch names can be silently misresolved,
48+
// causing git to treat the ref as absent and delete the local tracking ref.
49+
// - <dst> refs/remotes/${remote}/${branch} is resolved locally and only moves the tracking ref
50+
// for the remote branch, not the local refs/heads/${branch} or its tracking config.
51+
const resolvedBranch = remote ? `+refs/heads/${branch}:refs/remotes/${remote}/${branch}` : undefined;
3752

38-
if (extraArgs.length) {
39-
description += ` (with ${extraArgs.join(' ')})`;
40-
}
53+
const shortDescription = `Fetching ${resolvedBranch ? `branch "${branch}" from remote "${remote}"` : 'all remotes'}`;
4154

55+
let description = shortDescription;
56+
resolvedBranch && (description += ` (${resolvedBranch})`);
57+
extraArgs.length && (description += ` (with ${extraArgs.join(' ')})`);
4258
shouldLog && console.log(description + '...');
4359

4460
const result: ReturnType<typeof gitFetch> = git(
4561
[
4662
'fetch',
4763
...extraArgs,
4864
// If the remote is unknown, don't specify the branch (fetching a branch without a remote is invalid)
49-
...(remote && branch ? [remote, branch] : remote ? [remote] : []),
65+
...(resolvedBranch ? [remote, resolvedBranch] : []),
5066
],
5167
{ cwd, stdio: shouldLog === 'live' ? 'inherit' : 'pipe' }
5268
);
@@ -59,7 +75,7 @@ export function gitFetch(params: GitFetchParams): GitProcessOutput & { errorMess
5975
result.stderr && log(result.stderr);
6076
}
6177

62-
let message = `${description} ${result.success ? 'completed successfully' : `failed (code ${result.status})`}`;
78+
let message = `${shortDescription} ${result.success ? 'completed successfully' : `failed (code ${result.status})`}`;
6379
if (shouldLog) {
6480
log(message);
6581
message += ' - see above for details';

0 commit comments

Comments
 (0)