Discarding a nested repository deletes its directory like that of any
untracked file. If the repository keeps its git dir in its directory,
this deletes all of its history, and nothing said so. The directory of
a removed submodule keeps its git dir elsewhere, but it may have
changes that aren't committed.
Check for both before discarding. If anything would be lost, list it
and ask for confirmation.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If you staged the directory of a removed submodule, a nested repository
or a worktree inside the repo, git added it as an embedded repository: a
gitlink without an entry in .gitmodules. For a removed submodule, this
brings it back in a broken state. git warns about it, but lazygit didn't
show the warning. Staging all files did the same to every such
directory.
Disable staging a selection that contains one of these, and disable
staging all files if that would stage one. The disabled reason tells you
how to get rid of it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Selecting one of these in the files panel showed the error "Could not
access 'modules/sub/null'". The main view showed the entry's diff
against /dev/null with --no-index, as for an untracked file. When git
compares a file with a directory that way, it looks for a file of the
same name inside the directory.
Show what the directory is and what to do about it instead. Below that,
show the status of the repository that it is, or the files in it if it
is a plain directory. The status tells you whether discarding the
directory would lose anything.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The files panel shows such a directory like an untracked file. Nothing
tells you that it is a directory, or that a submodule left it behind
when a commit that you checked out removed the submodule. This is the
case for the directory of a removed submodule, a nested repository, a
linked worktree inside the repo, and any untracked directory if
status.showUntrackedFiles is "normal".
Tell these apart when loading the files, by looking at the directory's
.git. Label removed submodules, nested repositories and worktrees, as we
already do for submodules, and put a slash after the name of a plain
directory, as git status does. With file icons on, a removed submodule
gets the submodule icon, and a nested repository or a plain directory
gets the directory icon.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
git status doesn't descend into a nested repository, such as the
directory that a removed submodule leaves behind. It reports the
directory as a whole, with a trailing slash. The file tree splits paths
at slashes, so it showed a directory with a file without a name in it.
Discarding the directory removed it, but then the cleanup of emptied
directories took the parent of "modules/sub/" to be "modules/sub"
itself, and failed to read it.
Take the slash off when loading the files. This also covers any
untracked directory if status.showUntrackedFiles is "normal", and linked
worktrees inside the repo. Their slash was taken off when they were
marked as worktrees; this is no longer needed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If the repo or linked worktree that lazygit has open is deleted while
lazygit is running, every git command fails. lazygit no longer panics
since the previous commit, but it shows empty lists, and there's
nothing useful left to do in it.
Check at the start of every refresh whether the worktree and its git
dir still exist. If one of them is gone, quit and print its path in
the terminal. The background files refresh does this check too, so
lazygit quits within ten seconds even if nobody is using it. To quit
from a refresh, return an error that wraps gocui.ErrQuit; this keeps
it out of the error popup.
Switching to another worktree or to a recent repo would be an
alternative, but it would be more surprising than quitting. Also,
there isn't always a main worktree to switch to.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If the repo that lazygit has open is deleted while lazygit is running,
the next full refresh panics. The same happens for a linked worktree
that is deleted. In both cases `git for-each-ref` fails because its
working directory is gone, and BranchLoader panics when that happens.
Return the error instead. The branches refresh logs it, like it does
with the other errors it gets from loading the branches.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If a linked worktree's directory is deleted and another worktree is
moved to the same path, `git worktree list` reports both worktrees with
that path. lazygit then crashes at startup with a stack overflow, and
the terminal is left in a broken state.
To tell the paths in a group apart, getUniqueNamesFromPathsAux keeps
looking at one more parent directory, but identical paths never come
apart, so it recurses forever. A stack overflow is a fatal error rather
than a panic. No deferred function runs, so the terminal is not
restored either.
Name identical paths like a single path instead of recursing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
It returns true if the path is missing. It started out as
IsWorktreePathMissing, and was renamed without inverting its result
when it moved into the loader in de57cfd6ff.
Invert the result instead of renaming it back, so that the call site
reads `IsPathMissing: !self.pathExists(path)`. If we can't check the
path, it still counts as existing, as it always did.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
time.AfterFunc and errgroup.Group.Go run the function they are given
on a goroutine of their own, so a panic in it leaves the terminal in
raw mode, like a panic in any other goroutine that doesn't use
utils.Safe.
Add utils.SafeFunc and utils.SafeFuncWithError, which return a copy
of a function that runs through utils.Safe or utils.SafeWithError, and
use them for the functions given to time.AfterFunc and errgroup.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Most goroutines run their function through utils.Safe, which closes
the screen if the function panics. Without it, the panic is printed to
a terminal that is still in raw mode and on the alternate screen, and
the terminal needs a reset afterwards. Go has no process-wide hook for
panics, so every goroutine has to do this itself.
Use utils.Safe for the goroutines that don't use it yet.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Several actions on the selected lines of a diff look up those lines in
the diff of their file by their line numbers: staging, unstaging and
discarding them, editing the hunk they are in, and adding them to a
custom patch. Showing which lines of a diff are in the custom patch
does the same lookup the other way round. For this, the change lines of
the file's diff are indexed by their line numbers.
The index asked the patch for the number of each change line separately,
and each of these calls counts the lines of the hunk before the line. So
for a new file of 100,000 lines, building the index took 2 seconds.
Number all lines of the patch in one pass instead. Building the index
for that file now takes 4 milliseconds.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With no diff renderer configured, opening the jump-to-file menu for the
diff of a commit that adds or deletes a file of 100,000 lines takes
more than 20 seconds, and so does picking a file from it. In such a
diff, moving the selection by change block takes as long for each key
press. All this time the UI is frozen.
To find out which line of which file each row of the main view shows,
lazygit parses the view's content as a diff. For every line of a file's
diff it asks the parsed patch for the line's number in the new and in
the old file, and each of these calls counts the lines of the hunk
before it. So parsing takes time quadratic in the length of a hunk, and
the diff of a new or deleted file is a single hunk. For a diff of
200,000 lines, one parse took about 10 seconds, and opening the menu
parses it twice. A diff renderer that states which line each row shows,
such as delta, doesn't go through this parse.
Number all lines of the patch in one pass instead. Parsing that diff
now takes about 40 milliseconds.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If a diff renderer is configured, its command shows up in the command
log once after startup, although the user didn't do anything. This is
the probe that asks the renderer whether it emits OSC 1717 metadata. It
changes nothing, so like other read-only commands it shouldn't be
logged.
The next commit carries a line of a diff forward to the file as another
worktree has it, and needs to diff against that worktree's working copy
for this.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If diff.mnemonicPrefix is set, pressing space on a line in the focused
main view does nothing. The command log shows "Apply patch", but the
line is not staged. Other commands that read a path out of the working
tree's diff go wrong as well, such as jumping to a file. Building a
custom patch from a commit's diff still works, because git uses
mnemonic prefixes only when one side of the diff is the index or the
working tree.
With this config, the paths in the working tree's diff start with i/
and w/ instead of a/ and b/. We only strip a/ and b/, so the path we
read out of the diff header is not a file of the files panel, and we
skip the line without a message.
Users who set the config want to see these prefixes, so keep them.
Strip the first component of the path instead, whatever it is. `git
apply` does the same by default, and ParseableDiffPrefixes makes sure
there is such a component.
A section without +++ and --- lines needs more than that, because we
find the new path on its "diff --git" line by searching for " b/".
Take the path from the "rename to" or "copy to" line instead where
there is one. Otherwise the two paths on the "diff --git" line are the
same. Split the line where the two halves match after stripping their
prefixes, like `git apply` does.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If diff.srcPrefix and diff.dstPrefix are set (git 2.45 and later),
pressing space on a line in the focused main view does nothing. These
configs change the diff of a commit too, so the other commands that
read a path out of a diff go wrong as well.
The configs replace the a/ and b/ in front of the paths of a diff with
any string. We only know how to strip a/ and b/, so the paths we read
out of the diff header don't name a file.
We can't tell from the diff where such a prefix ends, so override the
two configs along with diff.noprefix.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If diff.noprefix is set, pressing space on a line of a file under a
top-level directory named a or b does nothing. In a stash entry's diff,
the menu for jumping to a file shows such a file without its directory.
We read the path of a file out of the diff header and strip the a/ or
b/ in front of it. Most diff commands override diff.noprefix so that
this prefix is there, but the diff of the working tree and the diff of
a stash entry don't. With the config set, we strip the file's own
directory instead.
Use ParseableDiffPrefixes for these two commands too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Seven diff commands pass -c diff.noprefix=false so that the paths in
their output carry a prefix. The next commits use the override for
more commands and add more config to it, so give it a single home
first.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Nothing routes to the explorer contexts after line actions and file entry
moved into the normal main pair. Delete their contexts, views, main-pair
wiring, discovery API, test drivers, and selection-state package so the
surviving diff view is the only implementation.
Their names go from the contexts a custom command may bind to as well, so
that a config still naming one is reported as the config error it now is
rather than taken as a context we simply failed to find.
Co-Authored-By: GitHub Copilot <copilot@github.com>
A focused diff must replace the patch builder's whole-file toggle
without losing file creation or deletion metadata. Ordinary modifications
and renames must remain line-level selections even when every changed
content line is selected.
Ask the patch builder's canonical raw diff whether it consists of one
hunk containing only additions or only deletions and no context. Use the
whole-file operation only when the selection also covers every change.
Co-Authored-By: GitHub Copilot <copilot@github.com>
Space in the pane previewing the patch had nothing to act on: everything
there is in the patch already, so it can only mean taking those lines back
out, in the way that space in the staged side of the working tree's diff
only unstages.
A line of the patch can't be found by its number: the patch renumbers
whatever follows a change it leaves out, so a line of it names a line of
the commit's diff that isn't the one shown. A line is identified instead
by its position among its file's changes, counted in the diff the patch is
shown as. That is the same position it has among the changes the patch
holds for the file, since both are in the order the file has them. This
holds however the rendering came out, so a renderer that groups a hunk's
deletions before its additions, or leaves a change out altogether, no
longer matters.
The pane's rows are stated in terms of the trees the patch was
materialized into, so the paths come back to the repo's own before
anything is looked up in them. This also lets the lines there be copied
and edited, as the lines of a commit's diff can be.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pane beside a commit's diff showed the patch being built from it by
assembling the text itself. That text could not be handed to a diff
renderer the way a diff can: a stdin filter might have coped, but a tool
that diffs two files could not. Its idea of how much context to show
around a hunk was also its own rather than git's.
Materialize the patch instead: write each of its files as it is before the
patch into one tree and as it is after into another, and let git diff the
two trees. The patch becomes a diff of real files, rendered by whatever
renders the rest of them, with git's own context around it. Its lines can
then be pointed at; taking them back out of the patch will need that.
The trees are named a and b, so that with git's own prefixes suppressed
the paths read like an ordinary diff's over the repo's own paths. They are
written when the patch changes rather than when it is shown, the patch
builder counting its own versions for that, and they go away with the
patch.
A renamed file is materialized under the name the patch expects to find it
under. Where the patch carries the rename, that is the name the file had
before, so the rename comes out as a rename. git names the trees
themselves in the two rename lines, having only the two paths to go by.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A patch is built in terms of where a line sits in the file's diff, which is
what the patch explorer has to hand. The main view doesn't: a row of it
resolves to a line of a file, and what index that line has depends on how
much of the diff has been read and how a diff renderer chose to lay it out.
So let the two meet at the patch builder's edge, in the identity of a change
line — its number on the side it belongs to — leaving the main view to speak
only of lines it can see.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A diff renderer may lay a diff out however it likes: line numbers in a
gutter, the +/- column replaced by colour, the two sides in columns. Once
it has, we can only tell which line of which file a row shows if the
renderer says so. Under a renderer that doesn't say, the main view holds
a diff that can be read but not staged, edited or copied from. That is no
good now that the main view is where you stage.
So focusing it brings git's own diff instead, and every re-render while it
stays focused keeps to that, so staging a hunk doesn't flip back. Browsing
is untouched: you see what the renderer produced until you focus the view
to act on it.
Whether the renderer says anything is settled by asking it rather than by
watching it work: run it on empty input and see whether it announces the
protocol. Announcing is a property of the renderer, so the answer is known
before we render anything, and a diff with no lines to describe can't fool
it. Watching would have to see a diff go by first, and a binary file's diff
holds nothing that would tell the two cases apart. The answer is remembered
until the renderer changes. git itself is asked the same question, with the
renderer's own arguments, since it announces itself for exactly the formats
whose output can't be read back as a diff.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two things about a diff command follow from what its output is for: whether
the diff renderer produces it, and whether it is coloured. That was a
`plain` flag, which covers two of the three cases — the diff as configured,
and git's own uncoloured diff for building patches out of — and leaves no
room for the third, which is about to be needed: git's own diff, coloured,
for showing where the renderer's version of it can't be acted on.
So the flag becomes a mode. It also takes over deciding the colour, which
each command spelled out for itself, and it settles a question the flag
couldn't put: whether ignoring whitespace applies. It is about what the user
wants to see, so it holds for anything shown, and not for a diff a patch is
built from, which has to describe every change.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Parse is lenient: it takes any text and reads a diff out of it, which is
what we want when we hand it a diff, but it has no way to say "this
isn't one". We're about to parse the *rendered* contents of a diff view,
which a diff renderer is free to restructure — putting the line numbers
in a gutter, say, shifts the +/- marker off the start of each body line,
so every line reads as context and the parse silently lies about which
lines are changes.
Comparing each hunk's body against the lengths its header declares
catches exactly that, without teaching us anything about any particular
renderer's layout: a faithful unified diff agrees with its headers, a
restructured one doesn't.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Identifying a change line of a diff by its file line number needs both
sides: two consecutive deletions sit at the same new-file position, so
only their old-file line numbers tell them apart. LineNumberOfLine only
answers for the new file, which leaves deletions ambiguous.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This lets pushing a range selection of tags push them all with a single
git command.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This lets deleting a range selection of tags delete them all with a
single git command, the same way it works for branches.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The worktree loader associates a worktree that is mid-rebase or
mid-bisect with the branch involved, even though its HEAD is detached.
Fast-forwarding such a branch therefore ran the fast-forward in that
worktree. This moved the detached HEAD and put the upstream commits
into the rebase in progress, while the branch stayed where it was.
Remember in the worktree model when its branch comes from a rebase or
bisect, and refuse to fast-forward the branch in that case. Updating
only the ref is no option for a rebase; the rebase writes the branch
when it finishes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Branches that lazygit forwards to their upstream move by a direct ref
write, so no git command turns up in their reflog to explain it. Until
now the entry had no message either, leaving `git reflog <branch>` with
nothing but a blank line about it. Name lazygit and what it did, so that
a branch which moved on its own can be traced.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
After somebody else rebased and force-pushed a stack of branches, every
branch of the stack has to be brought back to its upstream, and pressing
`f` on them one by one is as tedious as the checking out and pulling it
replaces.
Let `f` work on a range selection. The upstream branches are fetched with
one `git fetch` per remote, and the branches that aren't checked out
anywhere move in a single `git update-ref` call. All the selected
branches are looked at before any of them is moved, so a branch that has
to be refused leaves the others alone rather than updating the stack
halfway.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
When somebody else rebases a branch and force-pushes it, the local
branch is left diverged from it, so `f` refuses to touch it. To get back
in sync, the branch has to be checked out and pulled, and for a stack of
branches that means doing it once per branch.
Such a branch has none of its own work in it, though: the commits it is
ahead by are the old versions of the ones that are now on the remote
branch. So when the branch has diverged and none of the commits it is
ahead by is ours, reset it to its upstream instead of refusing. This is
the same state that checking the branch out and pulling it would produce,
as rebase drops commits that the upstream already has.
A branch that isn't checked out anywhere is reset with the same
`git update-ref` that a fast-forward uses. One that is checked out
somewhere is reset with `git reset --keep`, and only when that worktree
has no changes to tracked files, so that the files under the user's feet
change no more than they have to. `--keep` also refuses to overwrite an
untracked file that the reset would bring in, which `--hard` would do
silently.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fast-forwarding a branch that isn't checked out ran a single
`git fetch <remote> refs/heads/<branch>:<branch>`, so git's own refusal
to move a local branch backwards served as the safety check. A later
commit widens the set of branches the command accepts, and for that
lazygit has to make that decision itself.
Fetch the remote branch on its own, updating only its remote-tracking
branch. Then ask git whether the branch is an ancestor of it, and only
then move the branch: with `git update-ref` when it isn't checked out
anywhere, guarded by the hash we knew it to have, and with
`git merge --ff-only` in its worktree when it is checked out there.
Keeping `git pull --ff-only` for the latter case would fetch a second
time.
Fast-forwarding a branch that isn't checked out in a worktree had no
integration test at all, so add one. The second new test covers a repo
that keeps no reflogs, because a later commit starts reading those and
this case has to keep working without them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The function asks git whether HEAD is an ancestor of a ref, and its name
says what the one caller wants to know. A later commit asks the same
question about a branch that isn't checked out, so let the caller name
both refs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
When somebody else rebases a branch and force-pushes it, our branch
shows up as diverged, say ↓5↑3, and that looks exactly like a branch
that carries three commits of our own. The two want different treatment.
The first one only has to be moved to its upstream, while the second one
needs a decision about what to do with those commits. The panel doesn't
tell them apart, so users have to look at each branch in turn to find
out which one they are dealing with.
Determine for every diverged branch whether it has commits of its own,
and dim its divergence when it doesn't. This runs on a worker, as it
takes a few git commands per diverged branch, and a branch keeps the
answer from the previous load while a new one is in flight, so that the
display doesn't flicker. A later commit teaches the fast-forward command
to act on such a branch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The flag says whether this load is the one that kicks off the values
determined in the background afterwards. A later commit adds a second of
those next to the behind-counts, so name the flag after what it controls
rather than after its only user so far.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A branch that has diverged from its upstream doesn't necessarily hold any
work of its own. If somebody else rewrote the remote branch and
force-pushed it, our branch is still at the commits it had before, and
every one of them was on the remote branch at some point. A later commit
offers to reset such a branch to its upstream, and for that it has to
tell this case apart from a branch that holds commits created here.
The reflog of the remote-tracking branch records the values it had before
it was rewritten, so a commit that was ever on the remote branch is
contained in its current value or in one of those. HasLocalOnlyCommits
asks git for a commit of the branch that none of them contains.
Two details of the reflog are worth knowing. The value the ref had before
its oldest entry is that entry's old value, and <ref>@{<number of
entries>} is the only way to name it. It doesn't exist when the oldest
entry is the one that created the ref, and asking for it then is an
error, not an empty result. Reflogs can also be missing altogether,
because core.logAllRefUpdates defaults to false in a bare repository.
Neither case must stop us from recognizing a branch that is strictly
behind its upstream, so the current value of the remote-tracking branch
is always used; missing previous values only make the answer more
conservative.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
After rebasing a stack of branches, every branch of the stack has to be
force-pushed, and so far the only way to do that in lazygit was to check
out each branch in turn and push it. For a stack of a dozen branches
that is a lot of work for a routine task.
When the current branch is pushed and there are branches below it in the
stack that have commits to push, show a menu that offers to push them
along with it. The menu lists the branches and how far each of them has
diverged from its remote branch, so that the user can see what is about
to happen. It doesn't show where each branch goes; the branches panel
doesn't show that for a normal push either. Pushing only the current
branch is the second entry. If any of the branches has diverged from its
remote branch, a single confirmation covers force-pushing all of them.
A branch is offered if its tip is a commit of the current branch that
isn't merged yet, it has an upstream that is stored locally, that
upstream is not gone, and it is ahead of its push destination. Branches
without an upstream are left out because a branch that was never pushed
can't be told apart from one that is meant to stay local. A branch that
is only behind is left out because pushing it would move the remote
branch back to an older commit; the lease doesn't catch that when the
remote-tracking branch is up to date.
Each branch is pushed to where `git push` would push it if it were
checked out, using the destination git reports in the %(push) field.
This honors push.default, remote.pushDefault and
branch.<name>.pushRemote without lazygit having to interpret them.
Branches going to the same remote are pushed in one command, with
--force-with-lease when the user confirmed force-pushing; the lease
checks each ref against its remote-tracking branch, so a coworker's
unseen push is still rejected. The current branch joins that command
when its remote-tracking branch is stored locally. Otherwise it needs a
plain push first, for example with --set-upstream, and that option
would apply to every refspec of a combined command; so in that case it
is pushed on its own as before, and the other branches follow in a
second command.
Pushing the current branch on its own still runs a bare `git push`, so
users who rely on push.default or remote.<name>.push for it see no
change. The push is non-atomic, as git defaults to; if one branch's
lease fails, the others still go through and the error names the one to
look at.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PushOpts can only describe a push of the current branch. It takes the
branch's name and the upstream to push it to, and builds a single
refspec from them. Pushing several branches in one command needs one
refspec per branch, so let callers pass the refspecs themselves. The
sync controller, the only caller so far, builds the same single refspec
that PushCmdObj built before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
To push a branch other than the checked-out one, lazygit has to name the
remote and the remote branch in the push command; a bare `git push` only
works for the current branch. The upstream isn't always the right
destination. In a triangular workflow the branch is pushed to a
different remote than it pulls from, and with push.default set to
"current" it goes to a branch of the same name whatever the upstream is
called.
Read the destination from git instead of working it out from the config.
The %(push) field of for-each-ref names the remote-tracking ref that a
push would update, taking push.default, remote.pushDefault and
branch.<name>.pushRemote into account. It is also the ref that the
push:track counts are computed against, so the force-push detection and
the destination agree. The field is available since git 2.5, well before
the oldest version lazygit supports.
Nothing uses the new fields yet.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On the Windows CI job,
TestStartPipelineReadsWhatTheCommandsComplainAbout fails now and then
because the output it reads has an extra line after the expected one.
The line starts with "error: coverage meta-data emit failed" and ends
with "The process cannot access the file because it is being used by
another process."
The pipeline tests run the test binary itself as the members of the
pipeline. CI builds it with -cover, and the members inherit GOCOVERDIR
from go test, so each of them writes coverage data to that directory
when it calls os.Exit. The meta-data file has the same name for every
process of one binary, and every process replaces it by renaming a new
copy onto it. (Go's check for an existing file compares its size against
the wrong length, so it never finds one.) On Windows this rename fails
if another process holds the file open. In this test both members exit
at the same time, and the runtime prints the failure to stderr.
StartPipeline puts every member's stderr into the output that the test
compares.
Exit the members with syscall.Exit instead. It skips the runtime's exit
hooks, so the members write no coverage data at all. Nothing is lost by
this. Their coverage data went to a temporary directory of go test, not
to the directory that CI uploads.
The test was added in dfd6a7dbf2.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Renders were kept from refreshing git's index because a pty-rendered
command on Windows is terminated at an arbitrary instruction when its
task stops, and one landing in the window where git holds index.lock to
write back refreshed stat information leaves that lock behind.
A render no longer runs in a pty there, and nothing kills it any more
either: the pipe to the renderer breaks, git's write fails, and git dies
through its own die path with its lock files cleaned up. So let the
refresh happen, and let renders heal stale stat info the way they do
everywhere else.
The teardown of a pseudoconsole took its reassurance about index.lock
from this, and renders were the reason it held. What still runs in a pty
there is a custom command asking for logWithPty and a command that may
be asked for a credential, so the claim no longer follows; drop it
rather than restate it for clients it was never about.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ConPTY doesn't carry a diff renderer's output to us as the renderer
wrote it. It parses the output into a screen buffer and re-encodes that
for the terminal side, and a sequence it can't represent there goes out
the moment it is parsed, separately from the text around it. The OSC
1717 records a renderer states its diff lines in therefore arrive
detached from the rows they describe, and the identity layer attributes
rows to the wrong diff line or to none.
Feed the renderer through a pipe there instead, so that its bytes reach
us unaltered. A stdin filter becomes a command of our own, since git
only invokes the one named by GIT_PAGER when it talks to a terminal; an
external diff renderer is git's own business either way and needs
nothing but the pipe.
Unix keeps the pty. A renderer reads the width to lay out to off it, so
taking it away would leave every configuration that doesn't name a
width rendering at whatever the renderer falls back to, and diff
renderers have worked on Unix far too long for that.
LAZYGIT_RENDER_WITHOUT_PTY asks for the piped path anyway, which is how
the integration tests cover it on a platform where they run at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An external diff command rides on the git command as a diff.external
config, which means it is fixed when the command is built. That is
before the layout pass, so the width the renderer is to lay out for
isn't known yet, and the command can't be told about it.
Pass it as GIT_EXTERNAL_DIFF from the render instead, where the width
is known and where a stdin filter is already handed to git the same
way. git ranks the variable exactly as it ranks the config, behind a
per-path diff driver from .gitattributes, so a repository that defines
one still gets it (verified on git 2.22.5 and 2.55). An empty command
means the user wants their own git config to apply, so leave the
variable unset for that; git takes it being set at all as an
instruction.
The renderer command leaves the git arguments, so it also leaves the key
that says which diff a render is of. Cycling between two external diff
renderers now keeps the view's place, the way cycling between two stdin
filters already does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rendering a diff through a renderer on Windows means running the
renderer ourselves, since ConPTY mangles the metadata records it emits
and git only invokes a renderer of its own when it talks to a terminal.
That needs a chain of commands whose output a view can be filled from
while it runs, where PipeCommands runs a chain to completion and reports
what it said afterwards.
StartPipeline starts such a chain and hands back the reader for its
output, along with a handle offering exactly what a render task asks of
a command: something to wait for, something to name it by, and a way to
stop it. Every command's stderr joins the output, so a renderer that
objects to its input says so where the diff would have been.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>