Subject: [PATCH] forbid a hard reset before the initial commit

From: "Viaceslavus via GitGitGadget" <[email-masked]>
Date: Wed, 02 Feb 2022 11:52:01 +0000

From: Viacelaus <[email-masked]>

Performing 'git reset --hard' on empty repo with staged files
may have the only one possible result - deleting all staged files.
Such behaviour may be unexpected or even dangerous. With this
commit, when running 'git reset --hard', git will check for the
existence of commits in the repo; in case of absence of such, and
also if there are any files staged, git will die with an error.

Signed-off-by: Viacelaus <[email-masked]>
---
    Forbid a hard reset on empty repo with staged files.
    
    Performing git reset --hard on empty repo (initialized repository
    without any commits) with staged files may have the only one possible
    result - deleting all staged files. Such behaviour may be unexpected or
    even dangerous, as it is possible to permanently delete the whole
    project. It is also absolutely useless. So with this patch, when running
    git reset --hard, git will check for the existence of commits in the
    repository; in case of absence of such, and also if there are files
    staged, git will return an error. All the tests were added into
    t/t7104-reset-hard.sh file.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1137%2FViaceslavus%2Fhard-reset-safety-on-empty-repo-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1137/Viaceslavus/hard-reset-safety-on-empty-repo-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/1137

 builtin/reset.c                | 14 ++++++++++++++
 t/t7104-reset-hard.sh          | 18 ++++++++++++++++++
 t/t7106-reset-unborn-branch.sh | 11 -----------
 3 files changed, 32 insertions(+), 11 deletions(-)

diff --git a/builtin/reset.c b/builtin/reset.c
index b97745ee94e..5a0e80d380f 100644
--- a/builtin/reset.c
+++ b/builtin/reset.c
@@ -301,6 +301,11 @@ static void die_if_unmerged_cache(int reset_type)
 
 }
 
+static int check_commit_exists(const char *refname, const struct object_id *oid, int f, void *d)
+{
+	return is_branch(refname);
+}
+
 static void parse_args(struct pathspec *pathspec,
 		       const char **argv, const char *prefix,
 		       int patch_mode,
@@ -474,6 +479,15 @@ int cmd_reset(int argc, const char **argv, const char *prefix)
 			die(_("Cannot do %s reset with paths."),
 					_(reset_type_names[reset_type]));
 	}
+
+	if (reset_type == HARD) {
+		int commits_exist = for_each_fullref_in("refs/heads", check_commit_exists, NULL);
+		if(!commits_exist) {
+			if(read_cache() == 1)
+				die(_("Hard reset isn`t allowed before the first commit."));
+		}
+	}
+
 	if (reset_type == NONE)
 		reset_type = MIXED; /* by default */
 
diff --git a/t/t7104-reset-hard.sh b/t/t7104-reset-hard.sh
index cf9697eba9a..30fb71e6fb9 100755
--- a/t/t7104-reset-hard.sh
+++ b/t/t7104-reset-hard.sh
@@ -44,4 +44,22 @@ test_expect_success 'reset --hard did not corrupt index or cache-tree' '
 
 '
 
+test_expect_success 'reset --hard on empty repo without staged changes works fine' '
+	git reset --hard
+'
+
+test_expect_success 'reset --hard on empty repo with staged changes results in an error' '
+	touch n &&
+	git add n &&
+	test_must_fail git reet --hard
+'
+
+test_expect_success 'reset --hard after a commit works fine' '
+	touch new &&
+	git add new &&
+	git commit -m "initial" &&
+	git reset --hard 2> error &&
+	test_must_be_empty error
+'
+
 test_done
diff --git a/t/t7106-reset-unborn-branch.sh b/t/t7106-reset-unborn-branch.sh
index ecb85c3b823..8d45f640480 100755
--- a/t/t7106-reset-unborn-branch.sh
+++ b/t/t7106-reset-unborn-branch.sh
@@ -53,15 +53,4 @@ test_expect_success 'reset --soft is a no-op' '
 	test_cmp expect actual
 '
 
-test_expect_success 'reset --hard' '
-	rm .git/index &&
-	git add a &&
-	test_when_finished "echo a >a" &&
-	git reset --hard &&
-
-	git ls-files >actual &&
-	test_must_be_empty actual &&
-	test_path_is_missing a
-'
-
 test_done

base-commit: 5d01301f2b865aa8dba1654d3f447ce9d21db0b5
-- 
gitgitgadget