본문 바로가기

Github

[GIST] gist 생성 후 clone 까지 자동화 코드 만들기

네이버 부스트캠프에서 gist를 생성한 후에 나의 디렉토리로 clone하여 미션을 매일 수행해야한다. 그러기 위해서 gist 접속 -> secret gist 생성 -> README.md 생성 -> 나의 디렉토리로 git clone 까지 해야한다. 매일 이런 반복 작업을 하기가 귀찮아서 JS 공부도 할겸 자동화 코드를 만들기로 했다.

필요한 라이브러리

import readline from "readline";
import { Octokit } from "@octokit/rest";
import { exec } from "child_process";
import fs from "fs";

이전에는 항상 require("readline") 모듈을 가져왔었다. 하지만 이번에 사용할 모듈인 "@octokit/rest" 와 "child_process" 모듈이 ES 모듈 형식으로 되어있기 때문에 require를 사용할 수 없다. 이걸 해결하기 위해 node_moudels 디렉토리가 있는 프로젝트 루트에 pakage.json파일에 "type" : "module" 을 추가해줘야 한다. (다른 방법으로는 파일 확장자를 .mjs 로 바꾸는 방법이 있다.)

라이브러리 설명을 조금 하자면

  • "@octokit/rest": github API와 상호작용하기 위한 라이브러리
  • "child_process": 외부 셀 명령을 실행하기 위한 모듈
  • "fs": 파일 시스템 작업을 위한 모듈
  • "readline": 데이터를 읽기 위해 인터페이스를 제공하는 모듈

그리고 import를 할 때 {} 를 사용하여 가져오는 경우가 보인다. {} 를 사용하는 경우는 사용하는 모듈에서 특정 내보낸(exported) 부분만 가져온다. 즉, { exec } 는 child_process 에서 내보내는 여러 객체 또는 함수 중 하나이다.

파일명 input 입력 받기

readline 라이브러리를 사용하여 clone할 파일명을 콘솔창에서 입력으로 받는 방법이다.

const rl = readline.createInterface({
    input:process.stdin,
    output:process.stdout
});

let file;

rl.on("line", (line) =>{
    console.log("write like day00")
    file = line;
    rl.close();
    }
).on("close", () => {
    cloneGist(file);
      // process.exit();
})

동작 순서는 node main.js 로 파일을 실행하면 "write like day00" 문구가 보이고 만들고자 하는 파일명을 입력하면 된다. "line"과 "close"는 rl.on에서 쓰이는 이벤트이다.

  • line : 입력받은 값을 한 줄씩 읽어 문자열 타입으로 전달하는 이벤트
  • close : 더 이상 입력값이 없을 경우에 해당하는 이벤트
    그리고 각각의 이벤트와 같이 전달되는 콜백함수들의 용도는 다음과 같다.
  • rl.close() : 인터페이스를 종료하여 무한히 입력받음을 방지.
  • process.exit() : 프로세스를 종료
    하지만 이 코드에서는 process.exit()를 사용하지 않는다. 이유는 뒤에서 설명할 것이다.

function cloneGist

const token = "개인 토큰";

async function cloneGist(fileName) {
    const octokit = new Octokit({
        auth: token
    });

    try {

        const response = await octokit.request('POST /gists', {
            description: 'description 내용',
            public: false,
            files: {
                'README.md': {
                    content: 'readme 내용'
                }
            },
            headers: {
                'X-GitHub-Api-Version': '2022-11-28'
            }
        });

        const gistUrl = response.data.git_pull_url;
        console.log('Gist created:', gistUrl);

        // Create a directory for the Gist
        const dir = `./${fileName}`;
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir);
        }

        // Clone the Gist
        exec(`git clone ${gistUrl} ${dir}`, (err, stdout, stderr) => {
            if (err) {
                console.error('Error cloning gist:', err);
                return;
            }
            console.log('Gist cloned successfully:', stdout);
        });
    } catch (error) {
        console.error('Error creating gist:', error);
    }
}

github에서 token을 받고 이 코드에 사용해주면 된다.

const response = await octokit.request('POST /gists', {
  description: 'description 내용',
  public: false,
  files: {
    'README.md': {
      content: 'readme 내용'
    }
  },
  headers: {
    'X-GitHub-Api-Version': '2022-11-28'
  }
});

그리고 이 부분은 github docs에서 제공해주는 API 사용 방법에 나와 있는 내용을 가져온 것이다. https://docs.github.com/ko/rest/gists/gists?apiVersion=2022-11-28#create-a-gist
이 코드에서 secret gist가 생성이 되고 해당 정보를 response에 담는다.

const gistUrl = response.data.git_pull_url;
exec(`git clone ${gistUrl} ${dir}`, (err, stdout, stderr) => {
  if (err) {
    console.error('Error cloning gist:', err);
    return;
  }
  console.log('Gist cloned successfully:', stdout);
});

response에서 data.git_pull_url을 사용하여 방금 생성한 secrit gist http 주소를 가져오고 exec 를 사용하여 console에 자동 입력되게 만든다. 그러면 방금 생성한 gist를 나의 디렉토리에 바로 clone할 수 있다.

왜 process.exit() 를 사용하지 않는가.

파일을 실행해보면서 가장 헤맸던 부분이다. 분명 코드에는 문제가 없는 것 같은데 파일을 실행하고 input을 넣어도 cloneGist 함수를 읽지 못하고 끝나버린다. 이유는 비동기함수이기 때문이다. async function 안에 awiat 로 생성된 비동기 함수가 완료되기 전에 process.exit()가 파일 실행을 종료시키기 때문이다. 그래서 process.exit()를 사용하지 않아도 되고, then() 을 사용하여 비동기 함수가 끝난 이후에 process.exit()가 실행되게 만드는 방법도 있다.

최종 코드

import readline from "readline";
import { Octokit } from "@octokit/rest";
import { exec } from "child_process";
import fs from "fs";

const rl = readline.createInterface({
    input:process.stdin,
    output:process.stdout
});

const token = "your_token";
let file;

rl.on("line", (line) =>{
    console.log("write like day00")
    file = line;
    rl.close();
    }
).on("close", () => {
    cloneGist(file);
})

async function cloneGist(fileName) {
    const octokit = new Octokit({
        auth: token
    });

    try {

        const response = await octokit.request('POST /gists', {
            description: "description 내용",
            public: false,
            files: {
                'README.md': {
                    content: 'readme 내용'
                }
            },
            headers: {
                'X-GitHub-Api-Version': '2022-11-28'
            }
        });

        const gistUrl = response.data.git_pull_url;
        console.log('Gist created:', gistUrl);

        // Create a directory for the Gist
        const dir = `./${fileName}`;
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir);
        }

        // Clone the Gist
        exec(`git clone ${gistUrl} ${dir}`, (err, stdout, stderr) => {
            if (err) {
                console.error('Error cloning gist:', err);
                return;
            }
            console.log('Gist cloned successfully:', stdout);
        });
    } catch (error) {
        console.error('Error creating gist:', error);
    }
}