[Git notes] git reset options and branch history backup

Clarify the differences between git reset --[options] <commit-hash or HEAD~n>

Using practical examples with simple code changes can definitely help clarify the differences between

git reset --soft <commit-hash or HEAD~n>

git reset --mixed <commit-hash or HEAD~n>

git reset --hard <commit-hash or HEAD~n>

Let's simulate a scenario where we modify a file called example.txt in different ways across several commits, and then see how each type of reset affects the repository.

Initial Setup

Suppose example.txt originally contains:

Line 1: Initial conten

Commit 1 (A):

  • Modify example.txt to:

    Line 1: Initial content
    Line 2: Added by commit A

  • Commit the change: git add example.txt
    git commit -m "Commit A: Add second line"

Commit 2 (B):

  • Modify example.txt to

Line 1: Initial content
Line 2: Added by commit A
Line 3: Added by commit B

  • Commit the change: git add example.txt
    git commit -m "Commit B: Add third line"

Commit 3 (C):

  • Modify example.txt to
复制代码
Line 1: Initial content
Line 2: Added by commit A
Line 3: Added by commit B
Line 4: Added by commit C
  • Commit the change: git add example.txt
    git commit -m "Commit B: Add third line"

Now, let's see how different resets work:

  1. git reset --soft HEAD~2

    • Goal: Undo the last two commits (B and C), but keep their changes staged.

    • Command :

      git reset --soft HEAD~

    • Result :

      • The repository HEAD is back to commit A.
      • Changes from commits B and C remain staged.
      • Running git status will show lines from commits B and C staged for recommitting.
      • Commit History After Reset : Only commit A remains in the main branch's history. Commits B and C are no longer part of the commit history, but their changes are ready to be committed anew.
      • A [main]
        Changes from B and C are staged.
  2. git reset --mixed HEAD~2 (default behavior of git reset)

    • Goal: Undo the last two commits (B and C), keep their changes in the working directory but not staged.

    • Command :

      复制代码
      git reset --mixed HEAD~2
    • Result :

      • HEAD is back to commit A.
      • Changes from commits B and C are in the working directory but not staged.
      • Running git status will show lines from commits B and C as modified but not staged.
      • Commit History After Reset : The commit history is similar to the --soft reset: only commit A is visible in main. The work from commits B and C is not staged but remains in the working directory, allowing you to stage and commit them again as desired.
      • A [main]
        Changes from B and C are modified but not staged.
  3. git reset --hard HEAD~2

    • Goal: Completely undo the last two commits (B and C) and remove all associated changes.

    • Command :

      git reset --hard HEAD~2

    • Result :

      • HEAD is back to commit A.
      • All changes from commits B and C are discarded.
      • example.txt is back to its state at commit A, with no trace of changes from B and C in the working directory.
      • A [main]
  • Soft Reset (--soft): Moves HEAD but leaves your working directory and staging area untouched as they were at the last commit before reset. Great for modifying commit history while keeping changes ready to recommit.
  • Mixed Reset (--mixed): Moves HEAD back and unstages changes but leaves them in your working directory. Useful for redoing commits if you want to modify how changes are staged.
  • Hard Reset (--hard): Moves HEAD back and discards all changes completely, both staged and unstaged. Use with caution as it can lead to data loss.

Steps to Safely Revert Changes and branch history backup

To preserve both the changes and the commit history while still reverting to a previous state in your original branch, the best practice is to create a new branch before performing any type of reset. This allows you to retain all commit history and changes in one branch while experimenting or reverting changes in another.

Suppose you are on the main branch and have made several commits that you wish to review or revert, but you want to ensure that none of your work is lost irreversibly:

  • Creating a Backup:

    git branch backup-branch # Creates a backup git checkout backup-branch # Switches to backup

  • Resetting the Main Branch:

    git checkout main # Return to main git reset --hard HEAD~3 # Reverts the last three commits

  • Result:

    • main branch is now reverted to an earlier state, with the last three commits removed.
    • backup-branch contains all the commits, including those removed from main.

Before creating backup-branch and resetting:

A -- B -- C -- D -- E [main]

After creating backup-branch and performing git reset --hard HEAD~3 on main:

A -- B [main]
\
C -- D -- E [backup-branch]

This approach gives you flexibility. You can move forward on the main branch from the older state, potentially taking a different direction, while still having the option to refer back to or reuse work done in those "removed" commits through the backup-branch.

Note: When using git reset, you can only directly influence the state of the branch from which you're performing the reset, and it directly affects the HEAD of that branch. Essentially, git reset modifies the position of HEAD and potentially changes the staging area and working directory depending on the options used (--soft, --mixed, or --hard).

Scenario: Local Reset with an Aligned Remote Repo

  1. Initial State: Assume your local and remote repositories are synchronized.

    复制代码
    Local and Remote: A -- B -- C -- D [main] 
  2. Create a Backup Branch Locally:

    • You create a backup branch from the current state of main:

      复制代码
      git branch backup-branch git checkout backup-branch 
    • At this point, both your local main and backup-branch are identical, but this new branch is only local.

  3. Reset the Main Branch Locally:

    • You reset the main branch to an earlier commit, for example, B:

      复制代码
      git checkout main git reset --hard B 
    • Locally, your main branch now looks like:

      复制代码
      A -- B [main] 

Updating the Remote Repository

  • Pushing Changes : If you wish to update the remote repository to match your local reset state, you would need to use force push (if the history has diverged, which it typically does after a reset):

    复制代码
    git push origin main --force 
  • Backup Branch : If you want to push the backup-branch to the remote, simply use:

    复制代码
    git push origin backup-branch 

    This will create a new branch on the remote repository with all commits up to D.

Scenario: Resetting and Pushing in the main Branch

Initial State:

  • Assume your local and remote main branches are synchronized with the following commits:

    复制代码
    A -- B -- C -- D [main] 

    Local Reset:
    You decide to reset the main branch back to commit B:
    *

    复制代码
      git reset --hard B 
    • Locally, your branch now looks like this:

      复制代码
      A -- B [main]                              

      Making a New Commit :

      After the reset, you make a new commit E:

      复制代码
      git add .                             
      git commit -m "Commit E" 
      Locally, your branch now looks like this:
      A -- B -- E [main]

What Happens to the Remote Repository?

  • If you use git push --force, Git will update the remote main branch to exactly match your local main branch, which now looks like:

    复制代码
      A -- B -- E [main]
    • When you attempt to push your local main to the remote without using force: git push origin main
    • This push will likely be rejected because it would remove commits C and D from the remote, which Git interprets as a non-fast-forward update. The remote still has:
      • A -- B -- C -- D [main]

When you try to push your local changes to the remote:

  • Git compares the histories of the two branches.
  • The push would require removing commits C and D from the remote, which Git identifies as a non-fast-forward update because the commit E does not directly follow commit D. Instead, it appears to "replace" commits C and D

Why Doesn't Git Just Append E?

Appending E to make the history A -- B -- C -- D -- E would be the straightforward solution if you hadn't reset and E was simply a continuation of D. However, because your local main branch's history effectively rewrites the sequence from B directly to E, Git cannot append E without potentially discarding C and D, unless it merges or rebases, which could change the contents and intention of E.

Why Git Rejects Non-Fast-Forward by Default

  • Data Loss Prevention : Allowing such a push would mean that commits C and D are lost in the remote's main branch. Git rejects this by default to prevent data loss that could affect other collaborators who might be basing their work on those commits.
  • Repository Integrity : Maintaining a linear history on shared branches like main ensures that all collaborators have a consistent view of the project history. Disruptions in this history could lead to conflicts and confusion.

Creating a new branch to backup or preserve commits before making significant changes or resets to your main development branch is indeed a best practice in many development workflows. This strategy ensures you maintain the flexibility to revert or modify your approach without losing any historical data or previous work. Here's why this is often the best approach:

Git Staging Area and Working Directory

Let's consider a practical scenario to better understand these relationships:

  • Suppose you modify file.txt in your working directory:

    复制代码
    // Initial content Line 1: Hello World 
  • You then edit file.txt to add a second line:

    复制代码
    // Modified in working directory 
    Line 1: Hello World
     Line 2: New line added 
  • You stage this change:

    复制代码
    git add file.txt 
  • After staging, the changes (the addition of Line 2) are in both the staging area and the working directory.

  • If you now modify the same file again without staging the new change:

    复制代码
    // Further modified in working directory 
    Line 1: Hello World 
    Line 2: New line added 
    Line 3: Another new line added 

    The staging area still only has up to Line 2 staged. Line 3 is in the working directory but not staged.

相关推荐
aini_lovee7 小时前
python在容器内克隆拉取git私有仓库
git·python·elasticsearch
zhangphil7 小时前
git merge合并分支push报错:Your branch is ahead of ‘xxx‘ by xx commits.
git
2018_XWJ7 小时前
本地项目push到git
git
漫步企鹅8 小时前
【Git】面对发布或重要节点,Git如何打Tag?
git·tag·节点·发布
能工智人小辰8 小时前
learngitbranching git游戏笔记
git
牧野星辰19 小时前
.gitignore文件的规范
git·github
二个半engineer1 天前
GitLab Web 界面创建分支后pathspec ... did not match any file(s)
git·gitlab
尽兴-1 天前
Git 清理指南:如何从版本库中移除误提交的文件(保留本地文件)
大数据·git·gitee·gitlab
飞翔的猪猪2 天前
GitHub Recovery Codes - 用于 GitHub Two-factor authentication (2FA) 凭据丢失时登录账号
前端·git·github
顾三殇2 天前
【编译工具】(版本控制)Git + GitHub Actions:自动化工作流如何让我的开发效率提升200%?
git·自动化·github