Repository: go-git/go-git

Issue:
fatal: protocol error: bad line length character: PACK
I'm getting a weird bug that I think is a bug in go-git.

On the client-side I'm seeing the following:

```
$ git pull -vvv --rebase
Server version is go-git/5.x
Server supports ofs-delta
want 526e79c8c4037c2fcbee50acc2d9be1992b60893 (refs/heads/main)
have 51ea2dea8b05b8e631928a990f16fe7566e3c96f
have 6901305097043b4a25e2e20bafab359e635fc476
have 5b5fab934eef3b3957fe5d712672487e9234d0ce
have 2f572924460cb3bcc140cb1b30fab1619bad9b5a
have b701bbb46c14441442376191fcd9476a0ede3120
have 070676a07101c3ddf22bc3169980e0c5068295c0
have f1b049c1238065e2b4908d8526a595dcc5e0a811
have 26fbee7ff623814fb37568c72c89ee515bfb7d8e
have 44101b43124b4eba0308dbeef85bd77b48230d93
have dae95318ea4236d3d157999b7ca2654edb06af36
have 6a7fff648f7922355a6fb965bf9b7c17d939d5e3
have c5bbbf3b1813827981a1746ad450afb10910c203
have f8d8ff5de51ba02b28a52b55ca0523b231ff8acd
have 6f8b2b27f5c3d35dfb41a5a38a3795d8c4962692
have 65d71b41dd0a5f531e9ca577ab11af88762a4e78
have 7d7bf6318f7470b17204767364ff22ff7ad2620d
POST git-upload-pack (903 bytes)
have 5fc5fd9ba13b98582cb2b7ae2b39f28ed7f127a0
have e99894102fc0c98583e35b42ffc2d578dc9a1c1c
have a0f95b37c000e3bda2af160f9ae24cbcc4def343
have 30b8f348eaf906caf0c32bba15b6b8868245d91f
have 5d4b79d0a19658b41ddbe2454f8699d841beb52f
have 99a6559b96f6fe976a5d5cd7b2f396d29492c8b9
have 74fcb64cc38fd5cc4d33c419e29c460f5be6f615
have 983dabb33c2dddcdba65b8ab9d0922bd338628aa
have 30559cd9c23ca4e803364ba1848442d217560143
have f661e6e4ce1177352b5bc40238351920cc25a69b
have 2b57b6e0b1b936f45aa4ed8fd0ed2f57b7d370f3
have 2fe14eca63e9d3e907c7b725407c146a82cc129f
have d58151dbb07a5d5dd7eaf6fc9f12566898823a46
have a64f9d8f14762e341a851d0e211814958e044a64
have d6b8143f569c95bda9a1e5a494affed26b058ce6
have 6e6648e344c889767240a33f42febf9498250131
fatal: protocol error: bad line length character: PACK
```

On the server-side the request/context is cancelled (probably becuase the client bailed above):

```
time="2022-12-29T14:59:47Z" level=error msg="error encoding upload-pack response" func=gitxt.net/gitxt/internal/git.UploadPackHandler.func1 file="/src/internal/git/handler.go:243" error="write tcp 10.0.2.216:5555->10.0.2.163:42390: write: broken pipe"
2022/12/29 14:59:47 http: superfluous response.WriteHeader call from gitxt.net/gitxt/internal/git.UploadPackHandler.func1 (handler.go:244)
```

I'm really not sure what's going wrong here 🤔

Discussion:
@prologic: Die some digging into this and this is what's happening on the wire:

```
C: GET /gitxt/info/refs?service=git-upload-pack HTTP/1.1
Host: localhost:5555
User-Agent: git/2.37.1 (Apple Git-137.1)
Accept: */*
Accept-Encoding: deflate, gzip
Proxy-Connection: Keep-Alive
Pragma: no-cache
Git-Protocol: version=2
content-length: 0

S: HTTP/1.1 200 OK
Content-Type: application/x-git-upload-pack-advertisement
Date: Thu, 29 Dec 2022 16:29:15 GMT
Content-Length: 336

001e# service=git-upload-pack
00000071a67c6daa52a3d45b5942e538e2986a44f931ba64 HEADagent=go-git/5.x ofs-delta symref=HEAD:refs/heads/main no-thin
003da67c6daa52a3d45b5942e538e2986a44f931ba64 refs/heads/main
003ea2802d9a1f443c70c76abcee7982c17c67f65ff4 refs/tags/v0.1.0
003ed6cbb0ee6c7f95a353a183c919d22428fc1ef31b refs/tags/v0.2.0
0000

C: POST /gitxt/git-upload-pack HTTP/1.1
Host: localhost:5555
User-Agent: git/2.37.1 (Apple Git-137.1)
Accept-Encoding: deflate, gzip
Proxy-Connection: Keep-Alive
Content-Type: application/x-git-upload-pack-request
Accept: application/x-git-upload-pack-result
Content-Length: 903

005fwant a67c6daa52a3d45b5942e538e2986a44f931ba64 ofs-delta agent=git/2.37.1.(Apple.Git-137.1)
00000032have 526e79c8c4037c2fcbee50acc2d9be1992b60893
0032have 5b5fab934eef3b3957fe5d712672487e9234d0ce
0032have 2f572924460cb3bcc140cb1b30fab1619bad9b5a
0032have b701bbb46c14441442376191fcd9476a0ede3120
0032have 070676a07101c3ddf22bc3169980e0c5068295c0
0032have f1b049c1238065e2b4908d8526a595dcc5e0a811
0032have 26fbee7ff623814fb37568c72c89ee515bfb7d8e
0032have 44101b43124b4eba0308dbeef85bd77b48230d93
0032have dae95318ea4236d3d157999b7ca2654edb06af36
0032have 6a7fff648f7922355a6fb965bf9b7c17d939d5e3
0032have c5bbbf3b1813827981a1746ad450afb10910c203
0032have f8d8ff5de51ba02b28a52b55ca0523b231ff8acd
0032have 6f8b2b27f5c3d35dfb41a5a38a3795d8c4962692
0032have 65d71b41dd0a5f531e9ca577ab11af88762a4e78
0032have 7d7bf6318f7470b17204767364ff22ff7ad2620d
0032have 5fc5fd9ba13b98582cb2b7ae2b39f28ed7f127a0
0000

S: HTTP/1.1 200 OK
Content-Type: application/x-git-upload-pack-result
Date: Thu, 29 Dec 2022 16:29:15 GMT
Transfer-Encoding: chunked

204c0
0008NAK
PACKp\xb0\x9fx\x9c\xb4\xbbI\x93\x9bJ׮=\xff~Ş+d2\x92\xe6\x8b8@
...
```

I've truncated the final server response as its ~130kB (let me know if you'd like the full response)

The server response `git` seems to be b0rking at starts like this:

```
0000000000 30 30 30 38 4e 41 4b 0a 50 41 43 4b 00 00 00 02   0008NAK.PACK....
0000000010 00 00 02 70 b0 9f 07 78 9c b4 bb 49 93 9b 4a d7   ...p...x...I..J.
0000000020 ae 3d ff 7e c5 9e 2b 64 32 13 92 e6 8b 38 03 40   .=.~..+d2....8.@
0000000030 0d 42 02 24 84 90 d0 e4 04 7d df b7 e2 d7 9f a8   .B.$.....}......
```@prologic: Some more useful information (discovered how to convince `git` to debug packets):

```
Fri Dec 30 15:14:48
~/tmp/gitxt
 (main) 0
$ GIT_TRACE=true GIT_TRACE_PACKET=true git pull --rebase
15:15:00.118967 exec-cmd.c:139          trace: resolved executable path from Darwin stack: /Library/Developer/CommandLineTools/usr/bin/git
15:15:00.119408 exec-cmd.c:238          trace: resolved executable dir: /Library/Developer/CommandLineTools/usr/bin
15:15:00.119917 git.c:460               trace: built-in: git pull --rebase
15:15:00.124015 run-command.c:654       trace: run_command: git fetch --update-head-ok
15:15:00.127583 exec-cmd.c:139          trace: resolved executable path from Darwin stack: /Library/Developer/CommandLineTools/usr/libexec/git-core/git
15:15:00.127988 exec-cmd.c:238          trace: resolved executable dir: /Library/Developer/CommandLineTools/usr/libexec/git-core
15:15:00.128398 git.c:460               trace: built-in: git fetch --update-head-ok
15:15:00.129112 run-command.c:654       trace: run_command: GIT_DIR=.git git remote-http origin http://localhost:5555/gitxt
15:15:00.132303 exec-cmd.c:139          trace: resolved executable path from Darwin stack: /Library/Developer/CommandLineTools/usr/libexec/git-core/git
15:15:00.132628 exec-cmd.c:238          trace: resolved executable dir: /Library/Developer/CommandLineTools/usr/libexec/git-core
15:15:00.132908 git.c:749               trace: exec: git-remote-http origin http://localhost:5555/gitxt
15:15:00.132915 run-command.c:654       trace: run_command: git-remote-http origin http://localhost:5555/gitxt
15:15:00.137006 exec-cmd.c:139          trace: resolved executable path from Darwin stack: /Library/Developer/CommandLineTools/usr/libexec/git-core/git-remote-http
15:15:00.137341 exec-cmd.c:238          trace: resolved executable dir: /Library/Developer/CommandLineTools/usr/libexec/git-core
15:15:00.143922 pkt-line.c:80           packet:          git< # service=git-upload-pack
15:15:00.143928 pkt-line.c:80           packet:          git< 0000
15:15:00.143933 pkt-line.c:80           packet:          git< a67c6daa52a3d45b5942e538e2986a44f931ba64 HEAD\0agent=go-git/5.x ofs-delta symref=HEAD:refs/heads/main no-thin
15:15:00.143939 pkt-line.c:80           packet:          git< a67c6daa52a3d45b5942e538e2986a44f931ba64 refs/heads/main
15:15:00.143941 pkt-line.c:80           packet:          git< a2802d9a1f443c70c76abcee7982c17c67f65ff4 refs/tags/v0.1.0
15:15:00.143943 pkt-line.c:80           packet:          git< d6cbb0ee6c7f95a353a183c919d22428fc1ef31b refs/tags/v0.2.0
15:15:00.143945 pkt-line.c:80           packet:          git< 0000
15:15:00.144222 pkt-line.c:80           packet:          git> a67c6daa52a3d45b5942e538e2986a44f931ba64 refs/heads/main
15:15:00.144228 pkt-line.c:80           packet:          git> 0000
15:15:00.144236 run-command.c:654       trace: run_command: git fetch-pack --stateless-rpc --stdin --lock-pack --include-tag --thin http://localhost:5555/gitxt/
15:15:00.147284 exec-cmd.c:139          trace: resolved executable path from Darwin stack: /Library/Developer/CommandLineTools/usr/libexec/git-core/git
15:15:00.147629 exec-cmd.c:238          trace: resolved executable dir: /Library/Developer/CommandLineTools/usr/libexec/git-core
15:15:00.147986 git.c:460               trace: built-in: git fetch-pack --stateless-rpc --stdin --lock-pack --include-tag --thin http://localhost:5555/gitxt/
15:15:00.148007 pkt-line.c:80           packet:   fetch-pack< a67c6daa52a3d45b5942e538e2986a44f931ba64 refs/heads/main
15:15:00.148013 pkt-line.c:80           packet:   fetch-pack< 0000
15:15:00.148016 pkt-line.c:80           packet:   fetch-pack< a67c6daa52a3d45b5942e538e2986a44f931ba64 HEAD\0agent=go-git/5.x ofs-delta symref=HEAD:refs/heads/main no-thin
15:15:00.148024 pkt-line.c:80           packet:   fetch-pack< a67c6daa52a3d45b5942e538e2986a44f931ba64 refs/heads/main
15:15:00.148027 pkt-line.c:80           packet:   fetch-pack< a2802d9a1f443c70c76abcee7982c17c67f65ff4 refs/tags/v0.1.0
15:15:00.148029 pkt-line.c:80           packet:   fetch-pack< d6cbb0ee6c7f95a353a183c919d22428fc1ef31b refs/tags/v0.2.0
15:15:00.148031 pkt-line.c:80           packet:   fetch-pack< 0000
15:15:00.148455 pkt-line.c:80           packet:   fetch-pack> want a67c6daa52a3d45b5942e538e2986a44f931ba64 ofs-delta agent=git/2.37.1.(Apple.Git-137.1)
15:15:00.148459 pkt-line.c:80           packet:   fetch-pack> 0000
15:15:00.148576 pkt-line.c:80           packet:   fetch-pack> have 526e79c8c4037c2fcbee50acc2d9be1992b60893
15:15:00.148590 pkt-line.c:80           packet:   fetch-pack> have 5b5fab934eef3b3957fe5d712672487e9234d0ce
15:15:00.148601 pkt-line.c:80           packet:   fetch-pack> have 2f572924460cb3bcc140cb1b30fab1619bad9b5a
15:15:00.148644 pkt-line.c:80           packet:   fetch-pack> have b701bbb46c14441442376191fcd9476a0ede3120
15:15:00.148656 pkt-line.c:80           packet:   fetch-pack> have 070676a07101c3ddf22bc3169980e0c5068295c0
15:15:00.148666 pkt-line.c:80           packet:   fetch-pack> have f1b049c1238065e2b4908d8526a595dcc5e0a811
15:15:00.148680 pkt-line.c:80           packet:   fetch-pack> have 26fbee7ff623814fb37568c72c89ee515bfb7d8e
15:15:00.148690 pkt-line.c:80           packet:   fetch-pack> have 44101b43124b4eba0308dbeef85bd77b48230d93
15:15:00.148701 pkt-line.c:80           packet:   fetch-pack> have dae95318ea4236d3d157999b7ca2654edb06af36
15:15:00.148711 pkt-line.c:80           packet:   fetch-pack> have 6a7fff648f7922355a6fb965bf9b7c17d939d5e3
15:15:00.148721 pkt-line.c:80           packet:   fetch-pack> have c5bbbf3b1813827981a1746ad450afb10910c203
15:15:00.148731 pkt-line.c:80           packet:   fetch-pack> have f8d8ff5de51ba02b28a52b55ca0523b231ff8acd
15:15:00.148741 pkt-line.c:80           packet:   fetch-pack> have 6f8b2b27f5c3d35dfb41a5a38a3795d8c4962692
15:15:00.148751 pkt-line.c:80           packet:   fetch-pack> have 65d71b41dd0a5f531e9ca577ab11af88762a4e78
15:15:00.149547 pkt-line.c:80           packet:   fetch-pack> have 7d7bf6318f7470b17204767364ff22ff7ad2620d
15:15:00.149559 pkt-line.c:80           packet:   fetch-pack> have 5fc5fd9ba13b98582cb2b7ae2b39f28ed7f127a0
15:15:00.149561 pkt-line.c:80           packet:   fetch-pack> 0000
15:15:00.149565 pkt-line.c:80           packet:   fetch-pack> 0000
15:15:00.149581 pkt-line.c:80           packet:          git< 005fwant a67c6daa52a3d45b5942e538e2986a44f931ba64 ofs-delta agent=git/2.37.1.(Apple.Git-137.1)00000032have 526e79c8c4037c2fcbee50acc2d9be1992b608930032have 5b5fab934eef3b3957fe5d712672487e9234d0ce0032have 2f572924460cb3bcc140cb1b30fab1619bad9b5a0032have b701bbb46c14441442376191fcd9476a0ede31200032have 070676a07101c3ddf22bc3169980e0c5068295c00032have f1b049c1238065e2b4908d8526a595dcc5e0a8110032have 26fbee7ff623814fb37568c72c89ee515bfb7d8e0032have 44101b43124b4eba0308dbeef85bd77b48230d930032have dae95318ea4236d3d157999b7ca2654edb06af360032have 6a7fff648f7922355a6fb965bf9b7c17d939d5e30032have c5bbbf3b1813827981a1746ad450afb10910c2030032have f8d8ff5de51ba02b28a52b55ca0523b231ff8acd0032have 6f8b2b27f5c3d35dfb41a5a38a3795d8c49626920032have 65d71b41dd0a5f531e9ca577ab11af88762a4e780032have 7d7bf6318f7470b17204767364ff22ff7ad2620d0032have 5fc5fd9ba13b98582cb2b7ae2b39f28ed7f127a00000
15:15:00.149602 pkt-line.c:80           packet:          git< 0000
15:15:00.245807 pkt-line.c:80           packet:   fetch-pack< NAK
15:15:00.245854 pkt-line.c:80           packet:   fetch-pack> have e99894102fc0c98583e35b42ffc2d578dc9a1c1c
15:15:00.245866 pkt-line.c:80           packet:   fetch-pack> have a0f95b37c000e3bda2af160f9ae24cbcc4def343
15:15:00.246156 pkt-line.c:80           packet:   fetch-pack> have 30b8f348eaf906caf0c32bba15b6b8868245d91f
15:15:00.246180 pkt-line.c:80           packet:   fetch-pack> have 5d4b79d0a19658b41ddbe2454f8699d841beb52f
15:15:00.246190 pkt-line.c:80           packet:   fetch-pack> have 99a6559b96f6fe976a5d5cd7b2f396d29492c8b9
15:15:00.246199 pkt-line.c:80           packet:   fetch-pack> have 74fcb64cc38fd5cc4d33c419e29c460f5be6f615
15:15:00.246208 pkt-line.c:80           packet:   fetch-pack> have 983dabb33c2dddcdba65b8ab9d0922bd338628aa
15:15:00.246217 pkt-line.c:80           packet:   fetch-pack> have 30559cd9c23ca4e803364ba1848442d217560143
15:15:00.246225 pkt-line.c:80           packet:   fetch-pack> have f661e6e4ce1177352b5bc40238351920cc25a69b
15:15:00.246233 pkt-line.c:80           packet:   fetch-pack> have 2b57b6e0b1b936f45aa4ed8fd0ed2f57b7d370f3
15:15:00.246242 pkt-line.c:80           packet:   fetch-pack> have 2fe14eca63e9d3e907c7b725407c146a82cc129f
15:15:00.246250 pkt-line.c:80           packet:   fetch-pack> have d58151dbb07a5d5dd7eaf6fc9f12566898823a46
15:15:00.246258 pkt-line.c:80           packet:   fetch-pack> have a64f9d8f14762e341a851d0e211814958e044a64
15:15:00.246267 pkt-line.c:80           packet:   fetch-pack> have d6b8143f569c95bda9a1e5a494affed26b058ce6
15:15:00.246276 pkt-line.c:80           packet:   fetch-pack> have 6e6648e344c889767240a33f42febf9498250131
15:15:00.246284 pkt-line.c:80           packet:   fetch-pack> have 0ed4b29fe0aa3e93557569c6f803eafc5e228a37
15:15:00.246288 pkt-line.c:80           packet:   fetch-pack> 0000
15:15:00.246291 pkt-line.c:80           packet:   fetch-pack> 0000
fatal: protocol error: bad line length character: PACK

```@prologic: @mcarmonaa / @pjbgf Happy New Year 🥳 Happy #2023 😅 -- Would either of you have time soon™ to help me out with this one? 🙏@prologic: ping@blmayer: Same error here.@gl-yziquel: Same error here. Happening with the legit server from icyphox. It's a regression introduced between versions 0.2.0 and 0.2.1 of legit, when it switched from git to go-git.

Probably the same issue that brought @prologic here.@gl-yziquel: @prologic I created a fork of legit, pinned to the 0.2.0 version, in order to advertise this issue.

Here: https://github.com/gl-yziquel/legit@prologic: @gl-yziquel I forked legit long ago and also wanted to switch to using go-git in [gittxt](https://gitxt.net/gitxt); sadly I was never able to solve this particular issue, so gitxt stalled as a PoC/concept.@gl-yziquel: @prologic That issue has been bogging me down for quite some time with legit. I like the bare bones aspect of legit. However, if one is not a native go speaker, it's hard as hell to debug. If I find time, I'll debug it. For the moment, I'll stick with the 0.2.0 version, and perhaps will rebase the codebase leaving out go-git because of this issue. The issue tracer on legit has been rather unresponsive, unfortunately. Thank you for mentioning your fork. Fetching it now.

Edit: yeah. frustrating: you can clone your gittxt repo, but not fetch it...@github-actions: To help us keep things tidy and focus on the active tasks, we've introduced a stale bot to spot issues/PRs that haven't had any activity in a while.

This particular issue hasn't had any updates or activity in the past 90 days, so it's been labeled as 'stale'. If it remains inactive for the next 30 days, it'll be automatically closed.

We understand everyone's busy, but if this issue is still important to you, please feel free to add a comment or make an update to keep it active. 

Thanks for your understanding and cooperation!@runxiyu: It seems that I could reliably reproduce this in some repos, although I couldn't tell what exactly is making them reproducible.
Either this same bug, or a very similar one, is present in 5.13.2.@runxiyu: I see that there's a `v6-transport` branch, but its API is a bit confusing and I don't see relevant examples. Does that branch have fixes for this issue by any means?@pjbgf: @runxiyu the `v6-transport` is still WIP, so documentation and examples are yet to be updated. However, this should be fixed there.

cc: @aymanbagabas in case you have any insights on this.@runxiyu: Ty, I'll work around this by execing git-receive-pack and friends for now, but I'll check back later when v6-transport gets updates



Solution patch:
diff --git a/plumbing/protocol/packp/common.go b/plumbing/protocol/packp/common.go
index a858323e..1dc97f62 100644
--- a/plumbing/protocol/packp/common.go
+++ b/plumbing/protocol/packp/common.go
@@ -32,6 +32,8 @@ var (
 	deepenCommits   = []byte("deepen ")
 	deepenSince     = []byte("deepen-since ")
 	deepenReference = []byte("deepen-not ")
+	have            = []byte("have ")
+	done            = []byte("done")
 
 	// shallow-update
 	unshallow = []byte("unshallow ")
diff --git a/plumbing/protocol/packp/srvresp.go b/plumbing/protocol/packp/srvresp.go
index f5d6cdb7..ce9832d4 100644
--- a/plumbing/protocol/packp/srvresp.go
+++ b/plumbing/protocol/packp/srvresp.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/go-git/go-git/v5/plumbing"
 	"github.com/go-git/go-git/v5/plumbing/format/pktline"
+	"github.com/go-git/go-git/v5/plumbing/protocol/packp/capability"
 	"github.com/go-git/go-git/v5/utils/ioutil"
 )
 
@@ -17,6 +18,7 @@ const ackLineLen = 44
 // ServerResponse object acknowledgement from upload-pack service
 type ServerResponse struct {
 	ACKs []plumbing.Hash
+	req  *UploadPackRequest
 }
 
 // Decode decodes the response into the struct, isMultiACK should be true, if
@@ -49,23 +51,8 @@ func (r *ServerResponse) Decode(reader io.Reader, isMultiACK bool) error {
 		}
 	}
 
-	if err == io.EOF {
-		err = nil
-	}
-
-	// isMultiACK is true when the remote server advertises the related
-	// capabilities when they are not in transport.UnsupportedCapabilities.
-	//
-	// Users may decide to remove multi_ack and multi_ack_detailed from the
-	// unsupported capabilities list, which allows them to do initial clones
-	// from Azure DevOps.
-	//
-	// Follow-up fetches may error, therefore errors are wrapped with additional
-	// information highlighting that this capabilities are not supported by go-git.
-	//
-	// TODO: Implement support for multi_ack or multi_ack_detailed responses.
-	if err != nil && isMultiACK {
-		return fmt.Errorf("multi_ack and multi_ack_detailed are not supported: %w", err)
+	if errors.Is(err, io.EOF) {
+		return nil
 	}
 
 	return err
@@ -75,7 +62,7 @@ func (r *ServerResponse) Decode(reader io.Reader, isMultiACK bool) error {
 // read in the buffer without moving the read pointer.
 func (r *ServerResponse) stopReading(reader ioutil.ReadPeeker) (bool, error) {
 	ahead, err := reader.Peek(7)
-	if err == io.EOF {
+	if errors.Is(err, io.EOF) {
 		return true, nil
 	}
 
@@ -135,17 +122,74 @@ func (r *ServerResponse) decodeACKLine(line []byte) error {
 }
 
 // Encode encodes the ServerResponse into a writer.
-func (r *ServerResponse) Encode(w io.Writer, isMultiACK bool) error {
-	if len(r.ACKs) > 1 && !isMultiACK {
-		// For further information, refer to comments in the Decode func above.
-		return errors.New("multi_ack and multi_ack_detailed are not supported")
+func (r *ServerResponse) Encode(w io.Writer) error {
+	multiAck := r.req.Capabilities.Supports(capability.MultiACK)
+	multiAckDetailed := r.req.Capabilities.Supports(capability.MultiACKDetailed)
+	readyHash := plumbing.ZeroHash
+	finalHash := plumbing.ZeroHash
+	for cmd := range r.req.UploadPackCommands {
+		if multiAck { //multi_ack
+			for _, h := range cmd.Acks {
+				if h.IsReady && readyHash.IsZero() {
+					readyHash = h.Hash
+				}
+				if h.IsCommon || !readyHash.IsZero() {
+					finalHash = h.Hash
+					if _, err := pktline.Writef(w, "%s %s continue\n", ack, h.Hash.String()); err != nil {
+						return err
+					}
+				}
+			}
+			if !cmd.Done {
+				if _, err := pktline.WriteString(w, string(nak)+"\n"); err != nil {
+					return err
+				}
+			}
+		} else if multiAckDetailed { //multi_ack_detailed
+			for _, h := range cmd.Acks {
+				if h.IsReady {
+					readyHash = h.Hash
+					finalHash = h.Hash
+					if _, err := pktline.Writef(w, "%s %s ready\n", ack, h.Hash.String()); err != nil {
+						return err
+					}
+				} else if h.IsCommon {
+					finalHash = h.Hash
+					if _, err := pktline.Writef(w, "%s %s common\n", ack, h.Hash.String()); err != nil {
+						return err
+					}
+				}
+			}
+			if !cmd.Done {
+				if _, err := pktline.WriteString(w, string(nak)+"\n"); err != nil {
+					return err
+				}
+			}
+		} else { // single ack
+			for _, h := range cmd.Acks {
+				if h.IsCommon && finalHash.IsZero() {
+					finalHash = h.Hash
+					if _, err := pktline.Writef(w, "%s %s\n", ack, finalHash.String()); err != nil {
+						return err
+					}
+					break
+				}
+			}
+			if !cmd.Done && finalHash.IsZero() {
+				if _, err := pktline.WriteString(w, string(nak)+"\n"); err != nil {
+					return err
+				}
+			}
+		}
 	}
-
-	if len(r.ACKs) == 0 {
-		_, err := pktline.WriteString(w, string(nak)+"\n")
-		return err
+	if !finalHash.IsZero() && (multiAck || multiAckDetailed) {
+		if _, err := pktline.Writef(w, "%s %s\n", ack, finalHash.String()); err != nil {
+			return err
+		}
+	} else if finalHash.IsZero() {
+		if _, err := pktline.WriteString(w, string(nak)+"\n"); err != nil {
+			return err
+		}
 	}
-
-	_, err := pktline.Writef(w, "%s %s\n", ack, r.ACKs[0].String())
-	return err
+	return nil
 }
diff --git a/plumbing/protocol/packp/ulreq.go b/plumbing/protocol/packp/ulreq.go
index ef4e08a1..6242f7ae 100644
--- a/plumbing/protocol/packp/ulreq.go
+++ b/plumbing/protocol/packp/ulreq.go
@@ -18,6 +18,12 @@ type UploadRequest struct {
 	Shallows     []plumbing.Hash
 	Depth        Depth
 	Filter       Filter
+	HavesUR      chan UploadRequestHave
+}
+
+type UploadRequestHave struct {
+	Done  bool
+	Haves []plumbing.Hash
 }
 
 // Depth values stores the desired depth of the requested packfile: see
@@ -65,6 +71,7 @@ func NewUploadRequest() *UploadRequest {
 		Wants:        []plumbing.Hash{},
 		Shallows:     []plumbing.Hash{},
 		Depth:        DepthCommits(0),
+		HavesUR:      make(chan UploadRequestHave, 1),
 	}
 }
 
diff --git a/plumbing/protocol/packp/ulreq_decode.go b/plumbing/protocol/packp/ulreq_decode.go
index fbee2497..ea736c1f 100644
--- a/plumbing/protocol/packp/ulreq_decode.go
+++ b/plumbing/protocol/packp/ulreq_decode.go
@@ -14,9 +14,12 @@ import (
 
 // Decode reads the next upload-request form its input and
 // stores it in the UploadRequest.
-func (req *UploadRequest) Decode(r io.Reader) error {
+func (req *UploadPackRequest) Decode(r io.Reader) error {
 	d := newUlReqDecoder(r)
-	return d.Decode(req)
+	if err := d.Decode(&req.UploadRequest); err != nil {
+		return err
+	}
+	return nil
 }
 
 type ulReqDecoder struct {
@@ -49,7 +52,6 @@ func (d *ulReqDecoder) error(format string, a ...interface{}) {
 		"pkt-line %d: %s", d.nLine,
 		fmt.Sprintf(format, a...),
 	)
-
 	d.err = NewErrUnexpectedData(msg, d.line)
 }
 
@@ -57,16 +59,20 @@ func (d *ulReqDecoder) error(format string, a ...interface{}) {
 // p.line and increments p.nLine.  A successful invocation returns true,
 // otherwise, false is returned and the sticky error is filled out
 // accordingly.  Trims eols at the end of the payloads.
-func (d *ulReqDecoder) nextLine() bool {
+func (d *ulReqDecoder) nextLine(reportError bool) bool {
 	d.nLine++
 
 	_, p, err := pktline.ReadLine(d.r)
 	if err == io.EOF {
-		d.error("EOF")
+		if reportError {
+			d.error("EOF")
+		}
 		return false
 	}
 	if err != nil {
-		d.err = err
+		if reportError {
+			d.err = err
+		}
 		return false
 	}
 
@@ -78,7 +84,13 @@ func (d *ulReqDecoder) nextLine() bool {
 
 // Expected format: want <hash>[ capabilities]
 func (d *ulReqDecoder) decodeFirstWant() stateFn {
-	if ok := d.nextLine(); !ok {
+	if ok := d.nextLine(true); !ok {
+		return nil
+	}
+
+	// if client send 0000 it don't want anything (already up to date after
+	// AdvertisedReferences) or ls-remote scenario
+	if len(d.line) == 0 {
 		return nil
 	}
 
@@ -125,7 +137,7 @@ func (d *ulReqDecoder) decodeCaps() stateFn {
 
 // Expected format: want <hash>
 func (d *ulReqDecoder) decodeOtherWants() stateFn {
-	if ok := d.nextLine(); !ok {
+	if ok := d.nextLine(true); !ok {
 		return nil
 	}
 
@@ -138,7 +150,7 @@ func (d *ulReqDecoder) decodeOtherWants() stateFn {
 	}
 
 	if len(d.line) == 0 {
-		return nil
+		return d.decodeHaves
 	}
 
 	if !bytes.HasPrefix(d.line, want) {
@@ -163,7 +175,7 @@ func (d *ulReqDecoder) decodeShallow() stateFn {
 	}
 
 	if len(d.line) == 0 {
-		return nil
+		return d.decodeHaves
 	}
 
 	if !bytes.HasPrefix(d.line, shallow) {
@@ -178,7 +190,7 @@ func (d *ulReqDecoder) decodeShallow() stateFn {
 	}
 	d.data.Shallows = append(d.data.Shallows, hash)
 
-	if ok := d.nextLine(); !ok {
+	if ok := d.nextLine(true); !ok {
 		return nil
 	}
 
@@ -199,10 +211,6 @@ func (d *ulReqDecoder) decodeDeepen() stateFn {
 		return d.decodeDeepenReference
 	}
 
-	if len(d.line) == 0 {
-		return nil
-	}
-
 	d.error("unexpected deepen specification: %q", d.line)
 	return nil
 }
@@ -220,7 +228,7 @@ func (d *ulReqDecoder) decodeDeepenCommits() stateFn {
 	}
 	d.data.Depth = DepthCommits(n)
 
-	return d.decodeFlush
+	return d.decodeOtherWants
 }
 
 func (d *ulReqDecoder) decodeDeepenSince() stateFn {
@@ -234,7 +242,7 @@ func (d *ulReqDecoder) decodeDeepenSince() stateFn {
 	t := time.Unix(secs, 0).UTC()
 	d.data.Depth = DepthSince(t)
 
-	return d.decodeFlush
+	return d.decodeOtherWants
 }
 
 func (d *ulReqDecoder) decodeDeepenReference() stateFn {
@@ -242,17 +250,44 @@ func (d *ulReqDecoder) decodeDeepenReference() stateFn {
 
 	d.data.Depth = DepthReference(string(d.line))
 
-	return d.decodeFlush
+	return d.decodeOtherWants
 }
 
-func (d *ulReqDecoder) decodeFlush() stateFn {
-	if ok := d.nextLine(); !ok {
-		return nil
-	}
-
-	if len(d.line) != 0 {
-		d.err = fmt.Errorf("unexpected payload while expecting a flush-pkt: %q", d.line)
-	}
+func (d *ulReqDecoder) decodeHaves() stateFn {
+	go func() {
+		inBetweenHave := []plumbing.Hash{}
+
+		for {
+			if ok := d.nextLine(false); !ok {
+				break
+			}
+
+			if len(d.line) == 0 {
+				d.data.HavesUR <- UploadRequestHave{Haves: inBetweenHave, Done: false}
+				inBetweenHave = []plumbing.Hash{}
+				continue
+			}
+
+			if bytes.Equal(d.line, done) {
+				d.data.HavesUR <- UploadRequestHave{Haves: inBetweenHave, Done: true}
+				break
+			}
+
+			if !bytes.HasPrefix(d.line, have) {
+				d.error("unexpected payload while expecting a have: %q", d.line)
+				break
+			}
+			d.line = bytes.TrimPrefix(d.line, have)
+
+			hash, ok := d.readHash()
+			if !ok {
+				break
+			}
+			inBetweenHave = append(inBetweenHave, hash)
+		}
+
+		close(d.data.HavesUR)
+	}()
 
 	return nil
 }
diff --git a/plumbing/protocol/packp/uppackreq.go b/plumbing/protocol/packp/uppackreq.go
index 9f7f071e..a8a5ad6a 100644
--- a/plumbing/protocol/packp/uppackreq.go
+++ b/plumbing/protocol/packp/uppackreq.go
@@ -15,14 +15,27 @@ import (
 type UploadPackRequest struct {
 	UploadRequest
 	UploadHaves
+	UploadPackCommands chan UploadPackCommand
+}
+
+type UploadPackCommand struct {
+	Acks []UploadPackRequestAck
+	Done bool
+}
+
+type UploadPackRequestAck struct {
+	Hash     plumbing.Hash
+	IsCommon bool
+	IsReady  bool
 }
 
 // NewUploadPackRequest creates a new UploadPackRequest and returns a pointer.
 func NewUploadPackRequest() *UploadPackRequest {
 	ur := NewUploadRequest()
 	return &UploadPackRequest{
-		UploadHaves:   UploadHaves{},
-		UploadRequest: *ur,
+		UploadHaves:        UploadHaves{},
+		UploadRequest:      *ur,
+		UploadPackCommands: make(chan UploadPackCommand, 1),
 	}
 }
 
@@ -33,8 +46,9 @@ func NewUploadPackRequest() *UploadPackRequest {
 func NewUploadPackRequestFromCapabilities(adv *capability.List) *UploadPackRequest {
 	ur := NewUploadRequestFromCapabilities(adv)
 	return &UploadPackRequest{
-		UploadHaves:   UploadHaves{},
-		UploadRequest: *ur,
+		UploadHaves:        UploadHaves{},
+		UploadRequest:      *ur,
+		UploadPackCommands: make(chan UploadPackCommand, 1),
 	}
 }
 
diff --git a/plumbing/protocol/packp/uppackresp.go b/plumbing/protocol/packp/uppackresp.go
index 4a5fb05d..be6328dc 100644
--- a/plumbing/protocol/packp/uppackresp.go
+++ b/plumbing/protocol/packp/uppackresp.go
@@ -34,8 +34,9 @@ func NewUploadPackResponse(req *UploadPackRequest) *UploadPackResponse {
 		req.Capabilities.Supports(capability.MultiACKDetailed)
 
 	return &UploadPackResponse{
-		isShallow:  isShallow,
-		isMultiACK: isMultiACK,
+		isShallow:      isShallow,
+		isMultiACK:     isMultiACK,
+		ServerResponse: ServerResponse{req: req},
 	}
 }
 
@@ -78,7 +79,7 @@ func (r *UploadPackResponse) Encode(w io.Writer) (err error) {
 		}
 	}
 
-	if err := r.ServerResponse.Encode(w, r.isMultiACK); err != nil {
+	if err := r.ServerResponse.Encode(w); err != nil {
 		return err
 	}
 
diff --git a/plumbing/revlist/revlist.go b/plumbing/revlist/revlist.go
index b9109870..ede75112 100644
--- a/plumbing/revlist/revlist.go
+++ b/plumbing/revlist/revlist.go
@@ -228,3 +228,26 @@ func hashListToSet(hashes []plumbing.Hash) map[plumbing.Hash]bool {
 
 	return result
 }
+
+// ObjectsWithRef find all hashes linked to objs
+// return a map of hashes containing an array of hash objs
+func ObjectsWithRef(
+	s storer.EncodedObjectStorer,
+	objs,
+	ignore []plumbing.Hash,
+) (map[plumbing.Hash][]plumbing.Hash, error) {
+	all := map[plumbing.Hash][]plumbing.Hash{}
+	for _, obj := range objs {
+		walkerFunc := func(h plumbing.Hash) {
+			if hashes, ok := all[h]; ok {
+				all[h] = append(hashes, obj)
+			} else {
+				all[h] = []plumbing.Hash{obj}
+			}
+		}
+		if err := processObject(s, obj, map[plumbing.Hash]bool{}, map[plumbing.Hash]bool{}, ignore, walkerFunc); err != nil {
+			return nil, err
+		}
+	}
+	return all, nil
+}
diff --git a/plumbing/server/server.go b/plumbing/server/server.go
index 0ec2cfa4..784c6a58 100644
--- a/plumbing/server/server.go
+++ b/plumbing/server/server.go
@@ -168,7 +168,7 @@ func (s *upSession) UploadPack(ctx context.Context, req *packp.UploadPackRequest
 		return nil, fmt.Errorf("shallow not supported")
 	}
 
-	objs, err := s.objectsToUpload(req)
+	havesWithRef, err := revlist.ObjectsWithRef(s.storer, req.Wants, nil)
 	if err != nil {
 		return nil, err
 	}
@@ -176,8 +176,30 @@ func (s *upSession) UploadPack(ctx context.Context, req *packp.UploadPackRequest
 	pr, pw := io.Pipe()
 	e := packfile.NewEncoder(pw, s.storer, false)
 	go func() {
-		// TODO: plumb through a pack window.
-		_, err := e.Encode(objs, 10)
+		allHaves := []plumbing.Hash{}
+		foundWants := map[plumbing.Hash]bool{}
+		for haves := range req.HavesUR {
+			acks := []packp.UploadPackRequestAck{}
+			for _, hu := range haves.Haves {
+				refs, ok := havesWithRef[hu]
+				if ok {
+					for _, ref := range refs {
+						foundWants[ref] = true
+					}
+				}
+				acks = append(acks, packp.UploadPackRequestAck{Hash: hu, IsCommon: ok, IsReady: ok && (len(refs) >= len(req.Wants) || len(foundWants) >= len(req.Wants))})
+				allHaves = append(allHaves, hu)
+			}
+
+			req.UploadPackCommands <- packp.UploadPackCommand{Acks: acks, Done: haves.Done}
+		}
+		close(req.UploadPackCommands)
+		objs, err := s.objectsToUpload(req.Wants, allHaves)
+		if err != nil {
+			pw.CloseWithError(err)
+			return
+		}
+		_, err = e.Encode(objs, 10)
 		pw.CloseWithError(err)
 	}()
 
@@ -186,13 +208,8 @@ func (s *upSession) UploadPack(ctx context.Context, req *packp.UploadPackRequest
 	), nil
 }
 
-func (s *upSession) objectsToUpload(req *packp.UploadPackRequest) ([]plumbing.Hash, error) {
-	haves, err := revlist.Objects(s.storer, req.Haves, nil)
-	if err != nil {
-		return nil, err
-	}
-
-	return revlist.Objects(s.storer, req.Wants, haves)
+func (s *upSession) objectsToUpload(wants, haves []plumbing.Hash) ([]plumbing.Hash, error) {
+	return revlist.Objects(s.storer, wants, haves)
 }
 
 func (*upSession) setSupportedCapabilities(c *capability.List) error {
diff --git a/plumbing/transport/common.go b/plumbing/transport/common.go
index d0cb1f62..9b6b6fd9 100644
--- a/plumbing/transport/common.go
+++ b/plumbing/transport/common.go
@@ -457,8 +457,6 @@ func isRepoNotFoundError(s string) bool {
 
 // uploadPack implements the git-upload-pack protocol.
 func uploadPack(w io.WriteCloser, _ io.Reader, req *packp.UploadPackRequest) error {
-	// TODO support multi_ack mode
-	// TODO support multi_ack_detailed mode
 	// TODO support acks for common objects
 	// TODO build a proper state machine for all these processing options
 
diff --git a/plumbing/transport/transport.go b/plumbing/transport/transport.go
index c6033847..6077e280 100644
--- a/plumbing/transport/transport.go
+++ b/plumbing/transport/transport.go
@@ -309,8 +309,6 @@ func parseFile(endpoint string) (*Endpoint, bool) {
 // UnsupportedCapabilities are the capabilities not supported by any client
 // implementation
 var UnsupportedCapabilities = []capability.Capability{
-	capability.MultiACK,
-	capability.MultiACKDetailed,
 	capability.ThinPack,
 }
 



Test patch:
diff --git a/internal/transport/test/upload_pack.go b/internal/transport/test/upload_pack.go
index f7842ebb..1d9c4832 100644
--- a/internal/transport/test/upload_pack.go
+++ b/internal/transport/test/upload_pack.go
@@ -86,7 +86,7 @@ func (s *UploadPackSuite) TestAdvertisedReferencesFilterUnsupported(c *C) {
 
 	info, err := r.AdvertisedReferences()
 	c.Assert(err, IsNil)
-	c.Assert(info.Capabilities.Supports(capability.MultiACK), Equals, false)
+	c.Assert(info.Capabilities.Supports(capability.MultiACK), Equals, true)
 }
 
 func (s *UploadPackSuite) TestCapabilities(c *C) {
diff --git a/plumbing/protocol/packp/srvresp_test.go b/plumbing/protocol/packp/srvresp_test.go
index c66a99e5..9a7b05a4 100644
--- a/plumbing/protocol/packp/srvresp_test.go
+++ b/plumbing/protocol/packp/srvresp_test.go
@@ -6,6 +6,7 @@ import (
 	"strings"
 
 	"github.com/go-git/go-git/v5/plumbing"
+	"github.com/go-git/go-git/v5/plumbing/protocol/packp/capability"
 
 	. "gopkg.in/check.v1"
 )
@@ -99,10 +100,6 @@ func (s *ServerResponseSuite) TestDecodeMalformed(c *C) {
 	c.Assert(err, NotNil)
 }
 
-// multi_ack isn't fully implemented, this ensures that Decode ignores that fact,
-// as in some circumstances that's OK to assume so.
-//
-// TODO: Review as part of multi_ack implementation.
 func (s *ServerResponseSuite) TestDecodeMultiACK(c *C) {
 	raw := "" +
 		"0031ACK 1111111111111111111111111111111111111111\n" +
@@ -117,3 +114,146 @@ func (s *ServerResponseSuite) TestDecodeMultiACK(c *C) {
 	c.Assert(sr.ACKs[0], Equals, plumbing.NewHash("1111111111111111111111111111111111111111"))
 	c.Assert(sr.ACKs[1], Equals, plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
 }
+
+func (s *ServerResponseSuite) TestEncodeEmpty(c *C) {
+	haves := make(chan UploadPackCommand)
+	go func() {
+		haves <- UploadPackCommand{
+			Acks: []UploadPackRequestAck{},
+			Done: true,
+		}
+		close(haves)
+	}()
+	sr := &ServerResponse{req: &UploadPackRequest{UploadPackCommands: haves, UploadRequest: UploadRequest{Capabilities: capability.NewList()}}}
+	b := bytes.NewBuffer(nil)
+	err := sr.Encode(b)
+	c.Assert(err, IsNil)
+
+	c.Assert(b.String(), Equals, "0008NAK\n")
+}
+
+func (s *ServerResponseSuite) TestEncodeSingleAck(c *C) {
+	haves := make(chan UploadPackCommand)
+	go func() {
+		haves <- UploadPackCommand{
+			Acks: []UploadPackRequestAck{
+				{Hash: plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e1")},
+				{Hash: plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e2")},
+				{Hash: plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e3"), IsCommon: true},
+			}}
+		close(haves)
+	}()
+	sr := &ServerResponse{req: &UploadPackRequest{UploadPackCommands: haves, UploadRequest: UploadRequest{Capabilities: capability.NewList()}}}
+	b := bytes.NewBuffer(nil)
+	err := sr.Encode(b)
+	c.Assert(err, IsNil)
+
+	c.Assert(b.String(), Equals, "0031ACK 6ecf0ef2c2dffb796033e5a02219af86ec6584e3\n")
+}
+
+func (s *ServerResponseSuite) TestEncodeSingleAckDone(c *C) {
+	haves := make(chan UploadPackCommand)
+	go func() {
+		haves <- UploadPackCommand{
+			Acks: []UploadPackRequestAck{
+				{Hash: plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e1")},
+				{Hash: plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e2")},
+				{Hash: plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e3"), IsCommon: true},
+			},
+			Done: true,
+		}
+		close(haves)
+	}()
+	sr := &ServerResponse{req: &UploadPackRequest{UploadPackCommands: haves, UploadRequest: UploadRequest{Capabilities: capability.NewList()}}}
+	b := bytes.NewBuffer(nil)
+	err := sr.Encode(b)
+	c.Assert(err, IsNil)
+
+	c.Assert(b.String(), Equals, "0031ACK 6ecf0ef2c2dffb796033e5a02219af86ec6584e3\n")
+}
+
+func (s *ServerResponseSuite) TestEncodeMutiAck(c *C) {
+	haves := make(chan UploadPackCommand)
+	go func() {
+		haves <- UploadPackCommand{
+			Acks: []UploadPackRequestAck{
+				{Hash: plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e1")},
+				{Hash: plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e2"), IsCommon: true, IsReady: true},
+				{Hash: plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e3")},
+			},
+		}
+		haves <- UploadPackCommand{
+			Acks: []UploadPackRequestAck{},
+			Done: true,
+		}
+		close(haves)
+	}()
+	capabilities := capability.NewList()
+	capabilities.Add(capability.MultiACK)
+	sr := &ServerResponse{req: &UploadPackRequest{UploadPackCommands: haves, UploadRequest: UploadRequest{Capabilities: capabilities}}}
+	b := bytes.NewBuffer(nil)
+	err := sr.Encode(b)
+	c.Assert(err, IsNil)
+
+	lines := strings.Split(b.String(), "\n")
+	c.Assert(len(lines), Equals, 5)
+	c.Assert(lines[0], Equals, "003aACK 6ecf0ef2c2dffb796033e5a02219af86ec6584e2 continue")
+	c.Assert(lines[1], Equals, "003aACK 6ecf0ef2c2dffb796033e5a02219af86ec6584e3 continue")
+	c.Assert(lines[2], Equals, "0008NAK")
+	c.Assert(lines[3], Equals, "0031ACK 6ecf0ef2c2dffb796033e5a02219af86ec6584e3")
+	c.Assert(lines[4], Equals, "")
+}
+
+func (s *ServerResponseSuite) TestEncodeMutiAckOnlyOneNak(c *C) {
+	haves := make(chan UploadPackCommand)
+	go func() {
+		haves <- UploadPackCommand{
+			Acks: []UploadPackRequestAck{}, //no common hash
+			Done: true,
+		}
+		close(haves)
+	}()
+	capabilities := capability.NewList()
+	capabilities.Add(capability.MultiACK)
+	sr := &ServerResponse{req: &UploadPackRequest{UploadPackCommands: haves, UploadRequest: UploadRequest{Capabilities: capabilities}}}
+	b := bytes.NewBuffer(nil)
+	err := sr.Encode(b)
+	c.Assert(err, IsNil)
+
+	lines := strings.Split(b.String(), "\n")
+	c.Assert(len(lines), Equals, 2)
+	c.Assert(lines[0], Equals, "0008NAK")
+	c.Assert(lines[1], Equals, "")
+}
+
+func (s *ServerResponseSuite) TestEncodeMutiAckDetailed(c *C) {
+	haves := make(chan UploadPackCommand)
+	go func() {
+		haves <- UploadPackCommand{
+			Acks: []UploadPackRequestAck{
+				{Hash: plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e1")},
+				{Hash: plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e2"), IsCommon: true, IsReady: true},
+				{Hash: plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e3"), IsCommon: true},
+			},
+		}
+		haves <- UploadPackCommand{
+			Acks: []UploadPackRequestAck{},
+			Done: true,
+		}
+		close(haves)
+	}()
+	capabilities := capability.NewList()
+	capabilities.Add(capability.MultiACKDetailed)
+	sr := &ServerResponse{req: &UploadPackRequest{UploadPackCommands: haves, UploadRequest: UploadRequest{Capabilities: capabilities}}}
+	b := bytes.NewBuffer(nil)
+	err := sr.Encode(b)
+	c.Assert(err, IsNil)
+
+	lines := strings.Split(b.String(), "\n")
+	c.Assert(len(lines), Equals, 5)
+	c.Assert(lines[0], Equals, "0037ACK 6ecf0ef2c2dffb796033e5a02219af86ec6584e2 ready")
+	c.Assert(lines[1], Equals, "0038ACK 6ecf0ef2c2dffb796033e5a02219af86ec6584e3 common")
+	c.Assert(lines[2], Equals, "0008NAK")
+	c.Assert(lines[3], Equals, "0031ACK 6ecf0ef2c2dffb796033e5a02219af86ec6584e3")
+	c.Assert(lines[4], Equals, "")
+}
diff --git a/plumbing/protocol/packp/ulreq_decode_test.go b/plumbing/protocol/packp/ulreq_decode_test.go
index 49978c76..200c855c 100644
--- a/plumbing/protocol/packp/ulreq_decode_test.go
+++ b/plumbing/protocol/packp/ulreq_decode_test.go
@@ -58,14 +58,14 @@ func (s *UlReqDecodeSuite) TestWantOK(c *C) {
 		"want 1111111111111111111111111111111111111111",
 		"",
 	}
-	ur := s.testDecodeOK(c, payloads)
+	ur, _ := s.testDecodeOK(c, payloads, 0)
 
 	c.Assert(ur.Wants, DeepEquals, []plumbing.Hash{
 		plumbing.NewHash("1111111111111111111111111111111111111111"),
 	})
 }
 
-func (s *UlReqDecodeSuite) testDecodeOK(c *C, payloads []string) *UploadRequest {
+func (s *UlReqDecodeSuite) testDecodeOK(c *C, payloads []string, expectedHaveCalls int) (*UploadRequest, []plumbing.Hash) {
 	var buf bytes.Buffer
 	for _, p := range payloads {
 		if p == "" {
@@ -81,7 +81,16 @@ func (s *UlReqDecodeSuite) testDecodeOK(c *C, payloads []string) *UploadRequest
 
 	c.Assert(d.Decode(ur), IsNil)
 
-	return ur
+	haves := []plumbing.Hash{}
+	nbCall := 0
+	for h := range ur.HavesUR {
+		nbCall++
+		haves = append(haves, h.Haves...)
+	}
+
+	c.Assert(nbCall, Equals, expectedHaveCalls)
+
+	return ur, haves
 }
 
 func (s *UlReqDecodeSuite) TestWantWithCapabilities(c *C) {
@@ -89,7 +98,7 @@ func (s *UlReqDecodeSuite) TestWantWithCapabilities(c *C) {
 		"want 1111111111111111111111111111111111111111 ofs-delta multi_ack",
 		"",
 	}
-	ur := s.testDecodeOK(c, payloads)
+	ur, _ := s.testDecodeOK(c, payloads, 0)
 	c.Assert(ur.Wants, DeepEquals, []plumbing.Hash{
 		plumbing.NewHash("1111111111111111111111111111111111111111"),
 	})
@@ -106,7 +115,7 @@ func (s *UlReqDecodeSuite) TestManyWantsNoCapabilities(c *C) {
 		"want 2222222222222222222222222222222222222222",
 		"",
 	}
-	ur := s.testDecodeOK(c, payloads)
+	ur, _ := s.testDecodeOK(c, payloads, 0)
 
 	expected := []plumbing.Hash{
 		plumbing.NewHash("1111111111111111111111111111111111111111"),
@@ -162,7 +171,7 @@ func (s *UlReqDecodeSuite) TestManyWantsWithCapabilities(c *C) {
 		"want 2222222222222222222222222222222222222222",
 		"",
 	}
-	ur := s.testDecodeOK(c, payloads)
+	ur, _ := s.testDecodeOK(c, payloads, 0)
 
 	expected := []plumbing.Hash{
 		plumbing.NewHash("1111111111111111111111111111111111111111"),
@@ -185,7 +194,7 @@ func (s *UlReqDecodeSuite) TestSingleShallowSingleWant(c *C) {
 		"shallow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
 		"",
 	}
-	ur := s.testDecodeOK(c, payloads)
+	ur, _ := s.testDecodeOK(c, payloads, 0)
 
 	expectedWants := []plumbing.Hash{
 		plumbing.NewHash("3333333333333333333333333333333333333333"),
@@ -211,7 +220,7 @@ func (s *UlReqDecodeSuite) TestSingleShallowManyWants(c *C) {
 		"shallow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
 		"",
 	}
-	ur := s.testDecodeOK(c, payloads)
+	ur, _ := s.testDecodeOK(c, payloads, 0)
 
 	expectedWants := []plumbing.Hash{
 		plumbing.NewHash("1111111111111111111111111111111111111111"),
@@ -242,7 +251,7 @@ func (s *UlReqDecodeSuite) TestManyShallowSingleWant(c *C) {
 		"shallow dddddddddddddddddddddddddddddddddddddddd",
 		"",
 	}
-	ur := s.testDecodeOK(c, payloads)
+	ur, _ := s.testDecodeOK(c, payloads, 0)
 
 	expectedWants := []plumbing.Hash{
 		plumbing.NewHash("3333333333333333333333333333333333333333"),
@@ -276,7 +285,7 @@ func (s *UlReqDecodeSuite) TestManyShallowManyWants(c *C) {
 		"shallow dddddddddddddddddddddddddddddddddddddddd",
 		"",
 	}
-	ur := s.testDecodeOK(c, payloads)
+	ur, _ := s.testDecodeOK(c, payloads, 0)
 
 	expectedWants := []plumbing.Hash{
 		plumbing.NewHash("1111111111111111111111111111111111111111"),
@@ -395,7 +404,7 @@ func (s *UlReqDecodeSuite) TestDeepenCommits(c *C) {
 		"deepen 1234",
 		"",
 	}
-	ur := s.testDecodeOK(c, payloads)
+	ur, _ := s.testDecodeOK(c, payloads, 0)
 
 	c.Assert(ur.Depth, FitsTypeOf, DepthCommits(0))
 	commits, ok := ur.Depth.(DepthCommits)
@@ -409,7 +418,7 @@ func (s *UlReqDecodeSuite) TestDeepenCommitsInfiniteImplicit(c *C) {
 		"deepen 0",
 		"",
 	}
-	ur := s.testDecodeOK(c, payloads)
+	ur, _ := s.testDecodeOK(c, payloads, 0)
 
 	c.Assert(ur.Depth, FitsTypeOf, DepthCommits(0))
 	commits, ok := ur.Depth.(DepthCommits)
@@ -422,7 +431,7 @@ func (s *UlReqDecodeSuite) TestDeepenCommitsInfiniteExplicit(c *C) {
 		"want 3333333333333333333333333333333333333333 ofs-delta multi_ack",
 		"",
 	}
-	ur := s.testDecodeOK(c, payloads)
+	ur, _ := s.testDecodeOK(c, payloads, 0)
 
 	c.Assert(ur.Depth, FitsTypeOf, DepthCommits(0))
 	commits, ok := ur.Depth.(DepthCommits)
@@ -456,7 +465,7 @@ func (s *UlReqDecodeSuite) TestDeepenSince(c *C) {
 		"deepen-since 1420167845", // 2015-01-02T03:04:05+00:00
 		"",
 	}
-	ur := s.testDecodeOK(c, payloads)
+	ur, _ := s.testDecodeOK(c, payloads, 0)
 
 	expected := time.Date(2015, time.January, 2, 3, 4, 5, 0, time.UTC)
 
@@ -473,7 +482,7 @@ func (s *UlReqDecodeSuite) TestDeepenReference(c *C) {
 		"deepen-not refs/heads/master",
 		"",
 	}
-	ur := s.testDecodeOK(c, payloads)
+	ur, _ := s.testDecodeOK(c, payloads, 0)
 
 	expected := "refs/heads/master"
 
@@ -495,8 +504,12 @@ func (s *UlReqDecodeSuite) TestAll(c *C) {
 		"shallow dddddddddddddddddddddddddddddddddddddddd",
 		"deepen 1234",
 		"",
+		"have 5555555555555555555555555555555555555555",
+		"",
+		"have 6666666666666666666666666666666666666666",
+		"done",
 	}
-	ur := s.testDecodeOK(c, payloads)
+	ur, haves := s.testDecodeOK(c, payloads, 2)
 
 	expectedWants := []plumbing.Hash{
 		plumbing.NewHash("1111111111111111111111111111111111111111"),
@@ -504,11 +517,18 @@ func (s *UlReqDecodeSuite) TestAll(c *C) {
 		plumbing.NewHash("3333333333333333333333333333333333333333"),
 		plumbing.NewHash("4444444444444444444444444444444444444444"),
 	}
+	expectedHave := []plumbing.Hash{
+		plumbing.NewHash("5555555555555555555555555555555555555555"),
+		plumbing.NewHash("6666666666666666666666666666666666666666"),
+	}
+	sort.Sort(byHash(expectedHave))
+	sort.Sort(byHash(haves))
+	c.Assert(haves, DeepEquals, expectedHave)
+	c.Assert(ur.Capabilities.Supports(capability.OFSDelta), Equals, true)
+	c.Assert(ur.Capabilities.Supports(capability.MultiACK), Equals, true)
 	sort.Sort(byHash(expectedWants))
 	sort.Sort(byHash(ur.Wants))
 	c.Assert(ur.Wants, DeepEquals, expectedWants)
-	c.Assert(ur.Capabilities.Supports(capability.OFSDelta), Equals, true)
-	c.Assert(ur.Capabilities.Supports(capability.MultiACK), Equals, true)
 
 	expectedShallows := []plumbing.Hash{
 		plumbing.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
diff --git a/plumbing/protocol/packp/uppackresp_test.go b/plumbing/protocol/packp/uppackresp_test.go
index ec56507e..08f73b75 100644
--- a/plumbing/protocol/packp/uppackresp_test.go
+++ b/plumbing/protocol/packp/uppackresp_test.go
@@ -60,10 +60,6 @@ func (s *UploadPackResponseSuite) TestDecodeMalformed(c *C) {
 	c.Assert(err, NotNil)
 }
 
-// multi_ack isn't fully implemented, this ensures that Decode ignores that fact,
-// as in some circumstances that's OK to assume so.
-//
-// TODO: Review as part of multi_ack implementation.
 func (s *UploadPackResponseSuite) TestDecodeMultiACK(c *C) {
 	req := NewUploadPackRequest()
 	req.Capabilities.Set(capability.MultiACK)
@@ -93,6 +89,13 @@ func (s *UploadPackResponseSuite) TestEncodeNAK(c *C) {
 	res := NewUploadPackResponseWithPackfile(req, pf)
 	defer func() { c.Assert(res.Close(), IsNil) }()
 
+	go func() {
+		req.UploadPackCommands <- UploadPackCommand{
+			Acks: []UploadPackRequestAck{},
+			Done: true,
+		}
+		close(req.UploadPackCommands)
+	}()
 	b := bytes.NewBuffer(nil)
 	c.Assert(res.Encode(b), IsNil)
 
@@ -108,6 +111,13 @@ func (s *UploadPackResponseSuite) TestEncodeDepth(c *C) {
 	res := NewUploadPackResponseWithPackfile(req, pf)
 	defer func() { c.Assert(res.Close(), IsNil) }()
 
+	go func() {
+		req.UploadPackCommands <- UploadPackCommand{
+			Acks: []UploadPackRequestAck{},
+			Done: true,
+		}
+		close(req.UploadPackCommands)
+	}()
 	b := bytes.NewBuffer(nil)
 	c.Assert(res.Encode(b), IsNil)
 
@@ -118,16 +128,31 @@ func (s *UploadPackResponseSuite) TestEncodeDepth(c *C) {
 func (s *UploadPackResponseSuite) TestEncodeMultiACK(c *C) {
 	pf := io.NopCloser(bytes.NewBuffer([]byte("[PACK]")))
 	req := NewUploadPackRequest()
+	req.Capabilities.Set(capability.MultiACK)
 
 	res := NewUploadPackResponseWithPackfile(req, pf)
 	defer func() { c.Assert(res.Close(), IsNil) }()
-	res.ACKs = []plumbing.Hash{
-		plumbing.NewHash("5dc01c595e6c6ec9ccda4f6f69c131c0dd945f81"),
-		plumbing.NewHash("5dc01c595e6c6ec9ccda4f6f69c131c0dd945f82"),
-	}
-
+	go func() {
+		req.UploadPackCommands <- UploadPackCommand{
+			Acks: []UploadPackRequestAck{
+				{Hash: plumbing.NewHash("5dc01c595e6c6ec9ccda4f6f69c131c0dd945f81")},
+				{Hash: plumbing.NewHash("5dc01c595e6c6ec9ccda4f6f69c131c0dd945f82"), IsCommon: true},
+			},
+		}
+		req.UploadPackCommands <- UploadPackCommand{
+			Acks: []UploadPackRequestAck{},
+			Done: true,
+		}
+		close(req.UploadPackCommands)
+	}()
 	b := bytes.NewBuffer(nil)
-	c.Assert(res.Encode(b), NotNil)
+	c.Assert(res.Encode(b), IsNil)
+
+	expected := "003aACK 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f82 continue\n" +
+		"0008NAK\n" +
+		"0031ACK 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f82\n" +
+		"[PACK]"
+	c.Assert(b.String(), Equals, expected)
 }
 
 func FuzzDecoder(f *testing.F) {
diff --git a/plumbing/transport/transport_test.go b/plumbing/transport/transport_test.go
index fb0812b3..f2967f16 100644
--- a/plumbing/transport/transport_test.go
+++ b/plumbing/transport/transport_test.go
@@ -217,9 +217,10 @@ func (s *SuiteCommon) TestNewEndpointInvalidURL(c *C) {
 func (s *SuiteCommon) TestFilterUnsupportedCapabilities(c *C) {
 	l := capability.NewList()
 	l.Set(capability.MultiACK)
+	l.Set(capability.MultiACKDetailed)
 
 	FilterUnsupportedCapabilities(l)
-	c.Assert(l.Supports(capability.MultiACK), Equals, false)
+	c.Assert(l.Supports(capability.ThinPack), Equals, false)
 }
 
 func (s *SuiteCommon) TestNewEndpointIPv6(c *C) {