skip to content

In a Gemfile, how do the `git:`, `github:` and `path:` options source a gem, and what do `branch:`, `tag:` and `ref:` pin?

level: middleimportance: should knowfreq 40%

answer

  1. not from a gem server
  2. a .gemspec at the repo or directory
  3. lockfile pins the commit, not the branch
  4. github: expands to an https URL
  5. tag plus branch: AmbiguousGitReference

basics

~20 s

git: clones a repository and builds the gem from its .gemspec, github: "owner/repo" is shorthand for its https URL, and path: uses a local directory in place. branch:, tag: or ref: choose what to check out, and Gemfile.lock pins the resulting commit.

solid answer

~40 s

With `git:` Bundler clones the repository, checks out a reference and builds the gem from the `.gemspec` it finds there (without one, you must give a version and Bundler writes a minimal spec with no dependencies). `github: "rack/rack"` expands to `https://github.com/rack/rack.git`; `gist:`, `bitbucket:` and `gitlab:` are siblings, and `git_source` defines your own. `branch:` follows a branch, `tag:` a tag and `ref:` a commit or other ref; with none, Bundler uses the remote's default branch. Whatever you name, the lockfile records the exact commit, so installs stay reproducible until `bundle update rack` moves it. `tag:` combined with `branch:` or `ref:` raises `AmbiguousGitReference`. `path: "../billing"` uses a directory relative to the Gemfile, reflects edits immediately, needs a `.gemspec` or a version, and does not compile C extensions.

code

ruby · 10 lines
ruby
source "https://rubygems.org"

# follow a branch; Gemfile.lock pins the commit
gem "rack", git: "https://github.com/rack/rack.git", branch: "main"

# shorthand for https://github.com/sinatra/sinatra.git at a tag
gem "sinatra", github: "sinatra/sinatra", tag: "v4.2.1"

# a sibling directory, used in place
gem "billing", path: "../billing"

go deeper

for a junior

Recall the three options: git: for a repository URL, github: as the owner/repo shorthand, path: for a local directory, and that each needs a .gemspec.

for a middle

Explain that Gemfile.lock pins the git commit whatever branch you name, when bundle update moves it, and why tag with branch or ref is rejected.

for a senior

Treat git and path sources as temporary: track forks with an exit plan, prefer https or ssh, use local overrides for gem development, and avoid path gems with native extensions.

for a principal

Decide when a shared internal gem should live as a path gem in a monorepo, a git dependency or a released gem on a private server, weighing release discipline against iteration speed.

## Three sources that are not a gem server Most gems in a **Gemfile** come from the global `source`, a RubyGems server that serves packaged `.gem` files. Three options point a single gem somewhere else: | Option | Where the code comes from | Typical use | |---|---|---| | `git:` | any git URL (https, ssh) | an unreleased fix, a fork | | `github:` (and `gist:`, `bitbucket:`, `gitlab:`) | a hosted repository, written as `owner/repo` | the same, with less typing | | `path:` | a directory on disk | a gem developed next to the app, a monorepo component | For all three, Bundler needs a **gem specification**. A git repository or directory should contain a `.gemspec` for the gem; each `.gemspec` in a repository defines a gem located where that file sits. Without one, you must give an explicit version, and Bundler generates a minimal spec that has no dependencies, executables or extension build steps, which often breaks the gem. ## How a git gem is resolved ```ruby gem "rack", git: "https://github.com/rack/rack.git", branch: "main" gem "sinatra", github: "sinatra/sinatra", tag: "v4.2.1" gem "mustermann", github: "sinatra/mustermann", ref: "a1b2c3d" ``` Bundler clones the repository into its cache, checks out the requested reference, and builds the gem from the `.gemspec`. The reference options are: - **`branch:`** - follow the tip of a branch. - **`tag:`** - a fixed tag. - **`ref:`** - a commit SHA or any other ref, such as a pull request head. - **none** - the remote's default branch (its `HEAD`). The gemfile(5) man page still says the default is `master`; the Bundler 4.0.21 source resolves the remote `HEAD` and follows a renamed default branch. `tag:` together with `branch:` or `ref:` raises `AmbiguousGitReference` ("Specification of branch or ref with tag is ambiguous"). `branch:` on a gem with no git source is a `GemfileError`. The crucial detail: whichever reference you name, **Gemfile.lock records the exact commit** that was checked out. A new commit on `main` changes nothing for `bundle install`; only `bundle update rack` fetches the branch again and moves the lock. So `branch:` means "which line of history to update along", not "always the latest". If the gem line also carries a version requirement, the repository's `.gemspec` must satisfy it; `gem "rails", "2.3.8", git: ...` fails when the checked-out spec says 3.0.0. ## The hosted shorthands `github: "rack/rack"` becomes `https://github.com/rack/rack.git`. A single name repeats itself, so `github: "rails"` means `rails/rails`. A pull request URL is accepted too: `github: "https://github.com/rails/rails/pull/43753"` checks out `refs/pull/43753/head`. `gist:`, `bitbucket:` and `gitlab:` work the same way, and `git_source(:name) { |repo| "https://git.example.com/#{repo}.git" }` defines your own shorthand. Current shorthands use https; older write-ups that warn about `github:` expanding to an insecure `git://` URL describe a pre-2.0 Bundler. Prefer https or ssh URLs for `git:` as well: the man page warns that `http://` and `git://` are unauthenticated, so a man-in-the-middle can deliver code. ## Path gems ```ruby gem "billing", path: "../billing" path "components" do gem "admin_ui" gem "public_ui" end ``` A relative path is resolved from the directory holding the Gemfile, and it must point at the unpacked source, not at a packaged `.gem` file. Bundler uses the directory **in place**: edits are visible to the next process without reinstalling. Two differences from `git:` matter: 1. Bundler does **not compile C extensions** for path gems. 2. The code is whatever is on disk, so a path gem is only reproducible if that directory is versioned with the app. ## Working on a git gem locally When you need to edit a gem that the Gemfile pulls from git, `bundle config set --local local.rack ~/code/rack` overrides the remote with a local checkout. It works only for gems with a git source, and the Gemfile must name a `branch:`; commits in the local checkout move the locked revision, so push them before you push the lockfile. ## Traps - Expecting `branch:` to pull the newest commit on every install. - Leaving a fork on `git:` for months; it silently stops receiving releases from the gem server. - Using `path:` for a gem with native extensions and wondering why it fails to load.

  • A teammate pushes a fix to the `main` branch your Gemfile follows. How do you pick it up, and what changes in the repository?
    Run `bundle update rack` (or whichever gem it is). Bundler fetches the branch again, checks out the new tip and rewrites the revision recorded for that gem in Gemfile.lock. Commit the lockfile change; without that step every machine keeps installing the old commit.
  • How do you develop against a local clone of a gem that the Gemfile pulls from git, without editing the Gemfile?
    Set a local override: `bundle config set --local local.rack ~/code/rack`. It works only for gems with a git source whose Gemfile entry names a `branch:`, and Bundler checks that the local checkout is on that branch. Commits there update the locked revision, so push the gem before pushing the lockfile.

saying these in an interview costs you the question

  • branch: makes every bundle install fetch the newest commit
  • github: shorthand clones over the unauthenticated git:// protocol
  • A path: gem must be rebuilt and reinstalled after every edit
  • tag: and branch: can be combined to pin a tag on a branch
  • A path: option can point at a packaged .gem file