Skip to content

Commit 0845346

Browse files
committed
Add the code
1 parent 1f7748e commit 0845346

13 files changed

Lines changed: 1036 additions & 1 deletion

.editorconfig

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
root = true
2+
3+
[*]
4+
indent_style = tab
5+
end_of_line = lf
6+
charset = utf-8
7+
trim_trailing_whitespace = false
8+
insert_final_newline = false
9+
indent_size = 4
10+
11+
[*.{config,yml,json}]
12+
indent_style = space
13+
indent_size = 2
14+
insert_final_newline = true
15+
trim_trailing_whitespace = true
16+
17+
[*.{js,jsx,ts,tsx,cs,html,cshtml,py,md,ahk}]
18+
insert_final_newline = true
19+
trim_trailing_whitespace = true
20+
21+
[*.ahk]
22+
end_of_line = crlf
23+
24+
[cli.js]
25+
end_of_line = lf
26+
27+
# Ignore paths
28+
[**/jspm_packages/**,**/node_modules/**,*.d.ts]
29+
charset = none
30+
end_of_line = none
31+
insert_final_newline = none
32+
trim_trailing_whitespace = none
33+
indent_style = none
34+
indent_size = none

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
dist
3+
data
4+
.vscode/launch.json

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"typescript.tsdk": "./node_modules/typescript/lib"
3+
}

README.md

Lines changed: 399 additions & 1 deletion
Large diffs are not rendered by default.

package.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"name": "sync-github-forks-cli",
3+
"version": "0.1.0",
4+
"description": "Keeps github forks up to date using node, the github api and git",
5+
"main": "dist/application.js",
6+
"bin": {
7+
"sync-github-forks": "dist/cli.js"
8+
},
9+
"scripts": {
10+
"postinstall": "tsc",
11+
"start": "tsc && node dist/cli.js",
12+
"test": "npm run lint",
13+
"lint": "tslint -c ./tslint.json src/**"
14+
},
15+
"files": [
16+
"dist",
17+
"src",
18+
"test"
19+
],
20+
"repository": {
21+
"type": "git",
22+
"url": "git+https://github.com/oledid/sync-github-forks-cli.git"
23+
},
24+
"keywords": [
25+
"sync",
26+
"github",
27+
"forks",
28+
"node",
29+
"api",
30+
"git",
31+
"fork",
32+
"typescript"
33+
],
34+
"author": "Ole Morten Didriksen <code@oledid.com>",
35+
"license": "MIT",
36+
"bugs": {
37+
"url": "https://github.com/oledid/sync-github-forks-cli/issues"
38+
},
39+
"homepage": "https://github.com/oledid/sync-github-forks-cli#readme",
40+
"devDependencies": {
41+
"@types/bluebird": "^3.0.37",
42+
"@types/node": "^7.0.4",
43+
"tslint": "^4.4.2",
44+
"typescript": "^2.1.5"
45+
},
46+
"dependencies": {
47+
"bluebird": "^3.4.7",
48+
"execa": "^0.6.0"
49+
}
50+
}

src/application.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import * as Promise from "bluebird";
2+
import { Options } from "./options";
3+
import { GitHubService, RepoResult, DetailedRepoResult } from "./gitHubService";
4+
import { GitService } from "./gitService";
5+
import { Logger } from "./logger";
6+
7+
export class Application {
8+
constructor(
9+
public options: Options,
10+
public logger: Logger,
11+
public githubService: GitHubService = new GitHubService(options, logger),
12+
public gitService: GitService = new GitService(options, logger),
13+
public maxGitHubConcurrency: number = 3,
14+
public maxGitConcurrency: number = Infinity
15+
) { }
16+
17+
main() {
18+
if (this.options.isValid() === false) {
19+
throw new Error("Invalid options");
20+
}
21+
22+
this.start();
23+
}
24+
25+
start() {
26+
this.getForkedRepositories()
27+
.then(repos => this.getRepositoryDetails(repos))
28+
.then(values => this.syncRepositories(values))
29+
.then(() => {
30+
this.logger.log("Finished", null);
31+
this.logger.flush();
32+
})
33+
.catch(err => {
34+
throw Error(err);
35+
});
36+
}
37+
38+
getForkedRepositories(page: number = 1, repositories: Array<RepoResult> = new Array<RepoResult>()) {
39+
this.logger.log("Looking for forked repositories at page " + page, null);
40+
return this.githubService.getRepos(this.options.username, page)
41+
.then(repos => {
42+
if (repos.length === 0) {
43+
return new Promise(resolve => {
44+
resolve(repositories);
45+
});
46+
}
47+
for (const repo of repos) {
48+
if (repo.fork === true) {
49+
repositories.push(repo);
50+
}
51+
}
52+
return this.getForkedRepositories(page + 1, repositories);
53+
});
54+
}
55+
56+
getRepositoryDetails(repos: Array<RepoResult>) {
57+
this.logger.log("Found " + repos.length + " forks. Fetching details...", null);
58+
const queue = new Array<any>();
59+
const results = new Array<DetailedRepoResult>();
60+
for (const repo of repos) {
61+
queue.push(new Promise((innerResolve, innerReject) => {
62+
this.logger.log("Fetching details for repository", repo.full_name);
63+
this.githubService.getRepo(this.options.username, repo.name)
64+
.then(repoResult => {
65+
innerResolve(repoResult);
66+
})
67+
.catch(err => {
68+
innerReject(err);
69+
});
70+
}));
71+
}
72+
return Promise.map(queue, (item: DetailedRepoResult) => {
73+
return item;
74+
}, { concurrency: this.maxGitHubConcurrency });
75+
}
76+
77+
syncRepositories(repos: Array<DetailedRepoResult>) {
78+
const queue = new Array<any>();
79+
const self = this;
80+
for (const value of repos) {
81+
queue.push(new Promise((resolve, reject) => {
82+
self.syncRepository(value)
83+
.then(() => {
84+
resolve();
85+
})
86+
.catch(err => {
87+
reject(err);
88+
});
89+
}));
90+
}
91+
return Promise.map(queue, (item: DetailedRepoResult) => {
92+
return item;
93+
}, { concurrency: this.maxGitConcurrency });
94+
}
95+
96+
syncRepository(repo: DetailedRepoResult) {
97+
return new Promise((resolve, reject) => {
98+
this.cloneRepository(repo.name, repo.full_name, repo.clone_url)
99+
.then(() => {
100+
return this.gitService.setUpstream(repo.name, repo.full_name, repo.parent.clone_url);
101+
})
102+
.then(() => {
103+
return this.gitService.pullUpstream(repo.name, repo.full_name, repo.default_branch);
104+
})
105+
.then(() => {
106+
return this.gitService.pushOrigin(repo.name, repo.full_name, repo.default_branch);
107+
})
108+
.then(() => {
109+
return this.trySyncMasterBranch(repo.name, repo.full_name, repo.default_branch);
110+
})
111+
.then(() => {
112+
return this.gitService.syncTags(repo.name, repo.full_name);
113+
})
114+
.then(() => {
115+
resolve();
116+
})
117+
.catch(err => {
118+
reject(err);
119+
});
120+
});
121+
}
122+
123+
cloneRepository(name: string, fullname: string, cloneUrl: string) {
124+
this.logger.log("Cloning repository " + cloneUrl, fullname);
125+
return new Promise((resolve, reject) => {
126+
this.gitService.clone(name, cloneUrl)
127+
.then(out => {
128+
resolve();
129+
})
130+
.catch(err => {
131+
if (err.toString().match(/destination path (.*) already exists and is not an empty directory/).length > 0) {
132+
this.logger.log("Repository folder already existed. Continuing.", fullname);
133+
resolve();
134+
}
135+
else {
136+
reject(err);
137+
}
138+
});
139+
});
140+
}
141+
142+
trySyncMasterBranch(name: string, fullname: string, branch: string) {
143+
return new Promise((resolve, reject) => {
144+
if (branch === "master") {
145+
resolve();
146+
return;
147+
}
148+
this.logger.log("Trying to sync master branch", fullname);
149+
this.gitService.pullUpstream(name, fullname, "master")
150+
.then(() => {
151+
return this.gitService.pushOrigin(name, fullname, "master");
152+
})
153+
.then(() => {
154+
this.logger.log("Master branch synced", fullname);
155+
resolve();
156+
})
157+
.catch(err => {
158+
this.logger.log("Failed syncing master branch for repository", fullname);
159+
resolve();
160+
});
161+
});
162+
}
163+
}

src/cli.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env node
2+
3+
import { Application } from "./application";
4+
import { CliOptions, Options } from "./options";
5+
import { Logger } from "./logger";
6+
7+
const options: Options = new CliOptions();
8+
const logger = new Logger(options);
9+
10+
if (options.isValid() === false) {
11+
logger.log(`Usage: ${options.appName} [github-username] [path-to-work-directory] [github-api-key]`, null);
12+
process.exit(1);
13+
}
14+
15+
const application = new Application(options, logger);
16+
try {
17+
application.main();
18+
}
19+
catch (err) {
20+
logger.error(err);
21+
logger.flush();
22+
process.exit(1);
23+
}

src/gitHubService.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as https from "https";
2+
import { Options } from "./options";
3+
import { Logger } from "./logger";
4+
5+
export class GitHubService {
6+
constructor(
7+
public options: Options,
8+
public logger: Logger
9+
) { }
10+
11+
getRepos(username: string, pageNumber: number) {
12+
return new Promise<Array<RepoResult>>((resolve, reject) => {
13+
const url = `/users/${username}/repos?page=${pageNumber}&per_page=30`;
14+
this.call(url, "GET", {}, result => { resolve(JSON.parse(result)); }, reject);
15+
});
16+
}
17+
18+
getRepo(username: string, name: string) {
19+
return new Promise<DetailedRepoResult>((resolve, reject) => {
20+
const url = `/repos/${username}/${name}`;
21+
this.call(url, "GET", {}, result => { resolve(JSON.parse(result)); }, reject);
22+
});
23+
}
24+
25+
private call(url, method, parameterData, resolve, reject) {
26+
const options = {
27+
host: "api.github.com",
28+
port: 443,
29+
method: method,
30+
path: url,
31+
headers: {
32+
"User-Agent": `${this.options.appName}/${this.options.version} (+${this.options.homepage})`,
33+
"Authorization": `token ${this.options.apiToken}`,
34+
"Content-Type": "application/json",
35+
"Accept": "application/vnd.github.v3+json"
36+
}
37+
};
38+
39+
let output = "";
40+
41+
const req = https.request(options, res => {
42+
res.setEncoding("utf8");
43+
44+
res.on("data", data => {
45+
output += data;
46+
});
47+
48+
res.on("end", () => {
49+
resolve(output);
50+
});
51+
});
52+
53+
req.on("error", err => {
54+
reject(err);
55+
});
56+
57+
req.end();
58+
};
59+
}
60+
61+
export class RepoResult {
62+
/* tslint:disable:variable-name */
63+
id: number;
64+
name: string;
65+
full_name: string;
66+
fork: boolean;
67+
clone_url: string;
68+
/* tslint:enable:variable-name */
69+
}
70+
71+
export class DetailedRepoResult extends RepoResult {
72+
/* tslint:disable:variable-name */
73+
parent: RepoResult;
74+
default_branch: string;
75+
/* tslint:enable:variable-name */
76+
}

0 commit comments

Comments
 (0)