Gitfence

Types only for Go, Python and TypeScript

Give restricted people the API of a folder without its implementation: TypeScript declarations, Go and Python stubs that compile, values removed.

Short answer: Types only gives a restricted person the API of a folder without its implementation, so their code still builds against it. It works for TypeScript, Go and Python. Every literal value is replaced and every function body is gone; other files under the folder are hidden.

What each language gets

Language What the person's clone contains What still works
TypeScript Generated declarations (.d.ts) in place of .ts and .tsx sources. Literal values are widened: constants, as const objects, tuples, static readonly fields and namespaces carry their types, not their values. Imports resolve and the project type-checks.
Go A stub of each source file at the same path, plus the folder's go.mod and a reduced Bazel BUILD.bazel. Exported types, struct fields, interfaces, methods, functions, constants and variables keep their names and types. Every body panics with gitfence: implementation hidden, except while packages initialise, when it returns zero values. go build, go vet, go list and gopls on code that imports the package, and tests that don't call into it.
Python Executable Python at the same path. Classes, functions and annotations stay. Every body raises NotImplementedError("gitfence: implementation hidden …"), except while a module is being imported, when it returns None. Imports, type checkers, IDE completion, and tests that don't call into it.

Steps

  1. Open the repository and its Files tab.
  2. Choose the folder and set the group's level to Types only.
  3. Check it with Preview as. The preview shows the generated files a person in that group will receive.
  4. Save. People in the group get the change on their next git pull, as a commit called Gitfence: access updated.

What never reaches them

  • Values. Constants and variables keep their type but not their value, in every language: a Go const Timeout = 30 * time.Second keeps its type, not the 30.
  • Unexported code. Private Go code is removed; an unexported type the API needs stays under an opaque name.
  • Comments and docstrings. Prose is not reviewed for secrets.
  • Tests and test data: _test.go, testdata/, test_*.py, *_test.py and conftest.py are hidden.
  • Decorator arguments. Python decorators that take arguments (such as @app.get("/internal")) are dropped: their arguments would disclose values.
  • Embedded files and build values: //go:embed targets and Bazel x_defs, embedsrcs, data and go_test rules are hidden.
  • Files it cannot describe: cgo files, Go files with //go:build ignore, and anything the parser cannot read. A file that cannot be stubbed is hidden, never served as it is.
  • Other files under a types-only folder (JSON, SQL, .env and so on) are hidden. The exceptions are what builds need: go.mod and go.sum, a reduced BUILD.bazel for Go packages, and py.typed.

Gotchas

  • Code that needs the hidden implementation at run time still cannot run on their machine. Use Fenced CI to run the real tests.
  • Types only cannot be edited: a push that changes a generated file is refused.
  • Every historical version of a types-only file is stubbed too, so history carries no implementation either.
  • The generated header line marks each file: Generated by Gitfence from fenced source: types only, implementation hidden. Do not edit.

Go monorepos: what works

We test this on a generated monorepo with go.work, nested modules, internal/ and cmd/, protobuf code, Bazel, //go:generate, build tags, generics, init(), go:embed and vendor/.

Fence go build / go vet / go list go test Bazel
A package other code imports is types only Works Works, except tests that call the hidden code. Those fail with gitfence: implementation hidden (types only). Tests that need it run in Fenced CI. Works: the package gets a BUILD.bazel stub
A whole nested module is types only Works (its go.mod stays) Works Works
A nested module nothing visible imports is hidden Works: its use line in go.work is commented out in the copy and restored on push Works Works
A module or package that visible code imports is hidden Fails for the importing packages Same Labels that point at it fail
.proto sources hidden, generated .pb.go visible Works Works A go_proto_library that names the hidden proto_library fails
Generated .pb.go hidden Fails Fails Fails
Generated .pb.go types only Works Getters panic like any hidden code Works

Advice:

  • Fence with types only, not hidden, when other code imports the folder. Hide only what nothing visible imports.
  • Commit generated code (.pb.go, stringer output). go generate and buf generate can't run without the hidden inputs, so regenerate in the full repository or in Fenced CI.
  • Tests that need hidden behaviour run in Fenced CI. Locally, a package-level var x = hidden.New() gets a zero value instead of crashing the whole test binary. A test that relies on that value fails or behaves differently there, so trust Fenced CI's result.
  • Bazel: keep proto_library targets visible if you build with go_proto_library. Bazel can't run go_test for a types-only package.
  • go work vendor: if a hidden nested module's go.mod has replace lines, Go reports "inconsistent vendoring" in the copy. Make that module types only, or don't vendor the workspace.
  • Python monorepos work the same way. Namespace packages split across source roots import, and py.typed and hand-written .pyi files are kept. A package's __init__.py re-exports are kept. A call into a types-only module at import time returns None. A types-only project's own pyproject.toml is hidden, so pip install -e doesn't work for it: add its source folder to PYTHONPATH or your IDE's source roots instead.

Next: access levels and performance and sizing.