# forgelink

[](https://crates.io/crates/forgelink-cli)
[](https://github.com/dpassen/forgelink/actions/workflows/test.yaml)
[](https://github.com/dpassen/forgelink/actions/workflows/lint.yaml)
[](https://github.com/dpassen/forgelink/actions/workflows/format.yaml)
Generate shareable URLs to files and line ranges in hosted git repositories,
from the command line.
Inspired by the Emacs package [git-link](https://github.com/sshaw/git-link).
## Supported Forges
- [GitHub](https://github.com)
- [GitLab](https://gitlab.com)
- [SourceHut](https://sourcehut.org)
- [Bitbucket](https://bitbucket.org)
- [Codeberg](https://codeberg.org) (including [forge.fedoraproject.org](https://forge.fedoraproject.org))
- [Tangled](https://tangled.org)
- [Azure DevOps](https://azure.microsoft.com/products/devops)
## Installation
```sh
cargo install forgelink-cli
```
This installs a binary named `forgelink`.
## Usage
Use `print` to generate a stable URL pinned to the current commit SHA:
```sh
$ forgelink print src/main.rs
https://github.com/user/repo/blob/abc123def.../src/main.rs
```
Append `:N` for a single line or `:N-M` for a range:
```sh
$ forgelink print src/main.rs:42
https://github.com/user/repo/blob/abc123def.../src/main.rs#L42
$ forgelink print src/main.rs:42-55
https://github.com/user/repo/blob/abc123def.../src/main.rs#L42-L55
```
Target a remote other than `origin` with `--remote`:
```sh
$ forgelink print --remote upstream src/main.rs
https://github.com/upstream-owner/repo/blob/abc123def.../src/main.rs
```
Use the current branch name instead of the commit SHA with `--branch`:
```sh
$ forgelink print --branch src/main.rs
https://github.com/user/repo/blob/main/src/main.rs
```
`--branch` requires HEAD to be on a named branch. It errors on a detached
HEAD, which includes Jujutsu (`jj`) working copies. Use the default
commit-pinned link in that case.
Copy the URL to the clipboard with `copy`:
```sh
forgelink copy src/main.rs
```
Open the URL in your default browser with `open`:
```sh
forgelink open src/main.rs
```
`copy` and `open` do not print the URL. Use `print` when you want stdout.
When available, all subcommands support the same `--remote` and `--branch` options.
Clipboard and browser support are default-on cargo features (`clipboard` and
`browser`). Build with `--no-default-features` to drop the `arboard` and `open`
dependencies, which also removes the `copy` and `open` subcommands. On Linux
under X11 the clipboard is owned by the running process, so the copied URL may
not persist after forgelink exits. macOS and Windows are unaffected.
It works from any subdirectory inside the repository:
```sh
$ cd src && forgelink print main.rs
https://github.com/user/repo/blob/abc123def.../src/main.rs
```
You can also pass an absolute path to link to a file in any repository,
regardless of your current directory:
```sh
$ forgelink print ~/Developer/other-repo/src/main.rs
https://github.com/user/other-repo/blob/abc123def.../src/main.rs
```
## Host configuration
forgelink applies Git's `url..insteadOf` rules to the remote URL before
detecting the forge. Aliases rewritten to a supported forge do not need a
`host` entry. Add one for other SSH aliases and forges on custom domains.
```toml
# git@gh-work:owner/repo.git
[[hosts]]
host = "gh-work"
base-url = "https://github.com"
forge = "github"
# git@git.company.tld:group/repo.git
[[hosts]]
host = "git.company.tld"
base-url = "https://git.company.tld"
forge = "gitlab"
```
Host entries require all three fields:
- `host` is the hostname after Git URL rewrites are applied.
- `base-url` is the HTTP or HTTPS URL used to build links. It may include a
custom port or path prefix, but not credentials, a query, or a fragment.
- `forge` is one of `github`, `gitlab`, `sourcehut`, `bitbucket`, `codeberg`,
`tangled`, or `azure-devops`.
Host matching is exact and case-insensitive. A matching entry skips automatic
forge detection. Wildcards and chained mappings are not supported.
Use `--config` to pick a file:
```sh
forgelink --config path/to/config.toml print src/main.rs
```
Otherwise, forgelink checks these locations in order:
1. `$XDG_CONFIG_HOME/forgelink/config.toml`, when `XDG_CONFIG_HOME` is an
absolute path.
2. The native platform configuration directory:
- Linux: `~/.config/forgelink/config.toml`
- macOS: `~/Library/Application Support/forgelink/config.toml`
- Windows: `%APPDATA%\forgelink\config.toml`
forgelink uses the first applicable location in the order shown. It does not
merge configuration files. A missing default file is ignored, while a missing
file passed with `--config` is an error. forgelink does not read
`~/.ssh/config`; add each SSH alias as a `host` entry.
## Editor integration
The file argument accepts `path:line` syntax, so any editor that can run a shell
command with the current buffer path and cursor line can bind it with no plugin
required. Use `copy` to copy directly, or `print` to capture stdout yourself.
### Helix
In `~/.config/helix/config.toml`:
```toml
[keys.normal.space.u]
c = ':sh forgelink copy "%{buffer_name}:%{selection_line_start}-%{selection_line_end}"'
o = ':sh forgelink open "%{buffer_name}:%{selection_line_start}-%{selection_line_end}"'
p = ':sh forgelink print "%{buffer_name}:%{selection_line_start}-%{selection_line_end}"'
[keys.select.space.u]
c = ':sh forgelink copy "%{buffer_name}:%{selection_line_start}-%{selection_line_end}"'
o = ':sh forgelink open "%{buffer_name}:%{selection_line_start}-%{selection_line_end}"'
p = ':sh forgelink print "%{buffer_name}:%{selection_line_start}-%{selection_line_end}"'
```
### Kakoune
In `~/.config/kak/kakrc`:
```kak
declare-user-mode forgelink
map global user u ':enter-user-mode forgelink' -docstring "forgelink"
map global forgelink c ':forgelink copy' -docstring "copy link"
map global forgelink o ':forgelink open' -docstring "open link"
map global forgelink p ':forgelink print' -docstring "print link"
define-command forgelink -params 1 -hidden %{
echo %sh{
anchor=${kak_selection_desc%%.*}
cursor=${kak_selection_desc#*,}
cursor=${cursor%%.*}
if [ "$anchor" -gt "$cursor" ]; then
start=$cursor end=$anchor
else
start=$anchor end=$cursor
fi
forgelink "$1" "$kak_buffile:$start-$end" 2>&1
}
}
```
### Neovim
The following is a Lua implementation for using forgelink in Neovim. It copies
a link to the file path in normal mode and a link to the line(s) selected in
visual mode to the system clipboard.
```lua
if vim.fn.exepath('forgelink') ~= '' then
local remotes = { "upstream", "private", "origin" }
-- Tries executing forgelink across remotes until one succeeds.
local function copy_forge_url(target)
for _, remote in ipairs(remotes) do
local output = vim.fn.system({ 'forgelink', 'print', '--remote', remote, target })
if vim.v.shell_error == 0 then
vim.fn.setreg('+', vim.trim(output))
vim.notify("Copied forge URL (" .. remote .. "): \n" .. vim.trim(output), vim.log.levels.INFO)
return
end
end
vim.notify("Failed to generate forge URL for any candidate branch", vim.log.levels.ERROR)
end
-- Normal mode: file URL
vim.keymap.set('n', 'cf', function()
local curFile = vim.api.nvim_buf_get_name(0)
copy_forge_url(curFile)
end, { desc = "Copy URL to Git forge for current file to clipboard" })
-- Visual mode: file URL with line range
vim.keymap.set("v", "cf", function()
-- Exit visual mode to update '<, '> marks
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "x", false)
local curFile = vim.api.nvim_buf_get_name(0)
local startLine = vim.fn.line("'<")
local endLine = vim.fn.line("'>")
local lineRef = (startLine == endLine) and tostring(startLine) or (startLine .. "-" .. endLine)
copy_forge_url(curFile .. ":" .. lineRef)
end, { desc = "Copy URL to Git forge for current file with selected line numbers to clipboard" })
end
```
## License
Licensed under either of [MIT](https://github.com/dpassen/forgelink/blob/main/LICENSE-MIT) or [Apache-2.0](https://github.com/dpassen/forgelink/blob/main/LICENSE-APACHE) at your option.