Like you, I felt much more comfortable using git after learning this.
Yeah, completely unreachable commits have 30 days, even when you run git gc. The default reflog for a branch is even longer: 90 days!
I often try to reassure people new to git that "if you'll just commit often, there's basically no way you can lose work so that I can't help you get it back. Apart from deleting the whole repo folder. Don't delete the repo folder.".
I also have the habit of preventing non-fast-forward pushes on origin/master, which also helps when I can tell my team that they can't trash the origin even if they try.
# Merge commit
git merge --no-ff -m <message> <hash>
# Squash and merge
git merge --no-commit --squash <hash>
git commit -m <message>
# Rebase and merge
git rebase --force-rebase <hash>
From https://stackoverflow.com/a/52301456I believe in clean, linear history, and strongly prefer rebase-based workflows to merges. That's actually one of the reasons I chose Phabricator for my current place, as it is also very opinionated towards the same way of working.
Edit: oh, and to answer your actual question, the third one.
Years ago when I was just reading about git instead of using it, I saw sentiments along the lines of "always use feature branches and merge them so your thoughts and process can still be looked at later". In the last ~5 years or so I've worked professionally, I've not once wished I could reference intermediate commits in my own code or someone else's. I've found that ambiguities and clarifications can and are caught during the code review process.
I'd say another couple of benefits:
- It's relatively easy to teach git workflows when the log/graph is linear, and similarly it's _way_ easier to reason about your workspace when you have an actual production codebase.
- Merge commits can make certain operations like reverts and patches harder to reason about
One command I often use is git blame which allows me to find the commit that's associated with a particular line of code. Then I can look at the commit message and the diff against its parent. Perhaps what I'm changing may undo a bugfix and I wouldn't have realized it without reading the associated commit message.
When I look back at the history, I'm only interested in seeing the changes that actually made it through, not every intermediate alley and dead end in between. If those dead ends are significant discoveries/results, I document them elsewhere.
But either way works fine. It just gives you a different history. My team likes merging because they don't understand exactly what happens when rebasing. In that environment `git log --topo-order` is practically a necessity, though.
If you are merging master into a feature branch that has ongoing work and continues without merging back to master, that's the problem.
If your feature branch is short-lived, it can be easily rebased.
If your other branch is more like a release branch, with lots of work that can't be rebased easily, some times you can't really avoid a merge from master without communicating it first. If your team is large or distributed it might not be practical to say "release has moved to (rebased ref), please catch up"
In that case you should treat merges to release the same as merges to master (they should be finished bits of work that are considered published) and any unmerged features for the release, are kept on feature branches that are based on the release. They can be rebased after the point where master is merged back into release to avoid the nasty merge conflicts.
git merge —-squash <tree-ish>
When merging into the baseline git merge —-no-ff <tree-ish>
The reason for the latter is subtle. Yes, a perfectly linear history is nice in the aesthetic sense. However, the merge commits are artifacts of reviews that are useful in process audits, which QA and QC like to preserve.They are still in the repo, but if no treeish item (eg. a branch) points to them, then they'll eventually get garbage collected.
Still, glad to see people are trying to elucidate git rebase. A small subset of its functionality is fundamental part of my workflow and I wouldn't know how I'd use Git without rebasing.
>git gc tries very hard not to delete objects that are referenced anywhere in your repository. In particular, it will keep not only objects referenced by your current set of branches and tags, but also objects referenced by the index, remote-tracking branches, refs saved by git filter-branch in refs/original/, or reflogs (which may reference commits in branches that were later amended or rewound). If you are expecting some objects to be deleted and they aren’t, check all of those locations and decide whether it makes sense in your case to remove those references.
Unfortunately, the reflog is confusing and hard to use correctly in the case of an interactive rebase with multiple steps. It is hard to figure out exactly how far back you need to go in the reflog to get to moment before the rebase started if you want to start over. It also just so happens that its when an interactive rebase goes awry that I really want to reach for the reflog to fix the damage.
It is rather unfortunate that there is no convenient documented shorthand for "show me the reflog of the current branch" (`git log -g` gives you HEAD's reflog). That said, after a bunch of experimentation, it seems like `git log -g '@{0}'` will give you the reflog for the current branch. Apparently this works because e.g. `git log -g '@{2}'` gives you the reflog for the current branch skipping the first 2 elements.
When you run git reflog after rebasing, you will see lines like the following:
29d82ac HEAD@{6}: rebase -i (finish): returning to refs/heads/your-branch
29d82ac HEAD@{7}: rebase -i (fixup): Commit message 2
4f8e996 HEAD@{8}: rebase -i (pick): Commit message 2
f3a954e HEAD@{9}: rebase -i (pick): Commit message 1
f74b8a5 HEAD@{10}: rebase -i (start): checkout origin/master
The line listed after the one that has rebase -i (start) is the commit you were on before you started the rebase. If I screw up a rebase, then I will stash any uncommitted changes and run a git reset --hard to the commit listed below the rebase -i (start) commit I see in the reflog and start the rebase again.I see the stash as kind of like a private remote, in that I can freely put whatever messy or experimental or half-baked WIP I like, gaining the benefits of a commit without inflicting it on anyone else.
When you turn on Recyclable Commits, every commit in the reflog shows up in the history tree just like any other commit. You can see exactly where they diverge from your other branches and can work with them as you normally work with any commit.
Same thing for stashes: check one and it just shows up as part of the commit tree as if it were a normal commit.
I've used SmartGit for years and highly recommend it over the Git command line for the way it gives you so much more insight into the state of your repo.
"Pretending the average user will know how to get things back to how they are is silly."
The reflog is faaar more complicated to use that any of the day-to-day git commands.
Getting folks to use it would be very difficult due to network effects however.
After having rewritten history locally (with Mercurial this is easy, powerful, safe and with a lot of tooling available) everything you need to do is doing an hg push --force.
For one, hg histedit with its curses interface (default since 4.9) is sweet.
In reality, git is actually far from ideal tool, with its own weak points and use-case scenarios which it simply not or badly supports. So it's never "just you" don't feel bad that it's "hard." On top of this, even the scenarios that it supposedly "good" supports demand sometimes totally "illogical" combination of the names and parameters.
git is used in spite of its flaws for different reasons. Some actions are really very fast, faster than by the competition. Sometimes that is a reason enough. Another is -- we have to use what our colleagues use. Even another: once you become familiar with it, even if you were aware of the weirdness, it can stop annoying you. Still to be able to realistically compare it with something else, you have to at least try it. And that something else too.
Anyway, once you get rid of the actual complexity in git, it's an easy step to work on the added complexity to the UI.
Linus wouldn't claim to be a brilliant designer of user interfaces. It's totally conceivable that somebody could come along and develop a new way of talking about Git's functionality, implemented by a new "porcelain". Changing the vocabulary, creating a more comprehensible map of the internal logic of the thing...
https://www.git-scm.com/docs/gitrevisions#Documentation/gitr...
git branch local/foo
If you mess up too hard, check it out again.Why else are you going to go through the trouble of rebasing master if this isn't the goal you're shooting for? I'm a big proponent of commit history hygiene but even I can't defend rebasing master except for egregious things.
I think the only time I rebased master except for this was to fix a poorly executed mass file rename that broke git annotate.
$ git co feature-branch
$ git branch before-rebase-feature-branch
$ git rebase develop> HEAD names the commit on which you based the changes in the working tree. FETCH_HEAD records the branch which you fetched from a remote repository with your last git fetch invocation. ORIG_HEAD is created by commands that move your HEAD in a drastic way, to record the position of the HEAD before their operation, so that you can easily change the tip of the branch back to the state before you ran them.
https://www.git-scm.com/docs/gitrevisions#Documentation/gitr...
The same changes are still in the repo (edit: I should have said branch here), but not the same commits, because the parents and children change and therefore the hash of the commits.
It is very important to be aware that the history is changed, because the previous history can not be without issues merged with the new one, which is the main pain point and the most problems that arise from a rebase.
Each commit has a link to its parent, and represents the tip of a linked list. .git/objects is a heap of all commits (and other objects), and .git/refs contains a list commit IDs that define each head (e.g. master). git rebase will often introduce new versions of a commit to the heap and update the heads to reference new histories, but the old commits stick around and can be accessed through the reflog - with their full original history intact.
A good UI should allow you to recover from mistakes. Like the trashcan vs rm example everyone is using.
It's good that git doesn't permanently delete stuff, it's bad that you need to be a relative expert to know that. If git branch showed rebased branches and told you they would disappear in x days then beginners might feel less fear and embrace the power of git faster.
git rebase has an "abort" feature when you need to redo it. git has tag & branch & stash features if you want to save what you're doing before you rebase. The problem with keeping and showing rebased branches are that 1- you don't need them after the rebase is successful. You only need them when the rebase is going badly, and 2- you'd have a lot of unnecessary noise pile up. I often rebase multiple times before every push. I don't want to see them all, you probably don't either.
That said, I fully agree that git's UI could be better and help beginners feel less fear!
They kinda are, but that's like saying that deleted files are not deleted, but stick around for a while.
While technically true, for most practical purposes _rm <file>_ deletes the file. The fact that each and every "git 101" manual has to explain how to recover deleted commits, means something is wrong.
It's like saying: "Here's the key, and in case it doesn't work there's a pry bar in the garage". This is usually a pretty good indicator that the lock is broken.
Someone who has never done it before won’t know how to retrieve them, but that’s hardly a surprise.
Git has a very good safety net when you know how to use it. The problem is knowing how to use it, not that it’s not there.
It’s a legitimate point that git’s UI sucks, that’s what’s wrong, and everyone agrees. But learn how to use the reflog and you will see the light!
I don’t understand what you mean. Why would you rebase a branch and then merge the rebased version with the old version of that branch? I don’t think that’s a common workflow, that is a specific case you should avoid.
Typical use of rebase to clean up the unpushed portion of a personal repo before pushing doesn’t normally cause any problems. The main thing that pops up for me is trying to re-order my own dependent commits when I forget that one commit touched the same code as another. Squashing them during the rebase is the easy fix.
> If you've made a mistake and in so doing lost commits which you needed, then git reflog is here to save the day.
Seeing that sentence in the article immediately makes me think the author has poor git practices or even understanding. reflog is useful at times, yes. But it is not what saves beginners from losing work while practicing their rebasing skills. What saves them is that they didn't rebase an important branch. They made a temp branch, pointing at the HEAD of their important branch perhaps, and they screwed up the temp branch.
If people would just make this one thing clearer to beginners, it would help a lot of people learn git more easily and with less fear.
In your local repo yes, for next 30 days. Then git will garbage collect them. But when you force push new pointers to GitHub, the GitHub repo will lose access to the unreferenced commits right away.
But of course those 30 days will give you plenty of time to go back, if you changed your mind about the rebase.
It creates new commits and moves the branch to the tip of the new sequence of commits. No existing commits are changed or deleted.
- Summarizing history: squashing "implemented subfeature A.A" and "implemented subfeature A.B" into "implemented feature A"
- Rewriting history: moving commits around, changing the base commit, and so on
In my opinion summarizing history is acceptable, you're making a creative decision that certain information will not be useful in the review/when trying to understand the code in the future.
Rewriting, on the other hand, is essentially lying. You're creating repository states that never existed, and which you have never tested. In the worst case, consider the following history:
* F: (master) Merge branch 'component2'
|\
| * E: (component2) Fixed component 2's integration with 1
| * D: (component2) Merge branch 'master' into component2
| |\
| |/
|/|
* | C: (master) Refactored component 1's API
| * B: (component2) Implemented component 2 that depends on 1
|/
* A: (master) Base
Yes, it could probably be completely linearized, but that would be a horrible idea. Commit B will leave the repository in a completely nonsensical state. Sure, you could squash in E to mitigate it (since, luckily, nothing else happened in component 2 in the meantime), but then you're still stuck explaining what will likely look like a bunch of really weird design decisions compared to if you had designed against component 1's new API immediately.Historical context matters. If in doubt, don't rebase. Never `git pull --rebase` blindly.
No one is advocating that you use rebase on your public branches or basically any branch that has been "published". We are talking about feature branches or spikes or branches that exist just on one developer's machine.
Whether you've published the true history earlier is irrelevant to that discussion.
If I make three commits and then realize that I should have included something in the first commit, I use rebase to create a new sequence of three commits that has the corrected version of the first commit. I haven't shared those commits with anyone, this is just work that I've done locally.
Are you seriously advocating that creating a pull request with: (A, B, C, A-fixup) is better than using rebase and then creating a pull request with: (better-A, B, C)?
You think that second case is "lying" because I didn't show the intermediate step that included the mistake?
You can mitigate most of the damage if it is convincing enough (for example, go through B' and C' and make sure everything still makes sense at each point), but realistically nobody is going to do that, because it's pretty inefficient way to spend your time. And even then, you're still removing context (unless you're just fixing a typo).
> I'm advocating that you use rebase to improve the quality of your changes that will be reviewed before merging or even before being reviewed at all.
That was clear from the start. But the fact that X breaks Y doesn't imply that Y is a good idea when X doesn't apply.
Us mere humans make mistakes all the time. Typos, omissions, false starts, and so on. What is the value of throwing that raw set of events at a reviewer or complicating the understanding of the changes when viewed in retrospect from the future? What is the reason you call curating the work into a more polished form "lying"? Why do you think the time spent being intentional about changes isn't valuable when compared to the time spent by a reviewer (or your future self) to sort through the flotsam and jetsam of your intermediate work?
I rather review the final patch series with changes in logical order and not necessarily in the order the code was written or with intermediate work that was later reverted or changed again. I do look at commits, because also every commit message counts and is supposed to explain the individual change.
The messy reality is valuable, when talking with your teammates about how the work was done and what kind of bumps were along the way. It's not about looking over their shoulders, it's about using data to develop together as a team, eliminating hinderances, etc - if you have the mutual trust to do that. And of course you yourself can go back and look for patterns of mistakes or problematic areas in code based on your history.
Like I said, to judge the change its, the whole PR diff is usually the most useful unit of inspection when you just want to see what happens. And if it's a big pr, you can of course always merge child PR's or branches against the big PR/branch, and look at the merge diffs.
The science principle of publishing your experiments, including failed ones, has the same benefits in sw engineering: others can build on your failed attempts, or save time by not replicating them.
You can do it in a way that isn't harmful (as I mentioned earlier), but good luck getting a team to actually stick to that. It also doesn't help that pretty much no tooling encourages doing it properly.
I made a typo when writing this reply, and pressed backspace to correct it. Is use of the backspace key lying?
I think you're placing a value on "history" that doesn't map onto all users of "rebase", or E-Mail client drafts. A lot of advanced users use it as the equivalent of "save" in an editor, sharing all those intermediate states is more noise than value v.s. crafting a sensible patch once you figure out what you want/what change to make.
The true problems begin once you start creating commits that represent repository trees that you never tested or reviewed, for example by editing past commits (invalidating any testing you've done of commits after that point), deleting past commits (aside from squashing an unbroken sequence of commits, or deleting them if the squash would result in a no-op), reordering commits, or rebasing commits.
But yeah, the history you push to a canonical branch should generally be made up of commits that have all been tested in isolation. The rebase command doesn't make this worse, but better, e.g. with "rebase -i --exec='make test'".
I also prune out history of some false steps taken. Have you never written a program and done something like "I'll use a hash here <save><compile><test>, no actually a list makes more sense <save><compile><test> ...". Those intermediate steps are commits for a lot of advanced git users.
Sharing all your mistakes-as-you-go-along with the world doesn't help anyone, I'd typically be sending you a 100 patch merge request for some rather trivial change instead of 1-3 sensible commits.
Of course you do! And it's not am inefficient use of your time, because it helps reviewers now, and yourself when you're bisecting later.
> And even then, you're still removing context (unless you're just fixing a typo).
You place that context in the commit message.
And how do you detect that you forgot / was too busy to do it, when you go back 6 months later? It's fragile, "fail-open".
https://github.com/tianocore/tianocore.github.io/wiki/Laszlo...
Say upstream is at A.
I clone it in my local work-space, and make a few commits over the course of a few days. So my local is A B C
During this time other changes have been merged into upstream, so upstream looks like A D E
I now have two options. I can try to merge from upstream or rebase off of upstream. Merging introduces a messy commit history that quickly becomes difficult to follow. Rebasing removes my local commits, applies the changes in upstream, and then re-applies my local commits.
So after rebasing my local is A D E B C. There are no messy merge commits. And ideally, I can squash my local changes into a single feature commit, so upstream ends up incredibly tidy.
At no place in this process is there any dishonesty or lying. I haven't changed the history upstream, which is the source of truth. What's the issue here?
No, your local is now A D E B' C'. Commits aren't just a diff between two tree snapshots, they are tree snapshots.
Hopefully you test and sanity check C' before submitting for review, but it's very unlikely that you're going to give B' the same treatment, making it more difficult for people to understand the history in the future (as well as breaking `git bisect`).
And even if you do, are your coworkers going to? Consistently? No CI tool that I'm aware of will enforce this for you.
> There are no messy merge commits.
No, but the underlying messy workflow is still there. You've just swept it under the rug for the sake of aesthetics, at the cost of future comprehension.
> At no place in this process is there any dishonesty or lying. I haven't changed the history upstream, which is the source of truth. What's the issue here?
Those are completely orthogonal concerns. You're presenting a false version of the repository state.
The common mantra of "don't rewrite public history" is about not creating a mess of duplicate commits, it doesn't imply that rewriting history is fine as long as it's not public.
Your commit history is just a somewhat arbitrary recording of your code at certain points in time that you choose. Rebasing simply makes that less arbitrary, allowing you to document the way your code is built up in a structured way. Rather than having to decide on the spot whenever a certain combination of code is a good candidate for a single, atomic commit, you can make that judgment with the benefit of hindsight.
Yes. A failed attempt is still a useful signal that people shouldn't try to simplify back to that way in the future (and why not). It's also a useful starting point in case the reasons it failed no longer apply.
Commit histories littered with commits that get back-and-forth reverted are frickin unreadable though. Extremely annoying to bisect, painful to comb through when looking for changes, noisy in git blame, etc. There's a ton of downsides for what in practice is very rarely even an upside.
Nobody is suggesting that rebase be used to change the history of a released or published branch (master, develop etc.). If that is your concern and the reason for you not trusting a "rebaser" then you are simply mistaken, you are arguing against an imaginary workflow for which no one is advocating.
Rebase should be used only to curate the commits on a feature branch and to keep the feature branch synchronized with the upstream branch.
The exception to this is when a feature becomes more involved and has several logical steps, or any kind of history worth providing. This should be rare and when it happens, use merge commits to preserve history.
Not polluting the history with N trivial branches for every 1 branch that needs historical context, is a benefit of this.
* F: (master) Merge branch 'component2'
|\
| * E: (component2) Implemented component 2 (now with updated component1)
|/
* C: (master) Refactored component 1's API
|
* A: (master) Base
So we've lost B and D from your example, but who cares about those commits?That's on you if you don't test every commit. I don't care if you had failing tests (or even build) when you were writing your feature. I care that every one of your patches (and thus commit) does one logical thing, and that tests passes (making bisect useful).
You want your PR commit history to tell a coherent story. No one care if a writer had 15 bad draft of their story before publishing, and the same apply here.
This has been a common misunderstanding of git in the past, but thankfully is fading now. I was hoping it wouldn’t come back to haunt this thread. I don’t know where the extreme and hyperbolic idea of using git the way it was designed is “lying” and creating “false history” first came from, you aren’t the first person to suggest it, but it’s neither correct nor helpful to use that kind of language. This theoretical philosophical ideal that there’s only one true history is trading away things git was specifically created to do, as well as the practicalities of real world software development, in favor of a strange unrealistic and abstract notion that once git commit has been used the commit should never be touched again.
Everyone knows and agrees that rearranging already published commits is a bad idea. Not because it’s “lying”, but because it causes problems, costs other people time, and can even inflict irreconcilable merge conflicts on their work.
Cleaning up your own commits before you push using interactive rebase is not just a good idea, it’s the way git was designed, it’s what Linus does, and it’s kind to your team. This includes reordering commits and pulling with rebase.
> Historical context matters. If in doubt, don’t rebase. Never `git pull -- rebase` blindly.
Maybe you could back up your assertion with some examples of why it always matters, and why that justifies using words like ‘never’?
Your rhetoric is ignoring the real-world fact that on a large team, the majority of commits at any given time are orthogonal to each other, and that the parent commit you end up with is completely arbitrary.
Not only do I use pull -- rebase, I always git config --global pull.rebase true, and I frequently recommend others do the same.
Having merge commits in master every single time someone checks in is incredibly noisy and it inflicts friction on the entire team to force everyone to read the noisy log. I’ve always worked on teams that decided to take the more practical approach of one-off commits should not have a merge, regardless of when they happen, to keep history cleaner, and feature branches with more than a couple of commits or by more than one person should have a merge commit, to keep the master branch from having broken commits or unfinished features and so it’s always bisectable.
And the cherry on top is that you can't easily tell later if you are looking at rewritten history. So if the above kind of rewriting might have happened in your project, you will essentially not be able to trust git history anymore as a record of engineering decisions.
I find that terminology terribly misleading and when I was learning git and rebase it confused the heck out of me.
No commits are harmed in the operation of `git rebase`. All the commits you had in the repo before the rebase are still in the repo. Git rebase creates a new sequence of commits and after doing its work relocates the branch name to the tip of the new sequence but you can easily access the previous commits if need be: