go-hglib reads Mercurial repositories directly from their on-disk files. It
is the Mercurial counterpart of the pkg/gitlib package in
Gitus: the changelog, manifests,
filelogs and revlog indexes are parsed in Go, so browsing history does not
spawn one hg process per object.
Operations that are not a storage format are delegated to the hg
executable, just as gitlib delegates diffing, blame and merging to git.
import "github.com/GitusCodeForge/go-hglib"
repo, err := hglib.NewLocalHgRepository("/srv/hg/example")
if err != nil {
log.Fatal(err)
}
defer repo.Close()
tip, err := repo.GetTip()
if err != nil {
log.Fatal(err)
}
fmt.Println("tip:", tip.Id, tip.CommitMessage)
// Named branches, bookmarks and tags.
branches, err := repo.GetAllBranchList()
bookmarks, err := repo.GetAllBookmarkList()
tags, err := repo.GetAllTagList()
fmt.Println(branches, bookmarks, tags)
// A directory listing is a view over the flat manifest.
tree, err := repo.GetTree(tip, "src")
for _, item := range tree.ObjectList {
fmt.Println(item.Name, item.Mode, item.IsDir)
}
// Read a file at a changeset.
blob, err := repo.GetFile(tip, "src/main.go")
fmt.Println(string(blob.Data))
// Or walk from a tree.
obj, err := repo.ResolveTreePath(tree, "sub/file.txt")
fmt.Println(obj.ObjectId())history, err := repo.GetCommitHistoryN("tip", 50)
fmt.Println(len(history))
// For every direct entry of a directory, the newest changeset that touched
// it. This is one walk of the history, not one hg process per entry.
last, err := repo.ResolveLastCommitAtDir(tip, "")
for name, node := range last {
fmt.Println(name, node)
}diff, err := repo.GetDiffRev(tip.Rev) // hg diff --git -c <rev>
blame, err := repo.Blame(tip, "src/main.go") // hg annotate -T jsonMercurial cannot commit into a store without a working directory. The
AddFileToRepo* helpers make a temporary clone at the branch head, commit
there and push the result back. The repository's own working directory is not
touched.
node, err := repo.AddFileToRepoString(
"default", "README.md",
"Jane Doe", "jane@example.com",
"Jane Doe", "jane@example.com",
"add README",
"# Hello\n",
)Merging works the same way:
check, err := repo.CheckBranchMergeConflict("feature", "default")
if check.Successful {
err = repo.Merge("feature", "default", "Jane Doe", "jane@example.com")
}- Repository discovery from a working directory or a
.hgdirectory. - Modern store layout:
share-safe,store,fncache,dotencode,sharedpath. - Revlog version 0 (the pre-2006 format, 76-byte entries with node-id parents)
and version 1 indexes, both inline (
00manifest.iusually) and split index/data files (.i/.d). - Revlog version 2 (
0xDEAD) and changelog version 2 (0xD34D): docket files, UUID-named.idx/.dat/.sdafiles, 96-byte index entries and the plain / default / inline chunk compression modes. fileindex-v1enumeration, including paths whose store name is hashed.- The experimental
plain-encodestore layout. generaldeltaand non-generaldeltadelta chains. Mercurial's own patch format is applied directly, so nohgprocess is needed to reconstruct a revision.- Chunk compression: zstd (the current default), zlib, bzip2 and the
uncompressed
u/ NUL-header forms. - Changelog entries: manifest node, user, date, extra metadata, named branch, changed files, description.
- Manifests: flat path-to-filelog-node listings,
x/l/tflags and directory views. - Filelogs, including the
\1\ncopy-metadata header and copy source/rev. - Tags from
.hgtagsand.hg/localtags. The.hgtagsused is the one at the tip, walking first parents if that changeset does not contain the file; Mercurial has no single server-side tag view, so tags that only exist on an unmerged side branch are not merged in. - Bookmarks from
.hg/bookmarks. - Named branch heads, using Mercurial's
branch2cache when it is current and scanning the changelog otherwise. dirstateversion 1 (working directory parents and entries).- hgrc parsing, including
%include,%unsetand line continuations, plus atomic single-key updates. - Revision hashing (
hashrevisionsha1), including the sorted-parent rule. ResolveLastCommitAtDirmemoization, in memory and under.hg/hglib/lastcommitso a restarted process does not walk the history again.ClearLastCommitStoreremoves the persistent entries.
sidedatacontents. The.sdafile and its offset/length fields are recognised, but the payload is not parsed. This mainly matters for the experimentalexp-copies-sidedata-changesetfeature, where copy source/rev information lives in sidedata instead of the filelog header;BlobObject.CopySourcecan therefore be empty for such repositories. Version 2 itself is experimental: Mercurial's own help calls it "in-development" and enabling it requires a configuration value literally namedenable-unstable-format-and-corrupt-my-data. Unknown requirement sub-versions are rejected rather than guessed at.dirstateversion 2 (dirstate-v2).- Mercurial's wire protocol (bundle2,
getbundle, …). This package is for local storage. - Narrow repositories are read from
data/with ameta/fallback, but the narrow manifest format itself is not implemented. - Git-style remotes. Mercurial named paths are ordinary configuration; use
hg pull/hg pushdirectly if you need them.
| gitlib | go-hglib |
|---|---|
LocalGitRepository |
LocalHgRepository |
NewLocalGitRepository |
NewLocalHgRepository / InitLocalHgRepository |
CommitObject |
CommitObject (a changeset; TreeObjId is the manifest node) |
TreeObject |
TreeObject (a directory view over the flat manifest) |
BlobObject |
BlobObject (a filelog revision) |
Branch |
Branch (named branch; may have several heads) |
Tag |
Tag (from .hgtags / .hg/localtags) |
| — | Bookmark |
GetAllBranchList |
GetAllBranchList |
GetAllTagList |
GetAllTagList |
GetCommitHistoryN |
GetCommitHistoryN |
ReadObject |
GetCommit, GetManifest, GetFile, ReadBlob |
ResolveTreePath |
ResolveTreePath |
GetDiff |
GetDiff / GetDiffRev |
Blame |
Blame / BlameAtRev |
ResolveLastCommitAtDir |
ResolveLastCommitAtDir |
AddFileToRepoString |
AddFileToRepoString |
AddFileToRepoReader |
AddFileToRepoReader |
AddMultipleFileToRepoString |
AddMultipleFileToRepoString |
Merge |
Merge (branch names, not a remote) |
CheckBranchMergeConflict |
CheckBranchMergeConflict (two branch names) |
| hooks directory | [hooks] in hgrc (GetHook, SaveHook, DeleteHook) |
Unlike Git, Mercurial has no global object store: a node id alone does not say
whether it names a changeset, a manifest or a file revision. The API therefore
asks for the revlog explicitly (GetCommit, GetManifest, GetFile) instead
of offering one ReadObject.
LocalHgRepository keeps revlog indexes in memory. After another process
pushes into the repository, call Refresh to reopen the changelog and
manifest:
if err := repo.Refresh(); err != nil { ... }Existing revision numbers keep their meaning; new revisions become visible.
The tests create real repositories with the hg executable and compare the
parsed result against hg log, hg manifest --debug, hg cat, hg diff,
hg annotate and hg resolve. The suite also creates version 2 and
changelog-v2 repositories with Mercurial's experimental configurations. On
builds without the Rust file-index extension, those repositories require
storage.fileindex.slow-path=allow; the hg helpers in this package add that
config automatically, and the tests do the same for their own hg calls.
go test ./...If your /tmp is mounted noexec (common on hardened systems), point Go's
temporary directory at a directory that allows execution:
mkdir -p .gotmp
GOTMPDIR=$PWD/.gotmp go test ./...