Handle unknown object sizes in progress log and fix throttling by chrisd8088 · Pull Request #6288 · git-lfs/git-lfs · GitHub
Skip to content

Handle unknown object sizes in progress log and fix throttling - #6288

Merged
chrisd8088 merged 22 commits into
git-lfs:mainfrom
chrisd8088:update-progress-log-callbacks
Aug 5, 2026
Merged

chrisd8088 merged 22 commits into
git-lfs:mainfrom
chrisd8088:update-progress-log-callbacks

Conversation

@chrisd8088

Copy link
Copy Markdown
Member

This PR updates the format of the progress log entries written by the Git LFS client to clearly indicate when the total size of an object is unknown until it is fully processed.

This PR also resolves a bug which causes prevents writes to the progress log from being throttled when the total size of an object is unknown.

In addition, this PR refactors and expands our test suite to more comprehensively validate the callback mechanisms used to write progress log entries.

This PR will be most easily reviewed on a commit-by-commit basis with whitespace differences ignored.

Progress Log Format

The Git LFS client's progress log may be enabled by setting the GIT_LFS_PROGRESS environment variable with an absolute path to a log file. Entries will be written to this progress log file during object upload and download transfers, as well as during clean filter operations and when the git lfs checkout command is run.

For historical reasons, we have two functions which both write progress log entries in an identical format. Under specific circumstances, both of these may be passed a -1 value as their total parameter, which results in progress log entries of the form:

download 1/1 1234/-1 file.bin

At present, such log entries will only appear when the response to an HTTP object download request lacks a Content-Length header, as may occur when gzip content encoding is provided by the remote Git LFS service. (For the log entries to contain a -1 field, the objects must also be downloaded by one of our git lfs fetch, git lfs pull, or git lfs smudge commands, and not by the git lfs filter-process command, because it only writes progress log messages while copying the downloaded objects from the local Git LFS storage directories back to Git, at which point the total size of each object is known from the locally cached files.)

However, absent the changes from this PR, the bug fix from PR #6011 would result in all clean filter operations also always generating progress log entries with -1 fields, because the Git LFS client will no longer attempt to determine the size of the object data sent from Git by probing the size of the corresponding file in the working tree.

Therefore, before we adopt the changes from PR #6011, we first update our progress log entries to more clearly report when an object's total size is unknown using the following format:

download 1/1 1234/???? file.bin

As well, once the final size of the object is known because an EOF condition is reached, a progress log entry will now always be written in which the numerator and denominator of the "bytes read so far" field are equal, e.g.:

download 1/1 2345/2345 file.bin

Progress Log Throttling

In PR #5504 we revised one of the two functions which write progress log entries with the intent that it should skip writing an entry unless at least 200 ms have elapsed since the last time an entry was written, or if the current entry would be the last one for a given object because all of the object's data has been processed. (The function also always writes an entry the first time it is invoked.)

However, when the callback function receives a negative value in its total parameter, the value of the written parameter will always be larger, and so the function assumes the current entry is the final one for an object because all of the object's data has been processed. Thus the throttling mechanism is effectively disabled when the total size of a given object is unknown.

While this bug has limited impact at the moment, absent the changes from this PR, the bug fix from PR #6011 would result in the callback function always writing progress log entries during clean filter operations, thus bypassing the throttling mechanism completely.

Since excessive progress log writes during clean filter operations were the impetus for the change from PR #5504, we therefore revise the callback function created by the CopyCallbackFile() method of the GitFilter structure in our lfs package so that it throttles writes both when an object's size is unknown, and when an object's size is underestimated and the function is called with values of its written parameter which exceed the values of the total parameter.

We ensure, though, that once the EOF condition is reached and an object's total size is known, a final progress log entry is always written for that object.

As noted below, we defer to a future PR the work of consolidating the two functions that both write progress log entries. This would ensure that writes to the progress log were always throttled, not just during clean and smudge filter operations, which at present is when we make use of the callback function created by the CopyCallbackFile() method to write progress log entries. In the case of our other commands, such as git lfs pull, we utilize the logBytes method of the Meter structure in our tq package, and this method never throttles the number of writes to the progress log that it performs.

Test Updates

Before we make the revisions described above to the Git LFS client, we first refactor and expand the set of existing Go test functions and shell integration tests related to the implementation of the progress log feature.

In particular, we update the sole shell test in the t/t-progress.sh script to test all the possible types of progress log entries, and we add two new shell tests to verify that the git lfs smudge, git lfs fetch, and git lfs pull commands now generate progress log entries without -1 fields when they download an object and the HTTP response lacks a Content-Length header.

As well, we significantly refactor the TestCopyCallbackFileThrottle() function that was added with PR #5504 so that it now iterates through a series of test cases and repeats the same checks and assertions for each one. These test cases include an object whose total size is known and correct, one whose total size is unknown, one whose total size is underestimated, and one whose total size is overestimated. In all of these cases, the test checks that the progress log entries that are generated match the expected results.

We also introduce a new internal/testutil Go package and populate it with several interface implementations which are useful only in the context of Go test functions. One of these, our new EagerEOFByteReader structure, is simply a renamed version of the EOFReader structure that we currently define for the use of our test functions in the tools package. However, we add Seek() and Close() methods for our EagerEOFByteReader structure so we can make use of it in contexts which require an io.ReadSeekCloser interface.

Future Work

We defer to a future PR the changes necessary to consolidate the two structures we use to wrap io.Reader and io.ReadSeekCloser interfaces, namely the BodyWithCallback and CallbackReader structures in our tools package.

We also defer to a future PR the changes necessary to consolidate the two functions which both write progress log entries in an identical format, namely the anonymous callback function created by the CopyCallbackFile() method of the GitFilter structure in our lfs package and the logBytes() method of the Meter structure in our tq package. Such a change would have the benefit that all writes to the progress log would be throttled equally, and not just those performed by the anonymous callback function created by the CopyCallbackFile() method.

As well, we defer to a future PR the possibility that in some cases we should be able to determine the expected size of an object from the corresponding Git LFS pointer, and so reduce the number of instances where we invoke our progress logging callback functions with an unknown object size represented with a -1 value. In particular, during object download operations, we should always know the expected size of the object from the Git LFS pointer, including when a smudge filter operation has parsed a valid pointer from data sent by Git. However, note that during clean filter operations, which are the subject of PR #6011, this will not be possible because we only generate the Git LFS pointer once we have read all the object data sent by Git.

Finally, we defer to a future PR the removal of the closingByteReader structures and NewByteBody() functions found in both our lfsapi and lfshttp packages, and the necessary updates to those packages so they instead make use of the ClosingByteReader structure and NewClosingByteReader() function that we now define in our tools package. Note that these are not new, but simply renamed versions of the closingByteReader structure and NewByteBody() function also found in our tools package now (meaning we currently have three identical versions of these in three separate packages).

In commit e8325a9 of PR git-lfs#2608 we added
the original version of our "t/t-progress.sh" shell test script with
a single test named "GIT_LFS_PROGRESS".  This test remains the only
one in the script, and is intended to verify that the Git LFS client
writes progress log messages into the file specified with the
GIT_LFS_PROGRESS environment variable.

At present, the test creates a repository whose name is derived
solely from the name of the script.  In a subsequent commit in this PR
we expect to add additional tests to the "t/t-progress.sh" script,
and so we now revise the initial test to name its repository from
a local variable with a fixed value.  We also change the name of
the test to more clearly identify its purpose and to align its
name with that of its test repository.

The Git LFS client requires that the GIT_LFS_PROGRESS environment
variable contain an absolute file path before the client will
write any progress log messages to the file.  The test therefore
sets the environment variable with an absolute path to a file named
"progress.log" in the directory specified by the TRASHDIR shell
variable.  This directory is created during the initialization of
each shell test script and is unique to the script.

However, since we intend to add new tests to the "t/t-progress.sh"
script, if we use the same file path for the GIT_LFS_PROGRESS
environment variable in each test, we will need all the tests
which follow the initial one to begin by removing the file, or
else the tests may pass because they unintentionally match lines
in the log created by previous tests.

We therefore rewrite the initial test to create a series of
unique progress log files in a directory unique to the test,
whose path we construct using the TRASHDIR variable plus the
name of the test's repository.

As well, we expand the test so that it now verifies all of the
types of progress log messages output by the Git LFS client,
including for both "clean" filter operations and "upload" object
transfers.  Note that "clean" filter operations are logged separately
for each object, so we always find "1/1" fields in these messages
(i.e., "one file of one total file"), whereas our other messages
report "1/n" fields where "n" is the total number of objects being
downloaded or uploaded.

We also take the opportunity to insert clarifying whitespace to the
test, add a check that the "git push" command returned an exit code
of zero to indicate success, and change the name of the directory
into which the test clones its primary repository to follow our
current idiom, in which we append the suffix "-assert" to the name
of the source repository.
In PR git-lfs#1955 we resolved a problem in the Read() methods of the
interfaces we use to invoke callback functions that write entries
to the progress log file defined by the GIT_LFS_PROGRESS environment
variable.  (These methods are currently located in our "tools"
package, but at the time they were defined in a package named
"progress".)

The BodyWithCallback and CallbackReader structure types in our
"tools" package both implement the Reader interface from the "io"
package of the Go standard library by passing through calls from
their Read() methods to the Read() method of another implementation
of the Reader interface.  Depending on the return values from that
call, our structures' Read() methods may then invoke a callback
function to write an entry in the progress log file.

In commit a2a7b09 of PR git-lfs#1955 we
fixed a bug in the Read() methods of our BodyWithCallback and
CallbackReader structures whereby the methods would fail to invoke
their callback functions if the underlying Read() method read a
non-zero number of bytes but also encountered an EOF condition.

The change from commit a2a7b09
now ensures the callback function is invoked under these circumstances,
but it also replaces the EOF error with the error returned by the
callback function, which will usually be nil.  As a result, the
callers of our structures' Read() methods then have to perform a
second call to determine that the EOF condition has been reached.
In this additional call, our structures' Read() methods will not
invoke their callback functions because the underlying Read() method
reads zero bytes; our methods will then return the EOF error from
the underlying Read() method call.

We will address this issue in a subsequent commit in this PR, and
make several other revisions to the Read() methods of the
BodyWithCallback and CallbackReader structures.  To ensure these
changes are effective, we will also expand the set of tests we
perform, both in our "tools" package and in our "lfs" package.

As such, we expect to make use of the EOFReader test helper structure
type added in commit 591307d of
PR git-lfs#1955, but we first need to move the definition of the structure
into a common location where it can be imported into both the
"tools" and "lfs" packages.

Our EOFReader structure was introduced to help verify that the changes
implemented in commit a2a7b09 were
effective.  As such, the structure's Read() method always returns an
EOF error as soon as possible, even if a non-zero number of bytes
were also read.

In the tests we expect to add in subsequent commits in this PR,
we will also need a test helper structure whose Read() method
consistently returns an EOF error as late as possible, meaning that
it only returns the EOF error when no bytes have also been read.

To distinguish these two test helpers, and to help clarify the
purpose of the existing EOFReader structure, we now rename it to
EagerEOFByteReader.  We also introduce a NewEagerEOFByteReader()
function which we can use to initialize a new structure, so all
of our test functions will not have to explicitly populate the
structure's fields.

As well, because we will need to use this structure (plus a
complementary DeferredEOFByteReader structure) in the test functions
from multiple packages, we create a new, common "internal/testutil"
package and move the definition of the EagerEOFByteReader and its
sole test function into this package.

Note that in future, we may begin to move some of our other, existing
packages into the "internal" directory, as we do not intend for their
APIs or ABIs to be used in third-party applications.  In PR git-lfs#4582 we
added a note to our "go.mod" file to caution developers not to rely
on our packages having stable APIs or ABIs, but a more comprehensive
approach would be to move the majority of our packages into the
"internal" directory.  However, we defer this work for the present
time as it far exceeds the scope of this PR.
In a previous commit in this PR we moved the definition of the
EOFReader structure type and its methods from our "tools" package to
a new package named "internal/testutil", and we also renamed the
structure to EagerEOFByteReader and introduced a factory function
named NewEagerEOFByteReader() to initialize an EagerEOFByteReader
structure.

In subsequent commits in this PR we expect to add new tests of
the interfaces we use to invoke callback functions that write entries
to the progress log file defined by the GIT_LFS_PROGRESS environment
variable.  In particular, we intend to expand the set of tests which
exercise the Read() methods of the BodyWithCallback and CallbackReader
structure types in our "tools" package.

Both of these structure types implement the Reader interface from the
"io" package of the Go standard library by passing through calls from
their Read() methods to the Read() method of another implementation
of the Reader interface.

The Read() method of any structure which implements the Reader
interface is permitted to return either a nil error or an EOF error
from the call which reads the final byte of data.  All subsequent
calls to the Read() method must, of course, return an EOF error.

We can use the EagerEOFByteReader structure to verify the behaviour
of the Read() methods of the BodyWithCallback and CallbackReader
structures when the Read() method they invoke returns an EOF error
at the same time as it reads the final byte of data.

However, some of our new tests will also require a way to verify the
behaviour of the Read() methods of the BodyWithCallback and
CallbackReader structures when the Read() method they invoke returns
a nil error at the same time as it reads the final byte of data.

To allow us to write these tests, we first introduce a new
DeferredEOFByteReader structure to our "internal/testutil" package,
with an accompanying NewDeferredEOFByteReader() factory function.
The Read() method of this new structure is guaranteed to always
return a nil error when it reads the last byte of data, and to
then return an EOF error from all subsequent calls.
In PR git-lfs#1955 we resolved a problem in the Read() methods of the
interfaces we use to invoke callback functions that write entries
to the progress log file defined by the GIT_LFS_PROGRESS environment
variable.  (These methods are currently located in our "tools"
package, but at the time they were defined in a package named
"progress".)

The BodyWithCallback and CallbackReader structure types in our
"tools" package both implement the Reader interface from the "io"
package of the Go standard library by passing through calls from
their Read() methods to the Read() method of another implementation
of the Reader interface.  Depending on the return values from that
call, our structures' Read() methods may then invoke a callback
function to write an entry in the progress log file.

In commit a2a7b09 of PR git-lfs#1955 we
fixed a bug in the Read() methods of our BodyWithCallback and
CallbackReader structures whereby the methods would fail to invoke
their callback functions if the underlying Read() method read a
non-zero number of bytes but also encountered an EOF condition.

In the same PR git-lfs#1955 we then added a new Go test function named
TestCopyCallbackReaderCallsCallbackUnderfilledBuffer() which verifies
that the Read() method of the CallbackReader structure invokes the
callback function after the underlying Read() method returns the
EOF error after reading a non-zero number of bytes.

However, we did not include a corresponding test of the Read() method
of the BodyWithCallback structure, so we now revise the existing test
to verify the behaviour of both the CallbackReader structure and
the BodyWithCallback structure.

(Note that in a future PR, we may eliminate one of these two similar
structures, at which point this test will only need to check the
behaviour of that common structure's methods.)

Since the BodyWithCallback structure implements the ReadSeekCloser
interface from the "io" package, we need to add simple Seek()
and Close() methods for the EagerEOFByteReader structure in our
"internal/testutil" package.  Fortunately, the Seek() method only
needs to exist for the purposes of our test, which allows us to just
define a stub version of the method.  Otherwise, we would likely need
to mirror the complete implementation from the Seek() method of the
Reader structure in the Go standard library's "bytes" package:

  https://github.com/golang/go/blob/go1.26.4/src/bytes/reader.go#L115-L134

To alter the TestCopyCallbackReaderCallsCallbackUnderfilledBuffer()
function to check the behaviour of both the CallbackReader and
BodyWithCallback structures, we first create one of each using the
same initial settings.  We then iterate over the pair of these
structures and perform identical checks on each one, including an
additional check that after EOF has been reached and a final callback
has been made, no further callback occurs on a subsequent invocation
of the Read() method when no bytes are available to be read.

We also simplify the test in several respects.  First, because our
structures' Read() methods invoke their callback functions
synchronously within a single goroutine, there is no need for us
to use the "sync/atomic" package from the Go standard library to
increment the count of the number of times the callbacks occur.

Second, we revise the test to avoid the use of hard-coded constant
values in favour of local variables whose values are dependent on
the size of the byte slice read by the structures' Read() methods.

Third, where possible we check "int" values using just the Equal()
function from the "github.com/stretchr/testify/assert" package, as
we can avoid the use of the EqualValues() function except when the
integer types of the expected and actual values differ.

And fourth, to clarify the purpose of the test and its expanded scope,
we rename it to TestBothCallbackReadersInvokeCallbackOnEagerEOF().

In commit 589dffb of PR git-lfs#1958 we
added two other functions to test the Read() method of the
BodyWithCallback structure.  One of these test functions, the
TestBodyCallbackReaderUpdatesOffsetOnSeek() function, only pertains
to the specific behaviour of the BodyWithCallback structure's
Read() method.

However, the TestBodyCallbackReaderCountsReads() test function checks
that the BodyWithCallback structure's Read() method correctly updates
its internal count of the number of bytes read, which is an operation
also performed by the CallbackReader structure's Read() method.  We
therefore revise this function in a similar manner to we did for the
TestCopyCallbackReaderCallsCallbackUnderfilledBuffer() function so
that it now checks that both structures' Read() methods correctly
update the internal counts of the number of bytes read.

We likewise rename the function to TestBothCallbackReadersCountReads()
and replace the use of hard-coded constants with local variables
wherever possible.  We also alter the test so that it performs
multiple calls to each structure's Read() method and checks the
internal count after each call, rather than just performing a single
call to the Read() method which consumes all the available data.

Finally, we take the opportunity to also amend the existing
TestBodyCallbackReaderUpdatesOffsetOnSeek() function so that it
uses the same buffer initialization steps as the other two test
functions, avoids hard-coded constants in favour of local
variables, and checks that the results from the Read() method
match our expectations after the read position has been modified
by one or more Seek() method calls.
In a subsequent commit in this PR we expect to revise how the Read()
methods of the BodyWithCallback and CallbackReader structures in our
"tools" package behave when the underlying Read() method reads a
non-zero number of bytes but also encounters an EOF condition.

At present, under these conditions our structures' Read() methods
always replace the EOF error returned by the underlying Read()
method with the "error" value returned by the callback function,
even if that value is nil.

Before we adjust our structures' Read() methods to preserve the EOF
condition from the underlying resource when a callback function does
not encounter an error, we first add a pair of new Go test functions
to verify that other types of error conditions are handled
appropriately by these methods.

Our new TestBothCallbackReadersPreferCallbackErrorOverEOF() test
function simulates a callback function which returns a non-nil
"error" value, and checks that the Read() methods of our
BodyWithCallback and CallbackReader structures always return that
value if they perform the callback.

This test uses the EagerEOFByteReader structure we added to our
"internal/testutil" package in a prior commit in this PR to ensure
that the underlying Read() method for both the BodyWithCallback
and CallbackReader structures will return an EOF error as soon as
the final bytes of a byte slice are read.  The test then confirms
that a callback is performed in this case and that the error
returned by the callback function is preferred over the EOF error.

As well, the test checks that once all the bytes of the byte slice
have been read, if another call to the Read() method of either the
BodyWithCallback or CallbackReader structures is made, no callback
will be performed and the EOF error from the underlying Read()
method will be returned.

Our new TestBothCallbackReadersSkipCallbackAfterReadError() test
function, by comparison, simulates an underlying Read() method that
encounters an error condition other than the EOF condition, and
checks that the Read() methods of our BodyWithCallback and
CallbackReader structures do not perform a callback and just
return the "error" value from the underlying Read() method.

This test requires the use of a new ErrReader structure which we
add to our "internal/testutil" package.  We model this structure's
Read() method on that of the ErrReader structure from the
"testing/iotest" package of the Go standard library, but also
define no-op Seek() and Close() methods so that the structure's
methods implement the complete ReadSeekCloser interface from the
"io" package.  We require a full implementation of this interface
so that we can use our new ErrReader structure as the underlying
resource of a BodyWithCallback structure.
In commit 589dffb of PR git-lfs#1958 we
introduced Seek() and ResetProgress() methods for the BodyWithCallback
structure, which at the time was defined in a "progress" package
and is now located in our "tools" package.

While the Seek() method implements the expected behaviour of the
Seeker interface from the Go standard library's "io" package, the
ResetProgress() method simply invokes the callback function registered
with our BodyWithCallback structure and passes the current number of
bytes read so far as a negative value in the function's final
"readSinceLast" parameter.  We then call the ResetProgress() method
when an object upload transfer needs to be retried so that we can
adjust our progress meter's count of the bytes that have been
transferred successfully.

In PR git-lfs#1958 we added the TestBodyCallbackReaderUpdatesOffsetOnSeek()
test function to verify the behaviour of the BodyWithCallback
structure's new Seek() method, but we did not also create a test
for the ResetProgress() method at the same time.

Because we expect to refactor the BodyWithCallback structure and its
methods in a subsequent commit in this PR, we now add a new
TestBodyCallbackReaderResetsProgress() test function which verifies
that the ResetProgress() method works as expected.  In particular,
it invokes the ResetProgress() method and confirms that the
registered callback function receives a "readSinceLast" parameter
whose value is the negation of the number of bytes read so far
from the underlying source buffer.
In commit d7d9fd7 of PR git-lfs#4042 we
introduced the "closingFileReader" structure to our "tools" package,
which wraps a File structure from the Go standard library's "os"
package but replaces its Close() method with a no-op stub method.

In the same commit we also added the NewFileBody() and
NewFileBodyWithCallback() factory functions, the latter of which
calls the former and then passes the "closingFileReader" structure
it instantiates to the NewBodyWithCallback() function.  We then
call the NewFileBodyWithCallback() function in both our SSH and
HTTP upload transfer adapters in instances where we expect the
underlying File structure's Close() method to be called later
than the Close() method of the "closingFileReader" structure.

In the case of our HTTP upload transfer adapter in particular,
when the upload request for an individual object is redirected to
another URL, the Go standard library's "net/http" package will
invoke the Close() method on the Body field of the current Request
structure after the initial request is complete.  However, we do
not want this call to cause the underlying file resource to be
closed as well, so we need to provide a stub Close() method which
takes no action when it is called by the HTTP transport mechanism.

To verify that the changes in PR git-lfs#4042 were effective, in commit
d7d9fd7 we also implemented a new
"redirect upload" test in our "t/t-upload-redirect.sh" shell test
script, which checks that when the Git LFS client receives an HTTP
redirection in response to an object upload request, the client is
able to make a complete and successful object transfer to the new
URL provided in the initial response.

However, we did not also define a Go test function which exercises
the "closingFileReader" structure and related factory functions.
Because we expect to rename this structure and the NewFileBody()
function in a subsequent commit in this PR, we now add a new
TestFileBodyCallbackReaderIgnoresClose() test function which verifies
that the structure's Close() method works as expected.  In particular,
it invokes the Close() method and confirms that the underlying file
resource is not closed, and that additional read operations on the
file resource will still succeed.
In PR git-lfs#1454 we added the RetriableReader structure to our "tools"
package, and then in commit a9bbe0e
of PR git-lfs#1482 we introduced several Go test functions to exercise the
structure's methods under various conditions.  We defined these
functions in our "tools/iotools_test.go" source file and set the
Go package name in the file to "tools_test" rather than just "tools".

One consequence of this difference is that we then need to import
our own "tools" package into the "tools/iotools_test.go" source file
even though it resides in the same directory as the rest of our
"tools" package's source files.

Previously, in commit 1ef2d43
of PR git-lfs#1265, we moved the definition of the CopyWithCallback()
function from our "lfs" package to our "tools" package, and also
moved the corresponding TestCopyWithCallback() test function at
the same time.  These functions were previously defined in the
"util.go" and "util_test.go" source files, respectively, in our
"lfs" package.

However, while we now define both functions in our "tools" package,
the CopyWithCallback() function is located in a source file named
"iotools.go" but the TestCopyWithCallback() function is still located
in a source file named "util_test.go".

In a subsequent commit in this PR we intend to move the
TestCopyWithCallback() function into the "tools/iotools_test.go"
source file, to align with the location of the corresponding
CopyWithCallback() function in the "tools/iotools.go" file.

As well, in other future commits in this PR we expect to migrate
the "closingByteReader" structure and its related methods from the
"tools/copycallback.go" source file to the "tools/iotools.go" file,
and to add a new test function to the "tools/iotools_test.go" source
file which exercises the structure's Close() method.

At present, any test functions in the "tools/iotools_test.go"
source file must explicitly reference the structures and methods
they test using the "tools." prefix, since the source file
specifies a distinct "tools_test" Go package name.

To simplify the changes we expect to make in subsequent commits in
this PR, we therefore first change the package name in the
"tools/iotools_test.go" source file to just "tools", and update
the existing test functions for the RetriableReader structure to
remove the "tools." prefix from their references to the
NewRetriableReader() function.
In commit 1ef2d43 of PR git-lfs#1265 we
moved the definition of the CopyWithCallback() function from our
"lfs" package to our "tools" package, and at the same time we also
moved the corresponding TestCopyWithCallback() test function.  These
functions were previously defined in the "util.go" and "util_test.go"
source files, respectively, in our "lfs" package.

However, while we now define both functions in our "tools" package,
the CopyWithCallback() function is located in a source file named
"iotools.go" but the TestCopyWithCallback() function is still located
in a source file named "util_test.go".

In subsequent commits in this PR we expect to slightly modify the
CopyWithCallback() function.  We also intend to refactor the
CallbackReader structure and its methods, on whose implementation
the CopyWithCallback() function depends.

Before we make these changes, though, we first update the
TestCopyWithCallback() function and move it to the "iotools_test.go"
source file to correspond more closely with the location of the
CopyWithCallback() function.  We make several minor adjustments to
the TestCopyWithCallback() function so that it now follows the same
design as the tests of the CallbackReader structure and its methods,
which we revised in previous commits in this PR.

In particular, we replace a number of hard-coded constants with local
variables, change variable names to match those from the related tests
in the "tools/copycallback_test.go" source file, and make use of the
EqualValues() function from the "github.com/stretchr/testify/assert"
package to compare integer values of different bit lengths.
In commit e04316f of PR git-lfs#1839 we
added the initial version of what is now the BodyWithCallback
structure in our "tools" package, and to verify the behaviour of
its methods we included a new TestBodyWithCallback() Go test function
in our "lfs" package which copied the design of the existing
TestReadWithCallback() test function.

Both functions perform the same set of checks, with the only
substantive difference being that one exercises the methods of a
BodyWithCallback structure while the other exercises the methods
of a CallbackReader structure, which are also defined in our
"tools" package.

We expect to refactor both of these structures and their methods in
subsequent commits in this PR, so in previous commits in this PR
we have already updated the relevant tests in the "tools" package.
We now begin to do the same for the tests in the "lfs" package,
beginning with the TestBodyWithCallback() and TestReadWithCallback()
functions.

Following the pattern we have used for several test functions in our
"tools/copycallback_test.go" source file, we combine the two functions
into a single function named TestBothCallbackReadersWithCallback(),
which now exercises the methods of both the BodyWithCallback and
the CallbackReader structures.

(Note that in a future PR, we may eliminate one of these two similar
structures, at which point this test will only need to check the
behaviour of that common structure's methods.)

We also update our single new TestBothCallbackReadersWithCallback()
function to follow the same design as the related tests in the
"tools/copycallback_test.go" and "tools/iotools_test.go" source files.

In particular, we replace a number of hard-coded constants with
local variables, change variable names to match those of the other
tests, and make use of the EqualValues() function from the
"github.com/stretchr/testify/assert" package to compare integer
values of different bit lengths.
In PR git-lfs#5504 we modified the CopyCallbackFile() method of the
GitFilter structure in our "lfs" package so that the callback
function it creates only outputs a progress log message when
at least 200 milliseconds have elapsed since the last message
was written, unless the number of bytes read has now met or
exceeded the total size of the given Git LFS object.  (The
minimum time allowed between messages is specified by the
DefaultLoggingThrottle constant from our "tasklog" package.)

In commit 00f1e95 of PR git-lfs#5504 we then
also introduced the TestCopyCallbackFileThrottle() test function to
verify that the callback function created by the CopyCallbackFile()
method obeyed the new limits on how often it should write to the
progress log.

At present, the callback function checks whether the number of
bytes read has met or exceeded the total size of the given object,
and if it has, unconditionally writes a message to the progress
log, on the assumption that no further callbacks will occur because
the final bytes of the object have now been read.

However, this assumes the total size of the object is known and
correct when the callback is utilized by the CopyWithCallback()
function of our "tools" package.

While this remains true at present for most of the situations in
which we pass a callback function to the CopyWithCallback() function,
these will be more limited in number after we make the change to our
"clean" filter commands proposed in PR git-lfs#6011.

PR git-lfs#6011 resolves a bug in our "git lfs filter-progress" and
"git lfs clean" commands whereby they can pass an incorrect
"totalSize" parameter to the CopyWithCallback() function.  The only
solution for this problem is for the commands to simply not attempt
to determine the total size of an object.

Absent other changes, the result of this bug fix would be that
even if a progress log file is specified by the GIT_LFS_PROGRESS
environment variable, no messages would be written to the log file
during "clean" filter operations.  This would occur because the
copyToTemp() method of the GitFilter structure in our "lfs" package
would find a negative value in its "fileSize" parameter and then
set its "cb" callback function parameter to nil.

We intend to address this issue in PR git-lfs#6011, but before we do so,
in subsequent commits in this PR we will revise the callback
function created by the CopyCallbackFile() method to handle the
case where it is passed a negative value in its "total" parameter,
which indicates that the total size of the object is unknown.

Before we make those revisions to the CopyCallbackFile() method,
though, we first expand our test suite to exercise the callback
function created by the method under a wider range of conditions.

Specifically, we now refactor the TestCopyCallbackFileThrottle()
function so we will be able to easily expand it to simulate a
number of different test cases.  We define these test cases using
a new copyCallbackFileThrottleTestCase structure and an associated
copyCallbackFileThrottleTestMode enumerated value.

At present, the only test case we specify is one where the initial
total size of an object is both known and correct, but in future
commits in this PR we will add other test cases, including one
where the total size of an object is unknown and is therefore
represented by a negative value.

We alter the TestCopyCallbackFileThrottle() function itself to
iterate over one or more test cases and check each one using both
the EagerEOFByteReader and the DeferredEOFByteReader structures from
our "tools" package.  We use these structures' Read() methods to
simulate conditions where the method returns either an EOF error or
a nil error, respectively, at the same time as the final byte of an
input source is read.

For each test case and underlying implementation of the Reader
interface from the Go standard library's "io" package, the
TestCopyCallbackFileThrottle() function now calls the setup()
method of the copyCallbackFileThrottleTestCase structure, which
initializes the progress log file, mock time clock, callback
function, and input data source for a CallbackReader structure
from our "tools" package.

The test function then runs the Assert() method of the
copyCallbackFileThrottleTestCase structure to perform a series of
calls to the Read() method of the CallbackReader structure after
simulated periods of time have elapsed, and checks that the final
contents of the progress log file match the expected results for
the given test case.

In later commits in this PR we will expand the set of test cases run
by the TestCopyCallbackFileThrottle() function as we revise the
callback function created by the CopyCallbackFile() method and the
Read() methods of the BodyWithCallback and CallbackReader structures
in our "tools" package.
Our "tools" package defines two structures which perform similar
purposes, the BodyWithCallback and CallbackReader structures, whose
Read() methods are identical and invoke a callback function after
first calling the Read() method of an underlying resource.

The only significant difference between the two structures is that
the BodyWithCallback structure's methods implement the ReadSeekCloser
interface from the Go standard library's "io" package, while the
CallbackReader structure's methods only implement the Reader
interface from the "io" package.

In a prior commit in this PR we updated a number of the Go test
functions in both our "tools" and "lfs" packages to invoke these
structures' Read() methods and verify the results under various
conditions.

While our test functions can initialize a new BodyWithCallback
structure that reads from an underlying byte slice using a factory
function, they have to directly initialize a new CallbackReader
structure by setting its fields' values explicitly.  So too does
the CopyWithCallback() function in our "tools" package.

In a subsequent commit in this PR we intend to make substantial
revisions to the Read() methods of both the BodyWithCallback and
the CallbackReader structures, and to avoid duplication of this
code will introduce a new internal structure shared by both of
these structures.

As this change would otherwise require us to modify a number of
our test functions to reflect the new internal design and fields
of the CallbackReader structure, we first add a NewCallbackReader()
factory function which abstracts the initialization of a
CallbackReader structure.

We then update the CopyWithCallback() function to make use of
the NewCallbackReader() function, and do the same for all of the
test functions which require that they initialize a CallbackReader
structure.
We define a "closingByteReader" structure with a no-op Close() method
in several of our packages, including the "tools", "lfsapi", and
"lfshttp" packages.  In each case, the purpose of the structure is
to allow us to use the structure returned by the NewReader() function
of the Go standard library's "bytes" package in contexts that require
a structure that implements the ReadSeekCloser interface from the
standard library's "io" package.

We introduced the first of these structures to our "lfsapi" package
in commit 6659b9d of PR git-lfs#1839.
In a subsequent commit in that PR we then added a NewByteBody()
factory function which initializes a "closingByteReader" structure
by passing the byte slice provided as a parameter to the "bytes"
package's NewReader() function.  Next, in the same PR we replicated
the structure, its Close() method, and the NewByteBody() function
into what was at that time called our "progress" package.

In PR git-lfs#2749 we later moved the "progress" package's contents to
our "tools" package, and then in PR git-lfs#3244 we replicated the
structure, its Close() method, and the NewByteBody() function a
third time when we refactored portions of the "lfsapi" package
into our "lfshttp" package.

In our "tools" package we also provide a NewByteBodyWithCallback()
factory function which calls the NewByteBody() function and uses
the "closingByteReader" structure it returns as the "body"
parameter in a call to the NewBodyWithCallback() function.
The result is that a new BodyWithCallback structure is initialized
with the byte slice passed to the NewByteBodyWithCallback()
function as its underlying source of data.

This NewByteBodyWithCallback() function is used exclusively in
various test functions, both in our "tools" package and our "lfs"
package.  Ideally, though, we should not define functions or methods
in our non-test source files that are intended for use only in test
functions.

In prior commits in this PR we updated all of the test functions
which use the NewByteBodyWithCallback() function, and revised a
number of them so that they now perform their checks against the
methods of both a BodyWithCallback structure and the methods of a
CallbackReader structure.

To simplify the initialization of the CallbackReader structures, in
another prior commit in this PR we introduced a NewCallbackReader()
factory function, which operates in a similar manner to the
NewBodyWithCallback() function except that it returns a CallbackReader
structure instead of a BodyWithCallback structure.  We did not,
though, introduce a NewByteCallbackReader() function, as it is
relatively straightforward for our tests to simply pass the return
value from a call to the "bytes" package's NewReader() function to
our NewCallbackReader() function.

Given the marginal utility of the NewByteBodyWithCallback() function
and the fact that it is only used in our test functions, we remove
it now.  This also helps align the structure initialization steps
performed by our test functions, since they now just call the
NewBodyWithCallback() and NewCallbackReader() functions, which
initialize their respective structures in an equivalent manner.

To bring these initialization steps into even closer alignment, we
also rename the "closingByteReader" structure in the "tools" package
to ClosingByteReader, and rename its factory function from
NewByteBody() to NewClosingByteReader().

These changes will allow us to make use of the structure in our
"lfsapi" and "lfshttp" packages, replacing the identical definitions
of the "closingByteReader" structure and the related functions and
methods in those packages.  We defer this refactoring step to a
future PR, however, as it is outside of this PR's scope.

Finally, we move the definition of the ClosingByteReader structure,
its Close() method, and the NewClosingByteReader() function to
our "tools/iotools.go" source file, and add a new test function
named TestClosingByteReaderNopClose() to the "tools/iotools_test.go"
source file.  This test function simply verifies that the Close()
method of the ClosingByteReader structure performs no action, as we
expect that it should not do so.
In commit d7d9fd7 of PR git-lfs#4042 we
introduced the "closingFileReader" structure to our "tools" package,
which wraps a File structure from the Go standard library's "os"
package but replaces its Close() method with a no-op stub method.
In the same commit we also added the NewFileBody() factory function,
which initializes a new "closingFileReader" structure.

The "closingFileReader" structure and the NewFileBody() function were
modelled on the "closingByteReader" structure and NewByteBody()
function which were already present in our "tools/copycallback.go"
source file at the time when PR git-lfs#4042 was developed.  However, the
two structures and their methods serve different purposes.

The "closingByteReader" structure provides a no-op Close() method
so that we can use the structure returned by the NewReader() function
of the Go standard library's "bytes" package in contexts that require
a structure that implements the ReadSeekCloser interface from the
standard library's "io" package.  Since the underlying byte slice
cannot be closed, the Close() method we provide is necessarily just
a no-op.

By contrast, the "closingFileReader" structure's underlying resource
is a File structure from the Go standard library's "os" package,
which does provide a Close() method of its own.  The reason we
substitute this method with a no-op Close() method is so that we
can use the "closingFileReader" structure in instances where we
expect the underlying File structure's Close() method to be called
later than the Close() method of the "closingFileReader" structure.

In the case of our HTTP upload transfer adapter in particular,
when the upload request for an individual object is redirected to
another URL, the Go standard library's "net/http" package will
invoke the Close() method on the Body field of the current Request
structure after the initial request is complete.  However, we do
not want this call to cause the underlying file resource to be
closed as well, so we need to provide a stub Close() method which
takes no action when it is called by the HTTP transport mechanism.

Thus while the "closingByteReader" structure provided a way to
"close" a byte slice (although this operation is not actually
possible), the "closingFileReader" structure provides a way to
avoid "closing" a file resource.

In a prior commit in this PR we renamed the "closingByteReader"
structure to ClosingByteReader in order to export it for later use
in several of our packages, and we also renamed the NewByteBody()
function to NewClosingByteReader().

For greater clarity as to the purpose of the "closingFileReader"
structure, we now rename it to "nopClosingFile", which more clearly
indicates that the structure just wraps a File structure and provides
a no-op Close() method.  For consistency, we also rename the
NewFileBody() function to newNopClosingFile().  As there is only
one caller of this function, the NewFileBodyWithCallback() function,
this contains the sole reference we need to update to reflect the
revised newNopClosingFile() name.

Fortunately, we already have a test function which demonstrates that
these changes have not introduced a regression.  In a previous commit
in this PR we introduced the TestFileBodyCallbackReaderIgnoresClose()
function, which verifies that when the Close() method is called on
the structure returned by the NewFileBodyWithCallback() function,
it does not actually close the underlying file resource.
Our "tools" package defines two structures which perform similar
purposes, the BodyWithCallback and CallbackReader structures, whose
Read() methods are identical and invoke a callback function after
first calling the Read() method of an underlying resource.

The only significant difference between the two structures is that
the BodyWithCallback structure's methods implement the ReadSeekCloser
interface from the Go standard library's "io" package, while the
CallbackReader structure's methods only implement the Reader
interface from the "io" package.

We introduced the first version of our CallbackReader structure in
commit d7a787d of PR git-lfs#97, along
with a Read() method quite similar to the one currently defined
for the structure.

Later, in commit e04316f of PR git-lfs#1839,
we added the initial version of what is now the BodyWithCallback
structure, at which time we simply copied the Read() method of the
CallbackReader structure to create an identical method for the
BodyWithCallback structure.

The implementation of the two Read() methods is identical, so when we
fix a bug in one, we have to make the same change in the other also.
For instance, in commit a2a7b09
of PR git-lfs#1955 we revised both methods to address a problem whereby
they would fail to invoke their callback functions if the underlying
Read() method read a non-zero number of bytes but also encountered
an EOF condition.

In subsequent commits in this PR we intend to alter the design
of the Read() methods of the BodyWithCallback and CallbackReader
structures, and then to make further changes to handle instances
where the total size of a Git LFS object is initially unknown.

As we would prefer to avoid making these revisions in two places,
and to avoid future problems where we might accidentally update
only one of the two Read() methods, we begin by refactoring both
the methods and the BodyWithCallback and CallbackReader structures
so they share a common internal implementation.

Specifically, we introduce a "callbackState" structure whose fields
represent the current state for a given callback function, and then
we embed this structure in the BodyWithCallback and CallbackReader
structures.  These structures now only have one unique field each
to store their underlying data source.

We also define a read() method for the "callbackState" structure,
with an identical design to the existing Read() methods of the
BodyWithCallback and CallbackReader structures except that it
accepts a parameter of the Reader type from the "io" package.
We then revise the Read() methods of the BodyWithCallback and
CallbackReader structures to simply invoke this new read() method
and pass their respective data sources as its Reader type parameter.

Note that in a future PR we may further refactor the implementation
of the BodyWithCallback and CallbackReader structures to replace
them entirely with a single common structure.  However, as this
change would affect a number of our packages, it is outside the
scope of this PR, so for the time being we just revise the existing
structures to share a new, common internal structure and its read()
method.
In a previous commit in this PR we refactored the BodyWithCallback
and CallbackReader structures in our "tools" package so that they
now share a common internal implementation in the form of an internal
"callbackState" structure and its read() method.  The Read() methods
of both the BodyWithCallback and CallbackReader structures now just
invoke the "callbackState" structure's read() method, whereas they
were previously defined separately but with an identical design.

In subsequent commits in this PR we expect to revise the new, common
read() method to handle the condition where a Git LFS object's total
size is initially unknown.  Before we make this change, though, we
first refactor the existing read() method so that it will be easier
to modify and understand.

Rather than execute the callback within a nested pair of conditional
blocks, we introduce a new doCallbackIfRequired() method which checks
the same conditions (albeit expressed differently) and then executes
the callback if it should be performed, and otherwise just returns
the "error" value that was passed by the caller.  The read() method
can then simply invoke the doCallbackIfRequired() method and pass
the values returned by the underlying Read() method.

We also move the actual execution of the callback function into a
doCallback() method, which takes no action if the callback function
is not defined.  While this duplicates one of the conditions also
checked by the doCallbackIfRequired() method, it ensures that it is
no longer possible that the ResetProgress() method might cause a
panic by executing an undefined callback function.
In a previous commit in this PR we refactored the BodyWithCallback
and CallbackReader structures in our "tools" package so that they
now share a common internal implementation in the form of an internal
"callbackState" structure and its read() method.

We derived the design of the read() method from that of the Read()
methods of the BodyWithCallback and CallbackReader structures as
they were previously defined.  This design dates back to the
original introduction of the CallbackReader structure in commit
d7a787d of PR git-lfs#97.

One notable feature of the current implementation of both our new
read() method as well as that of its predecessors is that when
the registered callback function is invoked, the methods always
use the function's "error" return value as their own return value.

As a consequence, if the underlying resource's Read() method reads
a non-zero number of bytes and also encounters an EOF condition,
our read() method will perform a callback and then substitute the
callback function's return value for the EOF error.  In the normal
case where no error occurs during the callback, this will force
the caller to make another invocation of our read() method, at
which point no bytes will be read and so no callback will be
performed, and the EOF condition will be reported to the caller.

While this design is acceptable, because the Reader interface of
the Go standard library's "io" package allows Read() methods to
only return the EOF condition when no bytes are available to be
read, it is slightly less efficient than it could be.

We can improve this situation by returning an EOF error from the
underlying resource's Read() method unless our callback function
encountered an error condition.

To implement this change, we first revise the doCallbackIfRequired()
method of the "callbackState" structure so that the method returns a
nil "error" value when no callback should be performed, and otherwise
returns the "error" value from the callback function.  Next, we
update the read() method so that it saves the return value from the
doCallbackIfRequired() method into a local "callbackErr" variable,
and then returns the "error" value from the Read() method unless the
value of the "callbackErr" variable is non-nil, in which case the
read() method returns that value instead.

Finally, we update two of our existing Go test functions to reflect
the new behaviour of the Read() methods of the BodyWithCallback and
CallbackReader structures.  These tests are sufficient to demonstrate
that our changes to the "callbackState" structure's read() and
doCallbackIfRequired() methods in this commit mean that the read()
method now preserves an EOF error from its underlying resource if
the callback function is invoked and does not encounter an error.

The TestBothCallbackReadersInvokeCallbackOnEagerEOF() function in our
"tools" package and the TestCopyCallbackFileThrottle() function in
our "lfs" package both make use of the EagerEOFByteReader structure
we added to our "internal/testutil" package in a prior commit in
this PR.  This structure's Read() method always returns an EOF error
when it reads the final bytes from a given byte slice.  Previously,
these two test functions expected that when an EagerEOFByteReader
structure was the underlying resource of a BodyWithCallback or
CallbackReader structure, that structure's Read() method would return
a nil "error" value when it read the final bytes of a slice.
In subsequent commits in this PR we expect to make several revisions
to the anonymous callback function created by the CopyCallbackFile()
method of the GitFilter structure in our "lfs" package.  In
particular, we intend to alter the function to more appropriately
handle the case where its "total" parameter is negative, which
indicates that the total size of the current object is unknown.

At present, when the callback function is passed a negative value in
its "total" parameter, it simply writes that value to the progress
log as the denominator of the fraction representing the amount of
the object's data processed so far.

As well, if the callback function is passed a negative value in
its "total" parameter, it will always write to the progress log
regardless of how much time has elapsed since the last message was
written.

In PR git-lfs#5504 we modified the CopyCallbackFile() method of the
GitFilter structure in our "lfs" package so that the callback
function it creates only outputs a progress log message when
at least 200 milliseconds have elapsed since the last message
was written, unless the number of bytes read has now met or
exceeded the total size of the given Git LFS object.  (The
minimum time allowed between messages is specified by the
DefaultLoggingThrottle constant from our "tasklog" package.)

The callback function checks whether the number of bytes read has
met or exceeded the total size of the given object, and if it has,
unconditionally writes a message to the progress log, on the
assumption that no further callbacks will occur because the final
bytes of the object have now been read.

However, this assumes the total size of the object is known and
correct when the callback is utilized by the CopyWithCallback()
function of our "tools" package.  When the "total" parameter is
negative, though, the number of bytes read so far is always larger
than the total size, so a message is always written.

While it is currently rare for the value in the callback function's
"total" parameter to be negative, we expect it to become more common
in the future as a result of the bug fix to our "clean" filter
commands implemented by PR git-lfs#6011.

Before we address this issue, we first refactor the callback function
created by the CopyCallbackFile() method to make the logic in the
function easier to modify and understand.  In particular, we separate
the multiple parts of the function's single conditional expression
into distinct steps.  We also revise the function so that it returns
immediately when the condition checked by one of these individual
steps is indicates that no log message should be written.
In PR git-lfs#5504 we modified the CopyCallbackFile() method of the
GitFilter structure in our "lfs" package so that the callback
function it creates only outputs a progress log message when
at least 200 milliseconds have elapsed since the last message
was written, unless the number of bytes read has now met or
exceeded the total size of the given Git LFS object.  (The
minimum time allowed between messages is specified by the
DefaultLoggingThrottle constant from our "tasklog" package.)

The callback function checks whether the number of bytes read has
met or exceeded the total size of the given object, and if it has,
unconditionally writes a message to the progress log, on the
assumption that no further callbacks will occur because the final
bytes of the object have now been read.

However, this assumes the total size of the object is known and
correct when the callback is utilized by the CopyWithCallback()
function of our "tools" package.  When the "total" parameter is
negative, though, the number of bytes read so far is always larger
than the total size, so a message is always written.

While it is currently rare for the value in the callback function's
"total" parameter to be negative, we expect it to become more common
in the future as a result of the bug fix to our "clean" filter
commands implemented by PR git-lfs#6011.

PR git-lfs#6011 resolves a bug in our "git lfs filter-progress" and
"git lfs clean" commands whereby they can pass an incorrect
"totalSize" parameter to the CopyWithCallback() function.  The only
solution for this problem is for the commands to simply not attempt
to determine the total size of an object.

Absent other changes, the result of this bug fix would be that
even if a progress log file is specified by the GIT_LFS_PROGRESS
environment variable, no messages would be written to the log file
during "clean" filter operations.  This would occur because the
copyToTemp() method of the GitFilter structure in our "lfs" package
would find a negative value in its "fileSize" parameter and then
set its "cb" callback function parameter to nil.

We intend to address this issue in PR git-lfs#6011, but the consequence
when we do so will be that the callback function created by the
CopyCallbackFile() method will receive a negative value in its
"total" parameter during "clean" filter operations if a progress log
is configured with the GIT_LFS_PROGRESS environment variable.

At present, the callback function may already receive a negative
value in its "total" parameter, but only under a relatively unusual
combination of circumstances.  First, of course, a progress log must
be enabled with the GIT_LFS_PROGRESS environment variable.  Second,
the legacy "git lfs smudge" command must be used during a "smudge"
filter operation rather than the "git lfs filter-process" command;
this may occur if a version of Git older than v2.11.0 is in use.
Third, the "git lfs smudge" command must download an object that is
not available locally from a server which compresses the content
for transit and therefore sends a Content-Encoding header but no
Content-Length header.

Under these conditions, the lack of a Content-Length header means
the size of the HTTP response body will be represented with a value
of -1.  When our HTTP download adapter passes this negative value to
the CopyWithCallback() function, it will be used to initialize a new
CallbackReader structure from our "tools" package, and ultimately
passed to the callback function created by the CopyCallbackFile()
method.  Note that the callback function itself is passed from the
Smudge() method of the GitFilter structure to the same structure's
downloadFile() method, which creates a new transfer queue using
the WithProgressCallback() option function.

By contrast, when the "git lfs filter-process" command is in use,
it creates a transfer queue without a defined callback function.
As a result, objects that not are available locally are downloaded
and written to local storage without generating any progress log
messages.  When the Smudge() method is later invoked (because Git
re-sends a "smudge" command to our long-running filter process),
it runs the GitFilter structure's readLocalFile() method instead of
the downloadFile() method.  While the readLocalFile() method does
invoke the CopyWithCallback() function and passes a callback
function to it, at this point the size of the object can be
determined from the file in local storage, so the callback function
receives a non-negative value in its "total" parameter.

However, as described above, when the "git lfs smudge" command is
in use and a content-encoded download occurs, the callback function
created by the CopyCallbackFile() method will receive a negative
value in its "total" parameter, which means the function will write
to the progress log regardless of the amount of time that has
elapsed since the previous message was written.

Further, we expect the changes in PR git-lfs#6011 to result in a wider
range of conditions under which the callback function will receive
a negative value in its "total" parameter.

We therefore now revise the callback function created by the
CopyCallbackFile() method so that it checks whether the value of
the "total" parameter is negative, and if so will not write a
progress log message unless a sufficient period of time has
elapsed since the previous log message was written.

To verify that our change is effective, we define a second test
case for the TestCopyCallbackFileThrottle() test function that
we refactored in a prior commit in this PR.  Our new test case
is named "initial total unknown" and simulates multiple invocations
of the callback function where the "total" parameter has a negative
value each time.  In this test case, we expect that the modifications
from this commit should ensure that the callback function only writes
one log message, after the test advances the mock time clock by the
full duration specified in the DefaultLoggingThrottle constant from
our "tasklog" package.

Note that the log message expected by our new "initial total unknown"
test case contains a "-1" in the denominator of the fraction
representing the amount of source data read so far.  We will refine
this field in a subsequent commit in this PR so that negative
"total" values are no longer reported in a literal manner.
In PR git-lfs#5504 we modified the CopyCallbackFile() method of the
GitFilter structure in our "lfs" package so that the callback
function it created only output a progress log message when
at least 200 milliseconds had elapsed since the last message
was written, unless the number of bytes read had met or exceeded
the total size of the given Git LFS object.  (The minimum time
allowed between messages was specified by the DefaultLoggingThrottle
constant from our "tasklog" package.)

This design depended on the assumption that the total size of the
given object was known and correct when the callback was utilized
by the CopyWithCallback() function of our "tools" package.  When
the "total" parameter was negative, though, a message would always
be written because the number of bytes read so far would always be
larger than the total size.

In a previous commit in this PR we adjusted the callback function
so that it now checks whether the value of the "total" parameter
is negative, and if so will not write a progress log message unless
a sufficient period of time has elapsed since the previous log
message was written.

However, one consequence of this adjustment is that when the "total"
parameter is negative, it is not guaranteed that a log message will
be written when the last callback is made after the final bytes of
the given object's data are read.  Further, if an object's data is
fully processed in less than 200 milliseconds, no progress log
message will be written for the object at all.

While it is currently rare for the value in the callback function's
"total" parameter to be negative, we expect it to become more common
in the future as a result of the bug fix to our "clean" filter
commands implemented by PR git-lfs#6011.

PR git-lfs#6011 resolves a bug in our "git lfs filter-progress" and
"git lfs clean" commands whereby they can pass an incorrect
"totalSize" parameter to the CopyWithCallback() function.  The only
solution for this problem is for the commands to simply not attempt
to determine the total size of an object.

Absent other changes, the result of this bug fix would be that
even if a progress log file is specified by the GIT_LFS_PROGRESS
environment variable, no messages would be written to the log file
during "clean" filter operations.  This would occur because the
copyToTemp() method of the GitFilter structure in our "lfs" package
would find a negative value in its "fileSize" parameter and then
set its "cb" callback function parameter to nil.

We intend to address this issue in PR git-lfs#6011, but the consequence
when we do so will be that the callback function created by the
CopyCallbackFile() method will receive a negative value in its
"total" parameter during "clean" filter operations if a progress log
is configured with the GIT_LFS_PROGRESS environment variable.

At present, the callback function may already receive a negative
value in its "total" parameter, but only under a relatively unusual
combination of circumstances.  First, of course, a progress log must
be enabled with the GIT_LFS_PROGRESS environment variable.  Second,
the legacy "git lfs smudge" command must be used during a "smudge"
filter operation rather than the "git lfs filter-process" command;
this may occur if a version of Git older than v2.11.0 is in use.
Third, the "git lfs smudge" command must download an object that is
not available locally from a server which compresses the content
for transit and therefore sends a Content-Encoding header but no
Content-Length header.

Under these conditions, the lack of a Content-Length header means
the size of the HTTP response body will be represented with a value
of -1.  When our HTTP download adapter passes this negative value to
the CopyWithCallback() function, it will be used to initialize a new
CallbackReader structure from our "tools" package, and ultimately
passed to the callback function created by the CopyCallbackFile()
method.  Note that the callback function itself is passed from the
Smudge() method of the GitFilter structure to the same structure's
downloadFile() method, which creates a new transfer queue using
the WithProgressCallback() option function.

By contrast, when the "git lfs filter-process" command is in use,
it creates a transfer queue without a defined callback function.
As a result, objects that are not available locally are downloaded
and written to local storage without generating any progress log
messages.  When the Smudge() method is later invoked (because Git
re-sends a "smudge" command to our long-running filter process),
it runs the GitFilter structure's readLocalFile() method instead of
the downloadFile() method.  While the readLocalFile() method does
invoke the CopyWithCallback() function and passes a callback
function to it, at this point the size of the object can be
determined from the file in local storage, so the callback function
receives a non-negative value in its "total" parameter.

However, as described above, when the "git lfs smudge" command is
in use and a content-encoded download occurs, the callback function
created by the CopyCallbackFile() method will receive a negative
value in its "total" parameter, which means the function will write
to the progress log regardless of the amount of time that has
elapsed since the previous message was written.

Further, we expect the changes in PR git-lfs#6011 to result in a wider
range of conditions under which the callback function will receive
a negative value in its "total" parameter.

We therefore now enhance both the callback function and the internal
implementation of the Read() method of the BodyWithCallback and
CallbackReader structures in our "tools" package so that a progress
log message will always be written when the final bytes of a given
object are read.

First, we update the doCallbackIfRequired() method of the
"callbackState" structure in our "tools" package so that it checks
if an EOF condition has been encountered, and if so, checks whether
the amount of data read so far exceeds the total size that was
provided initially for the given object.  Should both of these
conditions apply, the method resets the "totalSize" field of the
"callbackState" structure to the final value of the "readSize"
field, and then runs the doCallback() method to invoke the callback
function.

As a result of this change, a callback will be performed the first
time the EOF condition is encountered while processing an object
whose total size was initially unknown, and when this callback is
made, the callback function's "total" parameter will not have a
negative value but will have the same value as the function's
"written" parameter.

Next, we update the CopyCallbackFile() method of the GitFilter
structure in our "lfs" package so that the callback function the
method creates now checks whether the "total" parameter passed to
the function has a different value than it did the previous time
the function was invoked.  If this is the case, and if the current
value of the "total" is also equal to the non-negative value of the
"written" parameter, then the function writes a message to the
progress log regardless of the amount of time that has elapsed since
the previous message was written.

This change has the key consequence that if the callback function
is invoked one or more times with a negative value for its "total"
parameter, and is invoked a final time with matching non-negative
values for its "written" and "total" parameters, it will always
write a progress log message at that time.

When this change is combined with the revisions we have made to the
"callbackState" structure's doCallbackIfRequired() method in this
commit, the result is that if an object's total size is initially
unknown, when an EOF condition is encountered while reading the
object's data, then a final progress log message is guaranteed to
be written with the total size of the object represented as equal
to the actual number of bytes that were read.

To verify this new behaviour, we add or update a number of our tests,
starting with the TestCopyCallbackFileThrottle() test function in
our "lfs" package.  Most significantly, we update this function's
"initial total unknown" test case so that it now expects a final
log message in which the total size of the object is not negative
but matches the total number of bytes read.

Note that the first log message expected by our new "initial total
unknown" test case still contains a "-1" in the denominator of the
fraction representing the amount of source data read so far.  We will
refine this field in a subsequent commit in this PR so that negative
"total" parameter values are no longer reported in a literal manner.

We also add two new test cases for the TestCopyCallbackFileThrottle()
function, in order to simulate situations where the total size of
an object is initially either overestimated or underestimated.  In
both cases, a final callback should be made in which the "total"
parameter value has been adjusted to match the actual number of
bytes that have been read when the EOF condition is reached.

To simulate these additional test cases, we expand the setup() and
Assert() methods of the "copyCallbackFileThrottleTestCase" structure
we introduced to our "lfs/util_test.go" source file in a previous
commit in this PR.

For the particular test case where the total size of an object is
underestimated, the Assert() method now performs three extra calls to
the Read() method of the CallbackReader structure, with a simulated
delay between the first two of these calls.  The first of these calls
should never result in a log message being written, while the second
should produce a log message in which the number of bytes read so far
exceeds the total size of the object.  For both new test cases, when
the EOF condition is reached, a callback should be made which produces
a log message with the object's total size adjusted in the final log
message to match the actual number of bytes read.

To independently verify the new behaviour of the "callbackState"
structure's doCallbackIfRequired() method, we add a test function to
our "tools" package which also simulates situations in which the
total size of the object is either unknown, overestimated, or
underestimated.

The TestBothCallbackReadersInvokeCallbackOnEOFWhenTotalSizeIncorrect()
function confirms that the Read() methods of both the BodyWithCallback
and CallbackReader structures perform a final callback when the EOF
condition is reached and pass the callback function an updated value
for the total size of the object which matches the actual number of
bytes read.  Note that in a prior commit in this PR we refactored the
Read() methods of both of the BodyWithCallback and CallbackReader
structures so that these methods now just invoke the read() method of
a "callbackState" structure, which in turn executes the
doCallbackIfRequired() method, so we do not need to provide a test
of the doCallbackIfRequired() method itself.

In addition to these changed or new Go test functions, we also expand
our "t/t-progress.sh" shell script with two new tests, one of which
exercises the "git lfs smudge" command under the specific set of
conditions we described above.

Our new "progress log: unknown initial size (smudge)" shell test
forces the use of the "git lfs smudge" command by a "git clone"
command and specified a progress log file using the GIT_LFS_PROGRESS
environment variable.  As well, in the repository cloned by the
"git clone" command, the test establishes a Git LFS object with
the special key string "storage-download-encoding-gzip" as its
contents, which causes our "lfstest-gitserver" test utility program
to return the object's contents in an HTTP response with a
gzip-encoded body and without a Content-Length header.

The test then checks that the progress log file contains an entry
with the full size of the object reported in both the numerator and
denominator of the field which represents the amount of data
downloaded for the object.  Previously, all progress log entries
generated by the "git lfs smudge" command while downloading a
content-encoded object would have a "-1" in the field's denominator.

Note that any intermediate entries preceding the final log entry
will still have a negative value in this field, but we will address
that issue in a subsequent commit in this PR.

(Note also that we use the same "storage-download-encoding-gzip"
special object content key for a more complete test of the Git LFS
client's handling of gzip-encoded responses in the "batch storage
HTTP download with gzip encoding" test in our
"t/t-batch-storage-encoding.sh" shell script.)

The other new shell test in our "t/t-progress.sh" script is named
"progress log: unknown initial size (fetch/pull)", and operates in
a similar manner to the "progress log: unknown initial size (smudge)"
test except that it exercises the "git lfs fetch" and "git lfs pull"
commands rather than the "git lfs smudge" command.

As noted in the following comment on PR git-lfs#6011, our "git lfs fetch"
and "git lfs pull" commands may write progress log entries with a "-1"
value in the denominator of the field which represents the amount of
data downloaded for the object, if the HTTP response to an object
download request is content-encoded and lacks a Content-Length header.

  git-lfs#6011 (comment)

In these commands, a transfer queue is created using the
WithProgress() option function rather than the WithProgressCallback()
option function, so the "cb" field of the TransferQueue structure
from our "tq" package is nil.  However, both commands pass a Meter
structure from our "tq" package to the WithProgress() function,
so the TransferQueue structure's "m" field is non-nil.

As a result, when the TransferQueue structure's ensureAdapterBegun()
method is executed, the callback function it creates will execute
only the TransferBytes() method of the Meter structure, which in
turn does not immediately return without taking any action because
its "m" receiver parameter is non-nil.  By comparison, when the
WithProgressCallback() option function is used instead of the
WithProgress() option function, this same callback function runs
the TransferBytes() method, which returns immediately, and then
runs the callback function specified by the non-nil "cb" field.

The callback function created by the ensureAdapterBegun() method
is then passed to the Begin() method of the transfer adapter, and
ultimately passed to the CopyWithCallback() function of our "tools"
package by the "basicDownloadAdapter" structure's download() method.
(Note that this is also true for our "git lfs filter-process" and
"git lfs smudge" commands, not just for the "git lfs fetch"
and "git lfs pull" commands.)

As described above regarding the "git lfs smudge" command, the
CopyWithCallback() function initializes a new CallbackReader structure
and so the callback function passed to the CopyWithCallback() function
will ultimately be invoked as object data is read from the HTTP
response body.  With the "git lfs fetch" and "git lfs pull" commands,
though, this callback function only runs the TransferBytes() method
of the Meter structure, and that method does not return immediately
but runs the logBytes() method of the same structure.

For historical reasons which date back to the introduction of the
logBytes() method in commit 710862b
of PR git-lfs#476, this method writes a line to the progress log file with
the identical format as the lines generated by the callback function
created by the CopyCallbackFile() method of the GitFilter structure.

However, unlike that anonymous callback function, the logBytes()
method does not limit its output based on the amount of time that
has elapsed since it was previously invoked.  When we revised the
CopyCallbackFile() method in PR git-lfs#5504 to implement a mechanism
which throttled the frequency of its output to the progress log,
we did not also update the logBytes() method to do the same.

While we likely should refactor the logBytes() method and the
CopyCallbackFile() method's callback function to share a common
implementation, that change falls outside of the scope of this PR,
so we defer it to a future PR.

What this PR does accomplish, though, is to ensure that regardless
of their source, when progress log messages are written for an object
whose total size is initially unknown, a final callback will be made
after the EOF condition is encountered, and this will generate a
progress log message in which the total size has been adjusted to
match the actual number of bytes read.

For this reason, our "progress log: unknown initial size (fetch/pull)"
shell test exercises the "git lfs fetch" and "git lfs pull" commands
with a progress log file specified in the GIT_LFS_PROGRESS environment
variable and an HTTP object download response that lacks a
Content-Length header.  Like our other new shell test, this test
also uses the "storage-download-encoding-gzip" special object
content key to cause the "lfstest-gitserver" utility to gzip-encode
its response to an object download request.

The test then confirms that the progress logs generated by the
"git lfs fetch" and "git lfs pull" commands contain an entry with
the full size of the object reported in both the numerator and
denominator of the field which represents the amount of data
downloaded for the object.  Previously, all progress log entries
generated by these commands while downloading a content-encoded
object would have a "-1" in the field's denominator.

Note that any intermediate entries preceding the final log entry
will still have a negative value in this field, but we will address
that issue in a subsequent commit in this PR.
As described in a previous commit in this PR, several of our Git LFS
commands may write progress log messages with a negative value in
the denominator of the fraction representing the amount of data
downloaded so far for an individual object.  Specifically, the
"git lfs fetch", "git lfs pull", and "git lfs smudge" commands
will write such log messages when a progress log is enabled with
the GIT_LFS_PROGRESS environment variable and the response to an
HTTP object download request is content-encoded and therefore lacks
a Content-Length header.

As described in a previous commit in this PR, for historical reasons
progress log messages are written either by the anonymous callback
function created by the CopyCallbackFile() method of the GitFilter
structure in our "lfs" package, or by the logBytes() method of the
Meter structure in our "tq" package.  However, the format of the
messages is identical regardless of which function or method
generates them.

When the logBytes() method was first introduced in commit
710862b of PR git-lfs#476, it duplicated
the format of the progress log messages generated by the callback
function created by the CopyCallbackFile() method.  The current
progress log message format in that method dates from commit
36fa23d of PR git-lfs#100, but the original
version of the method and its callback function themselves were added
in commit fd41906 of PR git-lfs#97.

While we likely should refactor the logBytes() method and the
CopyCallbackFile() method's callback function to share a common
implementation, that change falls outside of the scope of this PR,
so we defer it to a future PR.

However, one of the changes we made in a prior commit in this PR
now ensures that if an object's total size is initially unknown,
when an EOF condition is encountered while reading the object's
data, then a final progress log message is guaranteed to be written
with the total size of the object represented as equal to the
actual number of bytes that were read.  Further, this final
progress log entry will be output regardless of whether the
CopyCallbackFile() method's callback function or the logBytes()
method is being used to write messages to the log file.

While this change affects the content of the last progress log
entry written for a given object, until the EOF condition is
reached, intermediate log messages may be generated which still
have a negative value in the denominator of the fraction
representing the amount of the object's data processed so far.

These negative values occur because the length of the data to be
read is initially unknown.  At present, the only circumstance in
which this is possible is when the HTTP response to an object
download request lacks a Content-Length header, as will be the
case when the response body is gzip-encoded, as we noted in the
following comment on PR git-lfs#6011:

  git-lfs#6011 (comment)

PR git-lfs#6011 resolves a bug in our "git lfs filter-progress" and
"git lfs clean" commands whereby they can pass an incorrect
"totalSize" parameter to the CopyWithCallback() function.  The only
solution for this problem is for the commands to simply not attempt
to determine the total size of an object.  As a consequence, we
expect that our "git lfs filter-progress" and "git lfs clean" commands
will never know in advance the length of the object data they are
reading from Git, and it will become a more common occurrence for
the CopyCallbackFile() method's callback function and the logBytes()
method to receive a "total" parameter with a negative value.

We therefore now refine the format of the progress log when the
total size of an object is unknown by writing a series of question
mark characters ("?") in place of the denominator of the fraction
which represents the amount of the object's data processed so far.
We adjust the number of these question mark characters to match
the length of the numerator, e.g., "123/???" and "12345/?????".

To avoid implementing the necessary string formatting logic in
both the CopyCallbackFile() method's callback function and the
logBytes() method, we introduce a new FormatFraction() function
to our "tools" package and then substitute its output in place
of the appropriate fraction field in the log messages.

We then add a TestFormatFraction() Go test function which verifies
that the FormatFraction() function generates the expected output
under a range of conditions, including when the number of bytes
read so far exceeds the value provided as the total size of an
object, and when the total size of an object is unknown and should
be represented using various numbers of question mark characters.

Finally, we update the comments in several of the tests we added
to the "t/t-progress.sh" shell script in a prior commit of this PR,
and we revise the "initial total unknown" test case for our
TestCopyCallbackFileThrottle() test function.  In that test case,
we update the expected contents of the progress log because the
intermediate entry which precedes the final entry should now have
a series of question mark characters rather than a "-1" value in
the denominator of the fraction which represents the amount of the
object's data processed so far.
@chrisd8088
chrisd8088 merged commit d8a9171 into git-lfs:main Aug 5, 2026
10 checks passed
@chrisd8088
chrisd8088 deleted the update-progress-log-callbacks branch August 5, 2026 00:32
chrisd8088 added a commit to cynix/git-lfs that referenced this pull request Aug 5, 2026
In a prior commit in this PR we revised the clean() function in our
"commands/command_clean.go" source file so that when it is passed
a non-empty "fileName" parameter, the function no longer tries to
determine the size of the file at the location given by that
parameter.

Previously, the function would invoke the Stat() function from the
Go standard library's "os" package to attempt to read the size of
the given file.  If that succeeded, and if the clean() function's
"fileSize" parameter was negative, the file size returned by the
Stat() function would be used in the subsequent call to the Clean()
method of the GitFilter structure in our "lfs" package.

Instead, the clean() function now just passes its "fileSize"
parameter directly to the Clean() method, without modification.
This change ensures that we do not pass an incorrect size to the
Clean() method, as might previously occur under the circumstances
reported in the secondary issue from git-lfs#4974.

Specifically, as described in the following comment, when Git streamed
a file's data to our "git lfs filter-process" command, if the
corresponding file on disk was altered before the command checked
its size, an error would be reported:

  git-lfs#4974 (comment)

Both the "git lfs filter-process" command and the "git lfs clean"
command pass negative values for the "fileSize" parameter when they
invoke the clean() function, since they do not receive file size
information from Git during "clean" filter operations.  Hence the
Clean() method now always receives a negative value in its "fileSize"
parameter during "clean" filter operations.

In the normal case, when no Git LFS pointer extension programs are
configured, the Clean() method invokes the GitFilter structure's
copyToTemp() method to read the object data piped from Git, check
if it already contains a valid Git LFS pointer, and otherwise
calculate the data's SHA-256 hash while spooling the data to a
temporary file.  To write the object data to the file, the
copyToTemp() method uses the CopyWithCallback() function from our
"tools" package.

The purpose of our CopyWithCallback() function is simply to copy data
from a source to a destination, while periodically invoking a callback
function to write progress messages to a log file, if one is defined
with the GIT_LFS_PROGRESS environment variable.  These progress log
entries include a field which represents the amount of an object's
data that has been transferred so far as a fraction whose denominator
is the expected total size of the object.

To populate the denominator of this field in the progress log
entries, the CallbackReader structure in our "tools" package is
initialized with a "totalSize" parameter, which will then be
passed to the provided callback function each time it is invoked.

Since the CopyWithCallback() function creates a CallbackReader
structure, it therefore requires both its own "totalSize" parameter
and a pointer to a callback function.  During a "clean" filter
operation, these parameters are passed down from the clean() function
to the GitFilter structure's Clean() method and then on to the
copyToTemp() method, which passes them to the CopyWithCallback()
function.  Note that the GitFilter structure's methods use the name
"fileSize" for the parameter which is ultimately used for the
CopyWithCallback() function's "totalSize" parameter.

In commit b0d669c of PR git-lfs#3902 we
added a check to the copyToTemp() method which sets the callback
function pointer to nil if the "fileSize" parameter is negative.

As noted above, we revised the clean() function in a prior commit
in this PR so that it no longer attempts to read the size of the
file identified by its "fileName" parameter.  Since both the
"git lfs clean" and "git lfs filter-process" commands pass a
negative value to the clean() function, that is the value which
all "clean" filter operations now pass to the copyToTemp() method.

As a result of our changes, the check for a negative "fileSize"
parameter value added to the copyToTemp() method in PR git-lfs#3902 will
always succeed during "clean" filter operations.  This effectively
disables all progress log output from these operations even though
the clean() function explicitly creates a callback function for
this purpose using the CopyCallbackFile() method of the GitFilter
structure.

The "progress log: file counts" test that we renamed and
updated in the recent PR git-lfs#6288 provides a demonstration of
this change in the behaviour of the Git LFS client.  In commit
4a4e524 of that PR we added to
the test a check of the number of entries with the operation name
"clean" that appear in a progress log after a "git add" command.
Since our change to the clean() function in the prior commit in
this PR, that check now finds no matching entries in the progress
log, and so the test currently fails.

To resolve this problem, we now simply remove the conditional block
in the copyToTemp() method which sets the callback function pointer
to nil if the "fileSize" parameter is negative.

Note, though, that when we introduced that conditional block in
PR git-lfs#3902, we did so with the intention that it would act as a
fallback protection against the problem reported in git-lfs#3884.  Prior
to the changes from PR git-lfs#3902, the "git hash-object" command would
fail when its --path option's argument matched a Git LFS filter
entry from a "gitattributes" file and the command's input was
smaller than the size of the read buffer the Git LFS client uses
when decoding pointers.

In commit b0d669c of PR git-lfs#3902 we
resolved this problem by ensuring that the Read() method of what
was then the "pktlineReader" structure in our "git" package properly
handled subsequent calls after reading a non-zero number of bytes
and also encountering the EOF condition.  (Note that we later moved
this structure into our separate "git-lfs/pktline" repository.)

In the same commit we also attempted to protect against the issue
from git-lfs#3884 with a separate change to the copyToTemp() method.
Specifically, we added the conditional block which sets the callback
function pointer to nil if the "fileSize" parameter is negative,
with the understanding that this would prevent the method from
writing a file to the working tree when the Git LFS client was
invoked by a "git hash-object" command.  As we explained in the
commit description, this change was meant to ensure that we
"don't process the callback to write the file in this case,
since we don't actually want to write into the working tree".

However, we apparently made this change under a misapprehension.
The callback function provided to the copyToTemp() method is not
responsible for writing to a file in the working tree.  In fact,
during a "clean" filter operation, no file will be written to the
working tree at all, including by Git.  (This stands in contrast
to a "smudge" filter operation, when Git does write a file to the
working tree.)

What does occur during a "clean" filter operation, though, is
that the copyToTemp() method creates a temporary file in the
local Git LFS storage directories and writes to the file using
the CopyWithCallback() function from our "tools" package.
This occurs regardless of whether a callback function is defined
or not.  The CopyWithCallback() function uses the Copy() function
from the Go standard library's "io" package to perform the actual
transfer of data.  The clean() function in our "commands" package
then moves the temporary file into a final location in the object
storage directory hierarchy within the local Git LFS storage
directory.  All of these steps are performed even if the "clean"
filter operation was invoked via a "git hash-object" command and
not a "git add" command.

As described further above, the only purpose of the callback function
that the CopyCallbackFile() method of the GitFilter structure creates
is to write messages to a progress log file, if one is specified with
the GIT_LFS_PROGRESS environment variable.  So the conditional block
that we added to the copyToTemp() method in commit
b0d669c only causes the progress
log to be disabled under some conditions, not to prevent a Git LFS
object file from being created.

Thus we can remove this conditional block entirely without concern
that we might introduce a regression and a re-introduction of the
problem reported in git-lfs#3884.

However, absent some of our other changes from PR git-lfs#6288, we would
cause a regression of another form, by re-introducing the issue
that was reported and resolved in PR git-lfs#5504.  That PR revised the
callback function created by the CopyCallbackFile() method so that
it would only write a message to the progress log if at least 200
milliseconds had elapsed since the previous message was written, or
if all the data for an object had been read and so the current
message would be the final entry for a given object.  We made this
change specifically to avoid excessive numbers of writes to the
progress log during "clean" filter operations.

In order to ensure that a final progress log message was written for
an object when its data had been fully read, the callback function
would check whether its "written" parameter was equal to or larger
than its "total" parameter, and if so, unconditionally write a
message even if less than 200 milliseconds had elapsed since the
previous message was written.  This logic depends on the "total"
parameter's value never being negative, though, because if it is
then the "written" parameter will always be larger and so every
message will be written, regardless of the amount of time that
has elapsed since the previous message was written.

On their own, our changes to the clean() function in our prior
commit in this PR would result in the callback function writing
every message to the progress log during "clean" filter operations,
which would re-introduce the problem that we tried to resolve in
PR git-lfs#5504.

Fortunately, though, we have recently addressed this issue in
PR git-lfs#6288 by refactoring both the callback function created by the
CopyCallbackFile() method and the internal implementation of the
Read() method of the CallbackReader structure.  The callback
function now avoids writing messages if insufficient time has
elapsed even when the "total" parameter's value is negative.

As well, when the callback function receives a negative value in
its "total" parameter, it represents the unknown total as a series
of question mark characters in the progress log entry, rather than
as a literal negative integer.

Further, the internal implementation of the CallbackReader
structure's Read() method will now change the value of the "total"
parameter to equal that of the "written" parameter when it invokes
the callback function for the final time for a given object.
This ensures that the final entry for an object whose size was
initially unknown will not contain question mark characters
(or a negative integer) but will instead have equal values in
the numerator and denominator of the field which represents the
amount of the object's data that has been processed.

To verify that these changes in PR git-lfs#6288 work in combination with
our changes from this PR to generate an entry in the progress log
during a "clean" filter operation, we now add a "progress log:
unknown initial size (clean)" test to our "t/t-progress.sh" shell
script.  We model this test on the "progress log: unknown initial
size (smudge)" and "progress log: unknown initial size (fetch/pull)"
tests that we added in commit 47d1e25
of PR git-lfs#6288.

Our new test performs a "git add" command and confirms that although
the "git lfs filter-process" command no longer attempts to determine
the size of an object by calling the Stat() function on the
corresponding file in the working tree, a final progress log entry
will be written where the total size of the object is correctly
reported.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants