This page describes the base branch and the workspace of a task. It also gives the reasons for the git flags of the diff and the reconcile.
Every revision input that reaches git is a full revision, or an exact ref that tasker read with git show-ref --verify. So git never gets a short name, a range or a suffix (a..b, x^{tree}) as input that changes what diff, rev-list or merge-base do. The code never reads the error text of git.
The short name origin/<x> can name another ref first, because git reads refs/tags/ and refs/heads/ before refs/remotes/. Examples are a tag origin/<x> that the fetch brings from the remote, and a local branch refs/heads/origin/<x> that a person makes in the clone. So tasker uses the full ref refs/remotes/origin/<x>.
tasker reads the exact ref refs/heads/<branch> with git show-ref, not git rev-parse, because rev-parse can read another ref, for example refs/heads/refs/heads/<branch>.
The existence check: a base exists when git show-ref --verify --quiet -- <revision> exits with 0. This matches only the exact ref. Exit code 1 means that the ref is missing. Every other error passes through unchanged.
Each fetch moves and prunes refs/remotes/origin/* in the clone, also when a later step fails. The prune removes each refs/remotes/origin/* ref of a branch that the remote deleted. A failed fetch stores and deletes nothing. The fetch uses the ssh key of the repo (see version-control.md). The base is read after the fetch, because the fetch can make or move the ref.
The resolve of the base and the containment read of the diff part fetch. No worktree or reconcile function fetches, so the caller resolves the base first.
The stored form of a base is one of these:
<x>is a branch onorigin.refs/heads/<x>is a local branch of the bare clone.
Names such as -x and a..b pass the parse of the text that a user types. The existence check then rejects them, and its error names them.
A stacked task is a task whose base has the form refs/heads/<x>, where <x> is the branch of a task of the same repo. Its parent is the newest task of the repo with that branch. The parent has landed when its task is in landed, finishing, cleanup_failed or finished. After tasker update-branch moves a task whose parent landed, the task is no longer stacked: its base is the base of the parent.
The checks that need no git run first, so a bad input causes no fetch and no write.
The source order is: the issue hint, then --base or the stored base of the task, then the blocker step of start, then the learned default, then defaultBaseBranch, then a question.
- Only
--baseand the blocker step can give a local branch (refs/heads/<x>). A hint and an answer must name a remote branch. - The resolve checks the blocker base with the same existence check as
--base refs/heads/<x>. - A missing
defaultBaseBranchfails and does not ask. - The hint comes from the issue tracker, so it is untrusted text. It cannot reach
refs/heads/*, because the check usesrefs/remotes/origin/<hint>, andgit show-ref --verifymatches only the exact ref.
The learned default is the row of the repo in the default_base_branches table. data-root.md holds the forget rule.
- A value from the configuration default or from the answer is saved only after the branch exists in the clone.
- A hint,
--baseand the blocker step store nothing. - tasker asks at most once for each repo, unless a learned value later stops existing.
- A learned value that is missing is deleted before the question runs. If the question or its check then fails, the learned value stays deleted.
The workspace of a task is a git worktree on a task branch. The worktree comes from the bare clone of the repo. No worktree function talks to the remote, so none needs the ssh key environment.
Git with a wrong working folder changes another repo. The assistant can write the .git file of a worktree and its admin dir (<clone>/worktrees/<name>), and git follows both. A git call in a changed worktree can read a configuration that the assistant chose. So each function that runs git in a worktree first makes sure that the worktree is a worktree of the clone, that it has a .git file, and that it uses its own admin dir in the clone. This check reads files and runs no git.
One fact of the check catches a .git file that names the admin dir of another task worktree of the same clone. The gitdir file of the admin dir must name the .git file of the worktree. A rewritten gitdir file never reaches this check, because the list of worktrees then shows another path.
Git lists the real path of a worktree. For a path through a symlink, git lists the real parent folder.
The name of a task branch is the short ref plus a slug of the title, so a person can read it. See issue-keys.md for the short ref. The folder of the worktree is named by the short ref, so it does not change when the title changes.
Every function that takes a branch runs the name check first, before any git call. As a result, git never gets a name with a leading -, .., /, ~, ^, @{ or a space.
The shape check runs after the name check. A function that writes or deletes a branch refuses a name without the task branch shape, so tasker never changes main, master or another base. A function that only reads a branch runs only the name check. A detached HEAD has no branch and passes.
Create is idempotent (safe to run again), so tasker start can continue a failed attempt. Create does not use the guard.
- The check that the path is empty runs before
git worktree add, because git makes the new branch before it reads the path. - A new branch has no upstream (
--no-track). - A failed
git worktree addcan leave the new branch, and the next create reuses it. A reused branch keeps its commits and its configuration, and it does not move to the base. - A stopped
git worktree addcan leave an admin folder in the clone with only alockedfile. Git does not list it, and the next create uses a new admin name. - For an unlocked worktree whose folder is gone, create removes that worktree entry and adds the worktree again.
Create does not repair a locked worktree that has no .git file or a detached head. No git command repairs one worktree without --force: git worktree remove refuses a locked worktree, git worktree repair changes every broken worktree of the clone, and git worktree prune acts on every entry that it can prune. The error names the lock and the reason, so a person can decide.
The repair of the initializing lock trusts the lock. Git sets this lock while git worktree add runs, so the folder holds no work of the task. The repair runs git reset --hard, so it writes the committed files over a partial checkout. A person who locks a worktree with the reason initializing by hand also gets the git reset --hard.
tasker never uses --force.
- Git refuses a worktree with a changed tracked file or an untracked file.
- Git refuses a locked worktree and a folder that is not a worktree.
- A registered worktree with no folder is removed.
- Ignored files do not block the remove. Git deletes them with the folder.
- The branch stays.
The delete uses -D, because a task branch is usually not merged in the clone. tasker deletes only a local branch with the task branch shape.
git reset --hard runs first, so a deleted .gitignore comes back before git clean reads it. git clean -ffd (two f) also deletes a nested git repo. There is no -x, so ignored files stay and do not block a later remove.
This part reads what a task changed against its base, and what is not committed yet. tasker reads the commits itself, because some assistants have no shell in planning.
The order in each function is: the input checks, then the worktree guard, then the ref reads, then git. A function that takes a worktree path throws the errors of the guard. The guard text gives the reasons. Apart from the fetch, these reads write no ref, no file and no configuration.
- The three-dot range
<base>...<branch>compares the branch with the merge base. A file that only the base got after the fork does not show. The two-dot range ofgit difflists those files, so the diffs use the three-dot range. - The own commits use the two-dot range
<base>..<branch>, which gives the commits that the branch has and the base does not. -zmakes git print each path raw and end it with a NUL. Without-z, git quotes a path with a newline, a tab, a quote or a non-ASCII character. A private parser splits the output on NUL and drops the empty item after the last NUL.--no-renamesmakes a rename give both paths: two paths in the diff, and two records in the status.--untracked-files=normalmakes untracked files show, also when the worktree hasstatus.showUntrackedFiles=no. An untracked folder gives one record, for example?? sub/. An ignored file does not show. Each path is relative to the root of the worktree.- The status call runs with
GIT_OPTIONAL_LOCKS=0in its environment. Without it,git statustakesindex.lockand can rewrite the index of the worktree. A git command of the assistant in the same worktree then fails on the lock. --no-color,--no-ext-diffand--no-textconvstop colors,diff.externaland textconv filters of the user configuration from changing the text.diff.noprefixstill changes thea/andb/prefixes of the diff text.- The explicit
--formatstopsformat.prettyandlog.decorateof the user from changing the text. --no-show-signaturestopslog.showSignatureof the user configuration. Git then starts no gpg and adds no signature lines to the text.--cherry-markwith--left-onlyor--right-onlyinremoteTaskBranchSidesmarks a commit that has a same-patch commit on the other side. With--left-rightand--cherry-mark, git prints=for such a commit and hides its side, so one call cannot give both sides. The function runs one call for each side.--topo-order --reversegives the oldest commit first on each side.--encoding=UTF-8makes git print a non-ASCII subject as UTF-8. Withi18n.logOutputEncoding=ISO-8859-1in the user configuration, git re-encodes the subject without it.
- A commit diff compares commits, so an uncommitted change does not show.
- A merge commit shows no diff, as git does by default.
rev-list --count <head> --notwith no others counts every commit that the head reaches.- Only exit code 1 of
merge-base --is-ancestormeans "no". Exit code 128, for example for a revision that the clone does not have, is an error. - A staged rename gives a delete and an add.
remoteTaskBranchSides reads the two sides of the range <local>...<remote> with git log --cherry-mark, one call for each side.
- The mark
%mis never+. It is=for a commit with a same-patch commit on the other side. Otherwise it is<on the left side and>on the right side. The code treats only=as the same patch. - A merge commit on either side is never
=. It is<or>, and%Pholds two hashes divided by a space. git cherryleaves out each merge commit on both sides, so the read does not usegit cherry.- A commit that differs only in its message and its author gives
=on both sides. A commit that differs only in whitespace at the end of a line also gives=. git rev-list <local> ^<remote> ^<base tip>prints the local-side commits that the base tip does not reach. It leaves out a base commit that the local side holds. It keeps a local merge commit and a rebased commit. The read uses this output foronBase.
Reconcile brings a task worktree onto a base that moved. When a step fails, it puts the worktree back where it was. It acts only on a worktree that is on a task branch or detached. The branch name gives the check.
The clean check runs before any step, because the restore runs checkout --force and clean -fd. Both delete uncommitted work.
runGit() removes the repo-local git variables. As a result, -c key=value is not possible, so each rule of the user configuration has a command option.
--onto applies only the commits of <upstream>..HEAD. The rebase options are:
| Option | Reason |
|---|---|
--merge |
rebase.backend=apply uses rebase-apply/. This option forces rebase-merge/. |
--no-autostash |
It stops rebase.autoStash. |
--no-update-refs |
It stops rebase.updateRefs, which moves other local branches. |
--no-rebase-merges |
It stops rebase.rebaseMerges, so the history stays linear. |
--no-gpg-sign |
It stops a gpg failure from commit.gpgSign. |
--no-verify |
It skips the pre-rebase hook. |
--no-rerere-autoupdate |
It stops rerere.autoUpdate, so a conflict stays unmerged. |
The reasons for the options of the merge are:
| Option | Reason |
|---|---|
-s ort |
It stops pull.twohead, which can drop the changes of the base. |
--ff |
It stops merge.ff=only. |
--commit and --no-squash |
They stop branch.<name>.mergeOptions=--squash. |
--no-edit |
It stops GIT_MERGE_AUTOEDIT=yes from running an editor. |
--no-log |
It stops merge.log, so the message stays the default. |
--no-autostash |
It stops merge.autoStash. |
--no-gpg-sign |
It stops a gpg failure from commit.gpgSign. |
--no-verify |
It skips the pre-merge-commit and commit-msg hooks. |
--no-verify-signatures |
It stops merge.verifySignatures. |
--no-rerere-autoupdate |
It stops rerere.autoUpdate, so a conflict stays unmerged. |
These settings need no option. rebase.autoSquash has no effect on a rebase that is not interactive. merge.conflictStyle changes only the markers, and the abort removes them. pull.rebase has no effect.
Exit code 1 does not mean a conflict. A refused dirty worktree, an untracked file in the way, a refusing pre-rebase hook and a gpg failure in a rebase also exit with 1. A merge can exit with 2 or 128 for other failures.
The code decides on a conflict only from the list of git diff --name-only --diff-filter=U -z. Each unmerged path shows one time, raw and relative to the root. An empty list means no conflict.
After a failed git step, the code runs the abort only when some path is unmerged. Then it always runs the restore. It throws the conflict error only when the list of unmerged paths is not empty. Otherwise it throws the original git error again. An error that is not a git error passes through with no restore.
- The code drops an error of the abort, because the restore runs next and checks the state.
- An error of the restore wins over the other errors.
- A base that got the task by a fast-forward outside tasker gives no move.
- A base that got the task with a merge commit outside tasker gives a fast-forward, because the merge commit is ahead of HEAD.
- A fast-forward has no conflicts.
The move puts the task branch, HEAD, the index and the tracked files at a revision. The revision can be an ancestor of HEAD, a descendant of HEAD or on another line. The command is git reset --keep --no-recurse-submodules <sha>.
- The clean check runs first, as in the other reconcile functions. A worktree with uncommitted changes is refused.
--keepis used and--hardis not.--keepmakes git refuse a change in a tracked file that the move changes (exit 128, HEAD and the change stay). It also refuses an untracked file in the path of a tracked file of the target (exit 128, HEAD stays). So a change that appears after the clean check is not lost.--harddeletes such a change.--no-recurse-submodulesstopssubmodule.recurseof the user configuration from changing the command. No submodule was tested.- A detached HEAD is refused before the reset. On a detached HEAD,
git reset --keepmoves only HEAD, and the move must change the task branch. - The branch must have the task branch shape, as for each function that writes a branch.
- After a
GitError, the restore of the reconcile runs.
Side effects:
git reset --keepruns nopost-checkouthook. It runs only thereference-transactionhook.- It prints nothing on stdout.
- An ignored file in the path of a tracked file of the target is written over.
rebase --quitruns first, because a failed or conflicted rebase leavesrebase-merge/, andcheckout --forcedoes not remove it.checkout --force -Bputs the branch back at the start, also when the failed step moved it. The checkout also removes the merge state.git symbolic-ref -q HEADexits with 1 for a detached HEAD. Every other failure passes through.- Git has no plumbing command with an exit code that tells if a rebase or a merge is in progress, and the text of
git statusis not stable. So the code tests the paths fromgit rev-parse --git-path <name>. - A git error of a restore command does not stop the restore. The code keeps the stderr and runs the next command.
When the restore cannot get the start state back, the error names the state and the git commands that a person runs. The function that makes the commands is exported, because the fixture cannot make each state.
A branch that another worktree has checked out, a refs/heads/<branch>.lock file and an index.lock file in the admin folder make the checkout fail.
- A successful rebase moves the task branch, or only HEAD for a detached start, and rewrites its commits. A merge adds a merge commit. A fast-forward moves the branch or HEAD.
- The hooks
post-checkout,post-rewriteandpost-mergein<clone>/hooksrun. No option skips them. - With
rerere.enabledin the user configuration, git writesrr-cache/andMERGE_RRin the clone. No option stops it. ORIG_HEADandAUTO_MERGEstay in the admin folder.- The restore drops the commits that the failed step made on the branch. It deletes untracked files and keeps ignored files.
- A refused dirty worktree changes nothing.