Something went wrong. Try again.
A Zig library that reads git repositories without running git, with log, show and diff output byte-identical to git's.
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Resolving a revision against a repository: the object a text such as//! `main~2:src/x.zig` or `@{upstream}` names, found as git finds it. The//! text is read by `revision.zig`; this looks up what it says.//!//! What it does, each rule found by asking `git rev-parse` and//! `git cat-file` (README.md lists where it differs)://!//! - **Names** are tried as a whole object ID (which names an object//! whether or not there is one), then as refs by gitrevisions(7)'s rules,//! then as an abbreviated object ID, then as `git describe` output//! (`<anything>-<n>-g<hex>`). A ref wins over an abbreviation, and a//! whole object ID over a ref, each with git's warning when//! `core.warnAmbiguousRefs` is on; so does a name more than one rule//! finds a ref for.//! - **Reflog entries** count back from the current value; a date finds//! the last entry made at or before it. Where the entries do not join up//! git warns of a gap, or that the log ended unexpectedly, and answers//! from the entries around it as `reflogValue` describes.//! - **`@{-<n>}`** reads `HEAD`'s reflog for its `checkout: moving from`//! entries. **`@{upstream}` and `@{push}`** are `tracking.zig`'s.//! - **Paths** in a tree or the index are looked up as written, except//! that `./` and `../` are relative to where the caller is in the//! working tree. When one is not there, git says why: what is on disk,//! at another stage, or under the caller's directory instead.//! - **Message searches** walk from their starting points newest commit//! first, by committer date, and answer the first commit whose message//! matches; among commits of the same date the one put in the queue//! last comes out first.//!//! Warnings, and the errors git reports without stopping, are kept in//! `messages` for the caller to show; an error that stops resolution has//! git's message in the diagnostic's detail, and `severity` says how git//! treats it. Everything that depends on the time or the caller's place//! in the working tree is passed in through `Options`.
const std = @import("std");const Allocator = std.mem.Allocator;const Io = std.Io;
const object = @import("object.zig");const Id = object.Id;const Kind = object.Kind;const Diagnostics = @import("Diagnostics.zig");const ObjectDatabase = @import("ObjectDatabase.zig");const Repository = @import("Repository.zig");const RefStore = @import("RefStore.zig");const refname = @import("refs/name.zig");const Index = @import("Index.zig");const abbrev = @import("abbrev.zig");const commit_format = @import("commit.zig");const Commit = commit_format.Commit;const Tag = @import("tag.zig").Tag;const tree = @import("tree.zig");const date = @import("date.zig");const regex = @import("regex.zig");const revision = @import("revision.zig");const tracking = @import("tracking.zig");const Grafts = @import("Grafts.zig");
const Resolver = @This();
gpa: Allocator,io: Io,repo: *Repository,options: Options,lookup: abbrev.Lookup,/// The index, read the first time a revision asks for it.index: ?Index = null,/// What the messages and the strings handed out live in.arena: std.heap.ArenaAllocator,/// Warnings and errors git reports without stopping, in order.messages: std.ArrayList(Message) = .empty,/// While set, nothing is added to `messages`: for trying a reading of a/// text that git tries without a word.quiet: bool = false,/// The candidates of the abbreviated object ID that last failed with/// `AmbiguousObjectName`.ambiguity: ?Ambiguous = null,/// Whether the revision last resolved steps to a parent or an ancestor,/// which makes git say some things however quietly it was asked: see/// `Message.hidden_by_quiet`.parent_steps: bool = false,/// Whether a failure of the revision last resolved that is/// `fatal_unless_quiet` is fatal however quietly git was asked, as it is/// for one that steps to a parent or an ancestor or takes a reflog entry.loud: bool = false,/// Where a bad setting read part way through is reported.setting_diag: ?*Diagnostics = null,
/// What resolving needs besides the repository.pub const Options = struct { /// The current time, for reflog dates. now: i64, /// The local zone, for reflog dates. zone: *const date.Zone, /// How an ambiguous abbreviation is told apart when the revision does /// not say: `core.disambiguate`. hint: abbrev.Hint = .none, /// `core.warnAmbiguousRefs`: warn of names that are more than one /// thing; null to read it from the configuration the first time it /// matters, as git does, which fails then if it is not a boolean. warn_ambiguous_refs: ?bool = null, /// `advice.objectNameWarning`: explain the warning about a ref named /// like an object ID; null to read it as `warn_ambiguous_refs` is. object_name_advice: ?bool = null, /// How to read the index, for `:<path>`. index: Repository.IndexOptions = .{}, /// How a path that is not found is explained, which git does a little /// differently for each command. explain: Explain = .rev_parse, /// Whether a text that is a range or another set of commits is /// refused as one (`UnsupportedRange`), as a command that takes them /// would read it, rather than looked up as a single revision, as /// `cat-file` looks it up and finds nothing. sets: bool = true, /// Whether `:/<text>` searches every ref's history. `cat-file` names /// nothing by it. search_refs: bool = true,};
/// The ways git explains a path it did not find.pub const Explain = enum { /// As `rev-parse` does: a path is also looked for under the caller's /// directory, for git's "Did you mean" hint, and an index path that /// is on disk is not explained at all. rev_parse, /// As `cat-file` does: no hint, and an index path that is on disk /// "exists on disk, but not in the index". cat_file,};
/// Something git says while resolving and goes on.pub const Message = struct { level: Level, /// Without `warning: ` or `error: `; may span lines. text: []const u8, /// Whether `rev-parse -q --verify` leaves it out: a warning that a /// name is ambiguous or a reflog goes back no further, unless the /// revision steps to a parent or an ancestor (`^<n>`, `~<n>`), which /// git warns of however quietly it was asked. hidden_by_quiet: bool = false,
/// `raw` is text written as it is, lines and their prefixes /// included. pub const Level = enum { warning, @"error", raw };
/// Writes the message as git does. pub fn write(m: Message, w: *Io.Writer) Io.Writer.Error!void { if (m.level == .raw) return w.writeAll(m.text); try w.print("{s}: {s}\n", .{ @tagName(m.level), m.text }); }};
/// An abbreviation that names more than one object.pub const Ambiguous = struct { prefix: abbrev.Prefix, ambiguity: abbrev.Ambiguity,};
/// What a revision names.pub const Resolved = struct { id: Id, /// The object's kind; null when the database does not hold it, which /// only a whole object ID can name, or its header cannot be read, /// which is for whoever reads it to report. kind: ?Kind, /// For `<rev>:<path>` and `:<path>`: the path from the top of the /// tree, as it was looked up. path: ?[]const u8 = null, /// For a path: the mode its tree or index entry records. mode: ?u32 = null,};
/// The full ref name a revision stands for, as `--symbolic-full-name`/// prints it.pub const FullName = union(enum) { /// The revision is not a ref: a suffix, an object ID, a reflog entry. none, /// More than one ref by that name, which git reports as "refname /// '<text>' is ambiguous". ambiguous: []const u8, name: []const u8,};
/// Why a revision was not resolved. Besides the repository's own/// failures to read:////// - `RevisionNotFound`: it names nothing; git's "unknown revision",/// "Needed a single revision" or "Not a valid object name"./// - `AmbiguousObjectName`: an abbreviation of more than one object;/// `ambiguity` holds the candidates./// - `InvalidObjectName`, `PathNotInTree`, `PathNotInIndex`: what git/// says when it explains a revision it could not resolve./// - `ReflogTooShort`, `ReflogEmpty`, `PathOutsideRepository`,/// `RelativePathOutsideWorkTree`: what git stops at whatever it was/// asked./// - `NoSuchBranch`, `DetachedHead`, and `tracking.Error`: there is no/// upstream or push branch, which git stops at unless asked to be/// quiet./// - `UnsupportedRange`: the text is a range or another set of commits;/// that is the walk's to answer./// - `UnsupportedRegex`: a search pattern uses something `regex.zig`/// refuses./// - `UnsupportedLegacyRemote`: the repository defines remotes in the/// files of `remotes/` or `branches/`, which git still reads and this/// does not./// - `BadBoolSetting`: `core.warnAmbiguousRefs` or/// `advice.objectNameWarning` is not a boolean, found when it was/// first needed.pub const Error = error{ RevisionNotFound, AmbiguousObjectName, InvalidObjectName, PathNotInTree, PathNotInIndex, ReflogTooShort, ReflogEmpty, PathOutsideRepository, RelativePathOutsideWorkTree, NoSuchBranch, DetachedHead, UnsupportedRange, UnsupportedRegex, UnsupportedLegacyRemote, BadBoolSetting,} || Grafts.LoadError || tracking.Error || RefStore.ReadError || ObjectDatabase.ReadError || Index.LoadError || Index.ExpandError || abbrev.Lookup.ResolveError || Io.Writer.Error || error{ MalformedTag, MalformedCommit, MalformedParent } || date.FormatError;
/// How git treats each failure.pub const Severity = enum { /// The revision names nothing; how that is said depends on the /// command. not_found, /// git explains it, with the diagnostic's detail, when it explains /// rather than merely failing: `rev-parse` without `--verify`, and /// `cat-file`. diagnosed, /// git stops with the detail whatever it was asked. fatal, /// git stops with the detail unless asked to be quiet, when it fails /// without a word; but not for every revision (`loud`). fatal_unless_quiet, /// Not something this library does. unsupported, /// The repository could not be read. failure,};
/// How git treats `err`.pub fn severity(err: anyerror) Severity { return switch (err) { error.RevisionNotFound, error.AmbiguousObjectName => .not_found, error.InvalidObjectName, error.PathNotInTree, error.PathNotInIndex => .diagnosed, error.ReflogTooShort, error.ReflogEmpty, error.PathOutsideRepository, error.RelativePathOutsideWorkTree, error.InvalidRefspec, error.BadPushDefault, error.BadBoolSetting, error.BadShallowLine => .fatal, error.NoSuchBranch, error.DetachedHead, error.NoUpstream, error.UpstreamNotTracked, error.PushNothing, error.PushNotSingle, error.PushRefspecsExclude, error.PushNoTracking => .fatal_unless_quiet, error.UnsupportedRange, error.UnsupportedRegex, error.UnsupportedLegacyRemote => .unsupported, else => .failure, };}
/// A resolver for `repo`, which must outlive it.pub fn init(gpa: Allocator, io: Io, repo: *Repository, options: Options) Resolver { return .{ .gpa = gpa, .io = io, .repo = repo, .options = options, .lookup = .init(gpa, &repo.database), .arena = .init(gpa), };}
/// Frees everything, the strings it handed out included.pub fn deinit(r: *Resolver) void { if (r.index) |*ix| ix.deinit(); r.clearAmbiguity(); r.lookup.deinit(); r.messages.deinit(r.gpa); r.arena.deinit(); r.* = undefined;}
/// Forgets the messages so far.pub fn clearMessages(r: *Resolver) void { r.messages.clearRetainingCapacity();}
fn clearAmbiguity(r: *Resolver) void { if (r.ambiguity) |*a| a.ambiguity.deinit(r.gpa); r.ambiguity = null;}
/// A boolean setting, read once and kept.fn setting(r: *Resolver, kept: *?bool, key: []const u8) Error!bool { if (kept.*) |v| return v; var d: Diagnostics = .{}; const v = r.repo.boolSetting(key, &d) catch { const detail = d.detail(); const eq = std.mem.findScalar(u8, detail, '=') orelse detail.len; return fail(r.setting_diag, error.BadBoolSetting, "bad boolean config value '{s}' for '{s}'", .{ if (eq < detail.len) detail[eq + 1 ..] else "", detail[0..eq] }); } orelse true; kept.* = v; return v;}
fn warnAmbiguous(r: *Resolver) Error!bool { return r.setting(&r.options.warn_ambiguous_refs, "core.warnAmbiguousRefs");}
fn objectNameAdvice(r: *Resolver) Error!bool { return r.setting(&r.options.object_name_advice, "advice.objectNameWarning");}
fn say(r: *Resolver, level: Message.Level, comptime fmt: []const u8, args: anytype) Allocator.Error!void { if (r.quiet) return; const text = try std.fmt.allocPrint(r.arena.allocator(), fmt, args); try r.messages.append(r.gpa, .{ .level = level, .text = text });}
/// A warning `rev-parse -q --verify` leaves out unless the revision/// steps to a parent or an ancestor: that a name is more than one thing,/// or that a reflog goes back no further.fn sayQuietly(r: *Resolver, comptime fmt: []const u8, args: anytype) Allocator.Error!void { if (r.quiet) return; const text = try std.fmt.allocPrint(r.arena.allocator(), fmt, args); try r.messages.append(r.gpa, .{ .level = .warning, .text = text, .hidden_by_quiet = !r.parent_steps });}
fn fail(diag: ?*Diagnostics, err: Error, comptime fmt: []const u8, args: anytype) Error { if (diag) |d| { d.reset(); d.setDetail(fmt, args); } return err;}
fn db(r: *Resolver) ObjectDatabase { return r.repo.objects();}
fn store(r: *Resolver) RefStore { return r.repo.refs();}
fn format(r: *const Resolver) object.Format { return r.repo.objectFormat();}
// -- the way in ------------------------------------------------------------------
/// What `text` names.pub fn resolve(r: *Resolver, text: []const u8, diag: ?*Diagnostics) Error!Resolved { r.clearAmbiguity(); r.setting_diag = diag; if (r.options.sets) if (revision.parseSet(text)) |set| { if (try r.setResolves(set)) return fail(diag, error.UnsupportedRange, "'{s}' is {s}", .{ text, set.describe() }); }; var rev = revision.parse(r.gpa, text) catch |err| switch (err) { error.InvalidRevision => return error.RevisionNotFound, error.OutOfMemory => |e| return e, }; defer rev.deinit(r.gpa); return r.resolveRevision(&rev, diag);}
/// Whether git would take `set` as the set of commits it reads as: when/// each revision in it resolves.fn setResolves(r: *Resolver, set: revision.Set) Error!bool { const saved = r.quiet; r.quiet = true; defer r.quiet = saved; const sides: [2][]const u8 = switch (set) { .range => |x| .{ x.from, x.to }, .symmetric => |x| .{ x.left, x.right }, .exclude, .parents, .exclude_parents => |x| .{ x, x }, .parent_range => |x| .{ x.rev, x.rev }, }; for (sides) |side| { const t = if (side.len == 0) "HEAD" else side; var rev = revision.parse(r.gpa, t) catch return false; defer rev.deinit(r.gpa); _ = r.resolveRevision(&rev, null) catch |err| switch (err) { error.OutOfMemory, error.Canceled => |e| return e, else => { r.clearAmbiguity(); return false; }, }; } return true;}
/// What an already read revision names.pub fn resolveRevision(r: *Resolver, rev: *const revision.Revision, diag: ?*Diagnostics) Error!Resolved { r.parent_steps = for (rev.suffixes) |s| switch (s.step) { .parent, .ancestor => break true, .peel => {}, } else false; r.loud = r.parent_steps or rev.base == .reflog; switch (rev.base) { .index => |ix| return r.indexEntry(ix.stage, ix.path, ix.explicit_stage, diag), else => {}, } const id = r.objectOf(rev, diag) catch |err| { if (rev.path != null and err == error.RevisionNotFound) return fail(diag, error.InvalidObjectName, "invalid object name '{s}'.", .{rev.revText()}); return err; }; if (rev.path) |p| return r.treeEntry(rev, id, p, diag); return .{ .id = id, .kind = r.kindIfReadable(id) };}
/// The object a revision names before any path.fn objectOf(r: *Resolver, rev: *const revision.Revision, diag: ?*Diagnostics) Error!Id { const hint = r.hintFor(rev); var id: Id = switch (rev.base) { .ref => |e| try r.refValue(e, hint, diag) orelse return error.RevisionNotFound, .reflog => |l| try r.reflogValue(l.ref, l.selector, diag), .search => |s| if (r.options.search_refs) try r.searchAll(s, diag) orelse return error.RevisionNotFound else return error.RevisionNotFound, .index => unreachable, }; for (rev.suffixes) |s| id = try r.applySuffix(id, s, rev.text, diag) orelse return error.RevisionNotFound; return id;}
/// How what follows the base tells an ambiguous abbreviation apart.fn hintFor(r: *const Resolver, rev: *const revision.Revision) abbrev.Hint { if (rev.suffixes.len > 0) return switch (rev.suffixes[0].step) { .parent, .ancestor => .committish, .peel => |p| switch (p) { .search => .committish, .kind => |k| switch (k) { .commit => .commit, .tree => .treeish, else => r.options.hint, }, else => r.options.hint, }, }; if (rev.path != null) return .treeish; return r.options.hint;}
/// The object's kind, or null if it cannot be found or read.fn kindIfReadable(r: *Resolver, id: Id) ?Kind { const h = r.db().readHeader(r.io, id, null) catch return null; return h.kind;}
/// The object's kind, or null if the database does not hold it.fn kindOf(r: *Resolver, id: Id) Error!?Kind { const h = r.db().readHeader(r.io, id, null) catch |err| switch (err) { error.ObjectNotFound => return null, else => |e| return e, }; return h.kind;}
// -- names -----------------------------------------------------------------------
/// A ref found for a name.const Found = struct { /// The ref's full name at the end of its symbolic refs, as /// `--symbolic-full-name` shows it. full: []const u8, id: Id,};
/// The value of a ref expression.fn refValue(r: *Resolver, e: revision.RefExpression, hint: abbrev.Hint, diag: ?*Diagnostics) Error!?Id { if (e.tracking.len > 0) { const full = try r.trackingName(e, diag); return r.exactRef(full); } return switch (e.start) { .current => null, .name => |n| r.nameValue(n, e.text, hint), .previous => |k| blk: { const x = try r.previousCheckout(k) orelse break :blk null; if (x.len == r.format().hexLength()) if (Id.parseHex(r.format(), x)) |id| break :blk id else |_| {}; break :blk try r.nameValue(x, e.text, hint); }, };}
/// The object ID a ref of exactly this name holds, through symbolic refs.fn exactRef(r: *Resolver, full: []const u8) Error!?Id { var res = try r.store().resolve(r.io, r.gpa, full, null); defer res.deinit(r.gpa); return switch (res.outcome) { .direct => |d| d.id, else => null, };}
/// What a name names: a whole object ID, a ref, an abbreviation, or/// describe output. `typed` is how the caller wrote it, for warnings.fn nameValue(r: *Resolver, name: []const u8, typed: []const u8, hint: abbrev.Hint) Error!?Id { const format_ = r.format(); if (name.len == format_.hexLength() and abbrev.isHex(name)) { const id = Id.parseHex(format_, name) catch unreachable; if ((try r.warnAmbiguous())) { const saved = r.quiet; r.quiet = true; const found = r.dwim(name, typed); r.quiet = saved; if (try found) |_| { // git gives this one however quietly it was asked. if (try r.objectNameAdvice()) try r.say(.warning, "refname '{s}' is ambiguous.\n" ++ object_name_advice, .{typed}) else try r.say(.warning, "refname '{s}' is ambiguous.", .{typed}); } } return id; } if (try r.dwim(name, typed)) |f| { if ((try r.warnAmbiguous()) and name.len >= abbrev.min_length and abbrev.isHex(name)) { if (abbrev.Prefix.parse(format_, name)) |prefix| { var res = try r.lookup.resolve(r.io, prefix, .none, null); switch (res) { .found => try r.sayQuietly("refname '{s}' is ambiguous.", .{typed}), .missing => {}, .ambiguous => |*a| a.deinit(r.gpa), } } else |_| {} } return f.id; } if (name.len >= abbrev.min_length and name.len <= object.max_hex_length and abbrev.isHex(name)) return r.abbreviation(name, hint); if (describedHex(name)) |hex| return r.abbreviation(hex, .committish); return null;}
const object_name_advice = \\Git normally never creates a ref that ends with 40 hex characters \\because it will be ignored when you just specify 40-hex. These refs \\may be created by mistake. For example, \\ \\ git switch -c $br $(git rev-parse ...) \\ \\where "$br" is somehow empty and a 40-hex ref is created. Please \\examine these refs and maybe delete them. Turn this message off by \\running "git config set advice.objectNameWarning false";
fn abbreviation(r: *Resolver, hex: []const u8, hint: abbrev.Hint) Error!?Id { const prefix = abbrev.Prefix.parse(r.format(), hex) catch return null; const res = try r.lookup.resolve(r.io, prefix, hint, null); switch (res) { .found => |id| return id, .missing => return null, .ambiguous => |a| { r.clearAmbiguity(); r.ambiguity = .{ .prefix = prefix, .ambiguity = a }; return error.AmbiguousObjectName; }, }}
/// The abbreviated object ID at the end of `git describe` output:/// `<anything>-<digits>-g<at least four hexadecimal digits>`.fn describedHex(name: []const u8) ?[]const u8 { const g = std.mem.findLast(u8, name, "-g") orelse return null; const hex = name[g + 2 ..]; if (hex.len < abbrev.min_length or !abbrev.isHex(hex)) return null; const before = name[0..g]; const dash = std.mem.findScalarLast(u8, before, '-') orelse return null; const digits = before[dash + 1 ..]; if (digits.len == 0) return null; for (digits) |c| if (!std.ascii.isDigit(c)) return null; if (dash == 0) return null; return hex;}
/// The ref a short name stands for by gitrevisions(7)'s rules: the first/// that finds one, warning, as git does, when more do, and of broken and/// dangling refs on the way.fn dwim(r: *Resolver, name: []const u8, typed: []const u8) Error!?Found { var found: ?Found = null; var count: usize = 0; var candidate: std.ArrayList(u8) = .empty; defer candidate.deinit(r.gpa); for (refname.lookup_rules) |rule| { candidate.clearRetainingCapacity(); try candidate.appendSlice(r.gpa, rule.prefix); try candidate.appendSlice(r.gpa, name); try candidate.appendSlice(r.gpa, rule.suffix); const c = candidate.items; if (!refname.isValid(c, .{ .allow_onelevel = true })) continue; var res = try r.store().resolve(r.io, r.gpa, c, null); defer res.deinit(r.gpa); const under_refs = std.mem.startsWith(u8, c, "refs/"); switch (res.outcome) { .direct => |d| { count += 1; if (found == null) { found = .{ .full = try r.arena.allocator().dupe(u8, res.last()), .id = d.id }; } if (!(try r.warnAmbiguous())) break; }, .missing => if (res.isSymbolic() and under_refs) try r.say(.warning, "ignoring dangling symref {s}", .{c}), .malformed => if (under_refs) try r.say(.warning, "ignoring broken ref {s}", .{c}), else => {}, } } if (count > 1 and (try r.warnAmbiguous())) try r.sayQuietly("refname '{s}' is ambiguous.", .{typed}); return found;}
/// The branch or commit `@{-<n>}` names: the place `HEAD` was moved from/// by the `n`th checkout before the last, newest first. A slice of the/// arena.fn previousCheckout(r: *Resolver, n: u64) Error!?[]const u8 { var log = try r.store().reflog(r.io, r.gpa, "HEAD", null) orelse return null; defer log.deinit(); const marker = "checkout: moving from "; var seen: u64 = 0; var i = log.entries.len; while (i > 0) { i -= 1; const m = log.entries[i].message; if (!std.mem.startsWith(u8, m, marker)) continue; const rest = m[marker.len..]; const to = std.mem.find(u8, rest, " to ") orelse continue; seen += 1; if (seen == n) return try r.arena.allocator().dupe(u8, rest[0..to]); } return null;}
/// The branch `HEAD` names, without `refs/heads/`, or null when detached.fn currentBranch(r: *Resolver) Error!?[]const u8 { var res = try r.store().resolve(r.io, r.gpa, "HEAD", null); defer res.deinit(r.gpa); if (!res.isSymbolic()) return null; const last = res.chain[1]; const prefix = "refs/heads/"; if (!std.mem.startsWith(u8, last, prefix)) return null; return try r.arena.allocator().dupe(u8, last[prefix.len..]);}
// -- upstream and push ---------------------------------------------------------------
/// The full name of the ref a ref expression with `@{upstream}` or/// `@{push}` names.fn trackingName(r: *Resolver, e: revision.RefExpression, diag: ?*Diagnostics) Error![]const u8 { const config = &r.repo.config; // git reads push.default whenever it reads a remote. _ = try tracking.PushDefault.read(config, diag); try tracking.checkRemotes(config, diag); try r.refuseLegacyRemotes(diag); var branch: []const u8 = switch (e.start) { .current => try r.currentBranch() orelse return fail(diag, error.DetachedHead, "HEAD does not point to a branch", .{}), .name => |n| if (std.mem.eql(u8, n, "HEAD")) try r.currentBranch() orelse return fail(diag, error.DetachedHead, "HEAD does not point to a branch", .{}) else n, .previous => |k| try r.previousCheckout(k) orelse return error.RevisionNotFound, }; var full: []const u8 = ""; for (e.tracking, 0..) |t, i| { if (i > 0) { // Only a branch here has an upstream or a push branch. const prefix = "refs/heads/"; if (!std.mem.startsWith(u8, full, prefix)) return error.RevisionNotFound; branch = full[prefix.len..]; } const local = try std.mem.concat(r.arena.allocator(), u8, &.{ "refs/heads/", branch }); // git looks for the branch except where push.default needs nothing // of it. const checks = t == .upstream or try tracking.pushNeedsBranch(config, r.arena.allocator(), branch, diag); if (checks and !try r.store().exists(r.io, r.gpa, local, null)) return fail(diag, error.NoSuchBranch, "no such branch: '{s}'", .{branch}); full = switch (t) { .upstream => try r.upstreamName(branch, diag), .push => try tracking.push(Error, config, r.arena.allocator(), branch, PushContext{ .r = r, .diag = diag }, PushContext.upstreamOf, diag), }; } return full;}
/// Refuses a repository with remotes in `remotes/` or `branches/`, the/// files git read remotes from before the configuration, which could/// change what a remote means.fn refuseLegacyRemotes(r: *Resolver, diag: ?*Diagnostics) Error!void { const loc = &r.repo.location; for ([_][]const u8{ "remotes", "branches" }) |name| { const path = try std.fs.path.join(r.gpa, &.{ loc.common_dir, name }); defer r.gpa.free(path); var dir = r.repo.work_dir.openDir(r.io, path, .{ .iterate = true }) catch |err| switch (err) { error.Canceled => |e| return e, else => continue, }; defer dir.close(r.io); var it = dir.iterate(); const any = it.next(r.io) catch null; if (any != null) return fail(diag, error.UnsupportedLegacyRemote, "a repository with remotes in its {s}/ directory", .{name}); }}
const PushContext = struct { r: *Resolver, diag: ?*Diagnostics,
fn upstreamOf(c: PushContext, branch: []const u8) Error![]u8 { return c.r.arena.allocator().dupe(u8, try c.r.upstreamName(branch, c.diag)); }};
/// The full name of a branch's upstream.fn upstreamName(r: *Resolver, branch: []const u8, diag: ?*Diagnostics) Error![]const u8 { const up = try tracking.upstream(&r.repo.config, r.arena.allocator(), branch, diag); return switch (up) { .tracking => |t| t, .local => |merge| blk: { const saved = r.quiet; r.quiet = true; defer r.quiet = saved; // A merge value that is no ref here names nothing as an // upstream, and differs from every push branch. const f = try r.dwim(merge, merge) orelse break :blk merge; break :blk f.full; }, };}
// -- reflogs ---------------------------------------------------------------------
/// The value a reflog selector names, with git's warnings where the/// entries do not join up. With the entries `E[0]` (oldest) to `E[k-1]`:////// - `@{0}` is the ref's current value, with a warning that the log/// ended unexpectedly if `E[k-1]` did not leave it there./// - `@{n}`, for `n < k`, is `E[k-1-n]`'s new value, with a warning of a/// gap if the entry after it did not start from there; `@{k}` is/// `E[0]`'s old value, unless the ref was created there; beyond that/// the log is too short./// - A date finds the newest entry made at or before it and answers as/// its count would, except that exactly the newest entry's time is that/// entry's new value; before every entry it is `E[0]`'s old value, or/// its new one if the ref was created there, with a warning that the/// log goes back no further.fn reflogValue(r: *Resolver, e: revision.RefExpression, selector: revision.Selector, diag: ?*Diagnostics) Error!Id { if (selector == .empty) { // Only a detached HEAD's commit, which has no reflog of its own. const x = try r.previousCheckout(e.start.previous) orelse return error.RevisionNotFound; if (x.len == r.format().hexLength()) if (Id.parseHex(r.format(), x)) |id| return id else |_| {}; return error.RevisionNotFound; } var log_ref: []const u8 = undefined; var display: []const u8 = undefined; if (e.tracking.len > 0) { log_ref = try r.trackingName(e, diag); display = log_ref; } else switch (e.start) { .current => if (try r.currentBranch()) |b| { log_ref = try std.mem.concat(r.arena.allocator(), u8, &.{ "refs/heads/", b }); display = b; } else { log_ref = "HEAD"; display = "HEAD"; }, .name => |n| { log_ref = try r.dwimLog(n) orelse return error.RevisionNotFound; display = e.text; }, .previous => |k| { const x = try r.previousCheckout(k) orelse return error.RevisionNotFound; log_ref = try r.dwimLog(x) orelse return error.RevisionNotFound; display = e.text; }, } var log = try r.store().reflog(r.io, r.gpa, log_ref, null) orelse return error.RevisionNotFound; defer log.deinit(); const entries = log.entries; const k = entries.len;
var t: i64 = undefined; var stop: ?usize = null; switch (selector) { .nth => |n| { if (n == 0) return r.reflogCurrent(log_ref, entries); if (k == 0) return reflogEmpty(log_ref, diag); if (n < k) { stop = k - 1 - @as(usize, @intCast(n)); } else { if (n == k and !isZero(entries[0].old)) return entries[0].old; return fail(diag, error.ReflogTooShort, "log for '{s}' only has {d} entries", .{ display, k }); } }, .date => |text| { const parsed = date.approximate(text, r.options.now, r.options.zone); if (parsed.failed) return error.RevisionNotFound; if (k == 0) return reflogEmpty(log_ref, diag); t = parsed.timestamp; var i = k; while (i > 0) { i -= 1; if (entries[i].date.seconds <= t) { stop = i; break; } } if (stop == null) { try r.sayQuietly("log for '{s}' only goes back to {f}", .{ display, r.rfc(entries[0].date) }); return if (isZero(entries[0].old)) entries[0].new else entries[0].old; } if (stop.? == k - 1) { if (entries[k - 1].date.seconds == t) return entries[k - 1].new; return r.reflogCurrent(log_ref, entries); } }, .empty => unreachable, } const s = stop.?; const v = entries[s].new; if (!Id.eql(entries[s + 1].old, v)) try r.say(.warning, "log for ref {s} has gap after {f}", .{ log_ref, r.rfc(entries[s].date) }); return v;}
fn reflogCurrent(r: *Resolver, log_ref: []const u8, entries: []const RefStore.ReflogEntry) Error!Id { const current = try r.exactRef(log_ref) orelse return error.RevisionNotFound; if (entries.len > 0 and !Id.eql(entries[entries.len - 1].new, current)) try r.say(.warning, "log for ref {s} unexpectedly ended on {f}", .{ log_ref, r.rfc(entries[entries.len - 1].date) }); return current;}
fn reflogEmpty(log_ref: []const u8, diag: ?*Diagnostics) Error { return fail(diag, error.ReflogEmpty, "log for {s} is empty", .{log_ref});}
/// The ref whose reflog a name means: the first the lookup rules find/// that has one.fn dwimLog(r: *Resolver, name: []const u8) Error!?[]const u8 { var candidate: std.ArrayList(u8) = .empty; defer candidate.deinit(r.gpa); for (refname.lookup_rules) |rule| { candidate.clearRetainingCapacity(); try candidate.appendSlice(r.gpa, rule.prefix); try candidate.appendSlice(r.gpa, name); try candidate.appendSlice(r.gpa, rule.suffix); if (!refname.isValid(candidate.items, .{ .allow_onelevel = true })) continue; var log = try r.store().reflog(r.io, r.gpa, candidate.items, null) orelse continue; log.deinit(); return try r.arena.allocator().dupe(u8, candidate.items); } return null;}
/// A date as git shows it in reflog warnings: RFC 2822, in the zone it/// was recorded in.fn rfc(r: *Resolver, d: @import("Identity.zig").Date) DateText { return .{ .date = d, .zone = r.options.zone, .now = r.options.now };}
const DateText = struct { date: @import("Identity.zig").Date, zone: *const date.Zone, now: i64,
pub fn format(t: DateText, w: *Io.Writer) Io.Writer.Error!void { date.format(w, t.date, .{ .style = .rfc }, .{ .now = t.now, .zone = t.zone }, null) catch |err| switch (err) { error.WriteFailed => return error.WriteFailed, else => try w.print("{d}", .{t.date.seconds}), }; }};
fn isZero(id: Id) bool { for (id.slice()) |b| if (b != 0) return false; return true;}
// -- suffixes --------------------------------------------------------------------
/// `id` peeled as `^{<kind>}` peels it — through tags, and from a commit/// to its tree — or null if it leads to no object of that kind or to an/// object the database does not hold. Says nothing in `messages`.pub fn peel(r: *Resolver, id: Id, kind: Kind) Error!?Id { const saved = r.quiet; r.quiet = true; defer r.quiet = saved; return r.applySuffix(id, .{ .step = .{ .peel = .{ .kind = kind } }, .end = 0 }, "", null);}
/// `id` peeled to a commit, as `<rev>^{commit}` is.pub fn peelToCommit(r: *Resolver, id: Id) Error!?Id { return r.peel(id, .commit);}
/// `id` peeled to a tree, as `<rev>^{tree}` is: a commit's tree, or a/// tree a tag leads to.pub fn peelToTree(r: *Resolver, id: Id) Error!?Id { return r.peel(id, .tree);}
fn applySuffix(r: *Resolver, id: Id, s: revision.Suffix, text: []const u8, diag: ?*Diagnostics) Error!?Id { // git reads the grafts and the shallow file as it first reads a // commit as a commit, which every suffix but `^{object}` may do. if (!(s.step == .peel and s.step.peel == .object)) _ = try r.grafts(diag); switch (s.step) { .parent => |n| { const c = try r.commitOf(id) orelse return null; if (n == 0) return c; return r.parentOf(c, n); }, .ancestor => |n| { var c = try r.commitOf(id) orelse return null; var i: u64 = 0; while (i < n) : (i += 1) c = try r.parentOf(c, 1) orelse return null; return c; }, .peel => |p| switch (p) { .any => { var cur = id; while (true) { const k = try r.kindOf(cur) orelse return null; if (k != .tag) return cur; cur = try r.tagTarget(cur) orelse return null; } }, .object => return if (try r.kindOf(id) != null) id else null, .kind => |want| { var cur = id; while (true) { const k = try r.kindOf(cur) orelse return null; if (k == want) return cur; cur = switch (k) { .tag => try r.tagTarget(cur) orelse return null, .commit => try r.treeOf(cur) orelse return null, else => { try r.say(.@"error", "{s}: expected {s} type, but the object dereferences to {s} type", .{ text[0..s.end], want.name(), k.name() }); return null; }, }; } }, .search => |wanted| { const c = try r.commitOf(id) orelse return null; return r.search(&.{c}, wanted, diag); }, }, }}
/// `id` peeled through tags to a commit, or null, with git's complaint/// when it leads to something else.fn commitOf(r: *Resolver, id: Id) Error!?Id { var cur = id; while (true) { const k = try r.kindOf(cur) orelse return null; switch (k) { .commit => return cur, .tag => cur = try r.tagTarget(cur) orelse return null, else => { try r.say(.@"error", "object {f} is a {s}, not a commit", .{ id, k.name() }); return null; }, } }}
/// The contents of an object, or null if the database does not hold it.fn readObject(r: *Resolver, id: Id) Error!?ObjectDatabase.Object { return r.db().read(r.io, r.gpa, id, null) catch |err| switch (err) { error.ObjectNotFound => return null, else => |e| return e, };}
fn tagTarget(r: *Resolver, id: Id) Error!?Id { var o = try r.readObject(id) orelse return null; defer o.deinit(r.gpa); return Tag.targetOf(id.object_format, o.data);}
fn treeOf(r: *Resolver, id: Id) Error!?Id { var o = try r.readObject(id) orelse return null; defer o.deinit(r.gpa); return Commit.treeOf(id.object_format, o.data);}
/// The grafts and the shallow boundary, read the first time they are/// needed, with what git says of them kept in `messages`.fn grafts(r: *Resolver, diag: ?*Diagnostics) Error!*const Grafts { const g = r.repo.grafts(r.io, diag) catch |err| switch (err) { error.BadShallowLine => { var line: [Diagnostics.max_detail_len]u8 = undefined; const d = diag orelse return err; const text = line[0..d.detail().len]; @memcpy(text, d.detail()); return fail(diag, error.BadShallowLine, "bad shallow line: {s}", .{text}); }, else => |e| return e, }; if (!g.reported) { g.reported = true; var text: Io.Writer.Allocating = .init(r.arena.allocator()); if (!r.repo.replacements.reported) { r.repo.replacements.reported = true; try r.repo.replacements.writeWarnings(&text.writer); } const hint = (r.repo.boolSetting("advice.graftFileDeprecated", null) catch null) orelse true; try g.writeWarnings(&text.writer, hint); if (text.written().len != 0) try r.messages.append(r.gpa, .{ .level = .raw, .text = text.written() }); } return g;}
fn parentOf(r: *Resolver, id: Id, n: u64) Error!?Id { if ((try r.grafts(null)).lookup(id)) |g| { if (try r.kindOf(id) == null) return null; return if (n >= 1 and n <= g.parents.len) g.parents[@intCast(n - 1)] else null; } var o = try r.readObject(id) orelse return null; defer o.deinit(r.gpa); const c = Commit.parse(id.object_format, o.data) catch return null; var it = c.parents(); var i: u64 = 0; while (it.next()) |p| { i += 1; if (i == n) return p; } return null;}
// -- message searches -------------------------------------------------------------
/// `:/<text>`: from every ref, then `HEAD`.fn searchAll(r: *Resolver, s: revision.Search, diag: ?*Diagnostics) Error!?Id { var starts: std.ArrayList(Id) = .empty; defer starts.deinit(r.gpa); var list = try r.store().list(r.io, r.gpa, "refs/", null); defer list.deinit(); for (list.entries) |entry| { var res = try r.store().resolve(r.io, r.gpa, entry.name, null); defer res.deinit(r.gpa); switch (res.outcome) { .direct => |d| try starts.append(r.gpa, d.id), else => {}, } } if (try r.exactRef("HEAD")) |h| try starts.append(r.gpa, h); return r.search(starts.items, s, diag);}
const Queued = struct { id: Id, date: i64, order: u64,
fn before(a: Queued, b: Queued) bool { if (a.date != b.date) return a.date > b.date; return a.order > b.order; }};
/// The first commit, newest first from `starts`, whose message matches.fn search(r: *Resolver, starts: []const Id, s: revision.Search, diag: ?*Diagnostics) Error!?Id { var what: []const u8 = ""; var pattern = regex.Regex.compile(r.gpa, s.pattern, &what) catch |err| switch (err) { error.InvalidPattern => return null, error.Unsupported => return fail(diag, error.UnsupportedRegex, "a search pattern using '{s}'", .{what}), error.OutOfMemory => |e| return e, }; defer pattern.deinit();
_ = try r.grafts(diag); var heap: std.ArrayList(Queued) = .empty; defer heap.deinit(r.gpa); var seen: std.AutoHashMapUnmanaged([object.max_byte_length]u8, void) = .empty; defer seen.deinit(r.gpa); var order: u64 = 0; // Every starting point goes in, the same commit twice if two refs // name it, so that the one named last comes out first; a commit is // looked at once, and a parent goes in only if nothing has put it in // already. for (starts) |id| { const c = try r.peelToCommitQuietly(id) orelse continue; try r.enqueue(&heap, &seen, c, &order, true); } var looked: std.AutoHashMapUnmanaged([object.max_byte_length]u8, void) = .empty; defer looked.deinit(r.gpa); while (heap.items.len > 0) { const top = popHeap(&heap); if ((try looked.getOrPut(r.gpa, top.id.bytes)).found_existing) continue; var o = try r.readObject(top.id) orelse continue; defer o.deinit(r.gpa); const message = if (std.mem.find(u8, o.data, "\n\n")) |b| o.data[b + 2 ..] else ""; if (try pattern.matches(r.gpa, message) != s.negate) return top.id; if ((try r.grafts(diag)).lookup(top.id)) |g| { for (g.parents) |p| try r.enqueue(&heap, &seen, p, &order, false); continue; } const c = Commit.parse(top.id.object_format, o.data) catch continue; var it = c.parents(); while (it.next()) |p| try r.enqueue(&heap, &seen, p, &order, false); } return null;}
fn peelToCommitQuietly(r: *Resolver, id: Id) Error!?Id { const saved = r.quiet; r.quiet = true; defer r.quiet = saved; return r.commitOf(id);}
fn enqueue(r: *Resolver, heap: *std.ArrayList(Queued), seen: *std.AutoHashMapUnmanaged([object.max_byte_length]u8, void), id: Id, order: *u64, again: bool) Error!void { const gop = try seen.getOrPut(r.gpa, id.bytes); if (gop.found_existing and !again) return; var o = try r.readObject(id) orelse return; defer o.deinit(r.gpa); if (o.kind != .commit) return; const c = Commit.parse(id.object_format, o.data) catch return; const when: i64 = if (c.committer()) |who| (if (who.date()) |d| d.seconds else 0) else 0; order.* += 1; try heap.append(r.gpa, .{ .id = id, .date = when, .order = order.* }); var i = heap.items.len - 1; while (i > 0) { const parent = (i - 1) / 2; if (!heap.items[i].before(heap.items[parent])) break; std.mem.swap(Queued, &heap.items[i], &heap.items[parent]); i = parent; }}
fn popHeap(heap: *std.ArrayList(Queued)) Queued { const items = heap.items; const top = items[0]; items[0] = items[items.len - 1]; heap.shrinkRetainingCapacity(items.len - 1); const h = heap.items; var i: usize = 0; while (true) { const left = 2 * i + 1; const right = left + 1; var best = i; if (left < h.len and h[left].before(h[best])) best = left; if (right < h.len and h[right].before(h[best])) best = right; if (best == i) break; std.mem.swap(Queued, &h[i], &h[best]); i = best; } return top;}
// -- paths -----------------------------------------------------------------------
/// A path as git looks it up: `./` and `../` made relative to the top of/// the working tree from the caller's directory, anything else as/// written. Returns whether it was relative.fn lookupPath(r: *Resolver, path: []const u8, diag: ?*Diagnostics) Error!struct { path: []const u8, relative: bool } { if (!std.mem.startsWith(u8, path, "./") and !std.mem.startsWith(u8, path, "../")) return .{ .path = path, .relative = false }; const loc = &r.repo.location; const prefix = loc.prefix orelse return fail(diag, error.RelativePathOutsideWorkTree, "relative path syntax can't be used outside working tree", .{}); const a = r.arena.allocator(); var parts: std.ArrayList([]const u8) = .empty; var it = std.mem.splitScalar(u8, prefix, '/'); while (it.next()) |p| if (p.len != 0) try parts.append(a, p); var rest = std.mem.splitScalar(u8, path, '/'); while (rest.next()) |p| { if (p.len == 0 or std.mem.eql(u8, p, ".")) continue; if (std.mem.eql(u8, p, "..")) { if (parts.items.len == 0) return fail(diag, error.PathOutsideRepository, "'{s}' is outside repository at '{s}'", .{ path, loc.work_tree orelse "" }); _ = parts.pop(); continue; } try parts.append(a, p); } var out: std.ArrayList(u8) = .empty; for (parts.items, 0..) |p, i| { if (i != 0) try out.append(a, '/'); try out.appendSlice(a, p); } if (std.mem.endsWith(u8, path, "/") and out.items.len != 0) try out.append(a, '/'); return .{ .path = out.items, .relative = true };}
/// The entry at `path` in the tree `id` leads to: its object ID and mode,/// or null.fn findInTree(r: *Resolver, id: Id, path: []const u8) Error!?struct { id: Id, mode: u32 } { var cur = id; // Peel to a tree. while (true) { const k = try r.kindOf(cur) orelse return null; switch (k) { .tree => break, .tag => cur = try r.tagTarget(cur) orelse return null, .commit => cur = try r.treeOf(cur) orelse return null, .blob => return null, } } if (path.len == 0) return .{ .id = cur, .mode = 0o40000 }; var mode: u32 = 0o40000; var components = std.mem.splitScalar(u8, path, '/'); while (components.next()) |name| { if (name.len == 0) { // Only a final `/`, after a tree, is allowed. if (components.peek() == null and mode == 0o40000) break; return null; } if (mode != 0o40000) return null; var o = try r.readObject(cur) orelse return null; defer o.deinit(r.gpa); if (o.kind != .tree) return null; var entries = tree.iterate(cur.object_format, o.data); const entry = while (entries.next() catch return null) |e| { if (std.mem.eql(u8, e.name, name)) break e; } else return null; cur = entry.id; mode = @backingInt(entry.mode()); } return .{ .id = cur, .mode = mode };}
fn treeEntry(r: *Resolver, rev: *const revision.Revision, id: Id, path: []const u8, diag: ?*Diagnostics) Error!Resolved { const p = try r.lookupPath(path, diag); if (try r.findInTree(id, p.path)) |found| return .{ .id = found.id, .kind = r.kindIfReadable(found.id), .path = p.path, .mode = found.mode, }; const rev_text = rev.revText(); const prefix = r.repo.location.prefix orelse ""; if (!p.relative and r.options.explain == .rev_parse and prefix.len > 0) { const full = try std.mem.concat(r.arena.allocator(), u8, &.{ prefix, p.path }); if (try r.findInTree(id, full) != null) return fail(diag, error.PathNotInTree, "path '{s}' exists, but not '{s}'\nhint: Did you mean '{s}:{s}' aka '{s}:./{s}'?", .{ full, p.path, rev_text, full, rev_text, p.path }); } if (try r.onDisk(p.path)) return fail(diag, error.PathNotInTree, "path '{s}' exists on disk, but not in '{s}'", .{ p.path, rev_text }); return fail(diag, error.PathNotInTree, "path '{s}' does not exist in '{s}'", .{ p.path, rev_text });}
/// Whether `path`, from the top of the working tree, is there.fn onDisk(r: *Resolver, path: []const u8) Error!bool { if (r.repo.location.work_tree == null or path.len == 0) return false; _ = r.repo.work_dir.statFile(r.io, path, .{ .follow_symlinks = false }) catch |err| switch (err) { error.Canceled => |e| return e, else => return false, }; return true;}
fn loadIndex(r: *Resolver, diag: ?*Diagnostics) Error!*Index { if (r.index == null) { var ix = try r.repo.readIndex(r.io, r.options.index, diag); errdefer ix.deinit(); if (ix.hasSparseDirectories()) try ix.expandSparseDirectories(r.io, r.db(), diag); r.index = ix; } return &r.index.?;}
fn indexEntry(r: *Resolver, stage: u2, path: []const u8, explicit_stage: bool, diag: ?*Diagnostics) Error!Resolved { const p = try r.lookupPath(path, diag); const ix = try r.loadIndex(diag); if (ix.get(p.path, stage)) |e| return .{ .id = e.id, .kind = r.kindIfReadable(e.id), .path = p.path, .mode = e.mode, }; // rev-parse explains a path without a stage only when it begins with // a letter or a digit: not `./x`, `../x`, `!x` or `.x`. if (r.options.explain == .rev_parse and !explicit_stage and (path.len == 0 or !std.ascii.isAlphanumeric(path[0]))) return error.RevisionNotFound; const there = ix.stages(p.path); if (there.len > 0) return fail(diag, error.PathNotInIndex, "path '{s}' is in the index, but not at stage {d}\nhint: Did you mean ':{d}:{s}'?", .{ p.path, stage, there[0].stage(), p.path }); const prefix = r.repo.location.prefix orelse ""; if (!p.relative and r.options.explain == .rev_parse and prefix.len > 0) { const full = try std.mem.concat(r.arena.allocator(), u8, &.{ prefix, p.path }); if (ix.get(full, stage) != null) return fail(diag, error.PathNotInIndex, "path '{s}' is in the index, but not '{s}'\nhint: Did you mean ':{d}:{s}' aka ':{d}:./{s}'?", .{ full, p.path, stage, full, stage, p.path }); } if (try r.onDisk(p.path)) { if (r.options.explain == .rev_parse) return error.RevisionNotFound; return fail(diag, error.PathNotInIndex, "path '{s}' exists on disk, but not in the index", .{p.path}); } return fail(diag, error.PathNotInIndex, "path '{s}' does not exist (neither on disk nor in the index)", .{p.path});}
// -- full names ------------------------------------------------------------------
/// The full ref name `text` stands for, as `rev-parse/// --symbolic-full-name` prints it, for a text already resolved.pub fn fullName(r: *Resolver, text: []const u8, diag: ?*Diagnostics) Error!FullName { r.setting_diag = diag; var rev = revision.parse(r.gpa, text) catch |err| switch (err) { error.InvalidRevision => return .none, error.OutOfMemory => |e| return e, }; defer rev.deinit(r.gpa); if (rev.suffixes.len > 0 or rev.path != null) return .none; const e = switch (rev.base) { .ref => |e| e, else => return .none, }; if (e.tracking.len > 0) return .{ .name = try r.trackingName(e, diag) }; const name = switch (e.start) { .current => return .none, .name => |n| n, .previous => |k| try r.previousCheckout(k) orelse return .none, }; const saved = r.quiet; r.quiet = true; defer r.quiet = saved; const counted = try r.countRefs(name); if (counted.count == 0) return .none; if (counted.count > 1 and (try r.warnAmbiguous())) return .{ .ambiguous = e.text }; return .{ .name = counted.first.? };}
fn countRefs(r: *Resolver, name: []const u8) Error!struct { count: usize, first: ?[]const u8 } { var count: usize = 0; var first: ?[]const u8 = null; var candidate: std.ArrayList(u8) = .empty; defer candidate.deinit(r.gpa); for (refname.lookup_rules) |rule| { candidate.clearRetainingCapacity(); try candidate.appendSlice(r.gpa, rule.prefix); try candidate.appendSlice(r.gpa, name); try candidate.appendSlice(r.gpa, rule.suffix); const c = candidate.items; if (!refname.isValid(c, .{ .allow_onelevel = true })) continue; var res = try r.store().resolve(r.io, r.gpa, c, null); defer res.deinit(r.gpa); if (res.outcome != .direct) continue; count += 1; if (first == null) first = try r.arena.allocator().dupe(u8, res.last()); } return .{ .count = count, .first = first };}
test { _ = revision; _ = tracking;}
// -- tests -----------------------------------------------------------------------
const testing = std.testing;const fixture = @import("testing/fixture.zig");const discover = @import("discover.zig");
test "the library resolves what git rev-parse resolves" { const gpa = testing.allocator; var repo = try fixture.Repo.init(gpa, testing.io); defer repo.deinit(); try repo.tmp.dir.createDirPath(testing.io, "src"); try repo.commitFile("src/a", "1\n", "first"); try repo.git(&.{ "tag", "-a", "-m", "t", "v1" }); try repo.commitFile("src/a", "2\n", "second: the needle"); try repo.commitFile("src/a", "3\n", "third"); try repo.git(&.{ "branch", "side", "HEAD~1" }); const start = try std.fs.path.join(gpa, &.{ repo.path, "src" }); defer gpa.free(start); var r = try Repository.discoverAndOpen(gpa, testing.io, .{ .start = start, .ownership = if (discover.effectiveUid()) |uid| .{ .uid = uid } else null, }, .{ .environment = &repo.env }, null); defer r.close(testing.io); var resolver: Resolver = .init(gpa, testing.io, &r, .{ .now = 1_790_001_000, .zone = &date.Zone.utc }); defer resolver.deinit(); for ([_][]const u8{ "HEAD", "HEAD~2", "v1", "v1^{}", "side^{tree}", ":/needle", "HEAD:src/a", "HEAD:./a", ":./a", "main@{1}", "@{2}" }) |text| { errdefer std.debug.print("{s}\n", .{text}); const out = try repo.runIn("src", &.{ "rev-parse", "--verify", text }, ""); defer out.deinit(gpa); const got = try resolver.resolve(text, null); var buffer: [object.max_hex_length]u8 = undefined; try testing.expectEqualStrings(std.mem.trimEnd(u8, out.stdout, "\n"), got.id.toHex(&buffer)); } const found = try resolver.resolve("HEAD:./a", null); try testing.expectEqualStrings("src/a", found.path.?); try testing.expectEqual(0o100644, found.mode.?); try testing.expectEqual(Kind.blob, found.kind.?); var diag: Diagnostics = .{}; try testing.expectError(error.PathNotInTree, resolver.resolve("HEAD:nothere", &diag)); try testing.expectEqualStrings("path 'nothere' does not exist in 'HEAD'", diag.detail()); try testing.expectError(error.UnsupportedRange, resolver.resolve("HEAD~1..HEAD", &diag)); try testing.expectError(error.ReflogTooShort, resolver.resolve("main@{9}", &diag)); try testing.expectEqualStrings("log for 'main' only has 3 entries", diag.detail()); try testing.expectEqualStrings("refs/heads/side", (try resolver.fullName("side", null)).name); const tag = (try resolver.resolve("v1", null)).id; const tree_of_tag = (try resolver.peelToTree(tag)).?; try testing.expect(Id.eql(tree_of_tag, (try resolver.resolve("v1^{tree}", null)).id)); try testing.expectEqual(null, try resolver.peel(tree_of_tag, .commit));}