展示 HN:有效的 Git
Show HN: Effective Git

原始链接: https://github.com/nolasoft/okgit

## Git 最佳实践:总结 本文档概述了使用 Git 的最佳实践,强调其核心功能:有效跟踪更改。虽然功能强大,但 Git 需要贡献者(开发人员)注意细节,以避免混乱的历史记录。清晰的历史记录有助于理解、学习和代码审查。 该指南分为基础、中级和高级实践。基础部分涵盖了必要的命令,如 `git status`(了解仓库状态)、`git commit`(附带清晰的消息)、`git fetch`(更新远程引用)和 `git diff`(查看更改)。中级主题包括安全的开发流程,如频繁提交和推送,以及使用特性分支。 高级技术,如使用 `rebase` 重写历史记录,不建议在共享分支上使用,但在本地可能很有用。关键概念包括提交作为补丁、分支作为对提交的引用,以及避免强制更新的重要性。像 `git worktree` 这样的工具可以并行处理多个分支。 最终,持续应用这些实践——以及测试和编译——可以最大限度地减少错误,并确保协作、高效的开发流程。建议查阅官方 Git 文档 ([https://git-scm.com/book/en/v2](https://git-scm.com/book/en/v2)) 以获得更深入的理解。

Hacker News 新闻 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 展示 HN:高效 Git (github.com/nolasoft) 8 分,由 nola-a 1小时前发布 | 隐藏 | 过去 | 收藏 | 1 条评论 随着我们许多人从软件工程师转变为软件经理,以正确的方式跟踪更改变得越来越重要。 现在是时候真正理解和掌握 Git 了。 帮助 tomtom1337 0分钟前 [–] 我推荐使用 git switch 代替 checkout,因为 checkout 命令过于复杂。并且使用 restore 代替 checkout 来恢复更改。回复 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

This document wants to be a reference for best practices in the daily use of Git, despite the complexity of this tool it is worth mentioning that Git is a tool that tracks the changes in a repository (which is basically a directory containing a .git folder that has all the needed metadata to achieve file tracking).

Some concepts are stressed by the author for a simple reason: The main goal of Git is tracking the changes and years of experience have shown that Git is very good at that. However, this is not for free, it requires some attention by Individual Contributors (the engineers who commit changes, henceforth referred to as IC).

The history of a Git repository shows all the contributions and if ICs don’t follow some simple rules, it can easily turn into a mess, losing the primary goal: Tracking changes effectively.

Moreover, having a good history allows for some tasks, like understanding what happened, learning how to implement something and helping the reviewers with Pull Requests.

Git, like other tools, was designed to allow several ICs to work together efficiently.

This document is divided into items, which are designed to explain the pros and cons of every method.

To get the best out of this guide a little knowledge of command line tools is needed (also Git) so the reader, who is not proficient with those is strongly invited to read https://git-scm.com/book/en/v2 (especially the chapters 1. Getting Started, 2. Git Basics, and Git Branching).

Three categories are presented: Basic, intermediate and advanced, while the first two show safe practices the last one shows how to rewrite the history, which is forbidden when the branch is shared with other ICs but it can be safe when working alone (e. g. local branches).

A very quick introduction to how a Git repo works: Code contributions are grouped into commits, but what is a commit? It is just a patch https://en.wikipedia.org/wiki/Patch_(computing) whose hash function (currently sha1sum but with ongoing discussions to upgrade it) is referred to as the commit ID.

The history can be seen as a series of commit IDs, each of them brings changes to the source code: This structure is clear and safe to all its users and guarantees a strict policy against unwanted changes.

A branch is a reference to a specific commit ID and therefore to all its ancestor commits. ICs are free to decide whether to tag or not a specific commit (usually for tracking a release version). Scheme

Item 1: what's happening: git status

Git status is the most powerful and simple command that shows what is happening in the repo. But it does more than that; in fact, it suggests what you can do hinting any possible command. Let's look at the examples below to see the most common scenarios:

~/repos/testrepo$ git status
On branch develop
Your branch is up to date with 'origin/develop'.

nothing to commit, working tree clean

There is some changes in the local branch

~/repos/testrepo$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
       	modified:   new.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)
       	images/simplerepo.png

no changes added to commit (use "git add" and/or "git commit -a")

Git is telling us that on branch master:

  • Some changes in the file new.md are ready to be staged.
  • There is a new file called simplerepo.png that can be added.

There are some updates in the remote branch.

~/repos/testrepo$ git status
On branch develop
Your branch is behind 'origin/develop' by 2 commits, and can be fast-forwarded.
  (use "git pull" to update your local branch)

nothing to commit, working tree clean

Git is telling us that the branch develop which is tracking the remote branch origin/develop is behind by 2 commits. That is, some IC has added its own contributions, C3 and C4.

Scheme

The current reference develop can be fast-forwarded in order to point to C4 with:

or

$ git merge origin/develop

The command git pull is a shorthand for git fetch && git merge origin/develop -> see item 3 for insights on git fetch

Item 2: how to commit: git commit

When all the changes are steady and the tests have passed (e.g., mvn clean install is OK), it is time to stage the changes, to do that

for each snippet Git prompts you different choices asking whether the change can be added or not.

A well formatted commit message should contain:

  • Title of the commit and if available the ticket ID
  • A blank line
  • A brief summary of the changes that occurred and meaningful comments for helping future reviews

Item 3: references update: git fetch

Since Git is a distributed version control this means that more copies of the same repository could exist (ideally one for IC plus one as central). One of them will be considered as the central repository (the one called origin), therefore the repo contains references to both local and remote branches; in order to update local references to the remote branch you need to use git fetch:

$ git fetch
~/repos/testrepo$ git fetch
remote: Counting objects: 200, done.
remote: Compressing objects: 100% (139/139), done.
remote: Total 200 (delta 76), reused 3 (delta 1)
Receiving objects: 100% (200/200), 34.46 KiB | 1008.00 KiB/s, done.
Resolving deltas: 100% (76/76), completed with 21 local objects.
From https://foobar.com.com/scm/path/testrepo
   1ffa340..71b62d8  feature/feature1-develop -> origin/feature1-develop
   e59d286..e3ffe3c  feature/feature2-develop -> origin/feature2-develop

It is worth pointing out that fetch doesn't update the tracked files, only the references get updated. It is always safe to run git fetch. In the above example we noticed that two branches received updates in the syntax: oldsha..newsha localbranch -> remote tracked branch

Item 4: differences between references

Anytime you can check on the differences between two references using git diff, remember that a Git reference can be a TAG, a commit ID or a branch name

$ git fetch
$ git diff # show the contributions that are not yet in staged area
$ git diff HEAD 1.0.0 # show differences between local branch and tag 1.0.0
$ git diff develop origin/develop # show differences between local branch and tag 1.0.0
$ git diff develop origin/master # show differences between local branch and remote master

The output uses the Unified Diff Format (also supported by standard programs diff and patch):

~/repos/gitcourse$ git diff 008323949d8fe0af977af8980e5f3c0d9d0c6b07 f2de6c316b2ad361ede73461d25c260642e87108
diff --git a/README.md b/README.md
index 5e8fe7e..c101e79 100644
--- a/README.md
+++ b/README.md
@@ -52,11 +52,11 @@
 #### Advanced Commands
 - git reflog
 - git reset --hard origin/master
-- git reset 'commit'
+- git reset <commit>

Item 5: keep safe your contributions

Let's suppose that you are working on a long term task that could take several weeks to be done, there are a bunch of reasons to commit and push very often:

  • To prevent loosing your work in case of PC failures
  • It is easier to share contributions between ICs
  • The reviewers can get on with the work
  • Safely pause a task, switch to another, resume it later

Item 6: the simplest Git workflow: feature branch

Let's say that we are asked to work on a new feature called "find button", and that our staring point is the current state of the "develop" branch:

$ git checkout develop
$ git pull # just to be sure that our branch is updated with remote
$ git checkout -b feature-findbutton # create our feature branch
$ git push --set-upstream origin feature-findbutton # push our newly created branch to remote

Implementing the feature day 1

$ git fetch && git merge origin/develop # take contributions done by other developers from mainline and fix conflicts
$ vim file1 # edit file
$ git commit -am 'C1' # commit our changes
$ git push origin feature-findbutton # push our changes so interesed parties can start reviewing our work

Implementing the feature day N

$ git fetch && git merge origin/develop
$ vim file1
$ git commit -am 'CN'
$ git push origin feature-findbutton

And finally, when the feature is ready (optional)

$ git checkout develop
$ git merge --squash --ff-only feature-findbutton # merge our changes into the branch 'develop'
$ git commit -m 'Merge pull request #feature-findbutton'
$ git push origin develop # push the new changes made to the develop branch

The previous commands are not necessary when working with the Pull Request feature available in tools like bitbucket, github, gitlab, and others. In this case one ore more reviewers must accept the changes before those can (almost automatically) be integrated into the development branch.

Item 7: my push was rejected, what can I do?

~/repos/testrepo$ git push
To https://foobar.com.com/scm/user/testrepo.git
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'https://foobar.com/scm/user/testrepo.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

as usual Git is telling us why the updates were rejected

Scheme

Another IC pushed to remote the commit C3 and now we are trying to push our commit C4. The remote repository is expecting to also have the changes that C3 brought in before. In this case we have two options: We can follow what Git suggests (see above) or we can use the Git rebase command. The main difference between these two actions is that Git merge brings to the tree a merge commit which represents how the merge was done (sometimes it is done just for tracking purposes)

$ git pull # or git fetch && git merge origin/develop
$ git push

Scheme

Looking from CM to left, the history contains all commits including the merge commit

$ git rebase origin/develop
$ git push

Scheme

Unlike merge, a rebase rewrites the current history putting our commit as the last commit. Any previous commit will be rehashed, resulting in a history change.

Item 8: a dry run approach to git merge

Sometimes you would want to check the results of a merge without actually doing it. This can be achieved by creating a temporary branch and merging it for testing purpose only. If you push this branch remotely, you can share it with other ICs.

Suppose the target branch is develop and the feature branch is feature1

$ git checkout feature1
$ git checkout develop
$ git checkout -b develop-try-merge
$ git checkout merge feature1

Once the merge is done, the resulting outcome will be:

$ git checkout develop
$ git checkout merge develop-try-merge

Even though this approach might appear complex, just consider this use case: You are in the middle of a long term merge that requires 2 days to complete, by using this strategy, you can pause the merging task at anytime, do something else (e.g. facing a production issue), and resume the merging task later.

Item 9: how to approach conflicts

Sometimes merging and rebasing end up with conflicts, meaning that two commits are trying to change the same line and Git is not able to merge them properly (by applying the default merge strategy), so it is asking the IC to manually solve the conflict. Again, we can use the git status command to list the files affected by conflicts. Once all the conflicts are resolved, we might proceed compiling and testing the resulting source code.

~/repos/testrepo$ git pull
Auto-merging Minor fix
CONFLICT (content): Merge conflict in Minor fix
Automatic merge failed; fix conflicts and then commit the result.
~/repos/testrepo$ git status
On branch master
Your branch and 'origin/master' have diverged,
and have 1 and 5 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)

You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Changes to be committed:
       	new file:   file1.txt
       	new file:   file2.txt

Unmerged paths:
(use "git add <file>..." to mark resolution)
     	both modified:   file3.txt

In fact, opening file3.txt, you will find this content:

~/repos/testrepo$ cat file3.txt
<<<<<<< HEAD

this line 1
=======
this line 1
this line 2
this line 3
>>>>>>> ab8106e6b692ff14d5b6fa345021c7e61ceeefda

Once file3.txt is edited, just run (as suggested above by git status):

$ git add file3.txt
$ git commit

Item 10: why sometimes fast-forward is better

The picture below depicts why we chose a fast forward merge strategy when a feature branch is going to be merged into mainline develop. If the feature branch is merged with step 6 then all the contributions contained in feature1 were not tested against what C4 has brought. To prevent that one, step 7 is the right approach in fact feature1 can be merged only if no new contributions are committed in develop since the last merge of develop into feature1

Scheme

Item 11: restore a repo to a specific commit: the right way

Let's suppose our repo Scheme

And we want to restore the repo to the C2 commit using the following commands:

$ git rm -rf .         # includes hidden files (e.g., .gitignore, .env, etc.)
$ git checkout C2 . #note the dot at the end of git checkout
$ git commit -m 'Restore to C2'

Scheme

There are two main reasons to go for 'the right way' :

  • The history remains consistent (no rewriting)
  • All the changes are stored in the same commit that could easily be reverted by:

Item 12: spot a problem: force updated

~/repos/pippo11$ git fetch
remote: Counting objects: 9, done.
remote: Compressing objects: 100% (9/9), done.
remote: Total 9 (delta 6), reused 0 (delta 0)
Unpacking objects: 100% (9/9), 962 bytes | 7.00 KiB/s, done.
From https://foobar.com.com/scm/user/testrepo
 + 38b166c...c2b0abe master     -> origin/master  (forced update)

The last line indicates that there was an update on the master branch, the big issue is that the update was forced which means the history of that branch was rewritten. Generally origin repo is configured to avoid this kind of situations on important branches such as master/develop, but it is likely to happen on short-lived feature branches. It is important to remember that each time someone rewrites the history it is compromised for the others to commit on the same branch, which cannot be done anymore (unless resetting the branch version from the beginning)

Item 13: update last commit: git amend

Let's suppose you just committed (you haven't pushed yet) and then you remember you left something out, it is time to amend:

Keep in mind that its usage only applies to the local commits, in all of the other cases you're going to rewrite the Git history causing serious issues to the other commiters.

Item 14: rebuild history: rebase

We already saw this function in action on the item 6, now we can add something more to what we already know. Rebasing is the main feature offered by Git for rewriting history:

$ git checkout existing_feature_branch
$ git rebase develop

In a few words, we asked Git to rewind the history of that current branch to the first common ancestor commit and only then all the commits of the current branch will be applied. In case of unsuccessful rebase Git will ask us to solve the eventual conflicts (as usual Git hints you the resolution commands which are the same as the merge conflicts)

Item 15: rebuild history #2: interactive rebase

Git gives you more options rebasing, the most important one is the interactive rebase, which consists in a rebase with the opportunity to rewrite history for example melting multiple commits into a single one, deleting or editing specific commits which results in a more compact and clear history.

Item 16: Git insights: git reflog

Git tracks all commands executed on the local repository. All the operations can be shown by the reflog command

~/repos/core-services-cmlt-v1$ git reflog
5d80c9b (HEAD -> foobar, origin/develop, develop) HEAD@{0}: checkout: moving from develop to foobar
5d80c9b (HEAD -> foobar, origin/develop, develop) HEAD@{1}: pull: Fast-forward
bac5fe9 (origin/feature/feature22) HEAD@{2}: reset: moving to origin/develop
e80b719 HEAD@{3}: commit: Blabla
7c4bc6c HEAD@{4}: pull: Fast-forward
62b994f (tag: v1.0.0-rc2) HEAD@{5}: pull: Fast-forward
2e90498 HEAD@{6}: commit (amend): Minor fixes
8650e50 HEAD@{7}: rebase (continue) (finish): returning to refs/heads/develop
8650e50 HEAD@{8}: rebase (continue): Minor fixes
6fa2786 HEAD@{9}: rebase (start): checkout origin/develop
96a9246 HEAD@{10}: commit (amend): Minor fixes
0cd6399 HEAD@{11}: commit: Minor fixes
4c72dcf HEAD@{12}: pull: Fast-forward

It is also possible restoring the local repo to a specific point for example:

$ git reset --hard e80b719

Item 17: Git workflow: git worktrees

The Git worktree allows you to work simultaneously in many branches of the same repository. Imagine being in the middle of a new feature you want to add to your project, but suddendly a bug comes out, which needs to be fixed asap. Most of the people would stash or commit their changes to check out later to the affected branch. This is where the git worktree comes in, instead of stashing your changes (which you could forget about...) you can simply add a worktree and work from there, without doing anything else. The command to add a new worktree tracking the supposed feature branch feature/M1 is:

$ git worktree add featureM1 --track -b feature/M1

where featureM1 will be the name of the folder containing the repository freshly checked out at the feature/M1 branch. To remove a tree simply type:

$ git worktree remove featureM1

to list every tree present in the current directory:

Applying all of the previously described rules and testing/compiling correctly your local work might prevent introducing bugs/issues into the main branch. Whenever the code won't compile or the tests will break, it means that there is something wrong on your local branch. Always double check before asking for help or claiming bugs in develop.

联系我们 contact @ memedata.com