DEV Community

Uday Rana
Uday Rana

Posted on

Learning How git merge Works

This week in the Open Source Development course I'm taking, we dove deep into branch merges in git. We learned about two types of merges - fast-forward and three-way recursive merges. We learned how to work on multiple branches in parallel, and how to handle merge conflicts.

We were asked to add two new features to the projects each of us are working on - mine is a CLI tool called codeshift which translates source code files to a chosen programming language.

GitHub logo uday-rana / codeshift

A command-line tool that translates source code files into a chosen programming language.

codeshift

Codeshift is a command-line tool to translate and transform source code files between programming languages.

codeshift tool demo: translating an express.js server to rust

Features

  • Select output language to convert source code into
  • Support for multiple input files
  • Output results to a file or stream directly to stdout
  • Customize model and provider selection for optimal performance
  • Supports leading AI providers

Requirements

  • Node.js (Requires Node.js 20.17.0+)
  • An API key from any of the following providers:
    • OpenAI
    • OpenRouter
    • Groq
    • any other AI provider compatible with OpenAI's chat completions API endpoint

Installation

  • Clone the repository with Git:

    git clone https://github.com/uday-rana/codeshift.git
    Enter fullscreen mode Exit fullscreen mode
    • Alternatively, download the repository as a .zip from the GitHub page and extract it
  • In the repository's root directory (where package.json is located), run npm install:

    cd codeshift/
    npm install
    Enter fullscreen mode Exit fullscreen mode
  • To be able to run the program without prefixing node, run npm install -g . or npm link within the project directory:

    npm install -g 
    …
    Enter fullscreen mode Exit fullscreen mode

The features I chose to add were adding support for multiple AI providers, and proper error handling.

Add support for the providers OpenAI and OpenRouter #27

Is your feature request related to a problem? Please describe. Provider support is limited to Groq.

Describe the solution you'd like Add support for OpenAI and OpenRouter by:

  • Switching to the OpenAI library, which is compatible with Groq, OpenAI, and OpenRouter.
  • Allowing users to choose their provider by specifying the API base URL using the options --base-url or -u, or through an environment variable.

If no provider is specified, a default should be set.

Describe alternatives you've considered N/A

Additional context N/A

Handle uncaught exceptions #34

Is your feature request related to a problem? Please describe. Multiple uncaught exceptions:

  • All async function calls in src/index.js;
  • Missing environment variable throws Error

Describe the solution you'd like Catch the exceptions, print them to stderr, and exit with an error code.

Describe alternatives you've considered N/A

Additional context N/A

We were asked to created issues for each feature on GitHub, but to create branches for them locally instead of on GitHub. It seems to me that the goal was to practice merging fundamentals without GitHub getting involved.

Branch 1: Adding support for multiple AI providers

In order to add support for multiple AI providers, I had to switch from the Groq client library to the OpenAI client library for Node.js, which works with all of the AI providers I wanted to add support for. This on its own was not a big change, since Groq's library closely mimics that of OpenAI. I just had to update some comments and variable/function names to reflect the change. Groq is unique in that when streaming a response, it puts the token usage information inside an x_groq object. To support other providers without breaking support for Groq, I had to update token counting to check for how OpenAI and most other AI providers supply token information. With that done, everything was working well.

The problem was I ended up making more changes to this feature's branch than necessary and added an extra feature. Supporting multiple providers meant that each one would only work when passed models it supported. The least intrusive way to address this would have been to hard code the models for each provider. But I got this mixed up with another issue I'd opened on GitHub for a feature to let users pick the model, which I'd opened so I could later work on the feature to support running multiple models of the user's choice at once. So, I ended up adding model selection in this branch, even though it was beyond the scope of the issue. If I really wanted to, it would've been best to add that feature before creating the branches for these new features.

I also wanted to set a default provider if the user didn't supply the base URL, but this wouldn't really be possible since the user needs to supply the API key. One way I thought of getting around this was to parse the API key and choose the provider based on how it's formatted, but this seemed like kind of a shaky implementation, since if any of the providers were to change how their keys are structured, it would break, so I didn't go through with it.

Branch 2: Proper error handling and graceful shutdown

The second feature I originally wanted to work on was adding support for using multiple models of the user's choice at once, but after finishing up my first feature (supporting multiple AI providers), I realized it would require working on the same lines of code and changing functionality that the other branch had newly implemented. On that branch, I'd added user model selection, but only for a single model. I'd have to re-implement it on this branch except with support for multiple models. Rewriting code just to modify it slightly didn't make sense to me, so I went with a different feature. I'm sure there was a way to pull the changes in from the other branch (probably merge it into this one or cherry-pick the commits), but that would've further consolidated the changes that are supposed to be isolated, and I also didn't want to deviate from the assignment.

The Big Merge

Having made my changes on both branches, it was time to merge them both to main.

Merging the first branch went without a hitch. Since there hadn't been any changes to main since the branch was created (i.e. since the branches' common ancestor), they had a linear history, and git performed a fast-forward merge, where it simply pulls the HEAD on the main branch forward to where it is on the incoming branch. The resulting commit history looks as if all of the changes were made on the main branch, and there is no additional commit created when merging.

This was the last commit in my first branch (Forgive the ugly hyperlink, dev.to doesn't support embedding commits):
https://github.com/uday-rana/codeshift/commit/8c0288e25cd0ea103d6a437898f539d14bf84b1e

Merging the second branch was slightly more complicated. The second branch was created at the same time as the first, from the same common ancestor. This meant that since the first branch was merged into main, there were now changes on main that weren't on the second branch - their history had diverged.

When both branches have commits that aren't on the other, git does what's called a recursive three-way merge. It identifies the common ancestor of the two branches, which is the commit they split at. It performs a three-way comparison between the current branch, incoming branch, and the common ancestor. It then attempts to automatically merge the changes. Sometimes, there are conflicting changes which need to be manually handled, since git can't automatically determine which changes need to be kept. This is called a merge conflict. Finally, git creates a new commit combining the changes from the current and incoming branch.

In my case, I ended up with a merge conflict. Since I ended up making large, sweeping changes in my first branch, I was worried this would be where things go horribly wrong. Much to my surprise, it went rather smoothly. I took things slowly and used the Visual Studio Code merge editor. This helped a ton, since it provides options to choose the order you want the changes to be accepted in. I tried seeing what happened based on which branch's changes I accepted first, going hunk by hunk, and when everything looked good, I accepted the changes.

After merging, Git created a new commit which you can see here:
https://github.com/uday-rana/codeshift/commit/9dd8cab7c1b6c730b569b7ab7ead1f93288cc5a1

I tested my code before pushing to the remote repository, and much to my delight, everything appeared to be working perfectly, but I still had a few changes to make after merging. I had to add error handling for when the new BASE_URL environment variable, introduced in the other branch, was missing. In the first branch, I chose to stick with how I'd previously been handling errors for the sake of consistency, which was just letting the exception go uncaught (Hey, it prints the error to the console anyway, right?). Apart from that, I had inconsistent indentation from the merge, forgot to remove the Groq client from the list of dependencies, and accidentally left in a debugging console.error(). After that, all I had to do was update the documentation and everything was good.

Lessons Learned

I learned a lot about merging and handling merge conflicts. I used to be terrified of merge conflicts, but now I feel much more confident in my ability to resolve them.

There is one thing I'd like to do differently next time. I want to resist the urge to make more changes than necessary to resolve an issue. Stripping the changes down to the bare minimum required makes dealing with filed issues and merging much simpler.

That's it for this week. I also made my first open-source contribution, which I'll talk about another time. Next week, we'll be starting Hacktoberfest, which I'm looking forward to.

Thanks for reading!

Top comments (0)