From patchwork Fri Nov 20 16:53:40 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Elijah Newren X-Patchwork-Id: 11921397 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-12.7 required=3.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,FREEMAIL_FORGED_FROMDOMAIN,FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI,SPF_HELO_NONE,SPF_PASS,URIBL_BLOCKED autolearn=ham autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id DE5F8C63777 for ; Fri, 20 Nov 2020 16:54:07 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 85E3E2240B for ; Fri, 20 Nov 2020 16:54:07 +0000 (UTC) Authentication-Results: mail.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="GRWzN1uc" Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1730168AbgKTQxq (ORCPT ); Fri, 20 Nov 2020 11:53:46 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:54590 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1730167AbgKTQxq (ORCPT ); Fri, 20 Nov 2020 11:53:46 -0500 Received: from mail-wr1-x441.google.com (mail-wr1-x441.google.com [IPv6:2a00:1450:4864:20::441]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 300A5C0613CF for ; Fri, 20 Nov 2020 08:53:46 -0800 (PST) Received: by mail-wr1-x441.google.com with SMTP id d12so10640019wrr.13 for ; Fri, 20 Nov 2020 08:53:46 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=message-id:in-reply-to:references:from:date:subject:fcc :content-transfer-encoding:mime-version:to:cc; bh=RuxTs9b83Ix89XzYOrfwLE1zdEVUR0xwuJweDaIwmhI=; b=GRWzN1ucTzLuaoLc9sJyb26un5u98fI4EyB7gg86fBEYeho6A1QjK9R95MiT30iIbP pRIx/TRuXil4kLv3cT9Re3HSGmQFBHJtuRJ9L+nEL4WSPEYsix5ACdvaIWnoJ4lL9hVa EXJf1E+p2ZadRyaVwVdWThT1ro4uddplF3b9+rCnffIlpCQgjQmS3PqfgSJc1b3NfoQe fqoAPk+F8d1XfoLQ9ED+5p0dcT+DEb4BFS4QqxJUcZGgsWVskK2+HbgZUr5htFlEXrNo fphehlnq+FNglhJs8qnXBQPoqHf+5d0s1XVtb2ENJyjGWFOSju3skpVgj1D30pz4M12c LRqQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:message-id:in-reply-to:references:from:date :subject:fcc:content-transfer-encoding:mime-version:to:cc; bh=RuxTs9b83Ix89XzYOrfwLE1zdEVUR0xwuJweDaIwmhI=; b=K+QOvRsE0yd9/eYWB3R1eLA54CAYZ6P3aH/jhqa8r1Me7T96G7ddwdXAU8MeIqeHF/ VpV+z9NuZoFzAXoAoXpnCUHUqy5nviNebhci8CkaN4mbryhviOIsPtMKbX4UsdnqtpJh x8R+pDlrbeCSUnWS05Okeq6LUCYttN5/cq+MqFgqpOpqCSJgL5vfpAPKpbogQ8/LzXQ4 ElY4kkWUcOVgF5cjbHCDJH31cc610edDJieUs3FUhlhgvu67e0f/tzW4/7oEhOop8lHi k0wIhA4LiSB+iG0R8RvdTS5KCCY1+K7fuz72fJlwRlB7w8S5V5Wm44tKHfWwq6pFD411 v1+A== X-Gm-Message-State: AOAM533eepa1827Vq8yBy+PGywzUjwarKkvrSmt+nyAVHsUMTobX90iP z6zb1lISHBzWE04H4CybI8FRU6DuiV4= X-Google-Smtp-Source: ABdhPJy1FZzoTAxchCJ8ioif7D/V7duIaUFCtF9pGRDbLPkFLCQPfI/Dt5l/SLTIusp0/h49l9vvHg== X-Received: by 2002:a5d:4402:: with SMTP id z2mr16799357wrq.76.1605891224816; Fri, 20 Nov 2020 08:53:44 -0800 (PST) Received: from [127.0.0.1] ([13.74.141.28]) by smtp.gmail.com with ESMTPSA id c5sm5717103wrb.64.2020.11.20.08.53.44 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 20 Nov 2020 08:53:44 -0800 (PST) Message-Id: In-Reply-To: References: Date: Fri, 20 Nov 2020 16:53:40 +0000 Subject: [PATCH 1/3] t7012: add a testcase demonstrating stash apply bugs in sparse checkouts Fcc: Sent MIME-Version: 1.0 To: git@vger.kernel.org Cc: matheus.bernardino@usp.br, dstolee@microsoft.com, Elijah Newren , Elijah Newren Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org From: Elijah Newren From: Elijah Newren Applying stashes in sparse-checkouts, particularly when the patterns used to define the present paths have changed between when the stash was created and when it is applied, has a number of bugs. The primary problem is perhaps that stashes can be only partially applied (sometimes without any warning or error being displayed and with 0 exit status). In addition, there are cases where partial stash application comes with non-translated error messages and an early abort. The first is when there are files present despite the SKIP_WORKTREE bit being set, in which case the error message shown is: error: Entry 'PATHNAME' not uptodate. Cannot merge. The other situation is when a stash contains new files to add to the working tree; in this case, the code aborts early (but after at least partial stash application) with the following error message being shown: error: NEWFILE: does not exist and --remove not passed fatal: Unable to process path NEWFILE Add a test that can trigger all three of these problems that carefully checks that the working copy and SKIP_WORKTREE bits are as expected after the stash application...but currently marked as expected to fail. Signed-off-by: Elijah Newren --- t/t7012-skip-worktree-writing.sh | 88 ++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/t/t7012-skip-worktree-writing.sh b/t/t7012-skip-worktree-writing.sh index 7476781979..a184ee97fb 100755 --- a/t/t7012-skip-worktree-writing.sh +++ b/t/t7012-skip-worktree-writing.sh @@ -149,6 +149,94 @@ test_expect_success '--ignore-skip-worktree-entries leaves worktree alone' ' --diff-filter=D -- keep-me.t ' +test_expect_failure 'stash restore in sparse checkout' ' + test_create_repo stash-restore && + ( + cd stash-restore && + + mkdir subdir && + echo A >subdir/A && + echo untouched >untouched && + echo removeme >removeme && + echo modified >modified && + git add . && + git commit -m Initial && + + echo AA >>subdir/A && + echo addme >addme && + echo tweaked >>modified && + rm removeme && + git add addme && + + git stash push && + + git sparse-checkout set subdir && + + # Ensure after sparse-checkout we only have expected files + cat >expect <<-EOF && + S modified + S removeme + H subdir/A + S untouched + EOF + git ls-files -t >actual && + test_cmp expect actual && + + test_path_is_missing addme && + test_path_is_missing modified && + test_path_is_missing removeme && + test_path_is_file subdir/A && + test_path_is_missing untouched && + + # Put a file in the working directory in the way + echo in the way >modified && + git stash apply && + + # Ensure stash vivifies modifies paths... + cat >expect <<-EOF && + H addme + H modified + H removeme + H subdir/A + S untouched + EOF + git ls-files -t >actual && + test_cmp expect actual && + + # ...and that the paths show up in status as changed... + cat >expect <<-EOF && + A addme + M modified + D removeme + M subdir/A + ?? actual + ?? expect + ?? modified.stash.XXXXXX + EOF + git status --porcelain | \ + sed -e s/stash......./stash.XXXXXX/ >actual && + test_cmp expect actual && + + # ...and that working directory reflects the files correctly + test_path_is_file addme && + test_path_is_file modified && + test_path_is_missing removeme && + test_path_is_file subdir/A && + test_path_is_missing untouched && + + # ...including that we have the expected "modified" file... + cat >expect <<-EOF && + modified + tweaked + EOF + test_cmp expect modified && + + # ...and that the other "modified" file is still present... + echo in the way >expect && + test_cmp expect modified.stash.* + ) +' + #TODO test_expect_failure 'git-apply adds file' false #TODO test_expect_failure 'git-apply updates file' false #TODO test_expect_failure 'git-apply removes file' false From patchwork Fri Nov 20 16:53:41 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Elijah Newren X-Patchwork-Id: 11921399 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-12.7 required=3.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,FREEMAIL_FORGED_FROMDOMAIN,FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI,SPF_HELO_NONE,SPF_PASS,URIBL_BLOCKED autolearn=ham autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 1F988C56201 for ; Fri, 20 Nov 2020 16:54:08 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id BEFE12100A for ; Fri, 20 Nov 2020 16:54:07 +0000 (UTC) Authentication-Results: mail.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="Ip0voAmH" Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1730171AbgKTQxs (ORCPT ); Fri, 20 Nov 2020 11:53:48 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:54592 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1730167AbgKTQxr (ORCPT ); Fri, 20 Nov 2020 11:53:47 -0500 Received: from mail-wr1-x441.google.com (mail-wr1-x441.google.com [IPv6:2a00:1450:4864:20::441]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 3A426C0613CF for ; Fri, 20 Nov 2020 08:53:47 -0800 (PST) Received: by mail-wr1-x441.google.com with SMTP id u12so10773723wrt.0 for ; Fri, 20 Nov 2020 08:53:47 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=message-id:in-reply-to:references:from:date:subject:fcc :content-transfer-encoding:mime-version:to:cc; bh=5L9um2HsOpUj5ErYVGAEt/asMWXqgB4GcsakeEVwtW0=; b=Ip0voAmHSeJ7CKsLfDMtwLRUzzr8mg4OJskKq5JCp5fsrpZtmSHh8A3SrzM5TdkXs7 hXumz8PeeaY5xjpNfq0hq324otH1lg4leS7Fv884lRPurZm8iI9lDANlq7VhpODUkDah Ok7PeACegXahX522mq+Icz04fT49CfcV3ZwWczmztRGn/BlmhJ6x9/eDlUMn+PdhpE6r Bs/i2Q/rKlbHmOR+TWUy8olypeprrafX24zEZgNx1ghXXBiFDve86OqTeq/XKEuF5d2t uIK1EHRAY4FhDH2mPRTOdgliw9EBh8R5kcUYHQnIV9oZcqQydPYpy2g5YUdNrApeO3MV L2sA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:message-id:in-reply-to:references:from:date :subject:fcc:content-transfer-encoding:mime-version:to:cc; bh=5L9um2HsOpUj5ErYVGAEt/asMWXqgB4GcsakeEVwtW0=; b=h3GtFecAucNyRofJJObaZ9j22VQPGL/7rZTo8mOSUBsi6gfq3oR2Tl82vwAv1EX93O dvNm8j6QI3mL9Wtj3kB9PvQv0ykKa2lecIAgcrqI1EAcjE0jJRIlM85/OC0CdKrFFyvZ zI5DwNNsd6NyIwowjGJTuPaR2clv4umEEodQUrZFTU/VZKVL0N6ST84InCeYMpVd9MTz GBDes1YXhXIztOfCYvNKvMkJ9Wsq7DS2kdFHfG9UHo97Wa40j8Y650NsK2R9hFZbkTxK XZ3LqbMloByqKoCWUgaWbRrgw6anSdXj4MCyZatMA8pKPyaUp3LsSKMmeXNEm7L3WTy2 N6tQ== X-Gm-Message-State: AOAM530ivsIRP5TrhiI13EIAEZzxnzXYJeeJJhJN2RpK2Frj1EWvThMU Mu0sZEHnRAisWr/Jv5hDfOQcx4mTXcE= X-Google-Smtp-Source: ABdhPJwMwWN7uK02rlvzgTAWfp+4sg/Fm6EU87/Gg7da2wQQafYITas2d1UjyFlHVfm+oZlsyUnJjQ== X-Received: by 2002:adf:dccd:: with SMTP id x13mr18204582wrm.394.1605891225539; Fri, 20 Nov 2020 08:53:45 -0800 (PST) Received: from [127.0.0.1] ([13.74.141.28]) by smtp.gmail.com with ESMTPSA id 36sm5660415wrf.94.2020.11.20.08.53.45 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 20 Nov 2020 08:53:45 -0800 (PST) Message-Id: In-Reply-To: References: Date: Fri, 20 Nov 2020 16:53:41 +0000 Subject: [PATCH 2/3] stash: remove unnecessary process forking Fcc: Sent MIME-Version: 1.0 To: git@vger.kernel.org Cc: matheus.bernardino@usp.br, dstolee@microsoft.com, Elijah Newren , Elijah Newren Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org From: Elijah Newren From: Elijah Newren When stash was converted from shell to a builtin, it merely transliterated the forking of various git commands from shell to a C program that would fork the same commands. Some of those were converted over to actual library calls, but much of the pipeline-of-commands design still remains. Fix some of this by replacing the portion corresponding to git diff-index --cached --name-only --diff-filter=A $CTREE >"$a" git read-tree --reset $CTREE git update-index --add --stdin <"$a" rm -f "$a" into a library function that does the same thing. (The read-tree --reset was already partially converted over to a library call, but as an independent piece.) Note here that this came after a merge operation was performed. The merge machinery always staged anything that cleanly merges, and the above code only runs if there were no conflicts. Its purpose is to make it so that when there are no conflicts, all the changes from the stash are unstaged. However, that causes brand new files from the stash to become untracked, so the code above first saves those files off and then re-adds them afterwards. We replace the whole series of commands with a simple function that will unstage files that are not newly added. This doesn't fix any bugs in the usage of these commands, it simply matches the existing behavior but making it an actual builtin that we can then operate on as a whole. A subsequent commit will take advantage of this to fix issues with these commands in sparse-checkouts. This conversion incidentally fixes t3906.1, because the separate update-index process would die with the following error messages: error: uninitialized_sub: is a directory - add files inside instead fatal: Unable to process path uninitialized_sub The unstaging of the directory as a submodule meant it was no longer tracked, and thus as an uninitialized directory it could not be added back using `git update-index --add`, thus resulting in this error and early abort. Most of the submodule tests in 3906 continue to fail after this change, this change was just enough to push the first of those tests to success. Signed-off-by: Elijah Newren --- builtin/stash.c | 96 ++++++++++++++++++++++----------------- t/lib-submodule-update.sh | 16 +++---- 2 files changed, 62 insertions(+), 50 deletions(-) diff --git a/builtin/stash.c b/builtin/stash.c index 24ddb1bffa..8117d7647d 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -325,33 +325,64 @@ static void add_diff_to_buf(struct diff_queue_struct *q, } } -static int get_newly_staged(struct strbuf *out, struct object_id *c_tree) +static void unstage_changes_unless_new(struct object_id *cache_tree) { - struct child_process cp = CHILD_PROCESS_INIT; - const char *c_tree_hex = oid_to_hex(c_tree); - /* - * diff-index is very similar to diff-tree above, and should be - * converted together with update_index. + * When we enter this function, there has been a clean merge of + * relevant trees, and the merge logic always stages whatever merges + * cleanly. We want to unstage those changes, unless it corresponds + * to a file that didn't exist as of cache_tree. */ - cp.git_cmd = 1; - strvec_pushl(&cp.args, "diff-index", "--cached", "--name-only", - "--diff-filter=A", NULL); - strvec_push(&cp.args, c_tree_hex); - return pipe_command(&cp, NULL, 0, out, 0, NULL, 0); -} -static int update_index(struct strbuf *out) -{ - struct child_process cp = CHILD_PROCESS_INIT; + struct diff_options diff_opts; + struct lock_file lock = LOCK_INIT; + int i; - /* - * Update-index is very complicated and may need to have a public - * function exposed in order to remove this forking. - */ - cp.git_cmd = 1; - strvec_pushl(&cp.args, "update-index", "--add", "--stdin", NULL); - return pipe_command(&cp, out->buf, out->len, NULL, 0, NULL, 0); + diff_setup(&diff_opts); + diff_opts.flags.recursive = 1; + diff_opts.detect_rename = 0; + diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT; + diff_setup_done(&diff_opts); + + do_diff_cache(cache_tree, &diff_opts); + diffcore_std(&diff_opts); + + for (i = 0; i < diff_queued_diff.nr; i++) { + struct diff_filepair *p; + struct cache_entry *ce; + int pos; + + p = diff_queued_diff.queue[i]; + pos = index_name_pos(&the_index, p->two->path, + strlen(p->two->path)); + if (pos < 0) { + assert(p->one->oid_valid); + assert(!p->two->oid_valid); + ce = make_cache_entry(&the_index, + p->one->mode, + &p->one->oid, + p->one->path, + 0, 0); + add_index_entry(&the_index, ce, ADD_CACHE_OK_TO_ADD); + continue; + } + ce = active_cache[pos]; + if (p->one->oid_valid) { + ce = make_cache_entry(&the_index, + p->one->mode, + &p->one->oid, + p->one->path, + 0, 0); + add_index_entry(&the_index, ce, + ADD_CACHE_OK_TO_REPLACE); + } + } + diff_flush(&diff_opts); + + repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR); + if (write_locked_index(&the_index, &lock, + COMMIT_LOCK | SKIP_IF_UNCHANGED)) + die(_("Unable to write index.")); } static int restore_untracked(struct object_id *u_tree) @@ -467,26 +498,7 @@ static int do_apply_stash(const char *prefix, struct stash_info *info, if (reset_tree(&index_tree, 0, 0)) return -1; } else { - struct strbuf out = STRBUF_INIT; - - if (get_newly_staged(&out, &c_tree)) { - strbuf_release(&out); - return -1; - } - - if (reset_tree(&c_tree, 0, 1)) { - strbuf_release(&out); - return -1; - } - - ret = update_index(&out); - strbuf_release(&out); - if (ret) - return -1; - - /* read back the result of update_index() back from the disk */ - discard_cache(); - read_cache(); + unstage_changes_unless_new(&c_tree); } if (!quiet) { diff --git a/t/lib-submodule-update.sh b/t/lib-submodule-update.sh index bd3fa3c6da..4b714e9308 100644 --- a/t/lib-submodule-update.sh +++ b/t/lib-submodule-update.sh @@ -316,14 +316,7 @@ test_submodule_switch_common () { command="$1" ######################### Appearing submodule ######################### # Switching to a commit letting a submodule appear creates empty dir ... - if test "$KNOWN_FAILURE_STASH_DOES_IGNORE_SUBMODULE_CHANGES" = 1 - then - # Restoring stash fails to restore submodule index entry - RESULT="failure" - else - RESULT="success" - fi - test_expect_$RESULT "$command: added submodule creates empty directory" ' + test_expect_success "$command: added submodule creates empty directory" ' prolog && reset_work_tree_to no_submodule && ( @@ -337,6 +330,13 @@ test_submodule_switch_common () { ) ' # ... and doesn't care if it already exists. + if test "$KNOWN_FAILURE_STASH_DOES_IGNORE_SUBMODULE_CHANGES" = 1 + then + # Restoring stash fails to restore submodule index entry + RESULT="failure" + else + RESULT="success" + fi test_expect_$RESULT "$command: added submodule leaves existing empty directory alone" ' prolog && reset_work_tree_to no_submodule && From patchwork Fri Nov 20 16:53:42 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Elijah Newren X-Patchwork-Id: 11921401 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-12.7 required=3.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,FREEMAIL_FORGED_FROMDOMAIN,FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI,SPF_HELO_NONE,SPF_PASS,URIBL_BLOCKED autolearn=ham autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 494AEC63798 for ; Fri, 20 Nov 2020 16:54:08 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 062812225B for ; Fri, 20 Nov 2020 16:54:07 +0000 (UTC) Authentication-Results: mail.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="PqRiA78K" Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1730174AbgKTQxt (ORCPT ); Fri, 20 Nov 2020 11:53:49 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:54598 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1730167AbgKTQxs (ORCPT ); Fri, 20 Nov 2020 11:53:48 -0500 Received: from mail-wm1-x32c.google.com (mail-wm1-x32c.google.com [IPv6:2a00:1450:4864:20::32c]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 64EEDC0613CF for ; Fri, 20 Nov 2020 08:53:48 -0800 (PST) Received: by mail-wm1-x32c.google.com with SMTP id x13so3278387wmj.1 for ; Fri, 20 Nov 2020 08:53:48 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=message-id:in-reply-to:references:from:date:subject:fcc :content-transfer-encoding:mime-version:to:cc; bh=tGBsOn+CLPS2UIQsK+78dUG0cPNCdFdpBpPGhN6swd8=; b=PqRiA78KJ40QJCZSCQJZWbnn9Hl8y9UzFOESRJV9MTNHX5GZYiLDavHzjXCvtQQ/cN Z3TeN/NsGjKQgZtFyLdEyGir8+uwk2EA+eHK33il7lgLA88zEOfSzek1/FULjmvrPZGM 9FZ++ZPKA2A4VOGAlxdBbFZSRJIHDZwhSFUWYl/2+ySnfQ7uUfR4KAGiKN3W6474GdG1 1nSNUHuF4P0zb1bdV7Cd4hOacjCerpkj3i7tNfF29A7L20k8dLkHcL7hxStv6WsxIaj7 MT4sfoUE9qT7p9YNEmbaK/HdaNyWbia5zRJRSLjVolfHom+HkySwfnObrGNt8MM0ZGyV NdCQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:message-id:in-reply-to:references:from:date :subject:fcc:content-transfer-encoding:mime-version:to:cc; bh=tGBsOn+CLPS2UIQsK+78dUG0cPNCdFdpBpPGhN6swd8=; b=RjT9y47JFfDzBzyUfXKrl0/YDYhgWwvgPXTtAnZdVHzzlS4gK8B1fFJhINjSwv6Zgf nbzn/DOwAJBW09zAfzCSzcpPcALqbhDZCNoSn2kFA4DbrVz3lw3i85Qa9Chi+ervRA0B NZSudP3bapE9bEzj4VyxmsMRKbk/bRJyz0Z4HW45IgXbavYQx+QxEn26ftNI0PuepR+L +CgwiWivSwXNV7zfO5B7D4eP2SBNlEMoN0tJ21+WhxZ9ocfrym+v3P6Ly6O8YgbcgKN/ XbmZYW1vCJjaKEMIxyaIAYRjyGD0/sOv7AYdS09d26MPC+QHxOLtH6ow3jPBSda3gj3r kHRA== X-Gm-Message-State: AOAM530Dr97uPXmHOghWms19L4NraG62AGz/VUUdMNbNBDAyUSpjsk7/ /KWpI1ywSQxDk75XXubp6QOZpabDlFg= X-Google-Smtp-Source: ABdhPJxPdzNtSwytIZ9jtvnVge24eP5/hu33VfBQ9Z7gjmK32qh1SU8ak5Tk3ShABB64xPgByMVTJg== X-Received: by 2002:a1c:40d4:: with SMTP id n203mr11275550wma.102.1605891226547; Fri, 20 Nov 2020 08:53:46 -0800 (PST) Received: from [127.0.0.1] ([13.74.141.28]) by smtp.gmail.com with ESMTPSA id s202sm5090369wme.39.2020.11.20.08.53.45 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 20 Nov 2020 08:53:46 -0800 (PST) Message-Id: <5143cba7047d25137b3d7f8c7811a875c1931aee.1605891222.git.gitgitgadget@gmail.com> In-Reply-To: References: Date: Fri, 20 Nov 2020 16:53:42 +0000 Subject: [PATCH 3/3] stash: fix stash application in sparse-checkouts Fcc: Sent MIME-Version: 1.0 To: git@vger.kernel.org Cc: matheus.bernardino@usp.br, dstolee@microsoft.com, Elijah Newren , Elijah Newren Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org From: Elijah Newren From: Elijah Newren sparse-checkouts are built on the patterns in the $GIT_DIR/info/sparse-checkout file, where commands have modified behavior for paths that do not match those patterns. The differences in behavior, as far as the bugs concerned here, fall into three different categories (with git subcommands that fall into each category listed): * commands that only look at files matching the patterns: * status * diff * clean * update-index * commands that remove files from the working tree that do not match the patterns, and restore files that do match them: * read-tree * switch * checkout * reset (--hard) * commands that omit writing files to the working tree that do not match the patterns, unless those files are not clean: * merge * rebase * cherry-pick * revert There are some caveats above, e.g. a plain `git diff` ignores files outside the sparsity patterns but will show diffs for paths outside the sparsity patterns when revision arguments are passed. (Technically, diff is treating the sparse paths as matching HEAD.) So, there is some internal inconsistency among these commands. There are also additional commands that should behave differently in the face of sparse-checkouts, as the sparse-checkout documentation alludes to, but the above is sufficient for me to explain how `git stash` is affected. What is relevant here is that logically 'stash' should behave like a merge; it three-way merges the changes the user had in progress at stash creation time, the HEAD at the time the stash was created, and the current HEAD, in order to get the stashed changes applied to the current branch. However, this simplistic view doesn't quite work in practice, because stash tweaks it a bit due to two factors: (1) flags like --keep-index and --include-untracked (why we used two different verbs, 'keep' and 'include', is a rant for another day) modify what should be staged at the end and include more things that should be quasi-merged, (2) stash generally wants changes to NOT be staged. It only provides exceptions when (a) some of the changes had conflicts and thus we want to use staged to denote the clean merges and higher order stages to mark the conflicts, or (b) if there is a brand new file we don't want it to become untracked. stash has traditionally gotten this special behavior by first doing a merge, and then when it's clean, applying a pipeline of commands to modify the result. This series of commands for unstaging-non-newly-added-files came from the following commands: git diff-index --cached --name-only --diff-filter=A $CTREE >"$a" git read-tree --reset $CTREE git update-index --add --stdin <"$a" rm -f "$a" You might that between the merge that proceeded these commands and these different commands here, that we have commands from each of the different types of special sparsity handling listed at the beginning of this message, and in fact this precisely led to the following buggy behaviors: (1) If a path merged cleanly and it didn't match the sparsity patterns, the merge backend would know to avoid writing it to the working tree and keep the SKIP_WORKTREE bit, simply only updating it in the index. Unfortunately, the subsequent commands would essentially undo the changes in the index and thus simply toss the changes altogether since there was nothing left in the working tree. This means the stash is only partially applied. (2) If a path existed in the worktree before `git stash apply` despite having the SKIP_WORKTREE bit set, then the `git read-tree --reset` would print an error message of the form error: Entry 'modified' not uptodate. Cannot merge. and cause stash to abort early. (3) If there was a brand new file added by the stash, then the diff-index command would save that pathname to the temporary file, the read-tree --reset would remove it from the index, and the update-index command would barf due to no such file being present in the working copy; it would print a message of the form: error: NEWFILE: does not exist and --remove not passed fatal: Unable to process path NEWFILE and then cause stash to abort early. Basically, the whole idea of unstage-unless-brand-new requires special care when you are dealing with a sparse-checkout. Fix these problems by applying the following simple rule: When we unstage files, if they have the SKIP_WORKTREE bit set, clear that bit and write the file out to the working directory. (*) If there's already a file present in the way, rename it first. This fixes all three problems in t7012.13 and allows us to mark it as passing. Signed-off-by: Elijah Newren --- builtin/stash.c | 36 +++++++++++++++++++++++++++++++- t/t7012-skip-worktree-writing.sh | 2 +- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/builtin/stash.c b/builtin/stash.c index 8117d7647d..0f7e78d315 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -331,13 +331,23 @@ static void unstage_changes_unless_new(struct object_id *cache_tree) * When we enter this function, there has been a clean merge of * relevant trees, and the merge logic always stages whatever merges * cleanly. We want to unstage those changes, unless it corresponds - * to a file that didn't exist as of cache_tree. + * to a file that didn't exist as of cache_tree. However, if any + * SKIP_WORKTREE path is modified relative to cache_tree, then we + * want to clear the SKIP_WORKTREE bit and write it to the worktree + * before unstaging. */ + struct checkout state = CHECKOUT_INIT; struct diff_options diff_opts; struct lock_file lock = LOCK_INIT; int i; + /* If any entries have skip_worktree set, we'll have to check 'em out */ + state.force = 1; + state.quiet = 1; + state.refresh_cache = 1; + state.istate = &the_index; + diff_setup(&diff_opts); diff_opts.flags.recursive = 1; diff_opts.detect_rename = 0; @@ -367,6 +377,30 @@ static void unstage_changes_unless_new(struct object_id *cache_tree) continue; } ce = active_cache[pos]; + if (ce_skip_worktree(ce)) { + struct stat st; + if (!lstat(ce->name, &st)) { + struct strbuf new_path = STRBUF_INIT; + int fd; + + strbuf_addf(&new_path, + "%s.stash.XXXXXX", ce->name); + fd = xmkstemp(new_path.buf); + close(fd); + printf(_("WARNING: Untracked file in way of " + "tracked file! Renaming\n " + " %s -> %s\n" + " to make room.\n"), + ce->name, new_path.buf); + if (rename(ce->name, new_path.buf)) + die("Failed to move %s to %s\n", + ce->name, new_path.buf); + strbuf_release(&new_path); + } + checkout_entry(ce, &state, NULL, NULL); + } + + ce->ce_flags &= ~CE_SKIP_WORKTREE; if (p->one->oid_valid) { ce = make_cache_entry(&the_index, p->one->mode, diff --git a/t/t7012-skip-worktree-writing.sh b/t/t7012-skip-worktree-writing.sh index a184ee97fb..e5c6a038fb 100755 --- a/t/t7012-skip-worktree-writing.sh +++ b/t/t7012-skip-worktree-writing.sh @@ -149,7 +149,7 @@ test_expect_success '--ignore-skip-worktree-entries leaves worktree alone' ' --diff-filter=D -- keep-me.t ' -test_expect_failure 'stash restore in sparse checkout' ' +test_expect_success 'stash restore in sparse checkout' ' test_create_repo stash-restore && ( cd stash-restore &&