Git API
Git repository helpers are available under ptool.git and p.git.
This module is backed by git2 / libgit2, not by invoking the git
command-line tool.
Mutating operations accept confirm = true in their options table to ask the
user for confirmation before the action runs. Destructive operations also use
separate safety flags such as force = true; confirmation never replaces the
required safety flag.
Unknown option names are rejected instead of being silently ignored.
ptool.git.init
Unreleased- Introduced.
ptool.git.init(path?, options?) initializes a repository and returns a Repo
object.
Options:
bare(boolean): Create a bare repository. Defaults tofalse.initial_head(string): Initial branch name, for example"main".confirm(boolean): Ask before creating the repository.
Relative paths are resolved from the current ptool runtime directory.
local repo = p.git.init("tmp/project", {
initial_head = "main",
})
ptool.git.open
v0.6.0- Introduced.
ptool.git.open(path?) opens a repository directly and returns a Repo
object.
Arguments:
path(string, optional): Repository path. If omitted, the currentptoolruntime directory is used.
Behavior:
- Relative paths are resolved from the current
ptoolruntime directory, so they followptool.cd(...). - This does not search parent directories. Use
ptool.git.discover(...)when you want repository discovery behavior.
Example:
local repo = ptool.git.open(".")
print(repo:path())
ptool.git.discover
v0.6.0- Introduced.
ptool.git.discover(path?) finds a repository starting from path and walking
up parent directories, then returns a Repo object.
Arguments:
path(string, optional): Starting path. If omitted, the currentptoolruntime directory is used.
Behavior:
- Relative paths are resolved from the current
ptoolruntime directory. - This is useful when a script may run from a subdirectory inside a worktree.
Example:
local repo = ptool.git.discover("src")
print(repo:root())
ptool.git.clone
v0.6.0- Introduced.
ptool.git.clone(url, path[, options]) clones a repository and returns a
Repo object for the cloned repository.
Arguments:
url(string, required): Remote repository URL.path(string, required): Destination path.options(table, optional): Clone options. Supported fields:branch(string, optional): Branch name to check out after cloning.bare(boolean, optional): Whether to create a bare repository. Defaults tofalse.depth(integer, optional): Positive shallow-clone depth.checkout(boolean, optional): Whether to check out the selected branch. Defaults totrue.remote(string, optional): Name assigned to the cloned remote. Defaults to"origin".tags(string, optional):"auto","all", or"none". Defaults to"auto".confirm(boolean, optional): Whether to ask for confirmation before cloning. Defaults tofalse.auth(table, optional): Remote authentication settings.
auth fields:
kind(string, required): Authentication mode. Supported values:"default": Use libgit2 default credentials."ssh_agent": Authenticate through the local SSH agent."ssh_key": Authenticate with an SSH private key."userpass": Use a plaintext username and password."credential_helper": Ask the configured Git credential helper.
username(string): Optional for"ssh_agent"and"credential_helper"; required for"ssh_key"and"userpass".private_key(string, required for"ssh_key"): Private key path. Relative paths are resolved from the currentptoolruntime directory.public_key(string, optional): Public key path.passphrase(string, optional): SSH private-key passphrase.password(string, required for"userpass"): Password.
Behavior:
- Relative destination paths are resolved from the current
ptoolruntime directory. - Authentication options are also used by
repo:fetch(...)andrepo:push(...).
Example:
local repo = ptool.git.clone(
"[email protected]:example/project.git",
"tmp/project",
{
branch = "main",
auth = {
kind = "ssh_agent",
},
}
)
print(repo:root())
Repo
v0.6.0- Introduced.
Repo represents an open Git repository handle returned by ptool.git.init(), ptool.git.open(),
ptool.git.discover(), or ptool.git.clone().
It is implemented as a Lua userdata.
Methods are grouped below. Existing repository, status, staging, commit, checkout, switch, fetch, and push methods remain backward compatible. The workflow APIs described in the following sections are currently unreleased.
path
v0.6.0- Introduced.
Canonical API name: ptool.git.Repo:path.
repo:path() returns the repository git directory path.
- Returns:
string.
Notes:
- For a non-bare repository this is typically the
.gitdirectory. - For a bare repository this is the repository directory itself.
root
v0.6.0- Introduced.
Canonical API name: ptool.git.Repo:root.
repo:root() returns the worktree root directory.
- Returns:
string|nil.
Notes:
- This returns
nilfor bare repositories.
is_bare
v0.6.0- Introduced.
Canonical API name: ptool.git.Repo:is_bare.
repo:is_bare() reports whether the repository is bare.
- Returns:
boolean.
head
v0.6.0- Introduced.
Canonical API name: ptool.git.Repo:head.
repo:head() returns HEAD information as a table with:
oid(string|nil): The current commit OID if available.shorthand(string|nil): A short name for HEAD, such as a branch name.detached(boolean): Whether HEAD is detached.unborn(boolean): Whether the repository does not yet have an initial commit.
Example:
local head = repo:head()
print(head.oid)
print(head.detached)
current_branch
v0.6.0- Introduced.
Canonical API name: ptool.git.Repo:current_branch.
repo:current_branch() returns the current local branch name.
- Returns:
string|nil.
Notes:
- This returns
nilwhen HEAD is detached. - This also returns
nilfor an unborn branch before the first commit.
status
v0.6.0- Introduced.
Canonical API name: ptool.git.Repo:status.
repo:status([options]) summarizes repository status and returns a table with:
root(string|nil): The worktree root directory.branch(string|nil): The current local branch name.head(table): The same HEAD information returned byrepo:head().upstream(string|nil): The upstream branch name, when configured.ahead(integer): Number of commits ahead of upstream.behind(integer): Number of commits behind upstream.clean(boolean): Whether the repository has no visible status entries.entries(table): An array of status entry tables.
entries[i] contains:
path(string): Repository-relative path.index_status(string|nil): Index-side status. Supported values currently include"new","modified","deleted","renamed", and"typechange".worktree_status(string|nil): Worktree-side status. Supported values currently include"new","modified","deleted","renamed","typechange", and"ignored".conflicted(boolean): Whether the path is conflicted.ignored(boolean): Whether the path is ignored.
options fields:
include_untracked(boolean, optional): Whether to include untracked files. Defaults totrue.include_ignored(boolean, optional): Whether to include ignored files. Defaults tofalse.recurse_untracked_dirs(boolean, optional): Whether to recurse into untracked directories. Defaults totrue.
Example:
local st = repo:status()
print(st.clean)
print(st.branch)
for _, entry in ipairs(st.entries) do
print(entry.path, entry.index_status, entry.worktree_status)
end
is_clean
v0.6.0- Introduced.
Canonical API name: ptool.git.Repo:is_clean.
repo:is_clean([options]) returns whether the repository is clean.
options(table, optional): The same options accepted byrepo:status(...).- Returns:
boolean.
add
v0.6.0- Introduced.
Canonical API name: ptool.git.Repo:add.
repo:add(paths[, options]) stages one or more paths in the index.
Arguments:
paths(string|string[], required): A path or an array of paths.options(table, optional): Add options. Supported fields:update(boolean, optional): Update only paths already known to the index. Defaults tofalse.confirm(boolean, optional): Whether to ask for confirmation before staging paths. Defaults tofalse.
Behavior:
- Paths are interpreted relative to the repository worktree.
Example:
repo:add("README.md")
repo:add({"src", "Cargo.toml"})
commit
v0.6.0- Introduced.
Canonical API name: ptool.git.Repo:commit.
repo:commit(message[, options]) creates a commit from the current index and
returns the new commit OID.
Arguments:
message(string, required): Commit message.options(table, optional): Commit options. Supported fields:author(table, optional): Author signature.committer(table, optional): Committer signature.amend(boolean, optional): Replace the current HEAD commit. Defaults tofalse.allow_empty(boolean, optional): Permit a commit whose tree is unchanged. Defaults totruefor backward compatibility.confirm(boolean, optional): Whether to ask for confirmation before creating the commit. Defaults tofalse.
Signature fields:
name(string, required)email(string, required)time_seconds(integer, optional): Unix timestamp.offset_minutes(integer, optional): Time-zone offset from UTC.
Behavior:
- When
authorandcommitterare omitted,ptooltries to use the Git repository identity from configuration. - If no identity is configured and no explicit signature is provided, an error is raised.
Example:
local oid = repo:commit("Release v0.7.0", {
author = {
name = "Release Bot",
email = "[email protected]",
},
})
print(oid)
checkout
v0.6.0- Introduced.
Canonical API name: ptool.git.Repo:checkout.
repo:checkout(rev[, options]) checks out a revision.
Arguments:
rev(string, required): Revision expression such as a branch name, tag name, or commit OID.options(table, optional): Checkout options. Supported fields:force(boolean, optional): Whether to force checkout. Defaults tofalse.confirm(boolean, optional): Whether to ask for confirmation before checking out the revision. Defaults tofalse.
Behavior:
- This can detach HEAD when
revdoes not resolve to a named reference.
switch
v0.6.0- Introduced.
Canonical API name: ptool.git.Repo:switch.
repo:switch(branch[, options]) switches HEAD to a local branch.
Arguments:
branch(string, required): Local branch name.options(table, optional): Switch options. Supported fields:create(boolean, optional): Whether to create the branch first. Defaults tofalse.force(boolean, optional): Whether to force the checkout. Defaults tofalse.start_point(string, optional): Revision to branch from whencreate = true. Defaults toHEAD.track(string, optional): Upstream reference for the new branch.orphan(boolean, optional): Create an orphan branch.confirm(boolean, optional): Whether to ask for confirmation before switching branches. Defaults tofalse.
Example:
repo:switch("release")
repo:switch("release-next", {
create = true,
start_point = "origin/main",
})
fetch
v0.6.0- Introduced.
Canonical API name: ptool.git.Repo:fetch.
repo:fetch([remote[, options]]) fetches from a remote and returns transfer
statistics.
Arguments:
remote(string, optional): Remote name. Defaults to"origin".options(table, optional): Fetch options. Supported fields:refspecs(string|string[], optional): One refspec or an array of refspecs.depth(integer, optional): Positive shallow-fetch depth.prune(boolean, optional): Remove stale remote-tracking references.tags(string, optional):"auto","all", or"none".update_fetchhead(boolean, optional): UpdateFETCH_HEAD. Defaults totrue.confirm(boolean, optional): Whether to ask for confirmation before fetching. Defaults tofalse.auth(table, optional): Remote authentication settings. Uses the same structure asptool.git.clone(...).
Returns:
received_objects(integer)indexed_objects(integer)local_objects(integer)total_objects(integer)received_bytes(integer)updated_refs(string[])
Example:
local stats = repo:fetch("origin", {
auth = {
kind = "ssh_agent",
},
})
print(stats.received_objects, stats.received_bytes)
push
v0.6.0- Introduced.
Canonical API name: ptool.git.Repo:push.
repo:push([remote[, refspecs[, options]]]) pushes refs to a remote.
Arguments:
remote(string, optional): Remote name. Defaults to"origin".refspecs(string|string[], optional): One refspec or an array of refspecs.options(table, optional): Push options. Supported fields:force(boolean, optional): Force each push refspec. Defaults tofalse.set_upstream(boolean, optional): Configure the pushed current branch to track the destination remote branch.confirm(boolean, optional): Whether to ask for confirmation before pushing. Defaults tofalse.auth(table, optional): Remote authentication settings. Uses the same structure asptool.git.clone(...).
Behavior:
- When
refspecsis omitted,ptooltries to push the current local branch to the branch of the same name on the remote. - Omitting
refspecswhile HEAD is detached raises an error. - The returned table contains
ok,refspecs, andrejected. Each rejected entry containsreferenceandmessage.
Example:
repo:push("origin", nil, {
auth = {
kind = "ssh_agent",
},
})
repo:push("origin", "refs/heads/main:refs/heads/main")
Git workflow APIs
Unreleased- Introduced.
The APIs in this section cover repository maintenance, releases, history
inspection, collaboration, and CI automation. A string or a dense string array
is accepted wherever a parameter is documented as string|string[].
Shared result tables
Commit information returned by commit_info() and log() contains oid,
message, summary, author, committer, and parent_oids. A signature table
contains name, email, time_seconds, and offset_minutes.
Integration methods return:
{
outcome = "up_to_date" | "fast_forward" | "merged" | "conflicted",
oid = "..." | nil,
conflicts = {
{ path = "file", ancestor = true, ours = true, theirs = true },
},
}
Rebase results use "rebased" or "conflicted" outcomes and add current and
total. Operations that stop on conflicts can
be continued or aborted with the corresponding API.
Repository and status
repo:path() -> string
repo:root() -> string|nil
repo:is_bare() -> boolean
repo:head() -> GitHeadInfo
repo:current_branch() -> string|nil
repo:status(options?) -> GitStatusSummary
repo:is_clean(options?) -> boolean
status() and is_clean() options are include_untracked (default true),
include_ignored, recurse_untracked_dirs (default true), and paths.
GitStatusSummary contains root, branch, head, upstream, ahead,
behind, clean, and entries.
History and diff
repo:resolve(rev) -> GitObjectInfo
repo:commit_info(rev?) -> GitCommitInfo
repo:log(options?) -> GitCommitInfo[]
repo:diff(options?) -> GitDiff
repo:describe(options?) -> string|nil
resolve()returnsoid,kind, andshorthand.commit_info()defaults toHEAD.log()acceptsrev,max_count(default100),skip,first_parent,reverse, andpaths.diff()acceptsfrom,to,cached,paths,context_lines(default3), andfind_renames(defaulttrue). Withoutfromorto, it compares the appropriate worktree, index, or HEAD state.- A diff result contains
patch,files_changed,insertions,deletions, anddeltas. Each delta containsstatus,old_path,new_path, andbinary. describe()acceptsrev,pattern,always,abbrev(default7), anddirty_suffix.
local commits = repo:log({
rev = "HEAD",
max_count = 20,
first_parent = true,
paths = {"crates/ptool-engine"},
})
local changes = repo:diff({ from = "v0.10.0", to = "HEAD" })
Branches
repo:branches(options?) -> GitBranchInfo[]
repo:branch_create(name, options?) -> GitBranchInfo
repo:branch_delete(name, options?) -> nil
repo:branch_rename(old_name, new_name, options?) -> GitBranchInfo
repo:branch_set_upstream(name, upstream_or_nil, options?) -> nil
branches()acceptskind = "local" | "remote" | "all"; the default is"local".- Branch information contains
name,kind,oid,head,upstream,ahead, andbehind. branch_create()acceptsstart_point,force,checkout,upstream, andconfirm. The default start point isHEAD.branch_delete()acceptsforceandconfirm. Deleting the current branch fails, and deleting an unmerged branch requiresforce = true.branch_rename()acceptsforceandconfirm.- Pass
niltobranch_set_upstream()to remove an upstream. Its options only containconfirm.
Tags
repo:tags(pattern?) -> GitTagInfo[]
repo:tag_create(name, target?, options?) -> GitTagInfo
repo:tag_delete(name, options?) -> nil
tag_create() targets HEAD by default. Without message it creates a
lightweight tag; with message it creates an annotated tag. Options are
message, tagger, force, and confirm. A tagger uses the shared signature
fields.
Tag information contains name, oid, target_oid, target_kind,
annotated, message, and tagger. tags(pattern) uses Git's glob matching.
Deleting a tag only changes the local repository; delete a remote tag with an
explicit push refspec.
local tag = repo:tag_create("v1.0.0", "HEAD", {
message = "Release v1.0.0",
})
repo:push("origin", "refs/tags/v1.0.0:refs/tags/v1.0.0")
Remotes and transfer
repo:remotes() -> GitRemoteInfo[]
repo:remote(name) -> GitRemoteInfo
repo:remote_add(name, url, options?) -> GitRemoteInfo
repo:remote_remove(name, options?) -> nil
repo:remote_rename(name, new_name, options?) -> GitRemoteInfo
repo:remote_set_url(name, url, options?) -> GitRemoteInfo
repo:fetch(remote?, options?) -> GitFetchStats
repo:push(remote?, refspecs?, options?) -> GitPushResult
repo:pull(remote?, branch?, options?) -> GitIntegrateResult
Remote information contains name, url, push_url, fetch_refspecs, and
push_refspecs. remote_add() accepts push_url and confirm.
remote_set_url() accepts push = true to change the push URL instead of the
fetch URL. Remove, rename, and set URL operations accept confirm.
fetch() accepts refspecs, auth, depth, prune, tags,
update_fetchhead, and confirm. push() accepts auth, force,
set_upstream, and confirm.
pull() defaults to remote "origin", the current branch, and
strategy = "ff_only". It also accepts strategy = "merge" | "rebase", the
fetch options auth, depth, prune, tags, and update_fetchhead, plus
signature, message, and confirm. Pull requires a clean repository before
it starts.
Worktree, index, and recovery
repo:add(paths, options?) -> nil
repo:restore(paths, options?) -> nil
repo:reset(rev?, options?) -> nil
repo:remove(paths, options?) -> nil
repo:clean(options?) -> string[]
restore()acceptssource(default"HEAD"),staged,worktree, andconfirm. When onlystaged = trueis specified, the worktree is not changed.reset()acceptsmode = "soft" | "mixed" | "hard",force, andconfirm. A hard reset requiresforce = true.remove()acceptscached,force, andconfirm.clean()acceptsdry_run,force,dirs,ignored,paths, andconfirm. It defaults todry_run = true. Actual deletion requires bothdry_run = falseandforce = true. Directories are left untouched unlessdirs = true.
local candidates = repo:clean()
repo:clean({ dry_run = false, force = true, dirs = true, confirm = true })
Configuration
repo:config_get(name, options?) -> string|boolean|integer|nil
repo:config_list(options?) -> GitConfigEntry[]
repo:config_set(name, value, options?) -> nil
repo:config_remove(name, options?) -> nil
scope is "local", "global", or "system"; the default for reads is the
highest-priority available value and the default for writes is "local".
Configuration entries contain name, value, and scope. System configuration
is read-only. Global config_set() and config_remove() require
confirm = true and always show a confirmation prompt.
Merge, cherry-pick, and revert
repo:state() -> string
repo:conflicts() -> GitConflictEntry[]
repo:merge_analysis(rev) -> string
repo:merge(rev, options?) -> GitIntegrateResult
repo:merge_abort(options?) -> nil
repo:cherry_pick(rev, options?) -> GitIntegrateResult
repo:cherry_pick_abort(options?) -> nil
repo:revert(rev, options?) -> GitIntegrateResult
repo:revert_abort(options?) -> nil
merge() accepts ff = "allow" | "only" | "never", signature, message,
and confirm. Merge, cherry-pick, and revert require a clean repository before
they start so abort can safely restore ORIG_HEAD.
Cherry-pick and revert accept commit (default true), signature, message,
mainline, and confirm. Set commit = false to update the index and worktree
without creating a commit. Abort methods accept confirm.
Stash and rebase
repo:stash_save(message?, options?) -> string
repo:stashes() -> GitStashInfo[]
repo:stash_apply(index?, options?) -> GitIntegrateResult
repo:stash_pop(index?, options?) -> GitIntegrateResult
repo:stash_drop(index?, options?) -> nil
repo:rebase(options) -> GitRebaseResult
repo:rebase_continue(options?) -> GitRebaseResult
repo:rebase_abort(options?) -> nil
Stash indices default to 0. stash_save() accepts include_untracked,
include_ignored, keep_index, signature, and confirm. Apply and pop accept
reinstate_index and confirm; drop accepts confirm. Stash information
contains index, message, and oid.
rebase() requires upstream and accepts onto, branch (default "HEAD"),
signature, and confirm. The first version supports non-interactive pick
operations; interactive squash, fixup, reword, and edit operations are not
available. Continue accepts signature and confirm; abort accepts confirm.
Advanced repositories
repo:worktrees() -> GitWorktreeInfo[]
repo:worktree_add(name, path, options?) -> GitWorktreeInfo
repo:worktree_lock(name, reason?, options?) -> nil
repo:worktree_unlock(name, options?) -> nil
repo:worktree_prune(name, options?) -> nil
repo:submodules() -> GitSubmoduleInfo[]
repo:submodule_init(name?, options?) -> nil
repo:submodule_update(name?, options?) -> nil
repo:submodule_sync(name?, options?) -> nil
repo:blame(path, options?) -> GitBlameHunk[]
- Worktree information contains
name,path,locked,lock_reason, andvalid. Add options arereference,lock,checkout_existing, andconfirm. Prune options arevalid,locked,working_tree,force, andconfirm; lock and unlock also acceptconfirm. - Submodule information contains
name,path,url,branch,head_oid,index_oid, andworkdir_oid. Init options areoverwrite,recursive, andconfirm. Update options areinit,recursive,allow_fetch,auth, andconfirm. Sync options arerecursiveandconfirm. - Recursive submodule processing is disabled unless
recursive = true. blame()acceptsnewest,oldest,min_line,max_line,first_parent, copy/move tracking flags,ignore_whitespace, anduse_mailmap. Each hunk containsfinal_start_line,original_start_line,line_count,commit_oid,author,origin_path, andboundary.
Safety and compatibility
All mutating methods that accept confirm default it to false. force and
confirm express different intent: force enables behavior that is otherwise
rejected, while confirm asks the user before an already-valid action runs.
Existing defaults remain compatible: the default remote is "origin", push
without refspecs pushes the current branch, status includes untracked files,
and empty commits remain allowed unless allow_empty = false is supplied.
Network tag deletion is expressed as a push refspec:
repo:push("origin", ":refs/tags/v1.0.0", { confirm = true })