Skip to content

Commit 2dae9ef

Browse files
authored
feat(Git Sync): Introduce the option to select a branch when cloning a repository (#8883)
* Allow users to pick a branch when cloning a repo * save * cleanup * add ref option to git clone action * cleanup UI * fix ts in credentials * fix ts issues from rebase * fix lint issue * add ref option to git actions and update credentials handling in UI components * fix: include credentials in dependency array for useEffect in GitRemoteBranchSelect
1 parent 17540a9 commit 2dae9ef

15 files changed

+459
-104
lines changed

packages/insomnia/src/main/git-service.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ import type { GitRepository } from '../models/git-repository';
2626
import { isWorkspace, type WorkspaceScope, WorkspaceScopeKeys } from '../models/workspace';
2727
import { fsClient } from '../sync/git/fs-client';
2828
import GitVCS, {
29+
fetchRemoteBranches,
2930
GIT_CLONE_DIR,
3031
GIT_INSOMNIA_DIR,
3132
GIT_INSOMNIA_DIR_NAME,
3233
GIT_INTERNAL_DIR,
34+
type GitCredentials,
3335
MergeConflictError,
3436
} from '../sync/git/git-vcs';
3537
import { MemClient } from '../sync/git/mem-client';
@@ -189,6 +191,7 @@ export async function loadGitRepository({ projectId, workspaceId }: { projectId:
189191
try {
190192
const gitRepository = await getGitRepository({ workspaceId, projectId });
191193

194+
const bufferId = await database.bufferChanges();
192195
const fsClient = await getGitFSClient({ gitRepositoryId: gitRepository._id, projectId, workspaceId });
193196

194197
if (GitVCS.isInitializedForRepo(gitRepository._id) && !gitRepository.needsFullClone) {
@@ -241,6 +244,8 @@ export async function loadGitRepository({ projectId, workspaceId }: { projectId:
241244
legacyInsomniaWorkspace = await containsLegacyInsomniaDir({ fsClient });
242245
}
243246

247+
await database.flushChanges(bufferId);
248+
244249
return {
245250
branch: await GitVCS.getCurrentBranch(),
246251
branches: await GitVCS.listBranches(),
@@ -591,6 +596,7 @@ export const initGitRepoCloneAction = async ({
591596
token,
592597
username,
593598
oauth2format,
599+
ref,
594600
}: {
595601
organizationId: string;
596602
uri: string;
@@ -599,6 +605,7 @@ export const initGitRepoCloneAction = async ({
599605
token: string;
600606
username: string;
601607
oauth2format?: string;
608+
ref?: string;
602609
}): Promise<
603610
| {
604611
files: {
@@ -647,6 +654,7 @@ export const initGitRepoCloneAction = async ({
647654

648655
try {
649656
await shallowClone({
657+
ref,
650658
fsClient: inMemoryFsClient,
651659
gitRepository: repoSettingsPatch as GitRepository,
652660
});
@@ -701,6 +709,7 @@ export const cloneGitRepoAction = async ({
701709
token,
702710
username,
703711
oauth2format,
712+
ref,
704713
}: {
705714
organizationId: string;
706715
projectId?: string;
@@ -712,6 +721,7 @@ export const cloneGitRepoAction = async ({
712721
token: string;
713722
username: string;
714723
oauth2format?: string;
724+
ref?: string;
715725
}) => {
716726
try {
717727
if (!projectId) {
@@ -750,6 +760,7 @@ export const cloneGitRepoAction = async ({
750760

751761
try {
752762
await shallowClone({
763+
ref,
753764
fsClient: inMemoryFsClient,
754765
gitRepository: repoSettingsPatch as GitRepository,
755766
});
@@ -822,6 +833,7 @@ export const cloneGitRepoAction = async ({
822833
directory: GIT_CLONE_DIR,
823834
fs: fsClient,
824835
gitDirectory: GIT_INTERNAL_DIR,
836+
ref,
825837
});
826838

827839
await models.gitRepository.update(gitRepository, {
@@ -846,7 +858,10 @@ export const cloneGitRepoAction = async ({
846858
await migrateLegacyInsomniaFolderToFile({ projectId: project._id });
847859
}
848860

849-
await models.gitRepository.update(gitRepository, {
861+
const updateRepository = await models.gitRepository.getById(gitRepository._id);
862+
invariant(updateRepository, 'Git Repository not found');
863+
864+
await models.gitRepository.update(updateRepository, {
850865
cachedGitLastCommitTime: Date.now(),
851866
cachedGitRepositoryBranch: await GitVCS.getCurrentBranch(),
852867
});
@@ -900,6 +915,7 @@ export const cloneGitRepoAction = async ({
900915
const providerName = getOauth2FormatName(repoSettingsPatch.credentials);
901916
try {
902917
await shallowClone({
918+
ref,
903919
fsClient: inMemoryFsClient,
904920
gitRepository: repoSettingsPatch as GitRepository,
905921
});
@@ -1089,6 +1105,7 @@ export const updateGitRepoAction = async ({
10891105
oauth2format,
10901106
username,
10911107
token,
1108+
ref,
10921109
}: {
10931110
projectId: string;
10941111
workspaceId?: string;
@@ -1098,6 +1115,7 @@ export const updateGitRepoAction = async ({
10981115
oauth2format?: string;
10991116
username: string;
11001117
token: string;
1118+
ref?: string;
11011119
}) => {
11021120
try {
11031121
let gitRepositoryId: string | null | undefined = null;
@@ -1178,6 +1196,7 @@ export const updateGitRepoAction = async ({
11781196
gitDirectory: GIT_INTERNAL_DIR,
11791197
gitCredentials: gitRepository.credentials,
11801198
legacyDiff: Boolean(workspaceId),
1199+
ref,
11811200
});
11821201

11831202
await GitVCS.setAuthor();
@@ -1704,6 +1723,26 @@ export const pushToGitRemoteAction = async ({
17041723
};
17051724
};
17061725

1726+
export async function fetchGitRemoteBranches({
1727+
uri,
1728+
credentials,
1729+
}: {
1730+
uri: string;
1731+
credentials?: GitCredentials;
1732+
}): Promise<{ branches: string[]; errors?: string[] }> {
1733+
try {
1734+
const branches = await fetchRemoteBranches({
1735+
uri: parseGitToHttpsURL(uri),
1736+
credentials,
1737+
});
1738+
1739+
return { branches };
1740+
} catch (err) {
1741+
const errorMessage = err instanceof Error ? err.message : 'Error while fetching remote branches';
1742+
return { branches: [], errors: [errorMessage] };
1743+
}
1744+
}
1745+
17071746
export async function pullFromGitRemote({ projectId, workspaceId }: { projectId: string; workspaceId?: string }) {
17081747
try {
17091748
const gitRepository = await getGitRepository({ projectId, workspaceId });
@@ -2472,7 +2511,7 @@ export interface GitServiceAPI {
24722511
diffFileLoader: typeof diffFileLoader;
24732512
getRepositoryDirectoryTree: typeof getRepositoryDirectoryTree;
24742513
migrateLegacyInsomniaFolderToFile: typeof migrateLegacyInsomniaFolderToFile;
2475-
2514+
fetchGitRemoteBranches: typeof fetchGitRemoteBranches;
24762515
initSignInToGitHub: typeof initSignInToGitHub;
24772516
completeSignInToGitHub: typeof completeSignInToGitHub;
24782517
signOutOfGitHub: typeof signOutOfGitHub;
@@ -2489,6 +2528,9 @@ export const registerGitServiceAPI = () => {
24892528
loadGitRepository(options),
24902529
);
24912530
ipcMainHandle('git.getGitBranches', (_, options: Parameters<typeof getGitBranches>[0]) => getGitBranches(options));
2531+
ipcMainHandle('git.fetchGitRemoteBranches', (_, options: Parameters<typeof fetchGitRemoteBranches>[0]) =>
2532+
fetchGitRemoteBranches(options),
2533+
);
24922534
ipcMainHandle('git.gitFetchAction', (_, options: Parameters<typeof gitFetchAction>[0]) => gitFetchAction(options));
24932535
ipcMainHandle('git.gitLogLoader', (_, options: Parameters<typeof gitLogLoader>[0]) => gitLogLoader(options));
24942536
ipcMainHandle('git.gitChangesLoader', (_, options: Parameters<typeof gitChangesLoader>[0]) =>

packages/insomnia/src/main/ipc/electron.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export type HandleChannels =
4949
| 'secretStorage.decryptString'
5050
| 'git.loadGitRepository'
5151
| 'git.getGitBranches'
52+
| 'git.fetchGitRemoteBranches'
5253
| 'git.gitFetchAction'
5354
| 'git.gitLogLoader'
5455
| 'git.gitChangesLoader'

packages/insomnia/src/preload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ const secretStorage: secretStorageBridgeAPI = {
7070
const git: GitServiceAPI = {
7171
loadGitRepository: options => ipcRenderer.invoke('git.loadGitRepository', options),
7272
getGitBranches: options => ipcRenderer.invoke('git.getGitBranches', options),
73+
fetchGitRemoteBranches: options => ipcRenderer.invoke('git.fetchGitRemoteBranches', options),
7374
gitFetchAction: options => ipcRenderer.invoke('git.gitFetchAction', options),
7475
gitLogLoader: options => ipcRenderer.invoke('git.gitLogLoader', options),
7576
gitChangesLoader: options => ipcRenderer.invoke('git.gitChangesLoader', options),

packages/insomnia/src/sync/git/git-vcs.ts

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ interface InitOptions {
6868
gitCredentials?: GitCredentials | null;
6969
uri?: string;
7070
repoId: string;
71+
ref?: string;
7172
// If enabled git-vcs will only diff files inside a .insomnia directory
7273
legacyDiff?: boolean;
7374
}
@@ -78,6 +79,7 @@ interface InitFromCloneOptions {
7879
directory: string;
7980
fs: git.FsClient;
8081
gitDirectory: string;
82+
ref?: string;
8183
repoId: string;
8284
}
8385

@@ -102,7 +104,7 @@ function getInsomniaFileName(blob: void | Uint8Array | undefined): string {
102104
try {
103105
const parsed = parse(Buffer.from(blob).toString('utf-8'));
104106
return parsed?.fileName || parsed?.name || '';
105-
} catch (e) {
107+
} catch {
106108
// If the document couldn't be parsed as yaml return an empty string
107109
return '';
108110
}
@@ -120,13 +122,14 @@ interface BaseOpts {
120122
uri: string;
121123
repoId: string;
122124
legacyDiff?: boolean;
125+
ref?: string;
123126
}
124127

125128
export class GitVCS {
126129
// @ts-expect-error -- TSCONVERSION not initialized with required properties
127130
_baseOpts: BaseOpts = gitCallbacks();
128131

129-
async init({ directory, fs, gitDirectory, gitCredentials, uri = '', repoId, legacyDiff = false }: InitOptions) {
132+
async init({ directory, fs, gitDirectory, gitCredentials, uri = '', repoId, legacyDiff = false, ref }: InitOptions) {
130133
this._baseOpts = {
131134
...this._baseOpts,
132135
dir: directory,
@@ -137,6 +140,7 @@ export class GitVCS {
137140
uri,
138141
repoId,
139142
legacyDiff,
143+
ref,
140144
};
141145

142146
if (await this.repoExists()) {
@@ -158,7 +162,7 @@ export class GitVCS {
158162
});
159163

160164
defaultBranch = mainRef?.target?.replace('refs/heads/', '') || 'main';
161-
} catch (err) {
165+
} catch {
162166
// Ignore error
163167
}
164168

@@ -174,13 +178,13 @@ export class GitVCS {
174178
});
175179

176180
return remoteOriginURI;
177-
} catch (err) {
181+
} catch {
178182
// Ignore error
179183
return this._baseOpts.uri || '';
180184
}
181185
}
182186

183-
async initFromClone({ repoId, url, gitCredentials, directory, fs, gitDirectory }: InitFromCloneOptions) {
187+
async initFromClone({ repoId, url, gitCredentials, directory, fs, gitDirectory, ref }: InitFromCloneOptions) {
184188
this._baseOpts = {
185189
...this._baseOpts,
186190
...gitCallbacks(gitCredentials),
@@ -190,23 +194,27 @@ export class GitVCS {
190194
http: httpClient,
191195
repoId,
192196
};
197+
198+
const initRef = ref || this._baseOpts.ref;
199+
193200
try {
194201
await git.clone({
195202
...this._baseOpts,
196203
url,
197-
singleBranch: true,
204+
...(initRef ? { ref: initRef } : {}),
198205
});
199206
} catch (err) {
200207
// If we there is a checkout conflict we only want to clone the repo
201208
if (err instanceof git.Errors.CheckoutConflictError) {
202209
await git.clone({
203210
...this._baseOpts,
204211
url,
205-
singleBranch: true,
212+
...(initRef ? { ref: initRef } : {}),
206213
noCheckout: true,
207214
});
208215
}
209216
}
217+
210218
console.log(`[git] Cloned repo to ${gitDirectory} from ${url}`);
211219
}
212220

@@ -366,7 +374,7 @@ export class GitVCS {
366374

367375
try {
368376
return Buffer.from(blob).toString('utf-8');
369-
} catch (e) {
377+
} catch {
370378
return null;
371379
}
372380
});
@@ -974,6 +982,7 @@ export class GitVCS {
974982
await git.checkout({
975983
...this._baseOpts,
976984
ref: branch,
985+
977986
remote: 'origin',
978987
});
979988
const branches = await this.listBranches();
@@ -986,7 +995,7 @@ export class GitVCS {
986995
async repoExists() {
987996
try {
988997
await git.getConfig({ ...this._baseOpts, path: '' });
989-
} catch (err) {
998+
} catch {
990999
return false;
9911000
}
9921001

@@ -1067,4 +1076,35 @@ function assertIsPromiseFsClient(fs: git.FsClient): asserts fs is git.PromiseFsC
10671076
}
10681077
}
10691078

1079+
export async function fetchRemoteBranches({ uri, credentials }: { uri: string; credentials?: GitCredentials | null }) {
1080+
const [mainRef] = await git.listServerRefs({
1081+
...gitCallbacks(credentials),
1082+
http: httpClient,
1083+
url: uri,
1084+
prefix: 'HEAD',
1085+
symrefs: true,
1086+
});
1087+
1088+
const remoteRefs = await git.listServerRefs({
1089+
...gitCallbacks(credentials),
1090+
http: httpClient,
1091+
url: uri,
1092+
prefix: 'refs/heads/',
1093+
symrefs: true,
1094+
});
1095+
1096+
const defaultBranch = mainRef?.target?.replace('refs/heads/', '') || 'main';
1097+
1098+
const remoteBranches = remoteRefs
1099+
.filter(b => b.ref !== 'HEAD')
1100+
.map(b => b.ref.replace('refs/heads/', ''))
1101+
.sort((a, b) => {
1102+
if (a === defaultBranch) return -1;
1103+
if (b === defaultBranch) return 1;
1104+
return a.localeCompare(b);
1105+
});
1106+
1107+
return remoteBranches;
1108+
}
1109+
10701110
export default new GitVCS();

packages/insomnia/src/sync/git/shallow-clone.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@ import { gitCallbacks } from './utils';
88
interface Options {
99
fsClient: git.FsClient;
1010
gitRepository: Pick<GitRepository, 'credentials' | 'uri'>;
11+
ref?: string;
1112
}
1213

1314
/**
1415
* Create a shallow clone into the provided FS plugin.
1516
* */
16-
export const shallowClone = async ({ fsClient, gitRepository }: Options) => {
17+
export const shallowClone = async ({ fsClient, gitRepository, ref = undefined }: Options) => {
1718
await git.clone({
1819
...gitCallbacks(gitRepository.credentials),
20+
...(ref ? { ref } : {}),
1921
fs: fsClient,
2022
http: httpClient,
2123
dir: GIT_CLONE_DIR,

0 commit comments

Comments
 (0)