Cómo clonar y sincronizar un Github Repo a través de Node.js

Cómo usar el comando git clone a través de child_process.execSync() en Node.js para clonar un repositorio de Github y sincronizar los últimos cambios programáticamente.

Primeros pasos

Debido a que el código que estamos escribiendo para este tutorial es "independiente" (lo que significa que no es parte de una aplicación o proyecto más grande), vamos a crear un proyecto de Node.js desde cero. Si aún no tiene Node.js instalado en su computadora, lea este tutorial primero y luego regrese aquí.

Una vez que haya instalado Node.js en su computadora, desde la carpeta de proyectos en su computadora (por ejemplo, ~/projects ), crea una nueva carpeta para nuestro trabajo:

Terminal

mkdir clone

A continuación, cd en ese directorio y crea un index.js archivo (aquí es donde escribiremos nuestro código para el tutorial):

Terminal

cd clone && touch index.js

A continuación, queremos instalar dos dependencias, dotenv y express :

Terminal

npm i dotenv express

El primero nos dará acceso al dotenv paquete que nos ayuda a establecer variables de entorno en Node.js process.env y el segundo, Express, se usará para activar un servidor de demostración.

Un último paso:en el package.json archivo que se creó para usted, asegúrese de agregar el campo "type": "module" como propiedad. Esto habilitará la compatibilidad con ESModules y nos permitirá usar el import declaraciones que se muestran en el siguiente código.

Con eso en su lugar, estamos listos para comenzar.

Obtener un token de acceso personal de Github

Antes de profundizar en el código, queremos obtener un token de acceso personal de Github. Esto nos permitirá clonar tanto público y repositorios privados usando el patrón que aprenderemos a continuación.

Si aún no tienes una cuenta de Github, puedes registrarte en este enlace. Si haces tiene una cuenta, asegúrese de haber iniciado sesión y luego haga clic en su avatar en la parte superior derecha de la navegación y en el menú que aparece, seleccione la opción "Configuración" cerca de la parte inferior del menú.

En la página siguiente, desde la barra de navegación de la izquierda, cerca de la parte inferior, seleccione la opción "Configuración de desarrollador". En la página siguiente, desde la barra de navegación de la izquierda, seleccione la opción "Tokens de acceso personal". Finalmente, desde la página resultante, haga clic en el botón "Generar nuevo token".

En la página siguiente, en el campo "Nota", asigne al token un nombre relativo a la aplicación que está creando (por ejemplo, "tutorial de repositorio de clonación" o "clonador de repositorio").

Para "Caducidad", establezca el valor que considere apropiado. Si solo está implementando este tutorial por diversión, es aconsejable establecerlo en el valor más bajo posible .

En "Seleccionar ámbitos", marque la casilla junto a "repositorio" para seleccionar todos los ámbitos relacionados con el repositorio. Estos "alcances" le dicen a Github a qué tiene acceso cuando usa este token. Solo es necesario "repo" para este tutorial, pero no dude en personalizar los alcances de su token para satisfacer las necesidades de su aplicación.

Finalmente, en la parte inferior de la pantalla, haga clic en el botón verde "Generar token".

Nota :presta atención aquí. Una vez que se genera su token, se mostrará temporalmente en un cuadro verde claro con un botón de copia al lado. Github no volverá a mostrarte este token . Se recomienda copiarlo y almacenarlo en un administrador de contraseñas con un nombre como "Token de acceso personal de Github " donde <note> debe ser reemplazado por el nombre que escribió en el campo "Nota" en la página anterior.

Una vez que haya almacenado su token de forma segura, estaremos listos para acceder al código.

Configuración de un archivo .env

Anteriormente, instalamos un paquete llamado dotenv . Este paquete está diseñado para ayudarlo a cargar variables de entorno en el process.env objeto en Node.js. Para hacerlo, dotenv le pide que proporcione un archivo .env en la raíz de su proyecto. Usando el token de acceso personal que acabamos de generar en Github, queremos crear este .env archivo en la raíz de nuestro proyecto y agregue lo siguiente:

.env

PERSONAL_ACCESS_TOKEN="<Paste Your Token Here>"

En este archivo, queremos agregar una sola línea PERSONAL_ACCESS_TOKEN="" , pegando el token que obtuvimos de Github entre comillas dobles. A continuación, queremos abrir el index.js archivo en la raíz de nuestro proyecto y agregue lo siguiente:

/index.js

import 'dotenv/config';

Nota :esto debe estar en la parte superior de nuestro archivo. Cuando se ejecute este código, llamará al config() función en el dotenv paquete que localizará el .env archivo que acabamos de crear y cargar su contenido en process.env . Una vez que esto esté completo, podemos esperar tener un valor como process.env.PERSONAL_ACCESS_TOKEN disponible en nuestra aplicación.

Eso es todo por ahora. Usaremos este valor más adelante. A continuación, aún en el index.js archivo, queremos configurar el esqueleto para un servidor Express.js.

Configuración de un servidor Express y una ruta

Para activar una clonación de un repositorio, ahora queremos configurar un servidor Express.js con una ruta que podamos visitar en un navegador, especificando el nombre de usuario de Github, el repositorio y (opcionalmente) el nombre de la rama que queremos clonar. .

/index.js

import 'dotenv/config';
import express from "express";

const app = express();

app.get('/repos/clone/:username/:repo', (req, res) => {
  // We'll handle the clone here...
});

app.listen(3000, () => {
  console.log('App running at http://localhost:3000');
});

Directamente debajo de nuestro import 'dotenv/config'; línea, a continuación, queremos importar express del express paquete que instalamos anteriormente. Justo debajo de esto, queremos crear una instancia de servidor Express llamando al express() exportado función y almacenar la instancia resultante en una variable app .

app representa nuestra instancia de servidor Express. En él, queremos llamar a dos métodos:.get() y .listen() . El .get() El método nos permite definir una ruta que especifica un patrón de URL junto con una función de controlador que se llamará cuando la URL de una solicitud a nuestro servidor coincide ese patrón.

Aquí, llamamos app.get() pasando ese patrón de URL como una cadena /repos/clone/:username/:repo , donde :username y :repo son lo que se conoce como parámetros de ruta. Estas son "variables" en nuestra URL y nos permiten reutilizar el mismo patrón de URL esperando diferentes entradas.

Por ejemplo, esta ruta será accesible como /repos/clone/cheatcode/joystick o /repos/clone/motdotla/dotenv o incluso /repos/clone/microsoft/vscode . En ese último ejemplo, microsoft sería reconocido como el username y vscode sería reconocido como el repo .

Antes de escribir el código para clonar nuestro repositorio dentro de la función de controlador asignada como segundo argumento a app.get() , en la parte inferior de nuestro archivo, queremos asegurarnos de iniciar nuestro servidor Express.js, dándole un número de puerto para que se ejecute. Para hacerlo llamamos a app.listen() , pasando el número de puerto que queremos usar como primer argumento. Como segundo argumento, pasamos una función de devolución de llamada para que se active después de que se haya iniciado el servidor (agregamos un console.log() para enviarnos una señal de inicio en nuestro terminal).

/index.js

import 'dotenv/config';
import express from "express";
import fs from 'fs';
import cloneAndPullRepo from './cloneAndPullRepo.js';

const app = express();

app.get('/repos/clone/:username/:repo', (req, res) => {
  const username = req?.params?.username;
  const repo = req?.params?.repo;
  const repoPath = `${username}/${repo}`;
  const repoExists = fs.existsSync(`repos/${repoPath}`);
  const confirmation = repoExists ? `Pulling ${repoPath}...` : `Cloning ${repoPath}...`;

  cloneAndPullRepo(repoExists, username, repo, req?.query?.branch);
  
  res.status(200).send(confirmation);
});

app.listen(3000, () => {
  console.log('App running at http://localhost:3000');
});

Para trabajar en nuestra implementación real, queremos centrar nuestra atención justo dentro de la función del controlador que se pasa como segundo argumento a app.get() .

Aquí, estamos organizando la información que necesitaremos para realizar nuestra clonación. De nuestros parámetros de ruta (aquí, "parámetros"), queremos obtener el username y repo partes de nuestra URL. Para hacerlo basta con acceder al req.params objeto proporcionado automáticamente por Express. Esperamos req.params.username y req.params.repo para ser definido porque podemos ver esos parámetros declarados en nuestra URL (cualquier cosa prefijada con un : los dos puntos en nuestra URL se capturan como un parámetro).

Aquí almacenamos el username y repo de req.params en variables del mismo nombre. Con estos, a continuación, configuramos el repoPath que es una combinación del username y repo , separados por un / barra diagonal (imitando una URL que visitarías en Github).

Con esta información, a continuación, comprobamos si ya existe una carpeta en el repos carpeta en la que pretendemos almacenar todos los repositorios en la raíz de nuestro proyecto (esto no existe, pero Git lo creará automáticamente la primera vez que clonemos un repositorio).

En la siguiente línea, si lo hace existe, queremos devolver la señal a la solicitud que estamos retirando el repositorio (es decir, extraer los últimos cambios) y si no existe, queremos señalar que lo estamos clonando por primera vez. Almacenamos la cadena que describe cualquiera de los escenarios en una variable confirmation .

Podemos ver que este confirmation la variable se envía de vuelta a la solicitud original a través del res objeto que nos ha dado Express. Aquí, decimos "establezca el código de estado HTTP en 200 (éxito) y luego envíe el confirmation cadena de vuelta como el cuerpo de la respuesta".

Justo encima de esto, la parte que nos importa, llamamos a una función inexistente cloneAndPullRepo() que tomará las variables que acabamos de definir y clonará un nuevo repositorio o extraerá cambios para uno existente. Observe que pasamos nuestro repoExists predefinido , username y repo variables como los primeros tres argumentos, pero hemos agregado uno adicional al final.

Opcionalmente, queremos permitir que nuestros usuarios extraigan una rama específica para su repositorio. Porque esto es opcional (lo que significa que puede existir o no), queremos admitir esto como una consulta parámetro. Esto es diferente de un parámetro de ruta en que no dictar si la ruta coincide o no una dirección URL Simplemente se agrega al final de la URL como metadatos (por ejemplo, /repos/clone/cheatcode/joystick?branch=development ).

Sin embargo, al igual que los parámetros de ruta, Express también analiza estos parámetros de consulta para nosotros, almacenándolos en el req.query objeto. Al anticipado cloneAndPullRepo() función, pasamos req.query.branch como argumento final.

Con todo eso en su lugar, ahora, pasemos al paso de clonación y extracción. Queremos crear un archivo en la ruta que anticipamos cerca de la parte superior de nuestro archivo cloneAndPullRepo.js .

Conectando una función para clonar y extraer

Ahora, en un archivo nuevo, queremos conectar una función responsable de realizar la clonación o extracción de nuestro repositorio.

/cloneAndPullRepo.js

import child_process from 'child_process';

export default (repoExists = false, username = '', repo = '', branch = 'master') => {
  if (!repoExists) {
    child_process.execSync(`git clone https://${username}:${process.env.PERSONAL_ACCESS_TOKEN}@github.com/${username}/${repo}.git repos/${username}/${repo}`);
  } else {
    child_process.execSync(`cd repos/${username}/${repo} && git pull origin ${branch} --rebase`);
  }
}

Debido a que el código es limitado, hemos agregado aquí la fuente completa del archivo. Pasemos a través de él.

Primero, en la parte inferior de nuestro archivo, queremos crear una exportación predeterminada de una función (esta es la que anticipamos que existía en index.js ). Esa función debería tener en cuenta si el repoExists , el username del repositorio que queremos clonar (o extraer), y el nombre del repo queremos clonar, y potencialmente un branch .

Para cada argumento, establecemos un valor predeterminado, siendo los dos importantes repoExists que está configurado de forma predeterminada en false y branch que por defecto está establecido en master .

Mirando el código:reconociendo la importación de child_process arriba desde el child_process integrado de Node.js paquete de forma pasiva:si repoExists es falso , queremos llamar al child_process.execSync() función que nos permite ejecutar comandos relativos a nuestro sistema operativo (como si estuviéramos en una ventana de terminal) desde Node.js.

Aquí, execSync implica que estamos usando el síncrono versión del child_process.exec() función. Esto se hace intencionalmente para garantizar que el clon funcione para nuestro ejemplo, sin embargo, es posible que desee utilizar el .exec() asíncrono en su lugar para que, cuando se llame, el código no bloquee Node.js mientras se ejecuta.

Centrarse en qué pasamos a .execSync() , pasamos un comando largo usando la interpolación de cadenas de JavaScript para incrustar nuestras variables en el git clone comando que queremos ejecutar:

`git clone https://${username}:${process.env.PERSONAL_ACCESS_TOKEN}@github.com/${username}/${repo}.git repos/${username}/${repo}`

La mayor parte de esto debería explicarse por sí mismo, sin embargo, queremos llamar la atención sobre el process.env.PERSONAL_ACCESS_TOKEN parte. Este es el valor que establecimos anteriormente a través del dotenv paquete y nuestro .env expediente. Aquí, la pasamos como la contraseña que queremos para autenticar nuestro git clone solicitud con (Github reconocerá este token de acceso gracias a su ghp_ prefijado identidad y asociarla a nuestra cuenta).

Como ejemplo, asumiendo que visitamos la URL http://localhost:3000/repos/clone/cheatcode/joystick en nuestro navegador, esperaríamos que el código anterior generara una cadena como esta:

git clone https://cheatcode:[email protected]/cheatcode/joystick.git repos/cheatcode/joystick

Lo que esta línea ahora dice es "queremos clonar el cheatcode/joystick repositorio usando el nombre de usuario cheatcode con la contraseña ghp_xxx en el repos/cheatcode/joystick carpeta en nuestra aplicación."

Cuando esto se ejecute, Git notará que repos la carpeta aún no existe y créela, junto con una carpeta para nuestro nombre de usuario cheatcode y luego dentro de eso , una carpeta con nuestro repo nombre (donde se clonará el código de nuestro proyecto).

/cloneAndPullRepo.js

import child_process from 'child_process';

export default (repoExists = false, username = '', repo = '', branch = 'master') => {
  if (!repoExists) {
    child_process.execSync(`git clone https://${username}:${process.env.PERSONAL_ACCESS_TOKEN}@github.com/${username}/${repo}.git repos/${username}/${repo}`);
  } else {
    child_process.execSync(`cd repos/${username}/${repo} && git pull origin ${branch} --rebase`);
  }
}

Centrándonos en la segunda parte de la función, si repoExists es true , queremos recurrir al else declaración, nuevamente usando .execSync() , sin embargo, esta vez ejecutando dos comandos:cd para "cambiar directorios" al repos/username/repo existente carpeta y luego git pull origin ${branch} --rebase para extraer los últimos cambios para el branch especificado (ya sea el predeterminado master o lo que se haya pasado como parámetro de consulta a nuestra URL).

Eso es todo. Con todo esto en su lugar, ahora, si iniciamos nuestra aplicación y pasamos el nombre de usuario y el nombre del repositorio de un repositorio Github existente en nuestra URL (ya sea uno que sea público o, si es privado, uno al que tengamos acceso), nosotros debería activar el cloneAndPullRepo() función y ver el repositorio descargado en nuestro proyecto.

Terminando

En este tutorial aprendimos cómo clonar un repositorio de Github usando Node.js. Aprendimos a configurar un servidor Express.js, junto con una ruta en la que podíamos llamar a una función que clonaba un nuevo repositorio o extraía uno existente. Para hacer esa clonación o extracción, aprendimos a usar el child_process.execSync() función.