(for third-party Go packages)
NOTE: The tool will store an additional field "repositoryPath"
in the vendor.json file; this is allowed by
vendor-spec. In initial versions, for easier coding, it also won't retain any unknown fields
in vendor.json; this is an incompatibility with vendor-spec. TODO: [LATER] improve it be compatible with vendor-spec (retain the
unknown fields).
NOTE: All the pre-commit hooks described below (i.e. vendo-check-* commands) are assumed to check only "what is git-added" ("index"
in git parlance?), vs. what's in previous commit. Because that is what's going to make the contents of the new commit. In other words, each
of the checks should be wrapped with the following bash lines: git stash -q --keep-index; trap 'git stash pop -q' EXIT
(see: Tips for
using pre-commit hook). Unless the hook is carefully written in
such way that it's indifferent to that (i.e. only compares "staged" files to "previous commit" files), and then it should state so in usage
information. This also means that the hooks shall ignore git untracked & unstaged files.
TODO: What if vendored repos have Git submodules? At least we should detect such situation, so that we can then stop and look carefully if our tool works acceptably, or not.
TODO: [LATER] Also support vendoring specific commandline tools (e.g. go2xunit) with dependencies. But not now, we don't actually need it at the moment.
TODO: To make some operations simpler, and all of the idea overally, maybe require that all the .git/.hg/.bzr subdirs in _vendor MUST be deleted? (and do this deletion in vendo-add);
- (+) simpler regular operation and many commands;
- (-) vendo-update somewhat harder for user: he/she cannot easily verify & browse the subrepo's history;
- but this should really be needed only during the update; afterwards, after new version is fully committed into the main repo, he/she shouldn't need to browse history anymore;
- (-) may be harder to introduce git submodules in future; (or not? consider YAGNI)
- (-) if someone has something important in this .git/... repo locally, it would be lost;
- (+?) maybe then we can reuse bigger parts of http://github.com/skelterjohn/wgo ?
- actually, we already don't use .git/... much: only really when needed, otherwise we just check it as an additional safeguard (in the vendo-check-* commands);
List of subcommands of the vendo tool, as planned in below points (specific names are not final, can be changed):
vendo-recreate # internal subcommands: (vendo-forget; foreach GOOS,GOARCH {vendo-add}; vendo-ignore)
vendo-update
vendo-check-patches
vendo-check-consistency
vendo-check-dependencies
Example directory structure of a project using the vendo tool, on user's local disk (checkouted):
.git/ # - main project's repository metadata
libfoo/ # \
foo.go # |
cmd/ # |- main project's source code
fum/ # |
main.go # /
vendor.json # \
_vendor/ # |- managed by 'vendo' tool; checked-in to main repo
.gitignore # |
src/ # /
github.com/
bradfitz/
iter/ # - imported by libfoo/foo.go; checked-in to main repo
.git/ # NOTE: .git/ not checked-in to main repo; listed in _vendor/.gitignore
iter.go
rsc/
c2go/ # \ NOTE:
.git/ # |- not imported by main project => NOT CHECKED-IN to main repo,
main.go # / fully ignored because of "/" in _vendor/.gitignore
labix.org/
v2/
mgo/ # - imported by libfoo/foo.go; checked-in to main repo
.bzr/ # NOTE: .bzr/ not checked-in to main repo; listed in _vendor/.gitignore
mgo.go
code.google.com/
p/
gofpdf/ # - imported by libfoo; checked-in to main repo
gofpdf.go # NOTE: no .hg/.bzr/.git directory
- User adds pkgs from GOPATH to _vendor directory. User has some third-party pkgs already in GOPATH, non-vendored (i.e. outside the main
repo), and wants to save their full source code ("vendor" them) into into _vendor subdir of the main repo, keeping information about
original URL and revision-ids in a vendor.json file;
- Any .git/.hg/.bzr subdirs of the third-party pkgs should not be added into the main repo;
- Only those pkgs which are transitive dependencies of the main repo should be saved; other pkgs present in _vendor (e.g. because user
may develop with
GOPATH=$PROJ/_vendor;$PROJ
) should be "gitignored" by having "/
" in_vendor/.gitignore
file (cannot list each ignored pkg separately, because they may differ per user); - A warning/error should be printed if some dependencies cannot be found in _vendor or GOPATH; (user must download them explicitly);
- [Note] Some pkgs may already be present in _vendor;
- IMPLEMENTATION - vendo-recreate -platforms=linux_amd64,darwin_amd64[,...]:
- internal subcommand
vendo-forget
:git 'forget' _vendor
;mv vendor.json vendor.json.old
; (internally, vendor.json.old may exist only in memory, doesn't have to be created on disk);rm vendor.json
;rm _vendor/.gitignore
;- [Note] We must do this to remove a "
/
", which should be present in _vendor/.gitignore as result of vendo-ignore command. Also, we want to do this to make sure we're starting with a "clean slate" - this simplifies logic of vendo-add, as it can now work in a purely additive fashion;
- [Note] We must do this to remove a "
- internal subcommand
vendo-add -platforms=linux_amd64,darwin_amd64[,...] [./...]
;- analyze all *.go files (except
_*
,.*
,testdata
) for imports, regardless of GOOS and build tags;- [Note] Just ignoring GOOS and GOARCH here is simpler than trying to parse & match them. As to build tags, we specifically want to cover all combinations of them, as we want to make sure all ever dependencies of our main project are found.
- [Note] In this step only, we don't want to use
go list
, but a custom Go parser. That's because we want to "greedily" find any possible imports for any possible combinations of build tags.
- build a transitive list of import dependencies. If imported pkg is not found in GOPATH (including _vendor), then report
error, and exit. To build the import list we use
go list
, because it handles build tags (we assume that we want all the third-party/external imports built in "default" configuration, i.e. with no build tags). Finally,go list
result depends on GOOS and GOARCH, so we merge result from every GOOS & GOARCH combination (as listed in-platforms
mandatory argument). - add
.git
(and.hg
,.bzr
) to _vendor/.gitignore; - for each dependency pkg:
- if not present in _vendor, but present in GOPATH,
git/hg/bzr clone $GOPATH_REPO _vendor/$PKG_REPO_ROOT
(unless option--clone=false
is provided), and copy the source repo's origin URL to target repo (e.g.cd $PKG_REPO_ROOT; git remote set origin $REPO_URL
); - if not present in _vendor afterwards, report error, os.Exit(1);
- pkg is now for sure present in _vendor;
- "update revision-id & revision-date":
- if has .git/.hg/.bzr subdir, update vendor.json revision-id & revision-date;
- else if pkg not present in vendor.json.old, then error: "cannot detect repo type";
- add pkg to vendor.json, keeping any fields from vendor.json.old (including "comment", "revision", "revisionDate");
git add _vendor/$PKG_REPO_ROOT
;
- if not present in _vendor, but present in GOPATH,
- analyze all *.go files (except
- internal subcommand
vendo-ignore
; -- makes sure that any other random pkgs in _vendor (i.e. which are not dependencies of the main project, but exist there e.g. because of user's GOPATH) are ignored by Git;echo / >> _vendor/.gitignore
;git add _vendor/.gitignore
;
- internal subcommand
- User clones the main repo from central server and wants to compile & test it;
- Compilation & testing should use the vendored pkgs (i.e. from _vendor subdir);
- IMPLEMENTATION:
git clone ...
GOPATH=$PROJ/_vendor;$OLD_GOPATH
-- possibly with a helper tool:GOPATH=$(vendo-gopath)
;go build ./... ; go test ./...
etc.;
- User pulls the new version of the main repo from central server and wants to compile & test it;
- [Note] Some packages may already exist in _vendor subdir (not tracked by Git) from earlier work, and/or because of earlier use of the vendoring tool;
- IMPLEMENTATION:
- as in usecase above;
- User checkouts (via git) a different revision of the main repo and wants to compile & test it;
- IMPLEMENTATION:
- as in usecase above;
- IMPLEMENTATION:
- User wants to update a third-party repo in _vendor from the Internet (i.e.
go get -u
);- [Note] The repo may or may not have a .git/.hg/.bzr subdir; (no subdir e.g. when it was added by another user and pulled);
- [Note] The repo may be patched internally to fix a bug; it'd be desirable that this is detected and the update stopped;
- [Note] This will require updating all pkgs which have the same repo;
- IMPLEMENTATION:
vendo-update [-platforms=linux_amd64,darwin_amd64[,...]] PKG
;rm _vendor/.gitignore
; (required for avendo-recreate
step below and forgit status
calls);- if
git status _vendor/$PKG_REPO_ROOT
shows diff, then error (unless-f
|--force
option provided);- [Note] We don't have to check
cd _vendor/$PKG_REPO_ROOT ; git/hg/bzr status
. If the files are "unmodified" from perspective of the main repo, then it means they're at proper state for building the main project, regardless whether the "subrepos" are consistent. Similarly, if they are "modified" from perspective of the main repo, this means some work was maybe done in the main repo, and this is important to warn about.
- [Note] We don't have to check
rm -rf _vendor/$PKG_REPO_ROOT
;GOPATH=_vendor go get $PKG
; if failed, error (don't revert; user can retry with-f
option);- what if the pkg is in "external" GOPATH? (i.e. out of _vendor);
- setting
GOPATH=_vendor
(instead of earlier proposedGOPATH=_vendor;$GOPATH
) should fix this issue;
- setting
- what if the pkg is in "external" GOPATH? (i.e. out of _vendor);
- Remember for later the branch or revision used by go get:
(cd $PKG_REPO_ROOT; git symbolic-ref -q --short HEAD || git rev-parse HEAD
; - store the output in $GO_GET_REVISION; (cd $PKG_REPO_ROOT; git/hg/bzr checkout $PKG_REPO_REVISION)
; if failed, error; ($PKG_REPO_REVISION comes from vendor.json file);- if
git status _vendor/$PKG_REPO_ROOT
shows diff, then error (this means that the repo was patched locally after vendoring), unless--delete-patch
option provided;- [Note] We've done
rm
on the files, but we did NOT dogit rm
on them (in the main repo). So, after re-creating them,git status
in the main repo should see the same files as beforerm
. So, it should conclude: "meh, nothing changed", i.e.git status
is clean. Ifgit status
does show diff, this means our repo remembers something different (a "patch") than what we recreated based on revision-id listed in vendor.json. So, we must quit, and print an error message: "vendored pkg is patched locally; please merge manually".
- [Note] We've done
(cd $PKG_REPO_ROOT; git/hg/bzr checkout $GO_GET_REVISION)
;- [Note] We can't just
git checkout master
, because e.g. if tag 'go1' is present in repo, it is chosen bygo get
instead of 'master'.
- [Note] We can't just
vendo-recreate
;- [Note] Value of argument
-platforms
for vendo-add should be copied verbatim from argument-platforms
of vendo-update, or read from vendor.json custom global field "platforms" otherwise; - [Note] This will update revision-id & revision-date for $PKG in vendor.json;
- [Note] This will also add any new pkgs downloaded because they're dependencies of $PKG;
- [Note] Value of argument
- User does normal coding in the main project. User wants to change the code of the main repo, adding and removing some imports, then build
& test, then commit the changes, then push them to the central server;
- A pre-commit hook should detect if new imports were added that are not present in _vendor (or some imports removed which are
present there) and stop the commit (or just inform, without stopping?); still, user should be allowed to commit the code anyway if he
really wants ("--force");
- IMPLEMENTATION; VARIANT-A (faster, but won't detect removed repos):
- analyze all
*.go
files changed by the commit (except_*
etc.), including those in _vendor subdir; if they add any imports from outside main repo, which are not yet in vendor.json, then report error with appropriate message (list of pkgs and suggestion to call vendo-add);
- analyze all
- IMPLEMENTATION; VARIANT-B (slower, but will detect removed repos):
vendo-check-consistency
-- this checks that all repository roots listed in vendor.json exist as subdirs in the committed _vendor subdir, and that there are no other subdirs;git stash -q --keep-index
;- parse vendor.json, sort by pkg path;
os.Walk("_vendor", func...)
, where func...:- if path is a prefix of pkg in vendor.json, then return CONTINUE;
- if path not in vendor.json, then report error, return SKIP_SUBTREE;
- if has .git/.hg/.bzr subdir, then verify revision-id match with vendor.json; if failed, report error (see vendo-check-patched for error message details);
- mark pkg visited;
- return SKIP_SUBTREE;
- if any pkg in vendor.json is not visited, then report error;
- TODO: check that any .git/.hg/.bzr subdirs, if present, are at locations noted in $PKG_REPO_ROOT fields;
git stash pop -q
vendo-check-dependencies
-- this checks that all packages imported by project are listed in the vendor.json file, and no others;git stash -q --keep-index
; (or, work on files retrieved via git from index);- iterate all *.go files (except
_*
etc.), extract imports, and transitively their deps (same as in vendo-add - extract common code); - delete from the list all pkgs in "core main repo" - i.e. those in main repo, but not in _vendor;
- verify that the list is exactly equal to contents of vendor.json; if not equal, report error;
git stash pop -q
;
- TODO: add a
vendo-check-json
step before 1. - it should verify internal consistency of vendor.json (pkg paths <-> repository roots; same revision if same repositoryRoot; same revisionTime if same repositoryRoot);
- IMPLEMENTATION; VARIANT-A (faster, but won't detect removed repos):
- A tool must be available to auto-update (add & remove) packages in _vendor dir to satisfy the above pre-commit check; (still, we don't want to put the auto-update tool in pre-commit hook - we want user to run it explicitly, similar as with a go fmt hook);
- IMPLEMENTATION:
export GOPATH=$MAIN_REPO/_vendor:$GOPATH
- work work work, edit some *.go files; go get & go build & go test as needed;
git commit -a
-- if imports changed, this should fail because of pre-commit hook;vendo-recreate
;git commit -a
-- should complete successfully;
- A pre-commit hook should detect if new imports were added that are not present in _vendor (or some imports removed which are
present there) and stop the commit (or just inform, without stopping?); still, user should be allowed to commit the code anyway if he
really wants ("--force");
- User wants to patch a repo in _vendor to fix a bug in a third-party repo;
- A pre-commit Git hook detects that changes were made in some packages, and require changing (adding or editing) the repo's
"comment"
field in the vendor.json file [a new revision-id would be desirable too, but it may not exist in the original repo, thus becoming nonsensical; also, old revision-id has advantage of keeping info about base commit; disadvantage is that vendor.json drops consistency with _vendor contents];- IMPLEMENTATION - "vendo-check-patched" ("vendo-check-status"? "changes"?):
cd _vendor; git status
; if no changes, we're ok, exit early.- find out which repos changed (by taking repo roots from vendor.json);
- for each changed repo:
- if has .git/.hg/.bzr, then:
- if current revision (via git/hg/bzr) differs from revision-id from vendor.json, report error;
- [Note] The error message should list all possible (known) cases and suggested solutions. Suggested message contents: The revision in local repository at $PKG_REPO_ROOT: $PKG_LOCAL_REVISION $PKG_LOCAL_REV_DATE $PKG_LOCAL_REV_COMMENT is inconsistent with information stored in 'vendor.json' for package $PKG: $PKG_REPO_REVISION $PKG_REPO_REV_DATE comment: $PKG_JSON_COMMENT To fix the inconsistency, you are advised do one of the following actions, depending on which is most appropriate in your case: a) revert $PKG_REPO_ROOT to $PKG_REPO_REVISION; b) update "revision" in 'vendor.json' to $PKG_LOCAL_REVISION; c) delete $PKG_REPO_ROOT/.git [TODO: or .hg or .bzr]
- [Note] Possible (known) reasons for such situation:
- (a) user did
go get -u
without changing vendor.json; - (b) user did a patch in the subrepo, then did
git commit
in the subrepo - that would be OK here, but on disk this looks exactly the same as (a), so we cannot differentiate it; - (c) user pulled main repo with updated pkg in _vendor (and vendor.json), while having old (pre-update) .git dir
in the pkg's (sub)repo; (see also case (d) below, as it's effectively the same);
- solution proposal: run vendo-update for the pkg, with a flag which will make it stop after the step: "6. if `git/hg/bzr status [...]"; (+ updating the "master"/other appropriate branch in .git/.hg/.bzr);
- (d) user checkouts from pre-update revision of main repo, to post-update one (and reverse), while having the .git/... subdir; we can suggest deleting .git/..., or doing appropriate vendo-update each time;
- (a) user did
cd $PKG_REPO_ROOT; git/hg/... status
; if clean, go to next repo (this repo is ok);- otherwise (not clean), assume repo is "dirty"/"patched" -- see points below;
- if current revision (via git/hg/bzr) differs from revision-id from vendor.json, report error;
- assume repo is "dirty"/"patched"; for this repo, check if
"comment"
in vendor.json is changed between pre-commit and post-commit; if not, report error with message asking user to change the "comment" in vendor.json (user should notify in the comment about the patch);
- if has .git/.hg/.bzr, then:
- IMPLEMENTATION - "vendo-check-patched" ("vendo-check-status"? "changes"?):
- [Note] The repo in _vendor may or may not have a .git/.hg/.bzr subdir;
- IMPLEMENTATION:
- [first time] set up a pre-commit hook as described above;
- edit files in a pkg in _vendor dir;
- try
git add _vendor/... ; git commit
-- it should fail, because of pre-commit hook, with appropriate message (please edit "comment" in vendor.json for repo ... to mention that it was patched locally
); - edit vendor.json: add/modify a
"comment"
field for the repo, so that it mentions the patch contents and maybe version, e.g.:"comment": "PATCHED(v2) to fix a data race"
; (or maybe:"comment": "PATCHED(2015-07-01) to fix a data race"
?) - try
git add vendor.json ; git commit
-- now it should succeed;
- A pre-commit Git hook detects that changes were made in some packages, and require changing (adding or editing) the repo's
This solution looks kinda costly to build now; but the main benefit it brings, is that the repo should become fully self-contained, and especially all historic builds (since this solution is introduced) will be reproducible too, with correct versions of dependencies.