Hacker News new | ask | show | jobs
by mikepurvis 2652 days ago
I'm not an active golang user, but something which gives me pause here is the insistence on a strict 3-number semver.

For a commercial entity shipping software which may include upstream components, it's important to have options for maintaining (and versioning) in-house forks of upstream components. This becomes a problem when you fork v1.2.3 from upstream and want to internally release your fixes, but now your internal 1.2.4 and upstream's 1.2.4 both have the same number but diverge in content.

I like the Debian solution to this, which permits freeform textual suffixes, so that in the hypothetical scenario above, you can release v1.2.3bigco1, v1.2.3bigco2, with the final number indicating the version of the patchset being applied onto the upstream release; then it's also clear what to do when you rebase your fork because you've maintained the integrity of the upstream version.

3 comments

> I like the Debian solution to this, which permits freeform textual suffixes, so that in the hypothetical scenario above, you can release v1.2.3bigco1, v1.2.3bigco2, ...

Go also allows freeform textual suffixes, it just needs a `+` in there: v1.2.3+bigco1, v1.2.3+bigco2.

Oh wow, so they are not following semver? In semver everything after + is a build tag that does not "modify" the version (i.e. 1.2.3+a and 1.2.3+b are considered to both be the same version, just e.g. built at different times).
The blog post links to the Semantic Versioning spec, specifically item 9.

> A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers immediately following the patch version. Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. Numeric identifiers MUST NOT include leading zeroes. Pre-release versions have a lower precedence than the associated normal version. A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version. Examples: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92.

Item 10 explains how + can be used to add a build tag.

> Build metadata MAY be denoted by appending a plus sign and a series of dot separated identifiers immediately following the patch or pre-release version. Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. Build metadata SHOULD be ignored when determining version precedence. Thus two versions that differ only in the build metadata, have the same precedence. Examples: 1.0.0-alpha+001, 1.0.0+20130313144700, 1.0.0-beta+exp.sha.5114f85.

Instead of using v1.2.3bigco1 or v1.2.3+bigco1, we should use v1.2.3-bigco1. If we need to add a build tag we can use v1.2.3-bigco1+001.

Sounds pretty similar to the Debian scheme actually, which is pretty well thought out: https://www.debian.org/doc/debian-policy/ch-controlfields.ht...

Where semver is ivory tower idealism, Debian is the battle-tested, pragmatic reality.

This has never been a problem in Go, the repo name encodes the owner of a fork, and two separate repo paths with the same version are still completely different modules.

The nice thing about not doing what debian does is that there is consistency. Freeform usually means diverging opinions, which is not the Go way.

I believe parent's question was: Yes, you will have different repo names. But there is sub version pinning. i.e. The first patch of v1.2.3 is called v1.2.3.1 (or v1.2.3-1 or something). Then the repo itself will evolve, and keep publishing v1.2.3-2, v1.2.3-4 and so on.

Patched versions are "sub" versions of the main version, and those sub versions also keep evolving for the same upstream version.

For a single module, there is a "replace" directive that you can use to point to a local fork:

https://github.com/golang/go/wiki/Modules#when-should-i-use-...

But if you have an internal version that's used in lots of modules, it's probably better to use an import path under your own organization, to avoid confusion.

Does this account for the scenario where you have a dependency of a dependency? For example, if I'm patching a bug in something that several of my upstream dependencies import, do I have to fork/replace everything in that chain to get it to do the right thing?

Debian packages can Replace each other too, and that's certainly an option for managing this scenario, but I feel it's often more disruptive— it's a much more invasive change to have to go in and mutate the package name all over the place, and then it's harder to unwind it later when upstream merges and releases my change and I don't need my fork any more. Maybe this is better in go land?

Not an expert (I just read the docs), but if you only have one binary that you ship, or all your binaries are in the same module, then it looks like a single "replace" directive will do the right thing.

If your binaries are spread across multiple modules, each one would need its own replace directives, so you'd have a bit of duplication. (But maybe this independence is good, since you can upgrade them one at a time?)

If you're publishing a library and don't control the binaries (this is something your customers build) then it looks like "replace" isn't going to work (it's under their control, so they have to do it). If you need to control the exact versions used by your library, you'll want to look into vendoring.

No, you don't need to fork/replace everything in the chain. A `replace` in the top-level `go.mod` applies in all places where that package is used.

Doing a fork/replace on everything in the chain will have no affect; `replace` is only obeyed for the top-level `go.mod`, `replace` in dependencies is ignored.