Technical | 2021-04-11

Frictionless Code Review Workflow Using the GitLab API

Work smart - take your tools with you!

Like many software development teams, we use GitLab to manage our product’s lifecycle, from milestone planning to CI/CD. We heavily rely on code review to catch potential problems before they can affect our customers.

To make changes to a software project on GitLab, a developer needs to create a merge request (MR). Other developers can comment on this merge request to ask questions, request changes and finally, approve of these changes.

As developers discuss proposed changes, they often need to read related code. For example, if a change adds a function call, the reviewer might want to look up the definition of that function. In an integrated development environment (IDE), developers can navigate to a function’s definition at the press of a button1.

Why the Gitlab Code Review Workflow Can Be a Chore

As a result, many developers constantly need to switch focus between their IDE and GitLab. Consider the scenario where a developer wants to process incoming review comments, as displayed on the merge request page on GitLab. For each comment, they might need to visit the corresponding location in their IDE, either to address the comment by changing the source code, or to look up additional information. To do so, they need to do the following for every comment:

  1. Spot the filename and line number that the comment refers to.
  2. Visit the corresponding file and line in their IDE.
  3. Navigate related code and make changes if applicable.
  4. Go back to GitLab to resolve the comment.

Steps 1, 2 and 4 require the developer’s attention and induce avoidable context switches.

Eliminate Context Switches in Your Code Review Workflow

We claim that review comments need not interrupt a developer’s workflow but can assist them, in a similar fashion that an IDE highlights compiler errors2. Compiler errors are well-integrated in any modern IDE - the developer can simply click on an error message to navigate to the supposed source of the problem.

Meanwhile, there is a lack of IDE-integration for review comments3. To fix that, we render merge request comments to a single text file, which can be viewed in any IDE. Developers can write comments by simply editing that file. A small program submits those comments, and downloads new ones by talking to the GitLab API.

How to Streamline Your Code Review Workflow Today

The following example demonstrates how to discuss merge requests using our approach. If you use GitLab, you can try it yourself on your current MR.

Let’s say we want to make a small change to the software that guards our country’s borders. This diff shows the gist of the change:

--- a/src/main.c
+++ b/src/main.c
@@ -3154,4 +3154,6 @@ void check_border() {
src/main.c:3154

     while (sleep(3)) {
+        if (UNLIKELY(nation_is_under_attack = true))
+            launch_missiles();

After having submitted an MR with that change, we will use GitLab’s Merge Request Discussions API to read and write any review comments. The first step is to create a personal access token to authenticate interactions with the API. If you use gitlab.com, you can do this here. To check if your access token works, run the command below. Make sure to replace johndoe%2Fexample with your project’s ID, and replace 123 with your merge request’s ID.

$ GITLAB_TOKEN=your-access-token
$ curl -H"PRIVATE-TOKEN: $GITLAB_TOKEN" \
'https://gitlab.com/api/v4/projects/johndoe%2Fexample/merge_requests/123/discussions'

We get a JSON response like the one below. Please note that only fields that are relevant to us are shown here.

[
  {
     /* This object represents a single thread.
      * It can contain multiple comments, called "notes". */
     "notes": [
        {
          "id": 149778,
          "body": "Why are we using UNLIKELY here? What does it do? 🤔",
          "author": {
            "name": "Robert Reviewer",
            "username": "rreviewer"
          },
          "position": {
            "new_path": "src/main.c",
            "position_type": "text",
            "new_line": 3154
          },
          "resolved": false
        },
        {
          /* more comments in this thread here */
        }
     ]
  }
  /* more threads may follow here */
]

Looks like we already got some review comments. You can download gl.py 4 and use the following command to fetch comments for merge request number 123:

$ GITLAB_USER=johndoe GITLAB_TOKEN=your-access-token gl.py fetch 123

This will create a file gl/<source-branch>/todo.gl5 containing the two unresolved threads from this MR:

	src/main.c:3154
	+        if (UNLIKELY(nation_is_under_attack = true))
	+            launch_missiles();
	[rreviewer] Why are we using UNLIKELY here? What does it do? 🤔

	CHANGELOG:5
	+ Can now defend nations against they're enemies.
	[rreviewer] typo: they're -> their

Now let’s say we want to react to these two comments like this:

  1. Reply to the first thread, to explain the intentions behind the change. To be able to write a convincing answer, we might want to delve into the discussed change, by visiting the file src/main.c at line 31546 and looking up the definition of UNLIKELY.

  2. Resolve the second thread, after having applied the suggestion.

This should be as easy as possible for the developer who is viewing this file in their IDE. Now what’s more obvious than just adding replies directly inside this file? To allow this, we define a simple rule: all lines that are added by our script are indented with a single Tab character. Other lines are interpreted as replies to the surrounding thread.

After we add our comments, todo.gl looks like this:

	src/main.c:3154
	+        if (UNLIKELY(nation_is_under_attack = true))
	+            launch_missiles();
	[rreviewer] Why are we using UNLIKELY here? What does it do? 🤔

I'm glad you asked 😉 - adding "UNLIKELY" around an expression is just
an optimization that does not change the behavior of the code. It only
tells the compiler that this condition is usually false. This allows us
to secure our borders more efficiently!

This is how UNLIKELY is defined 🤓:

#define UNLIKELY(x) __builtin_expect((x), false)

	CHANGELOG:5
	+ Can now defend nations against they're enemies.
	[rreviewer] typo: they're -> their
Fixed, thanks!
r

The last line containing only r means to resolve the second thread. It is not included in the comment.

Use this command to submit both comments and resolve the second thread:

$ GITLAB_USER=johndoe GITLAB_TOKEN=your-access-token gl.py submit 123

Brilliant - we have finished our code review duties, without visiting GitLab once!

Conclusion

We have shown how you can communicate over GitLab from within the comfort of your IDE. This avoids needless context switches and gives you a consistent and efficient environment for both writing code and discussing code.

At Symflower we are creating a product that uses mathematical models to find software bugs. In our example above, the reviewer missed a crucial bug, but Symflower would have found it. Symflower can be integrated in your GitLab review workflow, so you’ll never miss such bugs again. If this sparks your interest, consider subscribing to our newsletter!


  1. Sourcegraph brings code navigation to GitLab but they have yet to see mainstream adoption. ↩︎

  2. We think that review comments should be treated with the same rigor as compiler errors: for the same reason that you wouldn’t commit code that doesn’t satisfy the compiler, you wouldn’t merge changes that your coworkers don’t agree with. ↩︎

  3. GitLab is working on a VSCode plugin that can display merge request comments in widgets directly next to the discussed line. Unfortunately, this plugin only works in VSCode, not in other IDEs. Additionally, while it does support replying to existing comments, as of March 2021 the plugin does not support adding new review comments. It also doesn’t allow to search past messages. ↩︎

  4. Please note that gl.py is a tool with a very specific use case - let us know if you can think of improvements. Alternatively you can always write your own script, it’s fairly easy. ↩︎

  5. Usually, the Git source branch uniquely identifies an MR, so we chose it over an opaque ID in the filename. ↩︎

  6. In some IDEs you can click on a <file>:<line> snippet to navigate to the referenced line. This is perfect for the second comment - we just need to fix a typo and go back. ↩︎