Use graphql in GitHubClient and misc fixes (#8)

* Use graphql in GitHubClient

* Replace usage of Array.find with _.find
This commit is contained in:
Tanuj Mittal
2020-06-05 12:22:41 -07:00
committed by GitHub
parent e9d3160b57
commit aa8236666e
24 changed files with 761 additions and 657 deletions

View File

@@ -1,92 +1,110 @@
import ko from "knockout";
import { HttpStatusCodes } from "../Common/Constants";
import { GitHubClient, IGitHubBranch, IGitHubRepo } from "./GitHubClient";
import { GitHubClient, IGitHubFile } from "./GitHubClient";
import { SamplesRepo, SamplesBranch, SamplesContentsQueryResponse } from "../Explorer/Notebook/NotebookSamples";
const invalidTokenCallback = jest.fn();
// Use a dummy token to get around API rate limit (same as AZURESAMPLESCOSMOSDBPAT in webpack.config.js)
const gitHubClient = new GitHubClient("99e38770e29b4a61d7c49f188780504efd35cc86", invalidTokenCallback);
const samplesRepo: IGitHubRepo = {
name: "cosmos-notebooks",
owner: {
login: "Azure-Samples"
},
private: false
};
const samplesBranch: IGitHubBranch = {
name: "master"
};
const sampleFilePath = ".gitignore";
const sampleDirPath = ".github";
// Use a dummy token to get around API rate limit (something which doesn't affect the API quota for AZURESAMPLESCOSMOSDBPAT in Config.ts)
const gitHubClient = new GitHubClient("cd1906b9534362fab6ce45d6db6c76b59e55bc50", invalidTokenCallback);
describe.skip("GitHubClient", () => {
const validateGitHubFile = (file: IGitHubFile) => {
expect(file.branch).toEqual(SamplesBranch);
expect(file.commit).toBeDefined();
expect(file.name).toBeDefined();
expect(file.path).toBeDefined();
expect(file.repo).toEqual(SamplesRepo);
expect(file.type).toBeDefined();
switch (file.type) {
case "blob":
expect(file.sha).toBeDefined();
expect(file.size).toBeDefined();
break;
case "tree":
expect(file.sha).toBeUndefined();
expect(file.size).toBeUndefined();
break;
default:
throw new Error(`Unsupported github file type: ${file.type}`);
}
};
describe("GitHubClient", () => {
it("getRepoAsync returns valid repo", async () => {
const response = await gitHubClient.getRepoAsync(samplesRepo.owner.login, samplesRepo.name);
expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data.name).toBe(samplesRepo.name);
expect(response.data.owner.login).toBe(samplesRepo.owner.login);
const response = await gitHubClient.getRepoAsync(SamplesRepo.owner, SamplesRepo.name);
expect(response).toEqual({
status: HttpStatusCodes.OK,
data: SamplesRepo
});
});
it("getReposAsync returns repos for authenticated user", async () => {
const response = await gitHubClient.getReposAsync(1, 1);
const response = await gitHubClient.getReposAsync(1);
expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data).toBeDefined();
expect(response.data.length).toBe(1);
expect(response.pageInfo).toBeDefined();
});
it("getBranchesAsync returns branches for a repo", async () => {
const response = await gitHubClient.getBranchesAsync(samplesRepo.owner.login, samplesRepo.name, 1, 1);
const response = await gitHubClient.getBranchesAsync(SamplesRepo.owner, SamplesRepo.name, 1);
expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data.length).toBe(1);
expect(response.data).toEqual([SamplesBranch]);
expect(response.pageInfo).toBeDefined();
});
it("getCommitsAsync returns commits for a file", async () => {
const response = await gitHubClient.getCommitsAsync(
samplesRepo.owner.login,
samplesRepo.name,
samplesBranch.name,
sampleFilePath,
1,
1
);
it("getContentsAsync returns files in the repo", async () => {
const response = await gitHubClient.getContentsAsync(SamplesRepo.owner, SamplesRepo.name, SamplesBranch.name);
expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data.length).toBe(1);
expect(response.data).toBeDefined();
const data = response.data as IGitHubFile[];
expect(data.length).toBeGreaterThan(0);
data.forEach(content => validateGitHubFile(content));
});
it("getDirContentsAsync returns files in the repo", async () => {
const response = await gitHubClient.getDirContentsAsync(
samplesRepo.owner.login,
samplesRepo.name,
samplesBranch.name,
""
it("getContentsAsync returns files in a dir", async () => {
const samplesDir = SamplesContentsQueryResponse.repository.object.entries.find(file => file.type === "tree");
const response = await gitHubClient.getContentsAsync(
SamplesRepo.owner,
SamplesRepo.name,
SamplesBranch.name,
samplesDir.name
);
expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data.length).toBeGreaterThan(0);
expect(response.data[0].repo).toEqual(samplesRepo);
expect(response.data[0].branch).toEqual(samplesBranch);
expect(response.data).toBeDefined();
const data = response.data as IGitHubFile[];
expect(data.length).toBeGreaterThan(0);
data.forEach(content => validateGitHubFile(content));
});
it("getDirContentsAsync returns files in a dir", async () => {
const response = await gitHubClient.getDirContentsAsync(
samplesRepo.owner.login,
samplesRepo.name,
samplesBranch.name,
sampleDirPath
it("getContentsAsync returns a file", async () => {
const samplesFile = SamplesContentsQueryResponse.repository.object.entries.find(file => file.type === "blob");
const response = await gitHubClient.getContentsAsync(
SamplesRepo.owner,
SamplesRepo.name,
SamplesBranch.name,
samplesFile.name
);
expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data.length).toBeGreaterThan(0);
expect(response.data[0].repo).toEqual(samplesRepo);
expect(response.data[0].branch).toEqual(samplesBranch);
expect(response.data).toBeDefined();
const file = response.data as IGitHubFile;
expect(file.type).toBe("blob");
validateGitHubFile(file);
expect(file.content).toBeUndefined();
});
it("getFileContentsAsync returns a file", async () => {
const response = await gitHubClient.getFileContentsAsync(
samplesRepo.owner.login,
samplesRepo.name,
samplesBranch.name,
sampleFilePath
);
it("getBlobAsync returns file content", async () => {
const samplesFile = SamplesContentsQueryResponse.repository.object.entries.find(file => file.type === "blob");
const response = await gitHubClient.getBlobAsync(SamplesRepo.owner, SamplesRepo.name, samplesFile.object.oid);
expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data.path).toBe(sampleFilePath);
expect(response.data.repo).toEqual(samplesRepo);
expect(response.data.branch).toEqual(samplesBranch);
expect(response.data).toBeDefined();
expect(typeof response.data).toBe("string");
});
});

View File

@@ -1,163 +1,228 @@
import { Octokit } from "@octokit/rest";
import { RequestHeaders } from "@octokit/types";
import { HttpStatusCodes } from "../Common/Constants";
import { Logger } from "../Common/Logger";
import UrlUtility from "../Common/UrlUtility";
import { isSamplesCall, SamplesContentsQueryResponse } from "../Explorer/Notebook/NotebookSamples";
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
export interface IGitHubPageInfo {
endCursor: string;
hasNextPage: boolean;
}
export interface IGitHubResponse<T> {
status: number;
data: T;
pageInfo?: IGitHubPageInfo;
}
export interface IGitHubRepo {
// API properties
name: string;
owner: {
login: string;
};
owner: string;
private: boolean;
// Custom properties
children?: IGitHubFile[];
}
export interface IGitHubFile {
// API properties
type: "file" | "dir" | "symlink" | "submodule";
encoding?: string;
size: number;
type: "blob" | "tree";
size?: number;
name: string;
path: string;
content?: string;
sha: string;
// Custom properties
sha?: string;
children?: IGitHubFile[];
repo?: IGitHubRepo;
branch?: IGitHubBranch;
repo: IGitHubRepo;
branch: IGitHubBranch;
commit: IGitHubCommit;
}
export interface IGitHubCommit {
// API properties
sha: string;
message: string;
committer: {
date: string;
};
commitDate: string;
}
export interface IGitHubBranch {
// API properties
name: string;
}
export interface IGitHubUser {
// API properties
login: string;
// graphql schema
interface Collection<T> {
pageInfo?: PageInfo;
nodes: T[];
}
interface Repository {
isPrivate: boolean;
name: string;
owner: {
login: string;
};
}
interface Ref {
name: string;
}
interface History {
history: Collection<Commit>;
}
interface Commit {
committer: {
date: string;
};
message: string;
oid: string;
}
interface Tree extends Blob {
entries: TreeEntry[];
}
interface TreeEntry {
name: string;
type: string;
object: Blob;
}
interface Blob {
byteSize?: number;
oid?: string;
}
interface PageInfo {
endCursor: string;
hasNextPage: boolean;
}
// graphql queries and types
const repositoryQuery = `query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
owner {
login
}
name
isPrivate
}
}`;
type RepositoryQueryParams = {
owner: string;
repo: string;
};
type RepositoryQueryResponse = {
repository: Repository;
};
const repositoriesQuery = `query($pageSize: Int!, $endCursor: String) {
viewer {
repositories(first: $pageSize, after: $endCursor) {
pageInfo {
endCursor,
hasNextPage
}
nodes {
owner {
login
}
name
isPrivate
}
}
}
}`;
type RepositoriesQueryParams = {
pageSize: number;
endCursor?: string;
};
type RepositoriesQueryResponse = {
viewer: {
repositories: Collection<Repository>;
};
};
const branchesQuery = `query($owner: String!, $repo: String!, $refPrefix: String!, $pageSize: Int!, $endCursor: String) {
repository(owner: $owner, name: $repo) {
refs(refPrefix: $refPrefix, first: $pageSize, after: $endCursor) {
pageInfo {
endCursor,
hasNextPage
}
nodes {
name
}
}
}
}`;
type BranchesQueryParams = {
owner: string;
repo: string;
refPrefix: string;
pageSize: number;
endCursor?: string;
};
type BranchesQueryResponse = {
repository: {
refs: Collection<Ref>;
};
};
const contentsQuery = `query($owner: String!, $repo: String!, $ref: String!, $path: String, $objectExpression: String!) {
repository(owner: $owner, name: $repo) {
owner {
login
}
name
isPrivate
ref(qualifiedName: $ref) {
name
target {
... on Commit {
history(first: 1, path: $path) {
nodes {
oid
message
committer {
date
}
}
}
}
}
}
object(expression: $objectExpression) {
... on Blob {
oid
byteSize
}
... on Tree {
entries {
name
type
object {
... on Blob {
oid
byteSize
}
}
}
}
}
}
}`;
type ContentsQueryParams = {
owner: string;
repo: string;
ref: string;
path?: string;
objectExpression: string;
};
type ContentsQueryResponse = {
repository: Repository & { ref: Ref & { target: History } } & { object: Tree };
};
export class GitHubClient {
private static readonly gitHubApiEndpoint = "https://api.github.com";
private static readonly samplesRepo: IGitHubRepo = {
name: "cosmos-notebooks",
private: false,
owner: {
login: "Azure-Samples"
}
};
private static readonly samplesBranch: IGitHubBranch = {
name: "master"
};
private static readonly samplesTopCommit: IGitHubCommit = {
sha: "41b964f442b638097a75a3f3b6a6451db05a12bf",
committer: {
date: "2020-05-19T05:03:30Z"
},
message: "Fixing formatting"
};
private static readonly samplesFiles: IGitHubFile[] = [
{
name: ".github",
path: ".github",
sha: "5e6794a8177a0c07a8719f6e1d7b41cce6f92e1e",
size: 0,
type: "dir"
},
{
name: ".gitignore",
path: ".gitignore",
sha: "3e759b75bf455ac809d0987d369aab89137b5689",
size: 5582,
type: "file"
},
{
name: "1. GettingStarted.ipynb",
path: "1. GettingStarted.ipynb",
sha: "0732ff5366e4aefdc4c378c61cbd968664f0acec",
size: 3933,
type: "file"
},
{
name: "2. Visualization.ipynb",
path: "2. Visualization.ipynb",
sha: "f480134ac4adf2f50ce5fe66836c6966749d3ca1",
size: 814261,
type: "file"
},
{
name: "3. RequestUnits.ipynb",
path: "3. RequestUnits.ipynb",
sha: "252b79a4adc81e9f2ffde453231b695d75e270e8",
size: 9490,
type: "file"
},
{
name: "4. Indexing.ipynb",
path: "4. Indexing.ipynb",
sha: "e10dd67bd1c55c345226769e4f80e43659ef9cd5",
size: 10394,
type: "file"
},
{
name: "5. StoredProcedures.ipynb",
path: "5. StoredProcedures.ipynb",
sha: "949941949920de4d2d111149e2182e9657cc8134",
size: 11818,
type: "file"
},
{
name: "6. GlobalDistribution.ipynb",
path: "6. GlobalDistribution.ipynb",
sha: "b91c31dacacbc9e35750d9054063dda4a5309f3b",
size: 11375,
type: "file"
},
{
name: "7. IoTAnomalyDetection.ipynb",
path: "7. IoTAnomalyDetection.ipynb",
sha: "82057ae52a67721a5966e2361317f5dfbd0ee595",
size: 377939,
type: "file"
},
{
name: "All_API_quickstarts",
path: "All_API_quickstarts",
sha: "07054293e6c8fc00771fccd0cde207f5c8053978",
size: 0,
type: "dir"
},
{
name: "CSharp_quickstarts",
path: "CSharp_quickstarts",
sha: "10e7f5704e6b56a40cac74bc39f15b7708954f52",
size: 0,
type: "dir"
}
];
private static readonly SelfErrorCode = 599;
private ocktokit: Octokit;
constructor(token: string, private errorCallback: (error: any) => void) {
@@ -169,167 +234,136 @@ export class GitHubClient {
}
public async getRepoAsync(owner: string, repo: string): Promise<IGitHubResponse<IGitHubRepo>> {
if (GitHubClient.isSamplesCall(owner, repo)) {
try {
const response = (await this.ocktokit.graphql(repositoryQuery, {
owner,
repo
} as RepositoryQueryParams)) as RepositoryQueryResponse;
return {
status: HttpStatusCodes.OK,
data: GitHubClient.samplesRepo
data: GitHubClient.toGitHubRepo(response.repository)
};
} catch (error) {
GitHubClient.log(Logger.logError, `GitHubClient.getRepoAsync failed: ${error}`);
return {
status: GitHubClient.SelfErrorCode,
data: undefined
};
}
const response = await this.ocktokit.repos.get({
owner,
repo,
headers: GitHubClient.getDisableCacheHeaders()
});
let data: IGitHubRepo;
if (response.data) {
data = GitHubClient.toGitHubRepo(response.data);
}
return { status: response.status, data };
}
public async getReposAsync(page: number, perPage: number): Promise<IGitHubResponse<IGitHubRepo[]>> {
const response = await this.ocktokit.repos.listForAuthenticatedUser({
page,
per_page: perPage,
headers: GitHubClient.getDisableCacheHeaders()
});
public async getReposAsync(pageSize: number, endCursor?: string): Promise<IGitHubResponse<IGitHubRepo[]>> {
try {
const response = (await this.ocktokit.graphql(repositoriesQuery, {
pageSize,
endCursor
} as RepositoriesQueryParams)) as RepositoriesQueryResponse;
let data: IGitHubRepo[];
if (response.data) {
data = [];
response.data?.forEach((element: any) => data.push(GitHubClient.toGitHubRepo(element)));
return {
status: HttpStatusCodes.OK,
data: response.viewer.repositories.nodes.map(repo => GitHubClient.toGitHubRepo(repo)),
pageInfo: GitHubClient.toGitHubPageInfo(response.viewer.repositories.pageInfo)
};
} catch (error) {
GitHubClient.log(Logger.logError, `GitHubClient.getReposAsync failed: ${error}`);
return {
status: GitHubClient.SelfErrorCode,
data: undefined
};
}
return { status: response.status, data };
}
public async getBranchesAsync(
owner: string,
repo: string,
page: number,
perPage: number
pageSize: number,
endCursor?: string
): Promise<IGitHubResponse<IGitHubBranch[]>> {
const response = await this.ocktokit.repos.listBranches({
owner,
repo,
page,
per_page: perPage,
headers: GitHubClient.getDisableCacheHeaders()
});
try {
const response = (await this.ocktokit.graphql(branchesQuery, {
owner,
repo,
refPrefix: "refs/heads/",
pageSize,
endCursor
} as BranchesQueryParams)) as BranchesQueryResponse;
let data: IGitHubBranch[];
if (response.data) {
data = [];
response.data?.forEach(element => data.push(GitHubClient.toGitHubBranch(element)));
}
return { status: response.status, data };
}
public async getCommitsAsync(
owner: string,
repo: string,
branch: string,
path: string,
page: number,
perPage: number
): Promise<IGitHubResponse<IGitHubCommit[]>> {
if (GitHubClient.isSamplesCall(owner, repo, branch) && path === "" && page === 1 && perPage === 1) {
return {
status: HttpStatusCodes.OK,
data: [GitHubClient.samplesTopCommit]
data: response.repository.refs.nodes.map(ref => GitHubClient.toGitHubBranch(ref)),
pageInfo: GitHubClient.toGitHubPageInfo(response.repository.refs.pageInfo)
};
} catch (error) {
GitHubClient.log(Logger.logError, `GitHubClient.getBranchesAsync failed: ${error}`);
return {
status: GitHubClient.SelfErrorCode,
data: undefined
};
}
const response = await this.ocktokit.repos.listCommits({
owner,
repo,
sha: branch,
path,
page,
per_page: perPage,
headers: GitHubClient.getDisableCacheHeaders()
});
let data: IGitHubCommit[];
if (response.data) {
data = [];
response.data?.forEach(element =>
data.push(GitHubClient.toGitHubCommit({ ...element.commit, sha: element.sha }))
);
}
return { status: response.status, data };
}
public async getDirContentsAsync(
owner: string,
repo: string,
branch: string,
path: string
): Promise<IGitHubResponse<IGitHubFile[]>> {
return (await this.getContentsAsync(owner, repo, branch, path)) as IGitHubResponse<IGitHubFile[]>;
}
public async getFileContentsAsync(
owner: string,
repo: string,
branch: string,
path: string
): Promise<IGitHubResponse<IGitHubFile>> {
return (await this.getContentsAsync(owner, repo, branch, path)) as IGitHubResponse<IGitHubFile>;
}
public async getContentsAsync(
owner: string,
repo: string,
branch: string,
path: string
path?: string
): Promise<IGitHubResponse<IGitHubFile | IGitHubFile[]>> {
if (GitHubClient.isSamplesCall(owner, repo, branch) && path === "") {
try {
let response: ContentsQueryResponse;
if (isSamplesCall(owner, repo, branch) && !path) {
response = SamplesContentsQueryResponse;
} else {
response = (await this.ocktokit.graphql(contentsQuery, {
owner,
repo,
ref: `refs/heads/${branch}`,
path: path || undefined,
objectExpression: `refs/heads/${branch}:${path || ""}`
} as ContentsQueryParams)) as ContentsQueryResponse;
}
let data: IGitHubFile | IGitHubFile[];
const entries = response.repository.object.entries;
const gitHubRepo = GitHubClient.toGitHubRepo(response.repository);
const gitHubBranch = GitHubClient.toGitHubBranch(response.repository.ref);
const gitHubCommit = GitHubClient.toGitHubCommit(response.repository.ref.target.history.nodes[0]);
if (Array.isArray(entries)) {
data = entries.map(entry =>
GitHubClient.toGitHubFile(
entry,
(path && UrlUtility.createUri(path, entry.name)) || entry.name,
gitHubRepo,
gitHubBranch,
gitHubCommit
)
);
} else {
data = GitHubClient.toGitHubFile(
{
name: NotebookUtil.getName(path),
type: "blob",
object: response.repository.object
},
path,
gitHubRepo,
gitHubBranch,
gitHubCommit
);
}
return {
status: HttpStatusCodes.OK,
data: GitHubClient.samplesFiles.map(file =>
GitHubClient.toGitHubFile(file, GitHubClient.samplesRepo, GitHubClient.samplesBranch)
)
data
};
} catch (error) {
GitHubClient.log(Logger.logError, `GitHubClient.getContentsAsync failed: ${error}`);
return {
status: GitHubClient.SelfErrorCode,
data: undefined
};
}
const response = await this.ocktokit.repos.getContents({
owner,
repo,
path,
ref: branch,
headers: GitHubClient.getDisableCacheHeaders()
});
let data: IGitHubFile | IGitHubFile[];
if (response.data) {
const repoResponse = await this.getRepoAsync(owner, repo);
if (repoResponse.data) {
const fileRepo: IGitHubRepo = GitHubClient.toGitHubRepo(repoResponse.data);
const fileBranch: IGitHubBranch = { name: branch };
if (Array.isArray(response.data)) {
const contents: IGitHubFile[] = [];
response.data.forEach((element: any) =>
contents.push(GitHubClient.toGitHubFile(element, fileRepo, fileBranch))
);
data = contents;
} else {
data = GitHubClient.toGitHubFile(
{ ...response.data, type: response.data.type as "file" | "dir" | "symlink" | "submodule" },
fileRepo,
fileBranch
);
}
}
}
return { status: response.status, data };
}
public async createOrUpdateFileAsync(
@@ -372,7 +406,9 @@ export class GitHubClient {
owner,
repo,
ref,
headers: GitHubClient.getDisableCacheHeaders()
headers: {
"If-None-Match": "" // disable 60s cache
}
});
const currentTree = await this.ocktokit.git.getTree({
@@ -380,7 +416,9 @@ export class GitHubClient {
repo,
tree_sha: currentRef.data.object.sha,
recursive: "1",
headers: GitHubClient.getDisableCacheHeaders()
headers: {
"If-None-Match": "" // disable 60s cache
}
});
// API infers tree from paths so we need to filter them out
@@ -425,7 +463,7 @@ export class GitHubClient {
public async deleteFileAsync(file: IGitHubFile, message: string): Promise<IGitHubResponse<IGitHubCommit>> {
const response = await this.ocktokit.repos.deleteFile({
owner: file.repo.owner.login,
owner: file.repo.owner,
repo: file.repo.name,
path: file.path,
message,
@@ -441,10 +479,31 @@ export class GitHubClient {
return { status: response.status, data };
}
private initOctokit(token: string) {
public async getBlobAsync(owner: string, repo: string, sha: string): Promise<IGitHubResponse<string>> {
const response = await this.ocktokit.git.getBlob({
owner,
repo,
file_sha: sha,
mediaType: {
format: "raw"
},
headers: {
"If-None-Match": "" // disable 60s cache
}
});
return { status: response.status, data: <string>(<unknown>response.data) };
}
private async initOctokit(token: string) {
this.ocktokit = new Octokit({
auth: token,
baseUrl: GitHubClient.gitHubApiEndpoint
log: {
debug: () => {},
info: (message?: any) => GitHubClient.log(Logger.logInfo, message),
warn: (message?: any) => GitHubClient.log(Logger.logWarning, message),
error: (message?: any) => GitHubClient.log(Logger.logError, message)
}
});
this.ocktokit.hook.error("request", error => {
@@ -453,53 +512,69 @@ export class GitHubClient {
});
}
private static getDisableCacheHeaders(): RequestHeaders {
private static log(logger: (message: string, area: string) => void, message?: any) {
if (message) {
message = typeof message === "string" ? message : JSON.stringify(message);
logger(message, "GitHubClient.Octokit");
}
}
private static toGitHubRepo(object: Repository): IGitHubRepo {
return {
"If-None-Match": ""
owner: object.owner.login,
name: object.name,
private: object.isPrivate
};
}
private static toGitHubRepo(element: IGitHubRepo): IGitHubRepo {
private static toGitHubBranch(object: Ref): IGitHubBranch {
return {
name: element.name,
owner: {
login: element.owner.login
},
private: element.private
name: object.name
};
}
private static toGitHubBranch(element: IGitHubBranch): IGitHubBranch {
private static toGitHubCommit(object: {
message: string;
committer: {
date: string;
};
sha?: string;
oid?: string;
}): IGitHubCommit {
return {
name: element.name
sha: object.sha || object.oid,
message: object.message,
commitDate: object.committer.date
};
}
private static toGitHubCommit(element: IGitHubCommit): IGitHubCommit {
private static toGitHubPageInfo(object: PageInfo): IGitHubPageInfo {
return {
sha: element.sha,
message: element.message,
committer: {
date: element.committer.date
}
endCursor: object.endCursor,
hasNextPage: object.hasNextPage
};
}
private static toGitHubFile(element: IGitHubFile, repo: IGitHubRepo, branch: IGitHubBranch): IGitHubFile {
private static toGitHubFile(
entry: TreeEntry,
path: string,
repo: IGitHubRepo,
branch: IGitHubBranch,
commit: IGitHubCommit
): IGitHubFile {
if (entry.type !== "blob" && entry.type !== "tree") {
throw new Error(`Unsupported file type: ${entry.type}`);
}
return {
type: element.type,
encoding: element.encoding,
size: element.size,
name: element.name,
path: element.path,
content: element.content,
sha: element.sha,
type: entry.type,
name: entry.name,
path,
repo,
branch
branch,
commit,
size: entry.object?.byteSize,
sha: entry.object?.oid
};
}
private static isSamplesCall(owner: string, repo: string, branch?: string): boolean {
return owner === "Azure-Samples" && repo === "cosmos-notebooks" && (!branch || branch === "master");
}
}

View File

@@ -10,27 +10,30 @@ const gitHubContentProvider = new GitHubContentProvider({
gitHubClient,
promptForCommitMsg: () => Promise.resolve("commit msg")
});
const gitHubCommit: IGitHubCommit = {
sha: "sha",
message: "message",
commitDate: "date"
};
const sampleFile: IGitHubFile = {
type: "file",
encoding: "encoding",
type: "blob",
size: 0,
name: "name.ipynb",
path: "dir/name.ipynb",
content: btoa(fixture),
content: fixture,
sha: "sha",
repo: {
owner: {
login: "login"
},
owner: "owner",
name: "repo",
private: false
},
branch: {
name: "branch"
}
},
commit: gitHubCommit
};
const sampleGitHubUri = GitHubUtils.toContentUri(
sampleFile.repo.owner.login,
sampleFile.repo.owner,
sampleFile.repo.name,
sampleFile.branch.name,
sampleFile.path
@@ -43,16 +46,9 @@ const sampleNotebookModel: IContent<"notebook"> = {
created: "",
last_modified: "date",
mimetype: "application/x-ipynb+json",
content: sampleFile.content ? JSON.parse(atob(sampleFile.content)) : null,
content: sampleFile.content ? JSON.parse(sampleFile.content) : null,
format: "json"
};
const gitHubCommit: IGitHubCommit = {
sha: "sha",
message: "message",
committer: {
date: "date"
}
};
describe("GitHubContentProvider remove", () => {
it("errors on invalid path", async () => {
@@ -125,9 +121,6 @@ describe("GitHubContentProvider get", () => {
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
);
spyOn(GitHubClient.prototype, "getCommitsAsync").and.returnValue(
Promise.resolve({ status: HttpStatusCodes.OK, data: [gitHubCommit] })
);
const response = await gitHubContentProvider.get(null, sampleGitHubUri, {}).toPromise();
expect(response).toBeDefined();
@@ -176,9 +169,6 @@ describe("GitHubContentProvider update", () => {
spyOn(GitHubClient.prototype, "renameFileAsync").and.returnValue(
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
);
spyOn(GitHubClient.prototype, "getCommitsAsync").and.returnValue(
Promise.resolve({ status: HttpStatusCodes.OK, data: [gitHubCommit] })
);
const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise();
expect(response).toBeDefined();
@@ -215,18 +205,14 @@ describe("GitHubContentProvider create", () => {
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(
Promise.resolve({ status: HttpStatusCodes.Created, data: gitHubCommit })
);
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
);
const response = await gitHubContentProvider.create(null, sampleGitHubUri, sampleNotebookModel).toPromise();
expect(response).toBeDefined();
expect(response.status).toBe(HttpStatusCodes.Created);
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
expect(gitHubClient.getContentsAsync).toBeCalled();
expect(response.response.type).toEqual(sampleNotebookModel.type);
expect(response.response.name).toEqual(sampleNotebookModel.name);
expect(response.response.path).toEqual(sampleNotebookModel.path);
expect(response.response.name).toBeDefined();
expect(response.response.path).toBeDefined();
expect(response.response.content).toBeUndefined();
});
});

View File

@@ -5,7 +5,7 @@ import { AjaxResponse } from "rxjs/ajax";
import { HttpStatusCodes } from "../Common/Constants";
import { Logger } from "../Common/Logger";
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
import { GitHubClient, IGitHubFile, IGitHubResponse, IGitHubCommit } from "./GitHubClient";
import { GitHubClient, IGitHubFile, IGitHubResponse, IGitHubCommit, IGitHubBranch } from "./GitHubClient";
import { GitHubUtils } from "../Utils/GitHubUtils";
import UrlUtility from "../Common/UrlUtility";
@@ -54,23 +54,14 @@ export class GitHubContentProvider implements IContentProvider {
throw new GitHubContentProviderError("Failed to get content", content.status);
}
const contentInfo = GitHubUtils.fromContentUri(uri);
const commitResponse = await this.params.gitHubClient.getCommitsAsync(
contentInfo.owner,
contentInfo.repo,
contentInfo.branch,
contentInfo.path,
1,
1
);
if (commitResponse.status !== HttpStatusCodes.OK) {
throw new GitHubContentProviderError("Failed to get commit", commitResponse.status);
if (!Array.isArray(content.data) && !content.data.content && params.content !== 0) {
const file = content.data;
file.content = (
await this.params.gitHubClient.getBlobAsync(file.repo.owner, file.repo.name, file.sha)
).data;
}
return this.createSuccessAjaxResponse(
HttpStatusCodes.OK,
this.createContentModel(uri, content.data, commitResponse.data[0], params)
);
return this.createSuccessAjaxResponse(HttpStatusCodes.OK, this.createContentModel(uri, content.data, params));
} catch (error) {
Logger.logError(error, "GitHubContentProvider/get", error.errno);
return this.createErrorAjaxResponse(error);
@@ -90,26 +81,26 @@ export class GitHubContentProvider implements IContentProvider {
const gitHubFile = content.data as IGitHubFile;
const commitMsg = await this.validateContentAndGetCommitMsg(content, "Rename", "Rename");
const newUri = model.path;
const newPath = GitHubUtils.fromContentUri(newUri).path;
const response = await this.params.gitHubClient.renameFileAsync(
gitHubFile.repo.owner.login,
gitHubFile.repo.owner,
gitHubFile.repo.name,
gitHubFile.branch.name,
commitMsg,
gitHubFile.path,
GitHubUtils.fromContentUri(newUri).path
newPath
);
if (response.status !== HttpStatusCodes.OK) {
throw new GitHubContentProviderError("Failed to rename", response.status);
}
const updatedContentResponse = await this.getContent(model.path);
if (updatedContentResponse.status !== HttpStatusCodes.OK) {
throw new GitHubContentProviderError("Failed to get content after renaming", updatedContentResponse.status);
}
gitHubFile.commit = response.data;
gitHubFile.path = newPath;
gitHubFile.name = NotebookUtil.getName(gitHubFile.path);
return this.createSuccessAjaxResponse(
HttpStatusCodes.OK,
this.createContentModel(newUri, updatedContentResponse.data, response.data, { content: 0 })
this.createContentModel(newUri, gitHubFile, { content: 0 })
);
} catch (error) {
Logger.logError(error, "GitHubContentProvider/update", error.errno);
@@ -169,14 +160,24 @@ export class GitHubContentProvider implements IContentProvider {
}
const newUri = GitHubUtils.toContentUri(contentInfo.owner, contentInfo.repo, contentInfo.branch, path);
const newContentResponse = await this.getContent(newUri);
if (newContentResponse.status !== HttpStatusCodes.OK) {
throw new GitHubContentProviderError("Failed to get content after creating", newContentResponse.status);
}
const newGitHubFile: IGitHubFile = {
type: "blob",
name: NotebookUtil.getName(newUri),
path,
repo: {
owner: contentInfo.owner,
name: contentInfo.repo,
private: undefined
},
branch: {
name: contentInfo.branch
},
commit: response.data
};
return this.createSuccessAjaxResponse(
HttpStatusCodes.Created,
this.createContentModel(newUri, newContentResponse.data, response.data, { content: 0 })
this.createContentModel(newUri, newGitHubFile, { content: 0 })
);
} catch (error) {
Logger.logError(error, "GitHubContentProvider/create", error.errno);
@@ -209,7 +210,7 @@ export class GitHubContentProvider implements IContentProvider {
const gitHubFile = content.data as IGitHubFile;
const response = await this.params.gitHubClient.createOrUpdateFileAsync(
gitHubFile.repo.owner.login,
gitHubFile.repo.owner,
gitHubFile.repo.name,
gitHubFile.branch.name,
gitHubFile.path,
@@ -221,14 +222,11 @@ export class GitHubContentProvider implements IContentProvider {
throw new GitHubContentProviderError("Failed to update", response.status);
}
const savedContentResponse = await this.getContent(uri);
if (savedContentResponse.status !== HttpStatusCodes.OK) {
throw new GitHubContentProviderError("Failed to get content after updating", savedContentResponse.status);
}
gitHubFile.commit = response.data;
return this.createSuccessAjaxResponse(
HttpStatusCodes.OK,
this.createContentModel(uri, savedContentResponse.data, response.data, { content: 0 })
this.createContentModel(uri, gitHubFile, { content: 0 })
);
} catch (error) {
Logger.logError(error, "GitHubContentProvider/update", error.errno);
@@ -283,7 +281,7 @@ export class GitHubContentProvider implements IContentProvider {
return commitMsg;
}
private getContent(uri: string): Promise<IGitHubResponse<IGitHubFile | IGitHubFile[]>> {
private async getContent(uri: string): Promise<IGitHubResponse<IGitHubFile | IGitHubFile[]>> {
const contentInfo = GitHubUtils.fromContentUri(uri);
if (contentInfo) {
const { owner, repo, branch, path } = contentInfo;
@@ -296,43 +294,37 @@ export class GitHubContentProvider implements IContentProvider {
private createContentModel(
uri: string,
content: IGitHubFile | IGitHubFile[],
commit: IGitHubCommit,
params: Partial<IGetParams>
): IContent<FileType> {
if (Array.isArray(content)) {
return this.createDirectoryModel(uri, content, commit);
return this.createDirectoryModel(uri, content);
}
if (content.type !== "file") {
return this.createDirectoryModel(uri, undefined, commit);
if (content.type === "tree") {
return this.createDirectoryModel(uri, undefined);
}
if (NotebookUtil.isNotebookFile(uri)) {
return this.createNotebookModel(content, commit, params);
return this.createNotebookModel(content, params);
}
return this.createFileModel(content, commit, params);
return this.createFileModel(content, params);
}
private createDirectoryModel(
uri: string,
gitHubFiles: IGitHubFile[] | undefined,
commit: IGitHubCommit
): IContent<"directory"> {
private createDirectoryModel(uri: string, gitHubFiles: IGitHubFile[] | undefined): IContent<"directory"> {
return {
name: GitHubUtils.fromContentUri(uri).path,
name: NotebookUtil.getName(uri),
path: uri,
type: "directory",
writable: true, // TODO: tamitta: we don't know this info here
created: "", // TODO: tamitta: we don't know this info here
last_modified: commit.committer.date,
last_modified: "", // TODO: tamitta: we don't know this info here
mimetype: undefined,
content: gitHubFiles?.map(
(file: IGitHubFile) =>
this.createContentModel(
GitHubUtils.toContentUri(file.repo.owner.login, file.repo.name, file.branch.name, file.path),
GitHubUtils.toContentUri(file.repo.owner, file.repo.name, file.branch.name, file.path),
file,
commit,
{
content: 0
}
@@ -342,17 +334,12 @@ export class GitHubContentProvider implements IContentProvider {
};
}
private createNotebookModel(
gitHubFile: IGitHubFile,
commit: IGitHubCommit,
params: Partial<IGetParams>
): IContent<"notebook"> {
const content: Notebook =
gitHubFile.content && params.content !== 0 ? JSON.parse(atob(gitHubFile.content)) : undefined;
private createNotebookModel(gitHubFile: IGitHubFile, params: Partial<IGetParams>): IContent<"notebook"> {
const content: Notebook = gitHubFile.content && params.content !== 0 ? JSON.parse(gitHubFile.content) : undefined;
return {
name: gitHubFile.name,
path: GitHubUtils.toContentUri(
gitHubFile.repo.owner.login,
gitHubFile.repo.owner,
gitHubFile.repo.name,
gitHubFile.branch.name,
gitHubFile.path
@@ -360,23 +347,19 @@ export class GitHubContentProvider implements IContentProvider {
type: "notebook",
writable: true, // TODO: tamitta: we don't know this info here
created: "", // TODO: tamitta: we don't know this info here
last_modified: commit.committer.date,
last_modified: gitHubFile.commit.commitDate,
mimetype: content ? "application/x-ipynb+json" : undefined,
content,
format: content ? "json" : undefined
};
}
private createFileModel(
gitHubFile: IGitHubFile,
commit: IGitHubCommit,
params: Partial<IGetParams>
): IContent<"file"> {
const content: string = gitHubFile.content && params.content !== 0 ? atob(gitHubFile.content) : undefined;
private createFileModel(gitHubFile: IGitHubFile, params: Partial<IGetParams>): IContent<"file"> {
const content: string = gitHubFile.content && params.content !== 0 ? gitHubFile.content : undefined;
return {
name: gitHubFile.name,
path: GitHubUtils.toContentUri(
gitHubFile.repo.owner.login,
gitHubFile.repo.owner,
gitHubFile.repo.name,
gitHubFile.branch.name,
gitHubFile.path
@@ -384,7 +367,7 @@ export class GitHubContentProvider implements IContentProvider {
type: "file",
writable: true, // TODO: tamitta: we don't know this info here
created: "", // TODO: tamitta: we don't know this info here
last_modified: commit.committer.date,
last_modified: gitHubFile.commit.commitDate,
mimetype: content ? "text/plain" : undefined,
content,
format: content ? "text" : undefined