Git memo

Stage, commit, push

https://stackoverflow.com/questions/572549/difference-between-git-add-a-and-git-add

1
2
3
git add -A # stages all changes
git commit -m "xxx"
git push

We can undo git add before commit. The following will remove file.txt from the current index (the “about to be committed” list) without changing anything else

1
git reset file.txt

To unstage all changes

1
git reset

To push a local branch to the remote repo

1
2
3
4
5
6
git push --set-upstream <remote> <branch>
git push --set-upstream origin new_branch_name

# -u is the shortcut for --set-upstream
git push -u <remote> <branch>
git push -u origin new_branch_name

To delete a remote branch

1
git push origin --delete new_branch_name

To discard or unstage uncommitted local changes, to restore working tree files

1
git restore file_name.txt

Index and the working tree

To tell Git not to track changes to your local file/folder (meaning git status won’t detect changes to it)

1
git update-index --skip-worktree xxx/yyy/file_name

There is no “recursive” option to git update-index, thus if we want to git skip worktree on all tracked files inside a directory and its subdirectories, we should first cd into that directory, then run the following command (without any modification):

1
find . -maxdepth 1 -type d \( ! -name . \) -exec bash -c "cd '{}' && pwd && git ls-files -z ${pwd} | xargs -0 git update-index --skip-worktree" \;

To tell Git to track changes to your local version once again (so you can commit the changes)

1
git update-index --no-skip-worktree xxx/yyy/file_name

Similarly, to the above operation recursively, run the following command:

1
find . -maxdepth 1 -type d \( ! -name . \) -exec bash -c "cd '{}' && pwd && git ls-files -z ${pwd} | xargs -0 git update-index --no-skip-worktree" \;

Show information about files in the index and the working tree

1
2
3
4
5
6
7
8
9
# list all files in the repo 
# (assuming we are in the root folder)
git ls-files .

# -v makes the output verbose, 
# meaning that it will abbreviate 
# the file status with a letter in 
# front of the filename
git ls-files -v .

The options (git ls-files -v .) are:

1
2
3
4
5
6
7
H cached
S skip-worktree
M unmerged
R removed/deleted
C modified/changed
K to be killed
? other

List files ignored with skip-worktree

1
2
3
4
5
# On Linux or Mac
git ls-files -v . | grep ^S

# On Windows
git ls-files -v . | findstr "^S"

You want to let git ignore deleted files? The code below works on deleted as well as modified files to ignore it when you do a git status.

1
git update-index --assume-unchanged xxx/yyy/

https://stackoverflow.com/a/3453788/7636942

查看某一个commit都改动了哪些文件

1
git show --name-only <commit_hash>

将某一个文件恢复到某次提交时的版本

1
git checkout <commit_hash> -- <file_path>

Check who (which commit, when, author) changed which lines in a file

1
git blame [file_path]

Undo the most recent commit in Git (GitHub)

https://stackoverflow.com/a/927386/7636942

1
2
3
4
git commit -m "Something terribly misguided" # (0: Your Accident)
git reset HEAD~                              # (1)
[ edit files as necessary ]                  # (2)
git push origin master --force               # (3)
  • #1 This command is responsible for the undo. It will undo your last commit while leaving your working tree (the state of your files on disk) untouched. You’ll need to add them again before you can commit them again).
  • #2 Make corrections to working tree files, for example, delete the wrongly committed files.
  • #3 To remove (not revert) a commit that has been pushed to the server, this command rewrites history on the GitHub server.

Replace git reset HEAD~ by git reset HEAD~3 to undo the 3 most recent commits.

Branching and merging

Warning: when creating a pull request on GitHub, it should alway be preferred to create a new branch dedicated for this, for example a branch called pull_request_branch. In this way, we can always commit to the master/main branch without affecting the created pull request.

Lists existing branches, the current branch will be highlighted in green and marked with an asterisk

1
2
3
4
5
6
7
8
9
10
git branch

# lists remote-tracking branches
git branch -r

# lists both local and remote branches
git branch -a

# checks tracking branches on the remote repo
git branch -vv

Creates a new branch and switch to it at the same time

1
2
git branch new_branch_name
git checkout new_branch_name

This is equivalent to

1
git checkout -b new_branch_name

Switches (back) to the master branch (It’s best to have a clean working state when one switches branches)

1
git checkout master 

Makes some changes in a new branch, then merges that branch back into the master branch

1
2
3
4
5
6
7
8
9
10
11
12
13
# creates a new branch to work on
git checkout -b new_branch_name

...
git commit -m "blablabla"

# merges the new branch 
git checkout master
git merge new_branch_name 

# deletes the new branch, because one no longer 
# needs it, the master branch points to the same place
git branch -d new_branch_name

If a branch branch_name_1 is not fully merged, git branch -d branch_name_1 raises an error which prevents this deletion. If we are sure we want to delete it, do the following

1
2
3
4
git branch -d --force branch_name_1

# -D is the shortcut for --delete --force
git branch -D branch_name_1

Shows changes

1
2
3
4
5
# between branches
git diff master new_branch_name

# between commits 
git diff 8496675381 93788e32b

Git fetch remote branch

1
2
3
4
5
# fetch a remote branch
git fetch <remote> <rbranch>:<lbranch>

# if needed, switch to the newly fetched branch 
git checkout <lbranch>

…where <rbranch> is the remote branch or source ref and <lbranch> is the as yet non-existent local branch or destination ref you want to track and which you probably want to name the same as the remote branch or source ref. If we are working with GitHub, <remote> can just be the https address that is usually used to do git clone (without any further modification).

How to create a new remote branch out of an old commit?

1
2
3
git checkout -b new_branch_name
git reset --hard <old_commit_id>
git push origin new_branch_name

How to clone a specific branch?

It seems that in some cases, only the specified branch will be cloned even if --single-branch is not used.

1
2
git clone --branch <branchname> <remote-repo>
git clone --single-branch --branch <branchname> <remote-repo>

Find the most recent common ancestor of two branches

1
git merge-base [branch1] [branch2]

这个命令会输出一个提交哈希值,这就是两个分支的最近共同祖先。

选择性地将一个commit的内容(哈希值为 [commit_hash])从一个branch复制到另一个branch

1
2
3
4
5
# 首先切换到目标分支
git checkout [target_branch]

# 然后将提交应用到目标分支
git cherry-pick [commit_hash]

cherry-pick 到新 branch 上生成的 commit 将会有一个自己独立的哈希值,只带来了原来 [commit_hash] 中的内容。

合并(squash)或编辑当前 branch 最近(N+1)个提交

1
git rebase -i HEAD~N

git rebase -i 交互模式里 commit 从上到下列出顺序提交的历史commits。可以通过修改每个 commit 前方的文本来进行操作。

  • pick 保持提交不变
  • squash 将该 commit 与前一个 commit 合并 并且合并 commit messages
  • reword 修改 commit message 但保留 commit content

N 可以适当多取一些,因为并不是必须修改其中的每一个 commit。

举例:

HEAD~3: 这里 HEAD 表示当前分支的最新 commit,HEAD~3 表示从当前 commit 回溯3个 commits,包括最新提交和最近的3个 commits(一共4个 commits)。

  • commitA (HEAD, the latest commit)
  • commitB (HEAD~1)
  • commitC (HEAD~2)
  • commitD (HEAD~3)

git rebase -i HEAD~3 进入一个交互模式 编辑从 commitDcommitA 的这些提交。该例子中假如其中一个commit(比如commitC)是一个 merge commit(commit message 为 Merge branch 'A' into B),那么git rebase -i的交互模式将展开 A branch 中的每一个commit,所以看到的 commit 行数有可能大于4。

子模块

git clone 之后,进行子模块的初始化和更新

1
git submodule init && git submodule update

Why submodules in git? It often happens that while working on one project, you need to use another project from within it. Perhaps it’s a library that a third party developed or that you’re developing separately and using in multiple parent projects. A common issue arises in these scenarios: you want to be able to treat the two projects as separate yet still be able to use one from within the other.

To add a new submodule you use the git submodule add command with the absolute or relative URL of the project you would like to start tracking. The given URL is recorded into .gitmodules. This file contains one subsection per submodule. This file is version-controlled with your other files, like your .gitignore file. It’s pushed and pulled with the rest of your project. This is how other people who clone this project know where to get the submodule projects from.

git submodule init 读取 .gitmodules 文件并在 .git/config 中添加子模块的信息。git submodule init creates a record about submodules in the .git/config file, but does not check out the contents of the submodule repositories.

git submodule update 更新和检出每一个子模块的内容:

  • 如果需要保持子模块在特定的已知状态,git submodule update 更新 submodules 到主项目中记录的特定 commit。当你运行 git submodule update 时,Git 会将子模块检出到父项目中记录的那个具体提交哈希,这确保了子模块的状态与父项目中的记录一致。
  • 如果需要最新的子模块版本,git submodule update --remote 更新 submodules 到 remote repository 特定分支的最新 commit。

如何确定父项目中记录的子模块的特定 commit 呢?

在 Git 中,子模块的目标 commit 记录其实是保存在父项目的某个特定 commit 里的。这些目标 commit 记录存储在父项目的 Git 对象中,而不是一个单独的配置文件。具体来说,父项目的每个 commit 都包含了子模块的特定 commit 哈希。当你添加一个子模块到父项目时,父项目会记录子模块在某个特定时间点的 commit 哈希。你可以通过查看父项目提交的树对象来找到子模块的 commit 哈希。

如何查看子模块的目标 commit 呢?.gitmodules 文件和 .git/config 文件并不包含子模块的具体提交哈希。可以使用 git ls-tree HEAD 查看子模块的 commit 哈希。

1
git ls-tree <HEAD/commit_hash> 

git ls-tree HEAD 用于查看指定 commit(在这种情况下是 HEAD)的树对象内容,它类似 ls命令,会列出当前路径下的文件和子文件夹。该命令的输出中,对应submodules的那些行显示的哈希值就是我们要找的子模块的具体 commit 哈希,这个哈希值对应子模块仓库的一个特定 commit。

The word tree is rather overloaded in Git (well, and, in computing in general).

  • A tree object, in Git, is an internal data structure that records a directory tree or sub-tree. It contains one entry per file or sub-directory (or, for submodules, a gitlink entry for that submodule).
  • The work-tree or working tree (or other variations of this spelling) refers to the place in which you do your work.

Checking states

Displays the state of the working directory and the staging area

1
git status 

Shows commit logs

1
2
3
4
5
# shows the current HEAD and its ancestry 
git log 

# compact mode
git log --oneline

Reference logs. git reflog doesn’t traverse HEAD’s ancestry at all. The reflog is an ordered list of the commits that HEAD has pointed to: it’s undo history for your repo. The reflog isn’t part of the repo itself (it’s stored separately to the commits themselves) and isn’t included in pushes, fetches or clones; it’s purely local.

1
git reflog

Checks the version of Git

1
git --version

To view file diff in git before commit, for a file that we haven’t git added yet

1
git diff myfile.txt

The above command does not work for files that are already git added. If we want to see already added changes

1
git diff --cached myfile.txt 

可视化不同分支之间的提交历史

1
2
3
4
5
6
7
8
9
# 不仅显示当前分支,还会包括所有本地分支的提交记录。
git log --graph --decorate --branches --oneline

# 显示所有引用的提交记录,包括本地分支、远程分支、标签以及其他引用(如 stash)。
git log --graph --decorate --all --oneline


# 仅查看当前分支的提交记录(没有意义,只查看当前分支就没必要用 --graph)
git log --graph --decorate --oneline

--oneline means oneline version.

git stash

git stash is handy if you need to quickly switch context and work on something else, but you’re mid-way through a code change and aren’t quite ready to commit. git stash 命令的作用就是将目前还不想提交的但是已经修改的内容进行保存至堆栈中,后续可以在某个分支上恢复出堆栈中的内容。stash中的内容不仅仅可以恢复到原先开发的分支,也可以恢复到其他任意指定的分支上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# stash changes including untracked files. 
git stash --include-untracked 

# removes the changes from your stash and reapplies them to your working copy. 
git stash pop

# reapply the changes to your working copy and keep them in your stash, 
# useful if you want to apply the same stashed changes to multiple branches.
git stash apply

# view all stashes
git stash list

# view the diff of a stash
git stash show

# view the full diff of a stash
git stash show -p
git stash show --patch

The git stash command takes your uncommitted changes (both staged and unstaged), saves them away for later use. The stash is local to your Git repository; stashes are not transferred to the server when you push. By default, running git stash will stash: (1) changes that have been added to your index (staged changes); (2) changes made to files that are currently tracked by Git (unstaged changes). But it will not stash: (1) new files in your working copy that have not yet been staged; (2) files that have been ignored. Adding the -u option (or --include-untracked) tells git stash to also stash your untracked files. You can include changes to ignored files as well by passing the -a option (or --all) when running git stash.

1
2
3

![git_stash.png](/blogs/assets/images/blog/git_stash.png)

You are not limited to a single stash. You can create multiple stashes, and then use git stash list to view them. By default, stashes are identified simply as a “WIP” – work in progress – on top of the branch and commit that you created the stash from. You can annotate your stashes with a description, using git stash save "message".

By default, git stash pop will re-apply the most recently created stash: stash@{0}. You can choose which stash to re-apply by doing e.g. git stash pop stash@{2}

删除在远程仓库中已不存在的分支的引用

git fetch -p-p--prune 的简写)清理本地引用列表,删除在远程仓库中已不存在的分支的引用。

Before fetching, remove any remote-tracking references that no longer exist on the remote.

Config

1
2
3
git config --list
git config --global user.name
git config --global user.email

git ignore

Ignore some files

1
vim .gitignore

Ignore some files locally

1
vim .git/info/exclude

Don’t forget Index and the working tree section.

Reset cached credentials on MacOS (password, personal access tokens, etc.)

On MacOS, we can use command line to delete existing credentials and then re-enter our new username and/or password when prompted.

To delete existing credentials, enter the following command:

1
2
3
4
git credential-osxkeychain erase
host=github.com
protocol=https
> [Press Return (twice)]

If successful, nothing will print out.

Now when we try to clone a GitHub repository (using https), we will be prompted to enter our credentials.

How to Change Your GitHub Remote Authentication from Username + Password to Personal Access Token

https://medium.com/geekculture/how-to-change-your-github-remote-authentication-from-username-password-to-personal-access-token-64e527a766cf

  • cd to the local directory, run git remote -v to check the current remote.
  • Remove the old remote. For example, run git remote remove origin to remove the origin remote.
  • Add your new remote in the following format: git remote add origin https://<TOKEN>@github.com/<USERNAME>/<REPO>.git

That’s all. This can be verified by running git remote -v

How to clone a private GitHub repo

1
git clone https://<pat>@github.com/<your account or organization>/<repo>.git

where <pat> means Personal Access Token.