You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Swiss research community publishes a large fraction of its code on self-hosted GitLab instances, not GitHub. Today GME only speaks GitHub, which leaves the bulk of the EPFL / ETHZ / Renku v1 corpus invisible to the open-pulse graph.
Public-only project counts (via GET /api/v4/projects?visibility=public, x-total header):
Instance
URL
Public
Active (non-archived)
gitlab.renkulab.io (Renku v1)
/explore
9,874
—
gitlab.ethz.ch
/explore
2,908
2,791
gitlab.epfl.ch
/explore/projects/active
1,206
1,177
Total
~14,000
Adding GitLab support roughly 10× the addressable corpus for free, with no architectural rewrite (GitLab's REST API is well-documented and similar in shape to GitHub's).
Extraction of the same fields GME currently pulls for GitHub repos: identity, metadata, contributors, languages, topics, license, README, dependents-where-feasible.
Two-token auth: GitHub token stays, plus a per-instance GitLab PRIVATE-TOKEN env var.
Out of scope (separate follow-ups):
Bitbucket / Gitea / Forgejo (architecturally easier once the provider abstraction lands, but defer).
GitHub is strictly two-level (owner/repo). GitLab supports arbitrary group nesting:
Pattern
Example
github.com/{owner}/{name}
github.com/sdsc-ordes/open-pulse
gitlab.example.com/{namespace*}/{name}
gitlab.epfl.ch/lasa/research/control-systems/foo
Practical consequences:
The extractor's notion of owner becomes a namespace path (slash-separated) for GitLab. Existing GitHub callers must keep seeing the single-segment owner.
Group-vs-user ownership is exposed via namespace.kind (group | user) in the API; map both to the same schema:author Object range, distinguished by rdf:type.
API resolution requires URL-encoding the full project path: GET /api/v4/projects/{URL_ENCODED_PATH}.
Field mapping
GitLab project payload → existing RDF shape (no new predicates needed for core fields):
GitLab field
RDF predicate
Notes
web_url
@id (subject IRI)
strip trailing /
name
schema:name
path_with_namespace
schema:identifier
description
schema:description
created_at
schema:dateCreated
last_activity_at
schema:dateModified
default_branch
pulse:defaultBranch
already exists
topics[]
schema:keywords
star_count
pulse:stargazers
forks_count
pulse:forks
visibility
pulse:visibility
public / internal / private
archived
schema:archivedAt (when true)
license.key
schema:license
SPDX lookup
namespace.full_path
schema:author (group/user IRI)
nested group joins
forked_from_project.web_url
pulse:forkOf
http_url_to_repo
pulse:cloneUrl
readme_url
dereference → schema:text
Endpoints used per repo:
GET /api/v4/projects/{enc(path)} — core metadata.
GET /api/v4/projects/{id}/repository/contributors — contributor list.
GET /api/v4/projects/{id}/languages — language breakdown.
GET /api/v4/projects/{id}/repository/files/README.md/raw?ref=HEAD — README content (404 fallback to README, README.rst, etc.).
Token is sent as PRIVATE-TOKEN: <token> (GitLab's native header) — distinct from GitHub's Authorization: Bearer. Anonymous works for public; private/internal need the appropriate per-host token.
Pagination + rate limiting
GitLab paginates with x-total, x-total-pages, x-page, x-per-page headers and a nextLink header (same shape as GitHub). Re-use existing pagination helper, parameterised on header names.
Rate limits are per-instance and configurable by admins — observe the RateLimit-* headers (lowercase, hyphenated; not the GitHub form) and back off on 429.
URL discovery / seeding
The crawler today seeds from GitHub orgs and users. For GitLab we want analogous seeds:
seeds:
gitlab:
- host: gitlab.epfl.chinclude: all-public # iterate /api/v4/projects?visibility=public
- host: gitlab.epfl.chinclude: group # iterate one group recursivelygroup: lasa
- host: gitlab.renkulab.ioinclude: topictopic: renku # filter by GitLab topic
Three discovery modes: whole-instance, single-group (recursive), topic-filtered. Mirrors the GitHub org / user / search seed types.
URL parser auto-resolves the provider; no config flag needed at call sites.
GitLabProvider supports gitlab.com + self-hosted (host passed at construction time).
All current schema:SoftwareSourceCode predicates emitted for GitLab repos with the same shape as GitHub, so downstream open-pulse SPARQL queries don't need a single change.
Per-host PRIVATE-TOKEN env vars supported; anonymous fallback works for public projects.
CLI + API both accept GitLab URLs interchangeably with GitHub URLs.
Test corpus: at least one repo from each of gitlab.epfl.ch, gitlab.ethz.ch, gitlab.renkulab.io, gitlab.com, mocked + recorded.
README + config docs updated with the new providers.gitlab.instances block.
Backwards-compatible: existing GitHub-only invocations and configs continue to work unchanged.
Companion work
Open-pulse seed config will gain gitlab.* seed types in a follow-up PR there.
Renku 2.0 ingest (Ingest Renku 2.0 projects + data connectors into the RDF graph #53) is independent: it consumes Renku's JSON API, not GitLab's, even though Renku v1 projects live on gitlab.renkulab.io — once GitLab support lands, v1 projects come in via the GitLab path automatically.
Open questions (non-blocking)
Owner abstraction: keep a single owner string field on Repo (storing the full namespace path for GitLab) vs introduce owner + namespace_path? Proposal: single field with semantic-by-provider; cleaner for downstream.
Group/user RDF type: emit schema:Organization for GitLab groups regardless of internal kind, or thread kind through? Proposal: schema:Organization for group, schema:Person for user.
Default branch handling: GitLab can have null default branch (empty repo). Skip extraction or emit minimal record? Proposal: emit minimal record with pulse:isEmpty true.
Motivation
The Swiss research community publishes a large fraction of its code on self-hosted GitLab instances, not GitHub. Today GME only speaks GitHub, which leaves the bulk of the EPFL / ETHZ / Renku v1 corpus invisible to the open-pulse graph.
Public-only project counts (via
GET /api/v4/projects?visibility=public,x-totalheader):gitlab.renkulab.io(Renku v1)/exploregitlab.ethz.ch/exploregitlab.epfl.ch/explore/projects/activeAdding GitLab support roughly 10× the addressable corpus for free, with no architectural rewrite (GitLab's REST API is well-documented and similar in shape to GitHub's).
Scope
In scope:
gitlab.ethz.ch,gitlab.epfl.ch,gitlab.renkulab.io, gitlab.com).PRIVATE-TOKENenv var.Out of scope (separate follow-ups):
Provider abstraction
The cleanest path is a thin provider layer so existing GitHub code doesn't get touched on the hot path:
Provider selection driven by URL parsing, not config:
URL shape differences
GitHub is strictly two-level (
owner/repo). GitLab supports arbitrary group nesting:github.com/{owner}/{name}github.com/sdsc-ordes/open-pulsegitlab.example.com/{namespace*}/{name}gitlab.epfl.ch/lasa/research/control-systems/fooPractical consequences:
ownerbecomes a namespace path (slash-separated) for GitLab. Existing GitHub callers must keep seeing the single-segment owner.namespace.kind(group|user) in the API; map both to the sameschema:authorObject range, distinguished byrdf:type.GET /api/v4/projects/{URL_ENCODED_PATH}.Field mapping
GitLab project payload → existing RDF shape (no new predicates needed for core fields):
web_url@id(subject IRI)/nameschema:namepath_with_namespaceschema:identifierdescriptionschema:descriptioncreated_atschema:dateCreatedlast_activity_atschema:dateModifieddefault_branchpulse:defaultBranchtopics[]schema:keywordsstar_countpulse:stargazersforks_countpulse:forksvisibilitypulse:visibilitypublic/internal/privatearchivedschema:archivedAt(when true)license.keyschema:licensenamespace.full_pathschema:author(group/user IRI)forked_from_project.web_urlpulse:forkOfhttp_url_to_repopulse:cloneUrlreadme_urlschema:textEndpoints used per repo:
GET /api/v4/projects/{enc(path)}— core metadata.GET /api/v4/projects/{id}/repository/contributors— contributor list.GET /api/v4/projects/{id}/languages— language breakdown.GET /api/v4/projects/{id}/repository/files/README.md/raw?ref=HEAD— README content (404 fallback to README, README.rst, etc.).Auth strategy
Token is sent as
PRIVATE-TOKEN: <token>(GitLab's native header) — distinct from GitHub'sAuthorization: Bearer. Anonymous works for public; private/internal need the appropriate per-host token.Pagination + rate limiting
GitLab paginates with
x-total,x-total-pages,x-page,x-per-pageheaders and anextLinkheader (same shape as GitHub). Re-use existing pagination helper, parameterised on header names.Rate limits are per-instance and configurable by admins — observe the
RateLimit-*headers (lowercase, hyphenated; not the GitHub form) and back off on 429.URL discovery / seeding
The crawler today seeds from GitHub orgs and users. For GitLab we want analogous seeds:
Three discovery modes: whole-instance, single-group (recursive), topic-filtered. Mirrors the GitHub
org/user/searchseed types.Acceptance criteria
providers/directory withbase.pyProtocol,github.py(existing code refactored),gitlab.py(new).GitLabProvidersupportsgitlab.com+ self-hosted (host passed at construction time).schema:SoftwareSourceCodepredicates emitted for GitLab repos with the same shape as GitHub, so downstream open-pulse SPARQL queries don't need a single change.PRIVATE-TOKENenv vars supported; anonymous fallback works for public projects.RateLimit-*headers.gitlab.epfl.ch,gitlab.ethz.ch,gitlab.renkulab.io,gitlab.com, mocked + recorded.providers.gitlab.instancesblock.Companion work
gitlab.*seed types in a follow-up PR there.gitlab.renkulab.io— once GitLab support lands, v1 projects come in via the GitLab path automatically.Open questions (non-blocking)
ownerstring field onRepo(storing the full namespace path for GitLab) vs introduceowner+namespace_path? Proposal: single field with semantic-by-provider; cleaner for downstream.schema:Organizationfor GitLab groups regardless of internalkind, or threadkindthrough? Proposal:schema:Organizationforgroup,schema:Personforuser.nulldefault branch (empty repo). Skip extraction or emit minimal record? Proposal: emit minimal record withpulse:isEmpty true.