DEV Community

vavilov2212
vavilov2212

Posted on

Nextjs and git

How to implement working with git in react web application

This article is an attempt to classify knowledge about using git in a nodejs environment. Particularly, this is going to tell about isomorphic-git library and how to implement it in web applications.

Source code for this example can be found here.

Prerequisites

  • nodejs 16.14.0 - this is the one i worked on, but it certainly should work in any version higher, than that
  • next.js - version 9 and higher (we need api routes feature)

Getting into development

First, let’s create blank nextjs application using create-next-app, just so we can skip all the boilerplate.

npx create-next-app@latest
Enter fullscreen mode Exit fullscreen mode

Now, let’s edit /pages/index.js. Let's remove default nextjs stuff, and instead put our own markup. Checkout source code for dedicated styles.

Our page will look like this

isomorphic-git example clone page

Clone from git

The clone function is a fetch request to our own api route that is /pages/api/clone.js in which we actually going to request git.

const [response, setResponse] = useState([]);
const [repoUrl, setRepoUrl] = useState('');

const simpleClone = async () => {
  const cloneRespone = await fetch(
    'api/clone',
      {
        method: 'POST',
        body: JSON.stringify({ repoUrl })
      }
  )
    .then(res => res.json());

    setResponse(cloneRespone);
};
Enter fullscreen mode Exit fullscreen mode

Nothing fancy so far. The input changes the repoUrl state variable, which is then put to the request body.

Let’s take a look at our /api/clone route.

import path from 'path';
import git from 'isomorphic-git';
import http from 'isomorphic-git/http/node';
import fs from 'fs';

export default async function handler(req, res) {
  const { repoUrl } = JSON.parse(req.body);

  // Make sure it's clean every time
  if (fs.existsSync('./test-clone')) {
    try {
      fs.rmSync('./test-clone', { recursive: true });
    } catch(e) {
      console.log('fs.rmSync error', e);
    }
  }

  const dir = path.join(process.cwd(), 'test-clone');

  try {
    await git.clone({
      fs,
      http,
      onMessage: console.log,
      onProgress: console.log,
      dir,
      url: repoUrl,
   })
    .then(console.log)
    .catch(console.log);
  } catch(e) {
    console.log('git.clone error', e);
  }

  // Just for sake of readability
  const recursiveDirStruct = (distPath) => {
    return fs.readdirSync(distPath)
      .filter(function (file) {
        if (file === '.git') return false;
        return fs.statSync(distPath + '/' + file).isDirectory();
      })
      .reduce(function(all, subDir) {
        return [...all, ...fs.readdirSync(distPath + '/' + subDir).map(e => subDir + '/' + e)]
      }, []);
  };

  res.status(200).json(recursiveDirStruct('./test-clone'));
};
Enter fullscreen mode Exit fullscreen mode

According to nextjs documentation, all files in the pages/api folder must export a request handler function instead of a React Component.

It first parses the incoming body parameters and makes shure to delete the folder, to which we will clone, if it was previously created.

Next, we call the clone function. When addressing the isomorphic-git object, it expects you to pass in two main things, on which it relies:

  • a way to interact with filesystem
  • and a way to make internal network requests

Look into the isomorphic-git documentation to find out, that it contains network modules on it’s own, both for nodejs and client-side use.

As for filesystem, the built-in nodejs fs module will totally do, but you are not restricted to it.

The recursiveDirStruct function just puts all filepaths from the cloned repository into an array, so that it can be conveniently mapped on our frontend.

Finally, let’s test it. Type in the repository address and hit clone. Don’t forget, that the repository must be public for this example, otherwise we’d have to have some authentication.

I cloned my knowledge base repo and this is what i got

isomorphic-git example clone result

Add and delete files

Allright, we have cloned the repo in nodejs environment. But can we make changes to it? To do this, lets’s add ability to delete and add files.

First lets add new page pages/change.js and copy contents of the index file. Now lets add two things:

  • a little trash icon to every line in the list of files, that we fetched from git server.
  • A textarea, where we can type in contents of a new file, that we want to create and add to the repository.

I also added some styles, just to make it look better. This is what it looks like

isomorphic-git example add/delete file page

For each of the input and textarea there are dedicated useState variables, that are used to make requests to the server. I’ve also added two request functions. We pass just filepath if we want to delete file, and both filepath and filecontent if we want to add a new file.

const [filepath, setAddFilepath] = useState('');
const [filecontent, setFilecontent] = useState('');

const simpleDelete = async (filepath) => {
  await fetch(
    'api/update',
    {
      method: 'POST',
      body: JSON.stringify({ filepath }) 
    }
  )
    .then(response => response.json())
    .catch(e => console.log('error', e));
  };

  const simpleAdd = async () => {
    await fetch(
      'api/update',
      {
        method: 'POST',
        body: JSON.stringify({
          filepath,
          filecontent
        }) 
      }
    )
      .then(response => response.json())
      .catch(e => console.log('error', e));
  };
Enter fullscreen mode Exit fullscreen mode

Now let’s take a look at the api/update. The concept is the same, so i won’t include the full code here, you can check it out in the source code.

  ...

  if (fs.existsSync(dir)) {
    try {
      if (filecontent) {
        fs.appendFileSync(file, filecontent);
        await git.add({ fs, dir: './test-clone', filepath  })
      } else {
        await git.remove({ fs, dir: './test-clone', filepath })
      }
    } catch(e) {
      console.log('git.add error', e);
    }
  }

  await git.commit({
    fs,
    dir,
    message,
    author: {
      name: 'Athors name',
      email: 'Authors email',
    },
  })
    .then(console.log)
    .catch(console.log);

  ...
Enter fullscreen mode Exit fullscreen mode

First we check the incoming request parameters and if filecontent is present, we assume it’s an addition, otherwise the filepath is removed from repo. If it seems a bit clunky, remember, that this just a simple example, not enterprise software.

Next step, we subsequently call commit and push commands, just like you would do in a terminal. Since we need to be authorized to push to remote git server, isomorphic-git api has a dedicated onAuth callback. User credentials may also be required to clone from a private repository.

try{
  await git.push({
    fs,
    dir,
    http,
    onProgress: console.log,
    onMessage: console.log,
    remote: 'origin',
    ref: 'master',
    onAuth: () => ({
      username: 'YOUR_USERNAME',
      password: 'YOUR_PASSWORD_OR_PERSONAL_ACCESS_TOKEN'
    }),
  })
    .then(() => res.status(200).json({ success: true }))

  } catch(e) {
    console.log('push error', e);
    res.status(400).json({ error: e.message });
  }
Enter fullscreen mode Exit fullscreen mode

For the sake of simplicity, i decided not to make input fields for auth credentials, for the reason, that i’d need to include all the other stuff too (name, email, username, branchname etc.). So just go ahead and change these values in the source code before running it.

If you're using github, you will likely use your personal access token, because github doesn’t support regular passwords anymore.

Now let’s run our program and see what happens. I simply typed in some random path and content and hit Add. After that, clone again to see, that the file was actually added and there you go.

isomorphic-git example add/delete file result

Ofcourse, git in front-end applications can do much more, so explore the capabilities of isomorphic-git and thank you for reading.

Top comments (1)

Collapse
 
saadbazaz profile image
Saad A. Bazaz

This is a good attempt, and possibly one of the few online, but it seems very painful; adding separate "wrapper" api routes for each and every function of git is going to get cumbersome, quite fast.

I think this could be done better by either:
Method 1: Doing everything purely on the browser with isomorphic-git + using Next API as a proxy to prevent CORS
Method 2: Use Server-side Components (?) to handle all the git work.

Interested to hear your thoughts.