{"thread":{"id":"66120","subject":"`git push --porcelain` has no effect when deleting a ref which does not exist","startedAt":"2026-08-05T07:19:18Z","lastAt":"2026-08-07T06:51:26Z","messageCount":3,"participants":["Xavier Morel","Junio C Hamano"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"549646","messageId":"27d95520-409e-4d1b-b8b1-37a910bff604@odoo.com","threadId":"66120","inReplyTo":null,"subject":"`git push --porcelain` has no effect when deleting a ref which does not exist","fromName":"Xavier Morel","fromEmail":"xmo@odoo.com","sentAt":"2026-08-05T07:19:13Z","receivedAt":"2026-08-05T07:19:18Z","isPatch":false,"body":"Using `push --delete --porcelain` with refs which are extant correctly \noutputs the relevant information in the documented format:\n\n-\t:refs/heads/<branch1>\t[deleted]\n-\t:refs/heads/<branch2>\t[deleted]\n\nHowever doing the same with refs which don't exist on the remote (e.g. \nbecause of a concurrent deletion) has the error written out in \nhuman-targeted text:\n\nerror: unable to delete '<branch1>': remote ref does not exist\nerror: unable to delete '<branch2>': remote ref does not exist\n\nI would have expected something along the lines of:\n\n!\t:refs/heads/<branch>\t[remote failure]\n\nwhich would be machine-readable as documented for the `--porcelain` \nflag. Was that intended or is it just something that fell through the \ncracks of code convolution?\n"},{"id":"549744","messageId":"xmqq33wssf6x.fsf@gitster.g","threadId":"66120","inReplyTo":"27d95520-409e-4d1b-b8b1-37a910bff604@odoo.com","subject":"Re: `git push --porcelain` has no effect when deleting a ref which does not exist","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-05T16:30:14Z","receivedAt":"2026-08-05T16:30:16Z","isPatch":false,"body":"Xavier Morel <xmo@odoo.com> writes:\n\n> Using `push --delete --porcelain` with refs which are extant correctly \n> outputs the relevant information in the documented format:\n>\n> -\t:refs/heads/<branch1>\t[deleted]\n> -\t:refs/heads/<branch2>\t[deleted]\n>\n> However doing the same with refs which don't exist on the remote (e.g. \n> because of a concurrent deletion) has the error written out in \n> human-targeted text:\n>\n> error: unable to delete '<branch1>': remote ref does not exist\n> error: unable to delete '<branch2>': remote ref does not exist\n>\n> I would have expected something along the lines of:\n>\n> !\t:refs/heads/<branch>\t[remote failure]\n>\n> which would be machine-readable as documented for the `--porcelain` \n> flag. Was that intended or is it just something that fell through the \n> cracks of code convolution?\n\nIf I have to guess, I would say it is because nobody thought of\ncovering this usage pattern, which allows you to randomly throw a\ndeletion request to probe what does and what does not exist on the\nother side.\n\nPatches welcome.\n\nThanks.\n\n"},{"id":"549938","messageId":"6ea78e82-0b35-4e73-99ff-ad6b653bc103@odoo.com","threadId":"66120","inReplyTo":"xmqq33wssf6x.fsf@gitster.g","subject":"Re: `git push --porcelain` has no effect when deleting a ref which does not exist","fromName":"Xavier Morel","fromEmail":"xmo@odoo.com","sentAt":"2026-08-07T06:51:23Z","receivedAt":"2026-08-07T06:51:26Z","isPatch":false,"body":"On 05/08/2026 18:30, Junio C Hamano wrote:\n> Xavier Morel <xmo@odoo.com> writes:\n> \n>> Using `push --delete --porcelain` with refs which are extant correctly\n>> outputs the relevant information in the documented format:\n>>\n>> -\t:refs/heads/<branch1>\t[deleted]\n>> -\t:refs/heads/<branch2>\t[deleted]\n>>\n>> However doing the same with refs which don't exist on the remote (e.g.\n>> because of a concurrent deletion) has the error written out in\n>> human-targeted text:\n>>\n>> error: unable to delete '<branch1>': remote ref does not exist\n>> error: unable to delete '<branch2>': remote ref does not exist\n>>\n>> I would have expected something along the lines of:\n>>\n>> !\t:refs/heads/<branch>\t[remote failure]\n>>\n>> which would be machine-readable as documented for the `--porcelain`\n>> flag. Was that intended or is it just something that fell through the\n>> cracks of code convolution?\n> \n> If I have to guess, I would say it is because nobody thought of\n> covering this usage pattern, which allows you to randomly throw a\n> deletion request to probe what does and what does not exist on the\n> other side.\n> \n> Patches welcome.\n\nLooking at the current code, the abort when requesting the deletion of a \nref which is not on the remote is pretty early in the process, during \nref matching, which then causes `push` to bail.\n\nReading some of the followup the following call \n`set_ref_status_for_push` can already set statuses on remote refs before \nthe push, in which case such refs with statuses set will be ignored for \nthe actual network operation (and the entire thing would be skipped if \natomic), and then we get to the reporting and teardown.\n\nSo it looks like\n\n- `match_explicit` could create a dummy dest ref and set its status to \nsome sort of failure value (either an existing one or a new one) instead \nof aborting\n- then `set_ref_status_for_push` should skip over refs which already \nhave a status set (so it doesn't overwrite a previous error)\n- push_refs_with_push all refs with a rejection status already set\n- and then the formatting needs to get adapted for the new mode / case\n\nAnd then trying to delete a non-existent ref would appear in the report \nnormally, and valid pushes would be performed instead of ignored, unless \n`atomic` was set in which case they'd all be aborted, similar to other \nabortions from \"pre-push\" checks by set_ref_status_for_push.\n\nDoes that seem to make sense or did I miss something critical? Do you \nforesee significant issues?\n\nWould you rather a new status code for this case or extending an \nexisting case? e.g. I could see REF_STATUS_REJECT_NODELETE on an \notherwise zeroed deletion ref for missing on remote, and a non-zeroed \ndeletion ref would be the existing \"remote rejecting the deletion\" case \nso less code (and notably not push_refs_with_push) would have to be \nadapted, but it would make the new case a bit more implicit.\n"}]}