Skip to content

Commit 708dcfb

Browse files
committed
Fix deepening of shallow clones in latest git
1 parent d9ec019 commit 708dcfb

8 files changed

Lines changed: 224 additions & 134 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"type": "patch",
5+
"comment": "Fix deepening of shallow clones in latest git by being more specific about the comparison ref being fetched. Also reduce git operations for the case where the branch ref doesn't exist locally.",
6+
"packageName": "beachball",
7+
"email": "elcraig@microsoft.com",
8+
"dependentChangeType": "patch"
9+
}
10+
]
11+
}

packages/beachball/src/__fixtures__/repositoryFactory.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,14 @@ export class RepositoryFactory {
162162
/** Cloned child repos, tracked so we can clean them up */
163163
private childRepos: Repository[] = [];
164164

165+
/** Get the root directory of the origin repository */
166+
public get originRoot(): string {
167+
if (!this.root) {
168+
throw new Error('Factory was already cleaned');
169+
}
170+
return this.root;
171+
}
172+
165173
/**
166174
* Create the "origin" repo and create+commit fixture files.
167175
* If `fixture` is a string, the corresponding default fixture is used.

packages/beachball/src/__functional__/git/ensureSharedHistory.test.ts

Lines changed: 60 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ describe('ensureSharedHistory', () => {
1818
let repositoryFactory: RepositoryFactory;
1919
const logs = initMockLogs();
2020
const testBranch = 'test';
21+
const defaultRefSpec = `+refs/heads/${defaultBranchName}:refs/remotes/${defaultRemoteBranchName}`;
2122

2223
const realGit = jest.requireActual<typeof workspaceTools>('workspace-tools').git;
2324
/**
2425
* Set this to override the git implementation for one test.
2526
* (Use this instead of `.mockImplementation()` to avoid interference with other mocks.)
2627
*/
2728
let gitOverride: typeof workspaceTools.git | undefined;
28-
const gitSpy = jest
29-
.spyOn(workspaceTools, 'git')
29+
const gitSpy = (workspaceTools.git as jest.MockedFunction<typeof workspaceTools.git>)
3030
// Attempt to force git to use English in logs, so we can check for absence of "warning" strings
3131
.mockImplementation((args, opts) => (gitOverride || realGit)(args, opts && optsWithLang(opts)));
3232

@@ -77,10 +77,10 @@ describe('ensureSharedHistory', () => {
7777

7878
ensureSharedHistory({ path: repo.rootPath, verbose: true, branch: defaultRemoteBranchName, fetch: true });
7979
// Ensure the expected git calls were made
80-
expect(filteredGitCalls()).toEqual(['fetch origin master']);
80+
expect(filteredGitCalls()).toEqual([`fetch origin ${defaultRefSpec}`]);
8181

8282
const allLogs = logs.getMockLines('all');
83-
expect(allLogs).toMatch('Fetching branch "master" from remote "origin"...');
83+
expect(allLogs).toMatch(`Fetching branch "master" from remote "origin" (${defaultRefSpec})...`);
8484
expect(allLogs).toMatch('Fetching branch "master" from remote "origin" completed successfully');
8585
expect(allLogs).not.toMatch('warning');
8686
});
@@ -118,7 +118,7 @@ describe('ensureSharedHistory', () => {
118118
gitSpy.mockClear();
119119

120120
ensureSharedHistory({ path: repo.rootPath, verbose: true, branch: defaultRemoteBranchName, fetch: true, depth: 1 });
121-
expect(filteredGitCalls()).toEqual(['fetch origin master']);
121+
expect(filteredGitCalls()).toEqual([`fetch origin ${defaultRefSpec}`]);
122122
});
123123

124124
it('errors if fetching is disabled and target branch does not exist locally', () => {
@@ -168,10 +168,10 @@ describe('ensureSharedHistory', () => {
168168
expect(() =>
169169
ensureSharedHistory({ path: repo.rootPath, verbose: true, branch: defaultRemoteBranchName, fetch: true })
170170
).toThrow('Fetching branch "master" from remote "origin" failed');
171-
expect(filteredGitCalls()).toContain('fetch origin master');
171+
expect(filteredGitCalls()).toContain(`fetch origin ${defaultRefSpec}`);
172172

173173
const allLogs = logs.getMockLines('all');
174-
expect(allLogs).toMatch('Fetching branch "master" from remote "origin"...');
174+
expect(allLogs).toMatch(`Fetching branch "master" from remote "origin" (${defaultRefSpec})...`);
175175
expect(allLogs).toMatch('Fetching branch "master" from remote "origin" failed');
176176
});
177177

@@ -183,8 +183,7 @@ describe('ensureSharedHistory', () => {
183183
ensureSharedHistory({ path: repo.rootPath, verbose: false, branch: 'origin/fake', fetch: true })
184184
).toThrow('Fetching branch "fake" from remote "origin" failed');
185185
const gitOps = filteredGitCalls();
186-
expect(gitOps).not.toContain('remote set-branches --add origin fake');
187-
expect(gitOps).toContain('fetch origin fake');
186+
expect(gitOps).toContain('fetch origin +refs/heads/fake:refs/remotes/origin/fake');
188187

189188
expect(logs.getMockLines('all')).toEqual('');
190189
});
@@ -198,12 +197,12 @@ describe('ensureSharedHistory', () => {
198197
).toThrow('Fetching branch "fake" from remote "origin" failed');
199198

200199
const gitOps = filteredGitCalls();
201-
expect(gitOps).toContain('remote set-branches --add origin fake');
202-
expect(gitOps).toContain('fetch origin fake');
200+
expect(gitOps).toContain('fetch origin +refs/heads/fake:refs/remotes/origin/fake');
203201

204202
const allLogs = logs.getMockLines('all');
205-
expect(allLogs).toMatch('Adding branch "fake" to fetch config for remote "origin"');
206-
expect(allLogs).toMatch('Fetching branch "fake" from remote "origin"...');
203+
expect(allLogs).toMatch(
204+
`Fetching branch "fake" from remote "origin" (+refs/heads/fake:refs/remotes/origin/fake)...`
205+
);
207206
});
208207

209208
it('errors if fetching is disabled and adequate history is not available', () => {
@@ -237,13 +236,13 @@ describe('ensureSharedHistory', () => {
237236
branch: defaultRemoteBranchName,
238237
fetch: true,
239238
depth: 2,
240-
// Run this with verbose git logs so we can verify arguments were correct (no "warning" logs).
241-
// However, this adds noise including filesystem paths to the output, so we can't snapshot it.
239+
// Run this with verbose git logs so we can verify arguments were correct (no "warning" logs)
242240
verbose: true,
243241
});
244242

245-
const allLogs = logs.getMockLines('all');
246-
expect(allLogs).toMatch('Adding branch "master" to fetch config for remote "origin"');
243+
const allLogs = logs.getMockLines('all', {
244+
replacePaths: { [repo.rootPath]: '<repo-root>', [repositoryFactory.originRoot]: '<origin-root>' },
245+
});
247246
expect(allLogs).toMatch('This is a shallow clone. Deepening to check for changes...');
248247
expect(allLogs).toMatch('Deepening by 2 more commits (attempt 1/3)...');
249248
expect(allLogs).toMatch('Deepening by 2 more commits (attempt 2/3)...');
@@ -252,17 +251,40 @@ describe('ensureSharedHistory', () => {
252251
expect(logs.mocks.warn).not.toHaveBeenCalled();
253252
expect(logs.mocks.error).not.toHaveBeenCalled();
254253

255-
expect(filteredGitCalls().filter(line => !line.startsWith('rev-parse') && !line.startsWith('merge-base')))
256-
.toMatchInlineSnapshot(`
257-
[
258-
"config --get-all remote.origin.fetch",
259-
"remote set-branches --add origin master",
260-
"fetch --depth=2 origin master",
261-
"fetch --deepen=2 origin master",
262-
"fetch --deepen=2 origin master",
263-
"fetch --deepen=2 origin master",
264-
]
265-
`);
254+
const deepen = `fetch --deepen=2 origin ${defaultRefSpec}`;
255+
expect(filteredGitCalls()).toEqual([`fetch --depth=2 origin ${defaultRefSpec}`, deepen, deepen, deepen]);
256+
});
257+
258+
it('deepens history if needed when tracking ref already exists locally', () => {
259+
// singleBranch: false means origin/master is already fetched at depth 1 before ensureSharedHistory
260+
// runs, so hasBranchRef returns true and the ref already exists — but history is still too shallow.
261+
// This verifies that deepening correctly updates the existing tracking ref (requires explicit refspec).
262+
const repo = repositoryFactory.cloneRepository({ depth: 1, branch: testBranch, singleBranch: false });
263+
264+
// Verify the precondition: origin/master already exists locally (at depth 1)
265+
expect(realGit(['rev-parse', '--verify', defaultRemoteBranchName], { cwd: repo.rootPath }).success).toBe(true);
266+
267+
gitSpy.mockClear();
268+
269+
ensureSharedHistory({
270+
path: repo.rootPath,
271+
branch: defaultRemoteBranchName,
272+
fetch: true,
273+
depth: 2,
274+
verbose: true,
275+
});
276+
277+
const allLogs = logs.getMockLines('all', {
278+
replacePaths: { [repo.rootPath]: '<repo-root>', [repositoryFactory.originRoot]: '<origin-root>' },
279+
});
280+
expect(allLogs).toMatch('This is a shallow clone. Deepening to check for changes...');
281+
expect(allLogs).toMatch('Deepening by 2 more commits (attempt 1/3)...');
282+
expect(allLogs).not.toMatch('warning');
283+
expect(logs.mocks.warn).not.toHaveBeenCalled();
284+
expect(logs.mocks.error).not.toHaveBeenCalled();
285+
286+
const deepen = `fetch --deepen=2 origin ${defaultRefSpec}`;
287+
expect(filteredGitCalls()).toEqual([`fetch --depth=2 origin ${defaultRefSpec}`, deepen, deepen, deepen]);
266288
});
267289

268290
it('unshallows if deepening attempts fail', () => {
@@ -279,17 +301,14 @@ describe('ensureSharedHistory', () => {
279301
});
280302

281303
expect(logs.getMockLines('all')).toMatch("Still didn't find a common commit after deepening by 3. Unshallowing...");
282-
expect(filteredGitCalls()).toMatchInlineSnapshot(`
283-
[
284-
"config --get-all remote.origin.fetch",
285-
"remote set-branches --add origin master",
286-
"fetch --depth=1 origin master",
287-
"fetch --deepen=1 origin master",
288-
"fetch --deepen=1 origin master",
289-
"fetch --deepen=1 origin master",
290-
"fetch --unshallow origin master",
291-
]
292-
`);
304+
const deepen = `fetch --deepen=1 origin ${defaultRefSpec}`;
305+
expect(filteredGitCalls()).toEqual([
306+
`fetch --depth=1 origin ${defaultRefSpec}`,
307+
deepen,
308+
deepen,
309+
deepen,
310+
`fetch --unshallow origin ${defaultRefSpec}`,
311+
]);
293312
});
294313

295314
it('errors if there is no common history', () => {
@@ -309,12 +328,12 @@ describe('ensureSharedHistory', () => {
309328

310329
// The fetch part succeeded
311330
const allLogs = logs.getMockLines('all');
312-
expect(allLogs).toMatch('Fetching branch "master" from remote "origin"...');
331+
expect(allLogs).toMatch(`Fetching branch "master" from remote "origin" (${defaultRefSpec})...`);
313332
expect(allLogs).toMatch('Fetching branch "master" from remote "origin" completed successfully');
314333

315334
expect(filteredGitCalls([])).toEqual([
316335
'rev-parse --verify origin/master',
317-
'fetch origin master',
336+
`fetch origin ${defaultRefSpec}`,
318337
'merge-base origin/master HEAD',
319338
'rev-parse --is-shallow-repository',
320339
]);

0 commit comments

Comments
 (0)