简体   繁体   English

如何使用 Git 撤消推送合并?

[英]How To Undo A Pushed Merge Using Git?

I am very new to git and have recently tried to do a merge using the following sequence of commands:我对 git 很陌生,最近尝试使用以下命令序列进行合并:

git checkout master

# merge Issue#2 branch into master
git merge Issue#2

git push origin master

It looks like a fast-forward merge has been performed by default as shown in the output listing for git reflog included at the end of the question???看起来默认情况下已经执行了快进合并,如问题末尾包含的git reflog的输出列表所示??? The history shows each individual commit of the merged branch.历史记录显示了合并分支的每个单独提交。

I would like to undo the Issue2# branch merge.我想撤消 Issue2# 分支合并。 I would then like to perform a squash merge.然后我想执行一个壁球合并。

I have read that revert is better for undoing a pushed merge, so I tried the following:读过revert更适合撤消推送合并,因此我尝试了以下操作:

# revert the commits for MERGE ISSUE#2: FAST_FORWARD??
git revert -m 1 d52c603

However, it only reverts the latest commit from the branch that I would like to merge.但是,它只会从我想要合并的分支中恢复最新提交。

How can I undo the Issue2# branch merge (rollback to commit ee59444)?如何撤消 Issue2# 分支合并(回滚到提交 ee59444)? How can I then perform the merge (commits 43d090e to d52c603) with just one commit for all my check-ins?然后,我如何才能执行合并(将 43d090e 提交到 d52c603),并为我的所有签入只提交一次?

I am new to this!我是新来的! I think what I should have done is:我认为我应该做的是:

git checkout master
git merge --squash Issue#2
git commit -m "Merge message"
git push origin master

I could do??我可以??

git checkout master
git reset --hard ee59444
git push --force

git merge --squash Issue#2
git commit -m "Merge message"
git push origin master

or is it safer to use revert instead of reset ?还是使用revert而不是reset更安全?

Git Reflog Git 参考日志

e2c6058 HEAD@{0}: pull: Fast-forward
ee59444 HEAD@{1}: reset: moving to ee59444
e2c6058 HEAD@{2}: commit: Doc: Add details of Kafka Configuration default provided by test utils
6481400 HEAD@{3}: checkout: moving from master to master
6481400 HEAD@{4}: checkout: moving from Issue#2 to master
d52c603 HEAD@{5}: checkout: moving from master to Issue#2
6481400 HEAD@{6}: checkout: moving from master to master
6481400 HEAD@{7}: reset: moving to 64814001c5d6162665fe0494d95275660b6ec2e0
6481400 HEAD@{8}: checkout: moving from Issue#2 to master
d52c603 HEAD@{9}: checkout: moving from master to Issue#2
6481400 HEAD@{10}: revert: Revert "Doc(Issue#2): Add doc header explaining default config"
d52c603 HEAD@{11}: checkout: moving from master to master
d52c603 HEAD@{12}: checkout: moving from master to master


d52c603 HEAD@{13}: MERGE ISSUE#2: FAST_FORWARD


ee59444 HEAD@{14}: checkout: moving from master to master
ee59444 HEAD@{15}: checkout: moving from master to master
ee59444 HEAD@{16}: reset: moving to origin/master
c5dfc28 HEAD@{17}: checkout: moving from Issue#2 to master
d52c603 HEAD@{18}: commit: Doc(Issue#2): Add doc header explaining default config
b41e109 HEAD@{19}: commit: Testing(Issue#2): Add tests for Kafka Consumer service
2c180b7 HEAD@{20}: commit: Testing(Issue#2): Add tests for MqttKafkaBridge
129f689 HEAD@{21}: commit: Testing(Issue#2): Add skeleton unit tests for WebApp.BackgroundServices
499f566 HEAD@{22}: commit: Testing(Issue#2): Add ProducerService tests
4b6e3f1 HEAD@{23}: commit: Tests(Issue#2): Add skeleton tests for ProducerService
ea811f0 HEAD@{24}: commit: Testing(Issue#2): Add Kafka tests
2255b09 HEAD@{25}: commit: Refactor: Remove unused import
0e27f22 HEAD@{26}: commit: Docker: Update port for rest-proxy
59519d7 HEAD@{27}: commit: Test(Issue#2): Add tests for S3 service
5783a02 HEAD@{28}: commit: Test(Issue#2): Add tests for Mqtt service factory
fbac0f7 HEAD@{29}: commit: Test(Issue#2): Add unit tests for Mqtt service
a367b03 HEAD@{30}: commit: Refactor(Issue#2): Remove unused imports
a717895 HEAD@{31}: commit: Test(Issue#2): Add Mqtt factory test
6c62f38 HEAD@{32}: commit: Test(issue#2): Add Mqtt factory tests
c5dfc28 HEAD@{33}: checkout: moving from master to Issue#2
c5dfc28 HEAD@{34}: pull origin Issue#2: Fast-forward
ee59444 HEAD@{35}: pull --tags origin master: Fast-forward
142ff05 HEAD@{36}: pull --tags origin master: Fast-forward
f3a82e2 HEAD@{37}: pull --tags origin master: Fast-forward
4730534 HEAD@{38}: commit: Fix: Fix blocking signalR disposal when tab closed
8fef99c HEAD@{39}: commit: Feature: Add automated Kafka Topic Creation
085da18 HEAD@{40}: pull --tags origin master: Fast-forward
8973ef4 HEAD@{41}: commit: Add signalR logging and dispose for hub
a6c3ed3 HEAD@{42}: pull --tags origin master: Fast-forward
8079575 HEAD@{43}: pull --tags origin master: Fast-forward
babec37 HEAD@{44}: pull --tags origin master: Fast-forward
601292d HEAD@{45}: commit: Chore(Structure): Add Autofac module tests into integration tests
588433b HEAD@{46}: commit: Test: Refactor to use core test utils
c9300f1 HEAD@{47}: commit: Test: Refactor to use core test utils
48c3d1a HEAD@{48}: commit: Chore(Dependencies): Update package dependencies
112d8ab HEAD@{49}: pull --tags origin master: Fast-forward
ccc2d97 HEAD@{50}: pull --tags origin master: Fast-forward
d786256 HEAD@{51}: commit: doc(README): Add explanation of BASE_URL
3c45bb0 HEAD@{52}: pull --tags origin master: Fast-forward
bbdfa12 HEAD@{53}: commit: Refactor: Remove temp upload test proj
e653daa HEAD@{54}: commit: Fix: Add mising project reference to WebApp.S3.AutofacModule
8282f59 HEAD@{55}: commit: Refactor: Rename mqttkafka bridge project
cc437cf HEAD@{56}: commit: Refactor: Restructure mqtt and kafka config
a4cad1c HEAD@{57}: pull --tags origin master: Fast-forward

Git Log Git日志

Output of git log... git 日志的输出...

git checkout Issue#2
git log --oneline
d52c603 Doc(Issue#2): Add doc header explaining default config
b41e109 Testing(Issue#2): Add tests for Kafka Consumer service
2c180b7 Testing(Issue#2): Add tests for MqttKafkaBridge
129f689 Testing(Issue#2): Add skeleton unit tests for WebApp.BackgroundServices
499f566 Testing(Issue#2): Add ProducerService tests
4b6e3f1 Tests(Issue#2): Add skeleton tests for ProducerService
ea811f0 Testing(Issue#2): Add Kafka tests
2255b09 Refactor: Remove unused import
0e27f22 Docker: Update port for rest-proxy
59519d7 Test(Issue#2): Add tests for S3 service
5783a02 Test(Issue#2): Add tests for Mqtt service factory
fbac0f7 Test(Issue#2): Add unit tests for Mqtt service
a367b03 Refactor(Issue#2): Remove unused imports
a717895 Test(Issue#2): Add Mqtt factory test
6c62f38 Test(issue#2): Add Mqtt factory tests
c5dfc28 Rfeactor(Issue#2): Add Implementation MQTT factory
43d090e Refactor(Issue#2): Add factory and builder contracts for Mqtt

# WOULD LIKE TO ROLLBACK TO THIS COMMIT!
ee59444 Chore(Structure): Add missing test projects to solution


64d968a Test(SignalR): Add signalR unit tests
972164f Merge branch 'lenses_io' into 'master'
0a86c8b Docker(Kafka): Update kafka services to lenses.io for dev
142ff05 Test: Add Kafka config unit tests
64ccf8a Test: Add Kafka Serdes Json Deserializer tests
2b75f7f Test: Add Kafka Producer Provider unit tests
28a2281 Test(TestUtils): Add test helpers for creating Kafka config
4e1bef6 Chore(Dependency): Add Moq dependency for Kafka Unit Tests
1d84adf Test: Add Config validation tests for mqtt and S3
f3a82e2 Test(Watcher): Add test watcher tool to Kafka Unit Test Project
aed2989 Test:(Watch): Add test watcher tool to Kafka Unit Test
0f6b343 style: align item group element
53d449e Style(log): Update  signalHub logs for clarity
726ad63 doc(README): Update Kafka Config and browser compatability
aedee3d Style: Remove usage of GetAwaiter().GetResult() in docstrings
4730534 Fix: Fix blocking signalR disposal when tab closed
8fef99c Feature: Add automated Kafka Topic Creation
085da18 Style: Update log message for when connecting to secondary signalR hub
2f67e22 Refactor: Add default Kafka consumer options
609f41f Style: Update logs to explicitly state that for Kafka Consumer
c232149 Fix: Dispose secondary signalR connection when page closed
8973ef4 Add signalR logging and dispose for hub
a6c3ed3 Style: Remove unused imports
8079575 Docker(Minio): Update to v1.0.2
4285cb4 Chore(Config): Update second MqttCameraTopic
7c4dbc9 doc(README): add future features section
ce22444 Chore(Config): Update appsettings dev for default local mqtt host
53fb8a2 doc(README): Add non TLS example for MQTT settings
babec37 Tests: Add Autofac Module tests
601292d Chore(Structure): Add Autofac module tests into integration tests
588433b Test: Refactor to use core test utils
c9300f1 Test: Refactor to use core test utils
48c3d1a Chore(Dependencies): Update package dependencies
112d8ab Test: Fix Kafka ModuleTest
2ddd170 Tests: Add tests for Kafka Autofac Module
abafae5 Tests(Fix): Fix WebApp.Data.Serializers.AutofacModule.UnitTests
8eecc0d chore(sln): Add all projects to solution
38e1afc doc(README): update wih demonstration and environment details
ad49959 docker(compose): update minio to use my public dcs3spp/minio image
50c9c39 fix(link): update about link to point to moved gitlab project
b043e62 doc(README): Add HTTPS config
e044c2f Fix: Fix serializaion error
fbd135e Fix(index): Update headings for motion detection snapshot
c950b2b fix(Index): update About link to README for this page
f21307a chore(config): update Kafka config with genric camera monitor
8f4ddd7 test(data): update for stream time
81d1b6c doc: update README with architecture and links to related cam project
ccc2d97 Refactor: Add received time for motion detection
5421967 Refactor: Remove commented factory code
93ea54c Refactor: Remove redundant pages
529a3e3 Refactor: Update link for about to this repository
a29e180 Refactor: Add logger and remove Api Client
9ef3872 Reafactor: Remove Api client
d786256 doc(README): Add explanation of BASE_URL
3c45bb0 docs(README): Update README with configuration and architectural overview
5f527f0 Refactor(Data): Update Time property in MotionDetection and MotionInfo models to be long
89529da Mqtt(TLS): Update MqttService with TLS
a4299b7 Refactor(log): Add logging of topics consumed
bbdfa12 Refactor: Remove temp upload test proj
e653daa Fix: Add mising project reference to WebApp.S3.AutofacModule
8282f59 Refactor: Rename mqttkafka bridge project
cc437cf Refactor: Restructure mqtt and kafka config
a4cad1c Feature: Refactor MqttKafkaBridge background service to depend on Mqtt, KafkaProducer and S3 service
c61cb23 Refactor(MqttBridge): Update MqttBridge background service to accept producer, mqtt and s3 service
787706a Feature(Kafka): Add kafka config
7b34f14 Chore(dependency): Update Autofac dependency to v6
e580fc4 Feature(MQttService): Add Mqtt Service
63224fd MqttClient: Add project to factor out MQTTCllient
5aa7a9d Refactor: Add code behind for index page
ef61297 Fix: Motion detection image now loaded via url to controller
afc9ba4 add consumer with deserialization restored
8a6d7cc SignalR: Add deserialization back to kafka queue....leads toward schema validation
d716ab0 Style: Remove trailing space
2bb847f Feature(Serializer): Add Serializer for MotionDetection
fc331a0 Feature(Serializer): add serializer for MotionDetection
5fb53ae Feature(ApiClient): add typed api client for api
53a9e59 SignalR: Add logging to server and client
668a007 Fix: Remove Client inject from Index page
f5e6cc2 Feature(Images): Add S3 storage for images
fa4663d Refactor: Update Hub interface to accept string for now.
03c84f3 Refactor: Add signalR into consi,er service and refactor into separate classlib
3ae8ed8 Refactor: Prepend kafka header to payload
dce28ac Chore(Backup): Commit, power going off
24e3848 Refactor: Update autofac dependencies for kafka
fdde284 Refactor: Update MotionDetectionSerializer to be generic type
0ecc15a Chore: Backup for electric going off
d230508 Feat: Add background service
54b9d61 Refactor: Add MqttListenerWorker project
6b00f4c Chore(DI): Add WebApp.Repository autofact dependencies
35a0a97 Chore(DI): Add Autofac module for WebApp.Data.Serializers
800c6f8 Refactor: Add Autofac Module for Serialization
b8772f2 Test(JsonParser): Add overflow value for testing TryGetDouble as false
1af7c47 Test(JsonVisitor): Add test for missing StartArray tag in json
b757bee Chore(VSCode): Add tasks
6e38430 Refactor: Remove Default motion detector serializer
3bf3a5c Chore(Test): Ignore CoverageReports folder
14c9367 Tests: Add test for deserializing from null stream
83502f6 Test: Add integration tests for JsonVisitor
babba07 Refactor: Add new WebApp.Testing.Mocks classlib
a6781c2 Test(JsonSerializerWrapper): Add JsonSerializerWrapper integration tests
391a433 Refactor: Add JsonSerializerWrapper and interface into separate file
458186a Test(JsonParser): Add tests for JsonParser
de53251 Refactor(MockJsonParser): Update to track exceptions thorwn for properties
5cf3938 Refactor: Update JsonVisitor to accept injectable JsonParser
73ba2ad chore(tools): add tool manifest for reportgenerator
1cf67aa Test: Add integration tests
d582904 Refactor(structure): update repository contracts project
0363795 Refactor: Add contacts for serialization
028382c chore(structure): refactor test structure
734e975 Style(Comments): remove redundant comments
ba13c54 Test(ToString): add tests for ToString for models
d574ab5 Test(ParsedMotionLocation): Add tests
8eb6b83 Test(ParsedMotionInfo): Refactor test names to reflect MotionInfo
f2b7863 test(ParsedMotionInfo): complete MotionInfo tests
2d9144a Test: Update visitor mock with reason person
df878d9 test(ParsedMotionDetection): completed tests for ParsedMotionDetection
6b4a635 Test(ParsedMotionDetection): Update ParsedMotionDetection_Accept_Calls_IVisitor_DeserializeMotionDetection with assertion of r call count of deserialize motion detection
be835d9 Tests(ParsedMotionInfo): Add unit tests
e5c4a23 Tes(ParsedMotionDetection): Add tests for ParsedMotionDetection
bf2cdb5 Refactor: Add constructor DI for JsonSerializerWrapper
6640a6e refactor: remove unused using statements
4dd2ed9 Test: Add unit tests for custom converters
5cf565d Refactor(DI): Add IParsed to converters to facilitate testing
94cb5d9 Refactor: remove unused using statement
83a233c Refactor(DI): Update for DI
2d1c16a Test(MotionDetectionRepository): Add tests for ArgumentNullException
ea6b298 Test(MotionLocationTest): Add GetHashCode tests
7460d7e Test(MotionLocation): Add != tests
778a4ee Test(MotionLocation): Add == tests
dc39c35 Test(MotionLocation): Update test names
eb41b2c Test(MotionLocation): Add tests for IEquatable Equals
0103cd8 Test(MotionLocation): Add Object Equals tests
89061d9 Test(MotionLocation): Add tests for copy constructor
d3ecf51 Test(MotionLocation): Add property constructor tests
f20395e Test(WebApp.Data.UnitTests): Update namespace
ffdb5ee Test(MotionInfo): Add GetHashCode tests
4e5996d Test(MotionDetectionTest): Update test names to reflect test correctly
a88412f Test(MotionInfo): Add == and != operator tests
9d194d1 Test(MotionInfo): Remove redundant IEquatable test
3a97cd4 Tests(MotionInfo): Add MotionIo IEquatable Equals tests
d548d93 Test(MotionInfo): Add object.Equal unit tests
4d0db06 Test(MotionInfo): Add constructor tests
e10b8cd Test(MotionDetection): Add test for null in copy constructor
8ec505e TesT(Projects): Add watch tool
5d35443 Test(MotionDetection): Add MotionDetection unit tests
9bc4b43 Fix: Update equals to return false if at least one attribute not equal
aa3caff Test(MotionDetection): Add tests for IEquatable Equals
d903eb6 Refactor structure
ef60e30 Initial Commit

TL;DR TL; 博士

Your proposed sequence:您建议的顺序:

git checkout master
git merge --squash Issue#2
git commit -m "Merge message"
git push origin master

was right.是正确的。 Your proposed repair would work.您提议的维修会起作用。 It's a good idea to know why , though.不过,知道原因是个好主意。

Long

I have read that revert is better for undoing a pushed merge ...我已经读过, revert更适合撤消推送合并......

"Better" is in the eye (or screen?) of the beholder. “更好”是旁观者的眼睛(或屏幕?)。 But you're being bitten by a simple fact here: what Git calls a fast-forward merge is not actually a merge at all.但是这里有一个简单的事实让您感到困惑:Git 所谓的快进合并实际上根本不是合并 A simple, single-commit revert won't undo it.简单的单次提交还原不会撤消它。

How can I undo the Issue# branch merge (rollback to commit ee59444)?如何撤消 Issue# 分支合并(回滚到提交 ee59444)? How can I then perform the merge (commits 43d090e to d52c603) with just one commit for all my check-ins?然后,我如何才能执行合并(将 43d090e 提交到 d52c603),并为我的所有签入只提交一次?

First, be sure you really want to do this (and to bother with it).首先,确保你真的想这样做(并为此烦恼)。 Some people hate messy commit history and argue that it's always worth cleaning everything up.有些人讨厌凌乱的提交历史,并认为清理一切总是值得的。 Other people argue that once you make a commit, you should never go back and fix anything that was wrong with it: you should just add new commits to repair the problem, so that the history is 100% accurate and includes every mistake ever.其他人认为,一旦你提交了,你不应该回去修复它的任何错误:你应该添加新的提交来修复问题,这样历史记录是 100% 准确的,并且包括所有错误。 I don't buy either extreme: I say that some mistakes are worth cleaning up so that nobody has to see them, and some aren't.我不相信任何一个极端:我说有些错误值得清理,这样没有人必须看到它们,有些则不需要。 Of course, it's then a value judgment about which ones to clean up.当然,接下来是对清理哪些进行价值判断。

Let's assume here that you do want to clean this all up (otherwise, you are done now, you just move on).让我们在这里假设您确实想要清理这一切(否则,您现在就完成了,您只需继续前进)。 The trick now becomes how you clean up.现在的窍门变成了如何清理。 The important part here, though, is not just doing the cleanup itself, but also understanding how this works and therefore what could go wrong.然而,这里的重要部分不仅是清理本身,还要了解它是如何工作的,因此可能会出现什么问题。

(This is going to be a bit long, but I will try to be brief and go fast.) (这会有点长,但我会尽量简短快速。)

Git is all about commits , so, what precisely is a commit? Git 是关于commits 的,那么,commit 究竟是什么?

Those new to Git often think it's about files or branches.那些刚接触 Git 的人通常认为它是关于文件或分支的。 It's not.它不是。 It's about commits .这是关于commits It's true that a commit contains files, and Git finds a commit using a branch name , but ultimately, everything is all about the commits.确实,提交包含文件,Git 使用分支名称查找提交,但最终,一切都与提交有关。 So it's crucial to understand what a commit is and does for you.因此,了解提交是什么以及为您做什么是至关重要的。

The first thing to know is that commits are numbered.首先要知道的是,提交是有编号的。 They're not simple counting numbers: instead, they are those hash IDs you see, like d52c603 and ee59444 .它们不是简单的计数:相反,它们是您看到的那些哈希 ID,例如d52c603ee59444 (These are actually abbreviated—the actual numbers are much bigger. They're expressed in hexadecimal, hence the letters a through f as well as digits.) These numbers look random, but are not; (这些实际上是缩写的——实际数字要大得多。它们以十六进制表示,因此字母af以及数字。)这些数字看起来是随机的,但不是; in fact, the values are arranged so that every Git everywhere will agree that commit ee59444 is numbered ee59444 , and no other commit can have this number (ever—that's why they're so big).事实上,这些值的排列方式是让每个地方的每个Git同意提交ee59444的编号为ee59444 ,并且没有其他提交可以有这个编号(永远——这就是它们如此大的原因)。 The number is computed by looking at the complete contents of the commit.该数字是通过查看提交的完整内容来计算的。 This means that no part of any commit can ever be changed, either: if you changed it, it would have a different number.这意味着任何提交的任何部分都不能更改,或者:如果您更改它,它将具有不同的编号。

In fact, you can take a commit out, work with it, and write back a new commit that's almost, but not completely, the same.事实上,你可以取出一个提交,使用它,然后写回一个几乎但不完全相同的新提交。 That gives you a new commit with a new (different) number.这会给你一个新的(不同的)编号的新提交。 The existing commit remains in your repository.现有提交保留在您的存储库中。 So all Git ever really does is add new commits.所以 Git 真正做的就是添加新的提交。 Existing commits remain—at least for some time.现有的提交仍然存在——至少在一段时间内。 But if you can't find the number of a commit, you'll never see it again, and eventually Git will notice that nobody can find it, and drop it from the database.但是如果你找不到提交的编号,你将永远不会再看到它,最终 Git 会注意到没有人可以找到它,并将它从数据库中删除。

As this suggests, all commits—in fact, all of Git's internal objects—are stored in a big database.正如这表明的那样,所有提交——实际上,所有 Git 的内部对象——都存储在一个大数据库中。 It's indexed by the object numbers.它由对象编号索引。 You just give Git the number, and Git pulls the object out of the database and makes it usable.你只需给 Git 编号,Git 就会从数据库中提取对象并使其可用。 Inside the database, it's in a form that only Git itself can use (and is read-only).在数据库内部,它采用只有 Git 本身可以使用的形式(并且是只读的)。 So now we can look at what's inside a commit—what Git can see and extract for us.所以,现在我们可以看看在提交-什么混帐可以看到和提取物对我们里面有什么。 There are two parts:有两个部分:

  • First, a commit contains a full snapshot of every file (that Git knew about at the time you, or whoever, made the commit, that is).首先,提交包含每个文件完整快照(即在您或任何人进行提交时 Git 知道的)。 The files inside commits are stored using this same big database, and they're kept in a compressed, read-only, Git-only format with content de-duplication.提交中的文件使用同一个大数据库存储,并且它们以压缩的、只读的、仅限 Git 的格式保存,内容重复数据删除。 So the fact that many commits in a row keep re-using most of the existing files means that those commits don't actually need any extra space: they literally re-use the existing files.因此,连续多次提交继续重复使用大部分现有文件的事实意味着这些提交实际上不需要任何额外空间:它们实际上是重复使用现有文件。 It's safe to do this because nothing put into the database can ever change.这样做是安全的,因为放入数据库的任何内容都不会改变。

  • Besides the data—the snapshot—each commit contains metadata , or information about the commit itself: who made it, when, and so on.除了数据——快照——每个提交都包含元数据,或关于提交本身的信息:谁做的,什么时候做的,等等。 Crucial to the operation of Git, each commit's metadata contains the hash ID of an earlier commit .对于 Git 的操作至关重要,每个提交的元数据都包含较早提交的哈希 ID。 More precisely, each commit contains zero or more hash IDs, which Git calls the parent hash IDs .更准确地说,每个提交都包含零个或多个哈希 ID,Git 将其称为父哈希 ID Most commits have just one parent.大多数提交只有一个父级。 At least one commit—the very first one someone made for the repository—has no parent, because there was no previous commit.至少一个提交——某人为存储库所做的第一个提交——没有父提交,因为之前没有提交。 Some commits have two or more parents, which makes them merge commits , but we won't look at this here.有些提交有两个或更多的父提交,这使得它们合并提交,但我们不会在这里看这个。

What this means is that when you have a bunch of commits, all in a row—which is what you have—they form a simple linear chain, with each commit pointing backwards to its immediate predecessor:这意味着当你有一堆提交时,所有的提交都排成一行——这就是你所拥有的——它们形成一个简单的线性链,每个提交都向后指向它的前一个:

... <-F <-G <-H

Here H stands in for the actual hash ID of the last commit in the chain.这里H代表链中最后一次提交的实际哈希 ID。 If you write down this hash ID, you can use it to have Git find commit H .如果你记下这个哈希 ID,你可以用它让 Git 找到提交H Inside commit H we have the data—the snapshot of all of your files—and the metadata showing when you made it and so on.在 commit H我们有数据——所有文件的快照——以及显示何时创建的元数据等等。 In the metadata, Git can find the hash ID of earlier commit G , which lets Git find commit G .在元数据中,Git 可以找到较早提交G的哈希 ID,这让 Git找到提交G

If we compare the snapshot in G to the snapshot in H , we'll see what you changed , going from G to H .如果我们将G中的快照与H的快照进行比较,我们将看到您更改了什么,从GH That's what git log -p or git show shows.这就是git log -pgit show显示的内容。

Of course, we can have Git use G to find the hash ID of earlier commit F , too.当然,我们也可以让 Git 使用G来查找较早提交F的哈希 ID。 So now Git can show what changed between F and G .所以现在 Git 可以显示FG之间的变化 And, Git can go on to the parent of F (presumably E ), and so on, all the way back to the beginning of time.而且,Git 可以继续到F的父级(大概是E ),依此类推,一直回到时间的开始。

Branch names hold one hash ID分支名称持有一个哈希 ID

To do all of the above, we had to jot down the hash ID of commit H —the last commit in the chain.要完成上述所有操作,我们必须记下提交H的哈希 ID——链中的最后一个提交。 Why should we bother to do that, when we have a computer?当我们有电脑时,我们为什么要费心去做呢? Why not have Git save that hash ID somewhere?为什么不让Git将哈希 ID 保存在某个地方?

This is exactly what a branch name does.这正是分支名称的作用。 A branch name like master just holds one hash ID: the hash ID of the commit that we should call "last in the chain".master这样的分支名称只包含一个哈希 ID:我们应该称之为“链中最后一个”的提交的哈希 ID。 In this case, let's have master hold hash ID H , like this:在这种情况下,让我们让master持有哈希 ID H ,如下所示:

...--F--G--H   <-- master

I've stopped drawing the arrows between commits as arrows, partly from laziness and partly because they literally can't change: the "arrow" from H to G is part of commit H and no commit can ever change.我已经停止在提交之间绘制箭头作为箭头,部分原因是懒惰,部分原因是它们实际上无法改变:从HG的“箭头”是提交H一部分,任何提交都不会改变。 That's not true for branch names: we can stuff a new hash ID into them any time.对于分支名称,情况并非如此:我们可以随时将新的哈希 ID 塞入其中。

Let's create a new name, now, so that we have two branch names, both of which select commit H :现在让我们创建一个新名称,以便我们有两个分支名称,它们都选择 commit H

...--F--G--H   <-- master, issue

Note that whichever name we use, we get commit H .请注意,无论我们使用哪个名称,我们都会得到 commit H But we'd like to know which name we're using anyway.但是我们想知道我们使用的是哪个名称。 So let's have Git attach the special name HEAD to one of the two branch names:因此,让 Git 将特殊名称HEAD附加到两个分支名称之一:

...--F--G--H   <-- master (HEAD), issue

This means we're using the name master to find commit H .这意味着我们使用名称master来查找提交H If we now run:如果我们现在运行:

git checkout issue

we get:我们得到:

...--F--G--H   <-- master, issue (HEAD)

We're still using commit H , but now we're using the name issue to find it.我们仍在使用 commit H ,但现在我们使用名称issue来找到它。

Making new commits进行新的提交

Now, let's make a whole bunch of new commits.现在,让我们进行一大堆新的提交。 The first new commit we make, we'll change a file or two, run git add , and run git commit , and Git will write out a whole new snapshot—reusing most files from H —with the appropriate metadata.我们进行的第一个新提交,我们将更改一两个文件,运行git add ,然后运行git commit ,Git 将写出一个全新的快照——重用来自H大多数文件——以及适当的元数据。 The parent of the new commit will be existing commit H ;新提交的父项将是现有提交H we'll call the hash Id of the new commit I .我们将调用新提交的哈希 Id I Having created commit I , Git will do its little trick: it writes the hash ID I into the current branch name , so that we get:创建提交I ,Git 会做它的小把戏:它将哈希 ID I写入当前分支名称,以便我们得到:

...--F--G--H   <-- master
            \
             I   <-- issue (HEAD)

As we make more new commits, each one points backwards to the current commit, so that the chain grows:随着我们进行更多的新提交,每个提交都向后指向当前提交,从而使链增长:

...--F--G--H   <-- master
            \
             I--J--K   <-- issue (HEAD)

The name HEAD remains attached to the name issue , but the commit selected by the name issue changes.HEAD保持连接的名称issue ,但提交名称为选定issue的变化。

We now have two branches: master ends at commit H , and therefore includes all commits up to and including H , and issue ends at commit K , and therefore includes all commits up to and including K .我们现在有两个分支: master在提交H处结束,因此包括直到并包括H所有提交,并且issue在提交K处结束,因此包括所有提交直到并包括K That means issue includes commit H too: commits H and all earlier commits are on both branches.这意味着issue包括提交H :提交H并且所有较早的提交都在两个分支上。 1 1


1 This fact, that commits are on more than one branch, is unusual: other version control systems declare that one a commit is made, that commit is on that branch, forever. 1提交在多个分支上这一事实是不寻常的:其他版本控制系统声明一个提交已提交,该提交永远在该分支上。 Git doesn't work this way. Git 不是这样工作的。 Git is weird.吉特很奇怪。


Fast-forward快进

Let's look at what you did next.让我们看看你接下来做了什么。 You ran:你跑了:

git checkout master

which got you this:这让你得到了这个:

...--F--G--H   <-- master (HEAD)
            \
             I--J--K   <-- issue

Git took all the files out of H and made them available to you, and made master the current name . Git 将所有文件从H取出并提供给您,并将master设为当前名称 You are now on branch master .您现在在分支master

Next, you ran:接下来,你运行:

git merge issue

This looked at the name issue , found that it identified commit K , and figured out that commit K was a direct descendant of commit H .这查看了名称issue ,发现它标识了提交K ,并发现提交K是提交H的直接后代。 This allowed Git to cheat.这允许 Git 作弊。 Instead of doing a real merge—which involves a lot of work—Git could simply check out the last commit on issue and drag the name master forward , like this:而不是做一个真正的合并,它涉及到很多的工作的Git可以简单地检查了最后一次提交的issue并拖动名master前进,就像这样:

...--F--G--H
            \
             I--J--K   <-- master (HEAD), issue

There's no reason to draw the kink in the graph any more, but I left it in to make it obvious how only the branch name moved.没有理由再在图中绘制扭结了,但我将其保留下来,以便清楚地表明只有分支名称是如何移动的。

What you'd like to have你想要什么

We already noted that you cannot change any existing commits.我们已经注意到您不能更改任何现有的提交。 So commits IJK are stuck the way they are.所以提交IJK被卡住了。 But what if you could copy all the files from commit K , and use them to make a new commit that comes right after commit H ?但是,如果您可以从提交K复制所有文件,并使用它们在提交H之后立即进行新提交,该怎么办? Let's call the new commit S and draw it in:让我们调用新的提交S并将其绘制:

             S   <-- ???
            /
...--F--G--H   <-- ???
            \
             I--J--K   <-- master (HEAD), issue

I've put in two ???我放了两个??? names too, because we're going to need to have some sort of name by which to find commit H , so that we can do the work.名称也是如此,因为我们将需要某种名称来查找提交H ,以便我们可以完成工作。 Then, having made S , we'll need some name to select commit S so that we can find it.然后,创建S ,我们需要一些名称来选择提交S以便我们可以找到它。

What name should we use?我们应该使用什么名字? Well, we could just create a new branch name right now:好吧,我们现在可以创建一个新的分支名称:

git branch redo <hash>

where we put in the hash ID of commit H .我们把提交H的哈希 ID 放在那里。 We can find this hash ID using git log , or whatever other command we like that finds it;我们可以使用git log或任何其他我们喜欢的命令找到这个哈希 ID; the git branch command will then make the new branch name:然后git branch命令将创建新的分支名称:

...--F--G--H   <-- redo
            \
             I--J--K   <-- master (HEAD), issue

From here, we can figure out how to make commit S .从这里,我们可以弄清楚如何提交S But we'd have to check out the name redo first, too.但是我们也必须首先检查名称redo Anyway, redo is not really the name we want here.无论如何, redo并不是我们真正想要的名称。 What if we force the name master to point to H ?如果我们强制名称master指向H怎样?

Moving a branch name: git reset移动分支名称: git reset

Moving a branch name is always possible.始终可以移动分支名称。 It's just a little tricky sometimes.只是有时有点棘手。

If we weren't using the name master right now, we could do this with git branch --force .如果我们现在不使用名称master ,我们可以使用git branch --force来做到这一点。 But because we are using master —we have our HEAD attached to it—we can't do this right now.但是因为我们正在使用master我们有我们的HEAD附加到它 - 我们现在不能这样做。 We could move HEAD over to some other name, such as issue , and do it with git branch --force .我们可以将HEAD移到其他名称,例如issue ,并使用git branch --force Or, we can use git reset .或者,我们可以使用git reset

Git's reset is, unfortunately, a horribly complicated command.不幸的是,Git 的reset是一个极其复杂的命令。 2 It has many modes. 2它有多种模式。 We'll only look at one of them right now, git reset --hard , so that we don't have to get into all the details of Git's index .我们现在只看其中一个, git reset --hard ,这样我们就不必深入了解 Git 的index 的所有细节。 This leaves out the easiest quick-recipe solution, which is using git reset --soft , but we'll come back to that.这遗漏了最简单的快速食谱解决方案,即使用git reset --soft ,但我们会回到那个。

Remember that git reset --hard wipes out all uncommitted work.请记住, git reset --hard清除所有未提交的工作。 Be careful with git reset --hard !小心git reset --hard Assuming you have no uncommitted work, though, let's see what running git reset --hard hash-of-H would do.不过,假设您没有未提交的工作,让我们看看运行git reset --hard hash-of-H会做什么。 It would result in what we can draw like this:这将导致我们可以这样绘制:

...--F--G--H   <-- master (HEAD)
            \
             I--J--K   <-- issue

This is, in effect, completely undoing your git merge that did a fast-forward.实际上,这完全撤消了进行快进的git merge 3 3

Note that we didn't change anything inside the commits (we can't!).请注意,我们没有更改提交中的任何内容(我们不能!)。 We didn't change anything except the hash ID stored in the name master —or rather, nothing inside the repository itself.除了存储在名称master哈希 ID之外,我们没有更改任何内容——或者更确切地说,存储库本身内没有任何内容。 We had Git wipe out our current checkout and replace it with the stuff from commit H , too, though: that's the --hard part of git reset --hard .不过,我们让 Git 清除了我们当前的结帐,并将其替换为来自 commit H的内容:那是git reset --hard--hard部分。


2 I find git reset as bad as git checkout . 2我发现git resetgit checkout一样糟糕。 In Git 2.23, the existing git checkout got split into two separate commands, git switch and git restore , each of which did about half the work.在 Git 2.23 中,现有的git checkout被拆分为两个单独的命令, git switchgit restore ,每个命令完成了大约一半的工作。 (But git checkout remains, and still does all the work.) I think much the same should happen for git reset . (但git checkout仍然存在,并且仍然完成所有工作。)我认为git reset应该发生同样的事情。 But that's another topic.但那是另一个话题。

3 That is, it undoes the git merge in your repository . 3也就是说,它会撤消存储库中git merge If you've sent updates to other Git repositories, using git push , it does not change those other repositories.如果您已使用git push将更新发送到其他Git 存储库,则不会更改这些其他存储库。 This is where the whole "revert is better" concept comes from in the first place: if you are going to undo things, you need to make sure nobody else can redo them behind your back.这就是整个“还原更好”概念的最初来源:如果您要撤消某事,则需要确保没有其他人可以在背后重做。 Revert doesn't really undo anything at all. Revert 并没有真正撤消任何事情。 Instead, it adds a new commit , so that the existing commits all remain for everyone to see.相反,它添加了一个新的提交,以便现有的提交都保留给所有人看。 But, again, that's another topic.但是,这又是另一个话题了。


Now that we're back at square one, let's make S现在我们回到了第一个广场,让我们制作S

The git merge --squash command is the one that makes commit S . git merge --squash命令是使提交S命令。 We run:我们跑:

git merge --squash issue

now, while we have:现在,虽然我们有:

...--F--G--H   <-- master (HEAD)
            \
             I--J--K   <-- issue

As before, this git merge locates commit K .和以前一样,这个git merge定位提交K Commit K would allow a fast-forward, not-a-merge operation, but --squash tells git merge not to do that.提交K将允许快进,非合并操作,但--squash告诉git merge不要这样做。 So git merge goes through all the work it would normally do, to make a real, normal merge.所以git merge完成了它通常会做的所有工作,以进行真正的、正常的合并。

If we hadn't used --squash , and had forced Git to make a normal merge, we would get this graph:如果我们没有使用--squash ,并迫使混帐做一个正常的合并,我们将得到此图:

...--F--G--H---------M   <-- master (HEAD)
            \       /
             I--J--K   <-- issue

Commit M would be one of those special merge commits , with two parents. Commit M将是那些具有两个父级的特殊合并提交之一。 Its first parent would be the commit we had out a moment ago: commit H .它的第一个父节点是我们刚才的提交:commit H Its second parent would be commit K , as found by the name issue .它的第二个父项将是 commit K ,如名称issue

The snapshot for merge M would be built by comparing commit H to commit H , which finds no changes at all, and comparing commit H to commit K , to see what changed on that branch.合并M快照将通过比较提交H和提交H来构建,提交H根本没有发现任何更改,并将提交H与提交K进行比较,以查看该分支上的更改。 The result would of course just be the changes required to make commit H 's snapshot match commit K 's snapshot.结果当然只是使提交H的快照匹配提交K的快照所需的更改。 Git would then add these changes to the snapshot in H . Git 然后会将这些更改添加到H的快照。 That gives a new snapshot that exactly matches commit K 's snapshot.这给出了一个与提交K的快照完全匹配的新快照。 That snapshot becomes the contents of new merge commit M .该快照成为新合并提交M的内容。

The end result would be a new commit M that contains the same files as commit K .最终结果将是一个新的提交M ,其中包含提交K相同的文件 This new merge commit would have two parents, and would be a real merge.这个新的合并提交将有两个父项,并且将是一个真正的合并。

The --squash option, however, stops Git from commit M .然而, --squash选项会阻止 Git 提交M Git still goes through all the merge work , to come up with the snapshot. Git 仍然会完成所有合并工作,以得出快照。 Of course, that snapshot is the one in commit K : the work is kind of silly, but Git does it anyway.当然,那个快照是提交K中的快照:这项工作有点傻,但 Git 还是做了。 Then, git merge --squash stops , as if you had used the --no-commit .然后, git merge --squash停止,就像您使用了--no-commit

To finish the squashing, you run:完成挤压,请运行:

git commit

and Git makes a new non-merge commit S :并且 Git 进行了一个新的非合并提交S

...--F--G--H---------S   <-- master (HEAD)
            \
             I--J--K   <-- issue

Commit S contains the same snapshot that commit M would have contained, if we'd had git merge do a real merge.如果我们让git merge进行真正的合并,则提交S包含与提交M相同的快照。

Making things easier: git reset --soft让事情变得更简单: git reset --soft

Here, let me first observe that, like any version control system, Git has to copy files out of commits, so that you have ordinary read/write files that you can use.在这里,让我首先观察到,与任何版本控制系统一样,Git 必须从提交中复制文件,以便您拥有可以使用的普通读/写文件。 The committed files are in a form that nothing else can really use, and are read-only, so they're no good for getting any actual work done.提交的文件是一种其他任何东西都无法真正使用的形式,并且是只读的,因此它们不利于完成任何实际工作 Git therefore extracts each commit's files as you check out that commit.因此,当您检出该提交时,Git 会提取每个提交的文件。

Now, instead of git reset --hard , git merge --squash , and git commit , we can use git reset --soft and git commit .现在,而不是git reset --hardgit merge --squash ,和git commit ,我们可以使用git reset --softgit commit But how and why does this work?但是,这是如何以及为什么起作用的? What you need to know here is that Git makes new commits, not from what is in your work-tree, but rather from Git's index .这里你需要知道的是 Git 进行新的提交,不是来自你的工作树中的内容,而是来自 Git 的index The index is a complicated little thing that, alas, Git eventually forces you to know all about.索引是一个复杂的小东西,唉,Git 最终会迫使你了解所有的信息。 It's also rather oddly named.它的名字也很奇怪。

Git's index is also called the staging area , which refers to how one uses it. Git 的索引也称为staging area ,指的是如何使用它。 What the index contains—most of the time, anyway—is your proposed next commit .无论如何,索引包含的大部分时间是您提议的下一次提交 This consists initially of all the files taken from the current commit , stored in Git's internal, compressed and de-duplicated format.这最初包括从当前提交中获取的所有文件,以 Git 的内部、压缩和重复数据删除格式存储。

Note that these files, like all parts of a commit, are in fact read-only.请注意,这些文件与提交的所有部分一样,实际上都是只读的。 Being de-duplicated, they take no actual space , but it's easiest to think of them as "copies".进行重复数据删除后,它们不占用实际空间,但最容易将它们视为“副本”。 What's special about the index copy of the files from a commit is that you can replace any one of these files.提交中文件的索引副本的特别之处在于您可以替换这些文件中的任何一个。 That's what git add generally does.这就是git add通常所做的。 You run git add file and Git reads the work-tree copy of the given file, compresses it into the internal Git format, and replaces the current (shared) index copy with a new (or shared, if appropriate) file, ready to go into the next commit.您运行git add file并且 Git 读取给定文件的工作树副本,将其压缩为内部 Git 格式,并将当前(共享)索引副本替换为新(或共享,如果合适)文件,准备就绪进入下一次提交。

When we ran git reset --hard , we used the --hard flag to tell Git: Replace all the work-tree and index file copies .当我们运行git reset --hard ,我们使用--hard标志告诉 Git:替换所有工作树和索引文件副本 That is, we had Git change both our work-tree copies of each file, and its index copy, to match the commit we moved to.也就是说,我们让 Git 更改每个文件的工作树副本及其索引副本,以匹配我们移动到的提交。

With --mixed , we can tell Git: Replace the index copies, but leave the work-tree copies alone.使用--mixed ,我们可以告诉 Git:替换索引副本,但保留工作树副本。 That doesn't really help us here, though there are cases where it makes sense.这在这里并没有真正帮助我们,尽管在某些情况下它是有意义的。 But with --soft , we can tell Git: Do not replace either of the copies.但是使用--soft ,我们可以告诉 Git:不要替换任何一个副本。 Keep your index copies untouched, and leave my work-tree copies alone.保持你的索引副本不变,不要管我的工作树副本。

If we start with:如果我们开始:

...--F--G--H
            \
             I--J--K   <-- master (HEAD), issue

so that Git's index and our work-tree are full of the files from commit K , and run:这样 Git 的索引和我们的工作树就充满了来自提交K的文件,然后运行:

git reset --soft <hash-of-H>

we end up with the name master identifying commit H , but Git's index and our work-tree having the files from commit K .我们最终以名称master标识提交H ,但 Git 的索引和我们的工作树包含来自提交K文件 If we now run:如果我们现在运行:

git commit

and make a new commit, we get:并进行新的提交,我们得到:

...--F--G--H--L   <-- master (HEAD)
            \
             I--J--K   <-- issue

where commit L has the same files in it as commit K .其中 commit L与 commit K具有相同的文件

So, in this case, git reset --soft hash && git commit is the shortest path to where we want to arrive.因此,在这种情况下, git reset --soft hash && git commit是我们想要到达的最短路径。

Distributed Git and git push分布式 Git 和git push

When you have some some set of commits, you often want to pass those commits on to some other Git running some other Git repository.当您有一些提交时,您通常希望将这些提交传递给运行其他 Git 存储库的其他 Git。 To do that, you can use git push .为此,您可以使用git push

The way git push works is that your Git calls up the other Git and starts offering it commits. git push工作方式是你的 Git 调用另一个 Git 并开始提供它的提交。 Your Git offers those commits by hash ID.您的 Git 通过哈希 ID 提供这些提交。 Their Git either says I have that ID already —which tells your Git that they have that commit , because the IDs are unique, but also that they have every previous commit , because Gits always add commits to the end—or it says please send it .他们的 Git 要么说我已经有了那个ID——这告诉你的 Git 他们有那个提交,因为 ID 是唯一的,而且他们有以前的每一次提交,因为 Git 总是在最后添加提交——或者它说请发送它. If your Git has to send the commit, your Git offers that commit's parents, too, so that their Git will get all the commits that lead up to that as the last commit.如果您的 Git 必须发送提交,您的 Git 也会提供该提交的父项,以便他们的 Git 将获得所有提交作为最后一次提交。

For that to be the last commit, then, your Git has to ask their Git to set one of their repository's branch names, so that it stores that hash ID.为了使该提交成为最后一次提交,您的 Git 必须要求他们的 Git 设置存储库的分支名称之一,以便它存储该哈希 ID。 So when you run:所以当你运行时:

git push origin master

you're telling your Git: Look up the name origin to find a URL.你告诉你的 Git:查找名称origin以找到一个 URL。 Call up a Git at that URL.在该 URL 调用 Git。 Hand over the commit hash ID that is the tip of my master , and see what commits they need.交出作为我的master提示的提交哈希 ID,看看他们需要什么提交。 Then, ask them to set their master to that hash ID.然后,让他们将他们的master设置为该哈希 ID。

The other Git will generally take any new commits that just add on, so if you have:另一个 Git 通常会接受任何刚刚添加的提交,所以如果你有:

...--F--G--H--S   <-- master (HEAD)
            \
             I--J--K   <-- issue

and run git push origin master , and they have:并运行git push origin master他们有:

...--F--G--H   <-- master

in their repository, 4 they will take the new commit and update their master .他们的存储库中, 4 他们将接受新的提交并更新他们的master But if they have:但如果他们有:

...--F--G--H--I--J--K   <-- master

in their repository, and you told them to make their repository have:在他们的存储库中,您告诉他们让他们的存储库具有:

...--F--G--H--S   <-- master
            \
             I--J--K   <-- ???

in their repository, they would balk at this.在他们的存储库中,他们会对此犹豫不决。 Their Git would tell your Git that this operation is not a fast-forward , which in this case means it removes commits IJK from access via master .他们的 Git 会告诉你的 Git 这个操作not a fast-forward ,在这种情况下,这意味着它从通过master访问中删除提交IJK If their master points to S and S 's parent is H , there's no way to go back from S to K , so they "lose" commit K .如果它们的master指向S并且S的父节点是H ,则无法从S返回到K ,因此它们“丢失”了提交K (This is true even if they have some other name by which to find K .) (即使他们有其他名称可以找到K 。)

Hence, when you're asking the other Git to remove some commits, you have to use a more forceful kind of git push .因此,当你要求另一个 Git删除一些提交时,你必须使用一种更有力的git push You can use --force , where you just send them a command: Set your master !您可以使用--force ,您只需向他们发送一个命令:设置您的master Or, you can use --force-with-lease , which has your Git send them a conditional command: I think your master points to ______.或者,您可以使用--force-with-lease ,它让您的 Git 向他们发送条件命令:我认为您的master指向 ______。 If so, make it point to ______ instead.如果是这样,请改为指向______。 Let me know what happened.让我知道发生了什么。 Your Git fills in the blanks, using a mechanism we haven't described properly here, involving your own Git repository's remote-tracking names .您的 Git 使用我们在此处未正确描述的机制来填补空白,包括您自己的 Git 存储库的远程跟踪名称 This makes --force-with-lease safer if someone else might be adding commits to the other Git repository.如果其他人可能将提交添加到另一个 Git 存储库,这会使--force-with-lease更安全。

Either way, though, at least one kind of force operation will be required to get their Git to drop commits from their repository.不管怎样,至少需要一种强制操作来让他们的 Git 从他们的存储库中删除提交。


4When looking at someone else's Git repository, we generally don't need to care so much about where their HEAD might be. 4当查看别人的 Git 存储库时,我们通常不需要太关心他们的HEAD可能在哪里。 There are exceptions to this rule, but that's why I left HEAD off here.这条规则也有例外,但这就是我把HEAD留在这里的原因。


Git is a tool-set, not a solution Git 是一个工具集,而不是一个解决方案

One of the big takeaways from all of this is that Git does not try to solve just one particular problem.所有这一切的重要收获之一是 Git 并不会试图解决一个特定的问题。 Instead, Git provides a bunch of programmable tools.相反,Git 提供了一堆可编程工具。 Each tool does something.每个工具都做一些事情。 Some of these things are useful on their own, but some are useful in combination with something else.其中一些东西本身很有用,但有些东西与其他东西结合起来很有用。 Some are only useful in combination, and some do a small job that could stand alone, or could be used to do a bigger job.有些在组合使用时才有用,有些则可以单独完成一项小工作,或者可以用来完成更大的工作。

The other is that, in the end, Git is all about the commits .另一个是,最终,Git 是关于commits 的 The commits hold files, and form chains.提交保存文件,并形成链。 The branch names let you and Git find the commits.分支名称可让您和 Git找到提交。 Git mostly works backwards: the names find a "last" commit and from there, Git works backwards, along the chains. Git 主要向后工作:名称找到“最后”提交,然后,Git 沿着链向后工作。 The hash IDs are big and ugly and hard for people to use, but are how Git finds the commits.哈希 ID 又大又难看,人们很难使用,但它是 Git 查找提交的方式。

git checkout master
git reset --hard <commit>
git push --force

Squash commit using rebase interactive:使用 rebase 交互压缩提交:

git checkout Issue#2
git rebase -i master

Interactive mode will offer you command to apply on the branch commit like squash, fixup, reword...交互模式将为您提供在分支提交上应用的命令,如压缩、修复、改写...

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM