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 <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.