Skip to content

Commit 700fd08

Browse files
authored
feat: implement fork repository script (#3)
This Pull Request implements the `fork` script; this script is intended to be used to programmatically fork the main project repo to a user account; It houses a main function and other helper functions it uses to perform some necessary actions in order to ensure an efficient repo fork operation. ### Changes Made - Implemented the main `forkRepository` function within script; this function is the main exported function that performs the main fork operation; it accepts a `userOctokit` (a user authenticated object with permission to act on behalf of the user) instance and the project's repository details i.e. `repoDetails` object and it does the following... - It checks whether the project repository has already been forked to the user's account using the `isRepositoryForked` helper function; this returns the `fork` of `null` - If the repo has already been forked, then we perform a check whether the fork is up-to-date/in sync with main project repo using the `isRepositoryForkUpdated` helper function; this returns the `updatedSHA` and a boolean `isUpdated` property that confirm whether fork is up-to-date - If fork is not up-to-date; then we perform the update by bringing it in sync with the main project repo using the `updateRepositoryFork` helper function - If repo is up-to-date/in sync with main project repo; we cancel out of the operation at this point with an early return; - If the project repository is not forked onto the user's account; then we proceed to initiating a fork process by calling the `"POST /repos/{owner}/{repo}/forks"` endpoint using the `userOctokit` instance. **_(This starts the fork process, we do not know exactly when the process completes 🤔)_** - Implement the following helper functions consumed within the main `forkRepository` function and within other helper functions too - `updateRepositoryFork` - used to update (Sync) repository to state of main (head) repository - `isRepositoryForkUpdated` - used to check whether a fork is (in Sync with head repo) up-to-date with main repo - `getBranch` - used to fetch a Branch/Ref details - `isRepositoryForked` - used to check for the presence of a specific repo in a user's fork repo list - Added `getRepoParts` to `/lib/utils`; its a utility function that is used to resolve `repoOwner` and `repoName` from a repository fullname. ### Related Issue Resolves #2 ### Screencast/Screenshot https://github.com/babblebey/jargons.dev/assets/25631971/16221b7e-3c28-4c6c-a1f3-24d583ce7e3a 📖
1 parent 4ada796 commit 700fd08

3 files changed

Lines changed: 175 additions & 1 deletion

File tree

.env.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
GITHUB_APP_ID=
33
GITHUB_APP_CLIENT_ID=""
44
GITHUB_APP_CLIENT_SECRET=""
5+
# IMPORTANT: private keys must be in PKCS#8 format, see https://github.com/gr2m/universal-github-app-jwt/#about-private-key-formats
56
GITHUB_APP_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMII...und==\n-----END PRIVATE KEY-----\n"
67

7-
CRYPTO_SECRET_KEY="secret"
8+
CRYPTO_SECRET_KEY="secret"
9+
10+
PROJECT_REPO="babblebey/jargons.dev"
11+
PROJECT_REPO_BRANCH_REF="heads/main"

src/lib/fork.js

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { getRepoParts } from "./utils/index.js";
2+
3+
/**
4+
* Fork the project (specified) Repo to user account
5+
* @param {import("octokit").Octokit} userOctokit
6+
* @param {{ repoFullname: string, repoMainBranchRef: string }} repo
7+
*/
8+
export async function forkRepository(userOctokit, repoDetails) {
9+
const { repoFullname, repoMainBranchRef } = repoDetails;
10+
const { repoOwner, repoName } = getRepoParts(repoFullname);
11+
12+
try {
13+
const { data: user } = await userOctokit.request("GET /user");
14+
15+
const fork = await isRepositoryForked(userOctokit, repoFullname, user.login );
16+
17+
if (!!fork) {
18+
console.log("Repo is already forked!");
19+
20+
const { isUpdated, updateSHA } = await isRepositoryForkUpdated(userOctokit, repoDetails, fork)
21+
22+
if (!isUpdated) {
23+
console.log("Repo is outdated!");
24+
25+
await updateRepositoryFork(userOctokit, fork, {
26+
ref: repoMainBranchRef,
27+
sha: updateSHA
28+
});
29+
}
30+
31+
return;
32+
}
33+
34+
const response = await userOctokit.request("POST /repos/{owner}/{repo}/forks", {
35+
owner: repoOwner,
36+
repo: repoName,
37+
});
38+
39+
if (response.status === 202) {
40+
console.log("Forking process initiated successfully!");
41+
} else {
42+
console.log("Error occurred while forking repository.");
43+
}
44+
} catch (error) {
45+
console.log("Error occurred while forking repository:", error);
46+
}
47+
}
48+
49+
/**
50+
* Update (Sync) repository to state of main (head) repository
51+
* @param {import("octokit").Octokit} userOctokit
52+
* @param {string} fork
53+
* @param {{ ref: string, sha: string }} headRepoRef
54+
*/
55+
async function updateRepositoryFork(userOctokit, fork, headRepoRef) {
56+
const { repoOwner, repoName } = getRepoParts(fork);
57+
const { ref, sha } = headRepoRef;
58+
59+
try {
60+
await userOctokit.request("PATCH /repos/{owner}/{repo}/git/refs/{ref}", {
61+
owner: repoOwner,
62+
repo: repoName,
63+
ref, //-> `heads/${branchToSync}`
64+
sha
65+
});
66+
67+
console.log("Fork is now updated and in-sync with upstream");
68+
} catch (error) {
69+
console.error("Error syncing with upstream:", error.message);
70+
throw error;
71+
}
72+
}
73+
74+
/**
75+
* Check whether a fork is (in Sync with head repo) up-to-date with main repo
76+
* @param {import("octokit").Octokit} userOctokit
77+
* @param {{ repoFullname: string, repoMainBranchRef: string }} repoDetails
78+
* @param {string} fork
79+
* @returns {{ isUpdated: boolean, updateSHA: string }}
80+
*/
81+
async function isRepositoryForkUpdated(userOctokit, repoDetails, fork) {
82+
const { repoFullname, repoMainBranchRef } = repoDetails;
83+
84+
const userForkedBranch = await getBranch(userOctokit, fork, repoMainBranchRef);
85+
const projectBranch = await getBranch(userOctokit, repoFullname, repoMainBranchRef);
86+
87+
return {
88+
isUpdated: userForkedBranch.object.sha === projectBranch.object.sha,
89+
updateSHA: projectBranch.object.sha
90+
};
91+
}
92+
93+
/**
94+
* Get a Branch/Ref details
95+
* @param {import("octokit").Octokit} userOctokit
96+
* @param {string} repo
97+
* @param {string} ref
98+
* @returns Branch/Ref details
99+
*/
100+
async function getBranch(userOctokit, repo, ref) {
101+
const { repoOwner, repoName } = getRepoParts(repo);
102+
103+
const response = await userOctokit.request("GET /repos/{owner}/{repo}/git/ref/{ref}", {
104+
owner: repoOwner,
105+
repo: repoName,
106+
ref: ref,
107+
});
108+
109+
return response.data;
110+
}
111+
112+
/**
113+
* Check for the presence of a specific repo in a user's fork repo list
114+
* @param {import("octokit").Octokit} userOctokit
115+
* @param {string} repoFullname
116+
* @param {string} userLogin
117+
* @returns { string | null }
118+
*
119+
* @todo paginate response to get a list of all forks in one call
120+
*/
121+
async function isRepositoryForked(userOctokit, repoFullname, userLogin) {
122+
try {
123+
const response = await userOctokit.graphql(`#graphql
124+
query forks($login: String!) {
125+
user (login: $login) {
126+
repositories(first: 100, isFork: true) {
127+
nodes {
128+
name
129+
owner {
130+
login
131+
}
132+
parent {
133+
name
134+
owner {
135+
login
136+
}
137+
}
138+
}
139+
}
140+
}
141+
}
142+
`, {
143+
login: userLogin
144+
});
145+
146+
const { repoOwner, repoName } = getRepoParts(repoFullname);
147+
148+
const matchingFork = response.user.repositories.nodes.find((fork) => fork.parent && fork.parent.owner.login === repoOwner && fork.parent.name === repoName)
149+
?? null;
150+
151+
return matchingFork ? matchingFork.owner.login + "/" + matchingFork.name : null;
152+
} catch (error) {
153+
console.log("Error occurred while checking repo fork: ", error);
154+
}
155+
}

src/lib/utils/index.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,19 @@
66
export function resolveCookieExpiryDate(expireIn) {
77
const now = new Date();
88
return new Date(now.getTime() + expireIn * 1000);
9+
}
10+
11+
/**
12+
* Get repository Owner and Name from fullname
13+
* @param {string} repoFullname
14+
* @returns an object with `repoOwner` and `repoName`
15+
*/
16+
export function getRepoParts(repoFullname) {
17+
const parts = repoFullname.split("/");
18+
const [ repoOwner, repoName ] = [ parts[0], parts[1] ];
19+
20+
return {
21+
repoOwner,
22+
repoName
23+
}
924
}

0 commit comments

Comments
 (0)