Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: root directory sync issue #1171

Merged
merged 5 commits into from
Aug 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/app/store/providers/jsonbin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ export function useJSONbin() {
dispatch.tokenState.setEditProhibited(false);
return {
...data,
metadata: {}
}
metadata: {},
};
}
notifyToUI('No tokens stored on remote', { error: true });
return null;
Expand Down
84 changes: 51 additions & 33 deletions src/storage/GithubTokenStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type ExtendedOctokitClient = Omit<Octokit, 'repos'> & {
}
};

function getTreeMode(type: 'dir' | 'file' | string) {
export function getTreeMode(type: 'dir' | 'file' | string) {
switch (type) {
case 'dir':
return '040000';
Expand Down Expand Up @@ -64,12 +64,51 @@ export class GithubTokenStorage extends GitTokenStorage {
}) as ExtendedOctokitClient;
}

public async fetchBranches() {
const branches = await this.octokitClient.repos.listBranches({
public async listBranches() {
return this.octokitClient.repos.listBranches({
owner: this.owner,
repo: this.repository,
headers: octokitClientDefaultHeaders,
});
}

public async getTreeShaForDirectory(path: string) {
// @README this is necessary because to figure out the tree SHA we need to fetch the parent directory contents
// however when pulling from the root directory we can not do this, but we can take the SHA from the branch
if (path === '') {
const branches = await this.listBranches();
const branch = branches.data.find((entry) => entry.name === this.branch);
if (!branch) throw new Error(`Branch not found, ${this.branch}`);
return branch.commit.sha;
}

// get the parent directory content to find out the sha
const parent = path.includes('/') ? path.slice(0, path.lastIndexOf('/')) : '';
const parentDirectoryTreeResponse = await this.octokitClient.rest.repos.getContent({
owner: this.owner,
repo: this.repository,
path: parent,
ref: this.branch,
headers: octokitClientDefaultHeaders,
});

if (Array.isArray(parentDirectoryTreeResponse.data)) {
const directory = parentDirectoryTreeResponse.data.find((item) => item.path === path);
if (!directory) throw new Error(`Unable to find directory, ${path}`);
return directory.sha;
}

// @README if the parent directory only contains a single subdirectory
// it will not return an array with 1 item - but rather it will return the item itself
if (parentDirectoryTreeResponse.data.path === path) {
return parentDirectoryTreeResponse.data.sha;
}

throw new Error('Could not find directory SHA');
}

public async fetchBranches() {
const branches = await this.listBranches();
return branches.data.map((branch) => branch.name);
}

Expand Down Expand Up @@ -112,45 +151,24 @@ export class GithubTokenStorage extends GitTokenStorage {

public async read(): Promise<RemoteTokenStorageFile<GitStorageMetadata>[] | RemoteTokenstorageErrorMessage> {
try {
const normalizedPath = compact(this.path.split('/')).join('/');
const response = await this.octokitClient.rest.repos.getContent({
path: normalizedPath,
owner: this.owner,
repo: this.repository,
path: this.path,
ref: this.branch,
headers: octokitClientDefaultHeaders,
});
// read entire directory
if (Array.isArray(response.data)) {
let treeResponse;
const filteredPath = this.path.replace(/^\/+/, '');
const parentPath = filteredPath.includes('/') ? filteredPath.slice(0, filteredPath.lastIndexOf('/')) : '';
const parentDirectoryTreeResponse = await this.octokitClient.rest.repos.getContent({
const directorySha = await this.getTreeShaForDirectory(normalizedPath);
const treeResponse = await this.octokitClient.rest.git.getTree({
owner: this.owner,
repo: this.repository,
path: parentPath,
ref: this.branch,
tree_sha: directorySha,
recursive: 'true',
headers: octokitClientDefaultHeaders,
});
if (Array.isArray(parentDirectoryTreeResponse.data)) {
const directory = parentDirectoryTreeResponse.data.find((item) => item.path === filteredPath);
if (directory) {
treeResponse = await this.octokitClient.rest.git.getTree({
owner: this.owner,
repo: this.repository,
tree_sha: directory.sha,
recursive: 'true',
headers: octokitClientDefaultHeaders,
});
}
} else if (parentDirectoryTreeResponse.data.path === filteredPath) {
treeResponse = await this.octokitClient.rest.git.getTree({
owner: this.owner,
repo: this.repository,
tree_sha: parentDirectoryTreeResponse.data.sha,
recursive: 'true',
headers: octokitClientDefaultHeaders,
});
}
if (treeResponse && treeResponse.data.tree.length > 0) {
const jsonFiles = treeResponse.data.tree.filter((file) => (
file.path?.endsWith('.json')
Expand All @@ -161,7 +179,7 @@ export class GithubTokenStorage extends GitTokenStorage {
treeItem.path ? this.octokitClient.rest.repos.getContent({
owner: this.owner,
repo: this.repository,
path: treeItem.path.startsWith(filteredPath) ? treeItem.path : `${filteredPath}/${treeItem.path}`,
path: treeItem.path.startsWith(normalizedPath) ? treeItem.path : `${normalizedPath}/${treeItem.path}`,
ref: this.branch,
headers: octokitClientDefaultHeaders,
}) : Promise.resolve(null)
Expand All @@ -174,7 +192,7 @@ export class GithubTokenStorage extends GitTokenStorage {
&& !Array.isArray(fileContent?.data)
&& 'content' in fileContent.data
) {
const filePath = path.startsWith(filteredPath) ? path : `${filteredPath}/${path}`;
const filePath = path.startsWith(normalizedPath) ? path : `${normalizedPath}/${path}`;
let name = filePath.substring(this.path.length).replace(/^\/+/, '');
name = name.replace('.json', '');
const parsed = JSON.parse(decodeBase64(fileContent.data.content)) as GitMultiFileObject;
Expand Down Expand Up @@ -234,7 +252,7 @@ export class GithubTokenStorage extends GitTokenStorage {
return [];
} catch (e) {
// Raise error (usually this is an auth error)
console.log('Error', e);
console.error('Error', e);
return [];
}
}
Expand Down
67 changes: 66 additions & 1 deletion src/storage/__tests__/GithubTokenStorage.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TokenSetStatus } from '@/constants/TokenSetStatus';
import { TokenTypes } from '@/constants/TokenTypes';
import { GithubTokenStorage } from '../GithubTokenStorage';
import { getTreeMode, GithubTokenStorage } from '../GithubTokenStorage';
import {
mockCreateOrUpdateFiles,
mockCreateRef,
Expand All @@ -23,6 +23,11 @@ describe('GithubTokenStorage', () => {
storageProvider.changePath('tokens.json');
});

it('should be able to get the tree mode', () => {
expect(getTreeMode('dir')).toEqual('040000');
expect(getTreeMode('file')).toEqual('100644');
});

it('should fetch branches as a simple list', async () => {
expect(
await storageProvider.fetchBranches(),
Expand Down Expand Up @@ -1245,4 +1250,64 @@ describe('GithubTokenStorage', () => {
commitMessage: '',
})).toBe(false);
});

it('should be able to get the tree sha for a given path', async () => {
mockListBranches.mockImplementationOnce(() => (
Promise.resolve({
data: [
{
name: 'main',
commit: { sha: 'root-sha' },
},
],
})
));
expect(await storageProvider.getTreeShaForDirectory('')).toEqual('root-sha');

mockGetContent.mockImplementationOnce(() => (
Promise.resolve({
data: [
{
path: 'companyA/ds',
sha: 'directory-sha',
},
],
})
));
expect(await storageProvider.getTreeShaForDirectory('companyA/ds')).toEqual('directory-sha');

mockGetContent.mockImplementationOnce(() => (
Promise.resolve({
data: {
path: 'companyA/ds',
sha: 'single-directory-sha',
},
})
));
expect(await storageProvider.getTreeShaForDirectory('companyA/ds')).toEqual('single-directory-sha');

mockGetContent.mockImplementationOnce(() => (
Promise.resolve({
data: [
{
path: 'companyA',
sha: 'directory-sha',
},
],
})
));
await expect(storageProvider.getTreeShaForDirectory('companyA/ds')).rejects.toThrow(
'Unable to find directory, companyA/ds',
);

mockGetContent.mockImplementationOnce(() => (
Promise.resolve({
data: {
path: 'companyA',
sha: 'single-directory-sha',
},
})
));
await expect(storageProvider.getTreeShaForDirectory('companyA/ds')).rejects.toThrow('Could not find directory SHA');
});
});