{"id":16088548,"url":"https://github.com/krobelus/gitlab-offline-review","last_synced_at":"2025-04-05T16:12:03.355Z","repository":{"id":78525149,"uuid":"356085425","full_name":"krobelus/gitlab-offline-review","owner":"krobelus","description":"Read and write GitLab/GitHub review comments without leaving your editor ","archived":false,"fork":false,"pushed_at":"2023-01-11T18:13:01.000Z","size":322,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-11T12:45:23.109Z","etag":null,"topics":["code-review","gitlab"],"latest_commit_sha":null,"homepage":"https://gitlab.com/krobelus/gitlab-offline-review","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/krobelus.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-04-09T00:27:38.000Z","updated_at":"2023-02-13T10:17:39.000Z","dependencies_parsed_at":"2023-03-07T13:15:50.823Z","dependency_job_id":null,"html_url":"https://github.com/krobelus/gitlab-offline-review","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krobelus%2Fgitlab-offline-review","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krobelus%2Fgitlab-offline-review/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krobelus%2Fgitlab-offline-review/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krobelus%2Fgitlab-offline-review/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/krobelus","download_url":"https://codeload.github.com/krobelus/gitlab-offline-review/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247361700,"owners_count":20926643,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["code-review","gitlab"],"created_at":"2024-10-09T13:36:54.041Z","updated_at":"2025-04-05T16:12:03.336Z","avatar_url":"https://github.com/krobelus.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# gitlab-offline-review\n\nGitLab and GitHub have many advantages but their web-based code review UIs\nlack flexibility. They are not meant to be customized or integrated with other\ntools. Additionally, they can be real slow and refuse to handle large diffs.\nTo overcome those limitations, I prefer to use native applications like\nan editor or a Git frontend. By default, those apps don't know how to talk\nto GitLab.\n\n[git-bug](https://github.com/MichaelMure/git-bug) can be used to automate\ninteraction with various Git forges. However, it [doesn't support merge\nrequests yet](https://github.com/MichaelMure/git-bug/issues/17).\n\nSo I wrote my own tool `gl.py` to make it easy to integrate common GitLab\ntasks with my system. Features include:\n- View and post comments.\n- Comment on individual lines of MR diffs.\n- (Un)resolve discussions.\n- Submit all of the above in batch, so you can carefully compose your responses\n- Create issues/MRs, retry MR pipelines, etc..\n\nVarious [integrations](#integrations) allow to\n- Browse a commit diff in the frontend of your choice, and compose a review\n  comment for the line at the cursor.\n- Integrate review comments in your editor:\n  - Have all review comments in a flat file.\n  - Add your responses by editing that file.\n  - Jump to the related file of each comment.\n\n`gl.py` should be fairly easy to integrate in your favorite editor, or your\nGit frontend of choice - speak up if you see anything that can be improved.\n\n## Tutorial\n\n`gl.py` needs to be run inside a Git repository.  It assumes that the first\nremote is the GitLab host.\n\nThe first step is to [create your personal access token for the GitLab\nAPI](https://gitlab.com/-/profile/personal_access_tokens) (use `api` scope).\nThen add the following environment variables:\n\n```shell\n$ export GITLAB_USER=your-user-name-on-gitlab\n$ export GITLAB_TOKEN=your-secret-access-token\n```\n\nInstall the dependencies (Python 3.6 or later is required):\n\n```shell\npip3 install unidiff GitPython\n```\n\nInstall the `gl.py` script:\n\n```shell\n$ git clone https://gitlab.com/krobelus/gitlab-offline-review \u0026\u0026 cd gitlab-offline-review\n$ ln -s $PWD/gl.py ~/bin/\n```\n\nSince the repository is hosted on gitlab.com, here's how you\ncan draft a new comment for the designated [test issue number\n1](https://gitlab.com/krobelus/gitlab-offline-review/-/issues/1):\n\n```\n$ gl.py fetch 1\n$ echo \"my new comment on issue #1\" \u003e\u003e gl/i/1/comments.gl\n$ gl.py --dry-run submit 1\n```\n\nRunning without the `--dry-run` option would post the comment.  Either way,\nthe output shows that this command would perform two API requests:\n\n```\ncurl -HPRIVATE-TOKEN:\\ $GITLAB_TOKEN -HContent-Type:\\ application/json -XPOST 'https://gitlab.com/api/v4/projects/krobelus%2Fgitlab-offline-reviews/issues/1/discussions/' --data \"{\n  \\\"body\\\": \\\"my new comment on issue #1\\\"\n}\"\ncurl -HPRIVATE-TOKEN:\\ $GITLAB_TOKEN -XGET 'https://gitlab.com/api/v4/projects/krobelus%2Fgitlab-offline-reviews/issues/1/discussions?per_page=100'\n```\n\nFirst, this posts the new comment. Second, this downloads the issue's comments,\nupdating the files stored in `gl/i/1`.\n\n## Documentation\n\n### Issues\n\n`gl.py fetch` retrieves comments from GitLab and stores them as text files\nin a directory named `gl` at the root of your Git repository.\n\nComments can be fetched for issues and merge requests.  To facilitate\nintegration with editors and browsers, issues and merge requests can be\nspecified in a number of ways - each of these three commands fetches the\nsame issue number 123:\n\n```shell\n$ gl.py fetch https://gitlab.com/example/example/-/issues/123\n$ gl.py fetch 123\n$ gl.py fetch gl/i/123/comments.gl\n```\n\nNow the directory `gl/i/123` contains all data relevant to issue 123.\nThe file `gl/i/123/comments.gl`, contains the issue description and all\ncomments. It might look like this:\n\n```\nThe issue title\n\nThe issue description.\nMultiple lines here.\n\n𑁍 assignees: artour\n𑁍 milestone: yesteryear\n𑁍    labels: Bug\n\nec486c4d974e9342144a10c54a367aa74c709d18\n\t[artour] test comment please ignore\n\t[539329142] ok\n\n𑁍\n```\n\nObserve:\n- The first line contains the issue title; followed by a blank line and the\n  issue description. These are currently not editable.\n- The special marker `𑁍` at the start of the line marks metadata fields.\n  These are currently not editable.\n- An internal ID at the start of a line marks a thread.\n- The actual comments are always indented with a tab character.\n- Comments are prefixed  with the name of the author in square brackets. If\n  you are the author, then a number will appear instead. This is the ID of\n  this comment, which is used internally, so you can edit this comment.\n- To add a new comment to a thread, simply write your comment in a new\n  line (after the \"ok\"), just make sure the first line of the comment doesn't\n  start with a tab character ;)\n- To start a new thread, add some text after the final line containing only\n  the special marker `𑁍`.  You can copy this marker line to start several\n  new threads.\n\nAfter modifying the `*.gl` files you can run `gl.py submit issue-or-mr` to\nsend your comments to GitLab. Pass the same parameter you used for\n`fetch`.\n\nThis can submit these updates to GitLab:\n- Update your modified comments.\n- Add your new comments to existing threads.\n- Add your new threads.\n- Finally, fetch all new comments from GitLab.\n\nRemember, you can use the `--dry-run` option to first check what API requests\nwould be performed before blowing up your coworker's inbox.\n\n\n### Merge Requests (MRs)\n\nMRs are fairly similar to issues, with some differences explained here.\n\nAssuming MR 456 is based off branch `the-source-branch` in the first remote,\neach of these three commands fetches the same MR comments:\n\n```shell\n$ gl.py fetch https://gitlab.com/example/example/-/merge_requests/5\n$ gl.py fetch the-source-branch\n$ gl.py fetch gl/the-source-branch/todo.gl\n```\n\nAn MR is identified by its sourch-branch name, not the numeric ID.  This is\nbecause it's easier to remember (at least parts of) the branch name than a\nmeaningless ID.\n\nThe file structure looks a bit different from the an issues single `comments.gl` file:\n\n```shell\n$ tree gl/the-source-branch\ngl/\n└── the-source-branch\n    ├── meta.gl\n    ├── resolved.gl\n    └── todo.gl\n```\n\n- `meta.gl` contains the equivalent of the \"metadata\" part of an issue's `comments.gl`.\n  Hence this includes the MR title, the description and metadata like reviewers, lables, etc..\n- `todo.gl` contains all *unresolved* threads.\n- `resolved.gl` is just like `todo.gl` but contains all *resolved* threads.\n- `review.gl` is missing by default, but will hold your pending review\n  comments (see the following section).\n\nA single MR thread looks like this:\n\n```diff\nREADME.md:2: 36fec6809fa431d765cc1654a3e8c2d8d04b7cbc\n 476046058 The commit subject\n # My to-do list\n-* [ ] Publish `gl.py`\n+* [x] Publish `gl.py`\n\t[rreviewer] Some review comment 🐣\n```\n\nObserve:\n- The start of the thread is marked by a line starting with `\u003cfile\u003e:\u003cline\u003e \u003cthread ID\u003e`. \n- The next line shows the abbreviated commit SHA and subject\n- There are a few lines of diff context.\n- Finally, the thread's comments are shown.\n\nAdding comments works just like for issues.\nAlso, anywhere in the above thread, you can add lines that only\ncontain single letters `r` or `u` to resolve or unresolve the surrounding\nthread, respectively. These lines are otherwise ignored.\n\nRunning `gl.py submit \u003cbranch\u003e` will submit the same data as for issues.\nAdditionally it will resolve/unresolve threads as specified, and submit\nreview comments from `review.gl`.\n\n#### Drafting review comments\n\n`gl.py discuss` can be used to draft review comments on specific diff lines.\nIt currently takes a very specific set of positional parameters.  This is\nnot a problem when using the [Tig integration](#Tig), but maybe this could\nbe simplified.\n1. the source branch name.\n2. the commit SHA of the diff to comment on.\n3. the relative path of the old filename\n4. the relative path of the new filename\n5. the first character of the diff line\n   - `\"+\"` if the line is added\n   - `\"-\"` if the line deleted\n   - `\" \"` (a space) for context lines\n   You can also pass the entire line, only the first character is relevant.\n6. the line number in the old version of the file\n7. the line number in the new version of the file\n\nA command like `gl.py discuss --branch=\u003cbranch\u003e --commit=\u003ccommit\u003e \u003cfile\u003e + \u003cold_line\u003e \u003cnew_line\u003e`\nwill add an entry like this to a MR's `review.gl`:\n\n```diff\n𑁍 9d163415852de25b9c1f0706126c75d8ad8aef85 README.md:7 + 3\n+1\n+2\n+3\n+4\n+5\n```\n\n- The first line contains most of the parameters passed to `gl.py discuss`:\n  the commit SHA, filename, new  line number, and old line number of the\n  pending comment.\n- This is followed by some context lines. The last line (here `+5`) is the one\n  you are commenting on.\n\nAfter appending the above template to `review.gl`, `gl.py discuss` will invoke\n`$EDITOR +123123 gl/\u003cbranch\u003e/review.gl`, so your cursor should be placed\nright after the `+5` line. Simply add your comment here. It will be\nsent the next time you run `gl.py submit`.\n\n### Miscellanea\n\n- `gl.py fetchstatic` fetches users, milestone and label data. It may be necessary\n  to run this once. TODO: run it automatically.\n  - Set `GITLAB_GROUP` to your GitLab group to fetch group-scoped milestones and labels.\n- The `gl` file extension was chosen to simplify filetype detection in editors.\n- There are several other subcommands which are not covered here, see `gl.py --help`.\n\n### Resolving conflicts between local and new incoming comments\n\nWhenever you fetch the comments of an issue or MR, there is the potential\nfor a conflict between incoming comments, and comments you have not\nyet submitted. The diverging local and remote threads are merged using\n[`git merge-file`](https://git-scm.com/docs/git-merge-file). If there is\na merge conflict, please resolve them and remove conflict markers before\nfetching/submitting again. Currently there is no error when there is\na conflict - I always run `gl.py fetch` from my editor, with the file I\nedited already open, so I'd notice straight away.\n\n## Integrations\n\n### [Tig](https://jonas.github.io/tig/)\n\nBrowse MR commits by going to the `refs` view (shortcut `r`) and selecting\none of the MR branches (usually `origin/the-source-branch`).  When scrolling\nthrough a commit diff, you can add review comments with a binding like this\none (by typing `ac`):\n\n```\nbind generic ac !gl.py discuss --branch=%(branch) --commit=%(commit) -- %(file) %(text) %(lineno_old) %(lineno)\n```\n\nNowadays I tend to use a setup with two split windows: Tig plus an editor\nwindow.  This binding also adds a review comment, but does not replace Tig\nwith an editor.  Instead, it copies the path of the file, which you can then\nopen in the editor.\n\n```\nbind generic as @sh -c 'EDITOR=true gl discuss --branch=%(branch) --commit=%(commit) -- \"$1\" \"$2\" %(lineno_old) %(lineno); echo -n \"gl/%(branch)/review.gl\" | xclip' -- %(file) %(text)\n```\n\n### [Kakoune](https://kakoune.org)\n\nSome basic comfort features:\n\n```kak\nhook global BufCreate .*[.]gl %{\n\t# The \"file:line\" locations  are compatible with the \"grep\" filetype,\n\t# so we can use that to jump to the referenced line by pressing Enter.\n\tset-option buffer filetype grep\n\t# Highlight diff context lines.\n\tadd-highlighter buffer/gl-diff ref diff\n\t# GitLab comments tend to be long lines, soft-wrap them.\n\tadd-highlighter buffer/gl-wrap wrap -word -indent -marker \u003c\n}\n```\n\nSince we are already editing `*.gl` files in our editor, we should teach\nthe editor to run `gl.py fetch` and `gl.py submit` for the current file.\n\n```kak\ndefine-command -override gl-fetch -docstring %{\n\tFetch new comments for the current issue/MR\n} %{\n\tnop %sh{\n\t\tgl fetch \"$kak_buffile\" \u003e/dev/null 2\u003e\u00261 \u003c/dev/null \u0026\n\t}\n}\ndefine-command -override gl-submit -docstring %{\n\tSubmit comments for the current issue/MR\n} %{\n\twrite -sync\n\tnop %sh{\n\t\tgl submit \"$kak_buffile\" \u003e/dev/null 2\u003e\u00261 \u003c/dev/null \u0026\n\t}\n\t# The review draft will be deleted, so  switch to the unresolved threads.\n\tevaluate-commands %sh{\n\t\t[ ${kak_bufname##*/} = review.gl ] \u0026\u0026 echo edit \"${kak_bufname%/*}/\"todo.gl \n\t}\n}\n```\n\nGitLab email notifications and browser tabs both give you URLs to issues\nor MRs. Let's teach the editor to visit those links:\n\n```kak\ndefine-command -override gl-visit-url-from-clipboard -docstring %{\n\tRead a GitLab URL from system clipboard and visit the corresponding file.\n\tFetch the latest comments of this issue or MR in the background.\n} %{\n\tedit %sh{\n\t\tset -e\n\t\tpath=$(gl.py url2path \"$(xclip -out)\")\n\t\tprintf %s \"$path\"\n\t\t( gl.py fetch \"$path\" \u003c/dev/null \u003e/dev/null 2\u003e\u00261 ) \u0026\n\t}\n}\n```\n\nFinally, it can be convenient to quickly switch to the browser for some tasks:\n\n```kak\ndefine-command gl-browse-url -docstring %{\n\tOpen the current file's GitLab issue or MR page in the browser\n} %{ nop %sh{\n\txdg-open \"$(gl.py path2url \"$kak_buffile\")\"\n} }\n```\n\n## Running tests\n\n```sh\npytest gl.py\n```\n\n## Related Projects\n\n- [Bichon](https://gitlab.com/bichon-project/bichon)\n\nSince I wrote this, I discovered two commandline tools that can be used to\nbake a similar workflow:\n\n- [lab](https://github.com/zaquestion/lab)\n- [glab](https://github.com/profclems/glab)\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkrobelus%2Fgitlab-offline-review","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkrobelus%2Fgitlab-offline-review","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkrobelus%2Fgitlab-offline-review/lists"}