The Git history command deserves more attention

(lalitm.com)

93 points | by turbocon 2 hours ago

10 comments

  • paxys 32 minutes ago
    I don't get all the effort people spend in perfectly curating git history. No one is ever going back and reading individual commits. Just squash everything before merging and call it a day.
    • jxf 3 minutes ago
      > No one is ever going back and reading individual commits.

      I violently disagree with this.

      At a minimum, when I review PRs I look at the commit history to understand what's up. If the path that was taken to commit this is full of "oops" and "fix" messages, it's an immediate reject for me. The commits tell the story and it's a kindness to your human reviewers to not make them work harder to understand the point you're trying to get across.

    • EPWN3D 1 minute ago
      Sorry, no. Just because you've never done it doesn't mean that no one ever will.

      Beyond the archival benefits, I've found plenty of bugs by going back through my "wip" commits and creating a sane history from them.

    • jolmg 20 minutes ago
      > No one is ever going back and reading individual commits.

      Straight from the git-log, maybe not, but sometimes you see code that makes you wonder how it came to be and it can help a lot to see it in context of the commit that introduced it. That'd be less helpful if that commit were some huge thing making lots of different changes at once.

      • paxys 14 minutes ago
        Seeing the individual change in the context of the larger feature is actually more helpful. Otherwise you find a tiny commit that changes A to B and then have to chase down 13 other commits around it to figure out why that change was even made.
        • jolmg 10 minutes ago
          Yeah, there's a balance. I try to make my commits so they make sense on their own.

          > commits around it to figure out why that change was even made.

          A commit should be such that the message can articulate the why.

    • broodbucket 16 minutes ago
      Having a well curated one-logical-change-per-commit history is incredibly valuable when bisecting a regression.
      • efilife 3 minutes ago
        Didn't you mean "dissecting"?
    • jhealy 15 minutes ago
      > No one is ever going back and reading individual commits

      I do, regularly! In a repository where care has been taken, it can be super valuable when tracking down a bug or regression, and understanding the intent of the author

    • Myrmornis 17 minutes ago
      One good reason is to keep your tests separate from the fixes that make your tests pass. That way you can check your test fails before the next commit makes it pass, eliminating the risk of a false negative (test passes that would have anyway).
      • assbuttbuttass 11 minutes ago
        That sounds like it would break bisect
        • jolmg 3 minutes ago
          Yeah, maybe a better way to get what they're going for would be

            git checkout @~ ^tests
          
          where ^tests is zsh's way of saying * except for tests. So you get the tests/ of the current commit and the code of the prior.
    • seba_dos1 19 minutes ago
      Of course we do and it helps tremendously when the graph is reasonable.
  • jolmg 1 hour ago
    > scary rebase -i commands that can leave your tree in a half-broken state if you so much as sneeze

    `git rebase --abort` exists. One can also set a tag or something before doing the rebase, do whatever, then `git reset --hard $set_tag` to go back. Nothing to be scared of. Not like the prior state is lost.

    • rmunn 1 hour ago
      I have so many branches named `temp` or `before-rebase` for exactly that reason; I'm using them effectively as tags, but branches can be moved around with less ceremony than tags (since tags are designed to be for things like v1.2.3, placed once and then almost never moved again), so I usually just do `git branch before-rebase/some-feature` before running a big `rebase -i`.

      I've almost never needed to run `get reset before-rebase`. But I have often done `git log -p before-rebase` and compared that to the post-rebase state of the branch, to ensure that the merge-conflict resolution(s) that came up during the rebase haven't accidentally introduced an unintended change.

      • jolmg 58 minutes ago
        > but branches can be moved around with less ceremony than tags

        `git tag -f` to move a tag.

        Personally, I just do `git show` when I'm feeling cautious, but I can generally just scroll up to find the last `git commit` I did with the hash in the output. `git reflog` should also have record of it, so everything else is kind of extra.

        • rmunn 51 minutes ago
          Good point, that's no more ceremony than moving a branch. I guess I've just gotten "branches are movable tags" so deep into my hindbrain that I absorbed "tags are hard to move". But that's not actually true, and for what I'm doing (save this point in history for a while) a tag makes slightly more sense, semantically, than a branch.
    • catlifeonmars 1 hour ago
      Do you use git reflog?
      • jolmg 1 hour ago
        One can also use that, or just `git log -n1` and taking note of the commit hash. So many options.
  • nativeit 1 hour ago
    I don’t consider myself a coder or programmer, but learning git was like an organizational superpower for my particular brain wiring. I use it for websites, design projects, electronics engineering, music composition, personal knowledge bases, remote administration scripts, config management, snippets, so many applications and so many features for one system. It’s not always perfect, but I tell everyone I work with they should learn it.
  • shepmaster 1 hour ago
    > That last part goes further than git rebase --update-refs, which only moves refs sitting inside the range you’re actively rebasing. git history instead finds and rewrites every local branch descended from the commit (while also having an option to limit it to only the current branch).

    I'm reading that to mean that when I use `git rebase --update-refs` in this situation, where I've currently checked out `D` and update `B` to `B'`:

      A ──► B ──► C ──► D  
            │              
            └───► E        
    
    I'll end up with this state, where `E` remains untouched?

      A ──► B' ─► C' ─► D'  
      │                   
      └───► B ──► E        
    
    (EDIT: Originally I had `E` point to `B'`, which doesn't make sense)

    If I use `git history fixup`, it would also update `E` and end up with this?

      A ──► B' ─► C' ─► D'  
            │              
            └───► E'        
    
    If that's the case, is there a way to get `git rebase` to have the same behavior? I've got decades of `git rebase` burned into my fingers at this point.
    • m463 8 minutes ago
      off-topic, but that is a very clear readable comment you left with those line-drawing characters.
    • jolmg 1 hour ago
      > I'll end up with this state, where `E` remains untouched?

      Can't, because a commit's hash takes into account the parent hashes.

      Haven't used --update-refs, but reading it, it should result in your third graph. So,

      > is there a way to get `git rebase` to have the same behavior?

      is already the case.

      • seba_dos1 15 minutes ago
        It won't result in the third graph on its own as E isn't reachable from D, but it could if you first merged D and E together and then used --update-refs with --rebase-merges. You could then just discard the merged branch and only take care of D' and E' on their own (and since you don't care about the merged branch, you don't have to care about resolving conflicts while preparing it either).
      • shepmaster 58 minutes ago
        > Haven't used --update-refs, but reading it, it should result in your third graph.

        I don't think it does. I tried locally:

          mkdir test; cd test; git init
          touch a; git add a; git commit -m 'a'
          touch b; git add b; git commit -m 'b'
          touch c; git add c; git commit -m 'c'
          git checkout -b branched-feature HEAD~
          touch d; git add d; git commit -m 'd'
          git checkout main
          echo 'change' > b; git add b; git commit -m 'fix b'
          git rebase -i --root --update-refs
        
        And ended up with this graph (`git log --graph --all`)

          * commit (HEAD -> main)
          |
          |     c
          |
          * commit 
          |
          |     b
          |
          | * commit (branched-feature)
          | |
          | |     d
          | |
          | * commit
          |/  
          |       b
          |
          * commit 
          
                a
        
        Replacing the `git commit -m 'fix b'; git rebase -i --root --update-refs` with `git history fixup HEAD~` produces what I'd like:

          * commit (branched-feature)
          |
          |     d
          |
          | * commit (HEAD -> main)
          |/
          |       c
          |
          * commit
          |
          |     b
          |
          * commit 
          
                a
        • jolmg 55 minutes ago
          Huh.. That's a shame :(. Maybe what it refers to is if you had a branch on B rather than D, it might update that.

          EDIT: Yeah, this seems to be it. `git branch b` on b, then `git rebase -i --update-ref @~3` from main caused branch ref `b` to move from d86229e to 02fcaf7:

            * 1e354fb (HEAD -> main) fix b
            * 40e6f70 c
            * 02fcaf7 (b) b
            | * f4188e0 (branched-feature) d
            | * d86229e b
            |/  
            * 5fe78fa a
      • rmunn 1 hour ago
        Unless E remained untouched because it was not rewritten, and ended up staying parented on B instead of getting reparented onto B'.

        Which is usually not what you want; most of the time you want E', which is E reparented onto B'. But sometimes you want E to remain untouched and stay parented on the original B. Depends on the situation.

        • jolmg 1 hour ago
          Exactly, but the second graph where untouched E is reparented to B' is not something that git would allow.
          • shepmaster 1 hour ago
            Ah, good catch. That's more of a graphic issue and not what I was trying to express. Updated the OP.
            • jolmg 1 hour ago
              In that case,

              > when I use `git rebase --update-refs` [...] I'll end up with this state, where `E` remains untouched?

              would be backwards. The second graph would be what you'd get if you don't use --update-refs. The third graph would be what you'd get when you do use it. At least, according to my reading:

              > --update-refs Automatically force-update any branches that point to commits that are being rebased. Any branches that are checked out in a worktree are not updated in this way.

              EDIT: This is wrong. Actual behavior in https://news.ycombinator.com/item?id=48901607

    • lelandfe 1 hour ago
      • shepmaster 1 hour ago
        I'm not sure that answers my question. That shows a linear set of branches (my-feature-v3 depends on my-feature-v2 depends on my-feature-v1 depends on main). I'm asking about the case where two or more branches fork from a common ancestor and you want to fix the common ancestor.
  • _nivlac_ 34 minutes ago
    I like `git history reword`, would've found that useful in the past (e.g. fixing a bad typo in an older commit).

    It sounds like there was probably an equivalent using an existing command but this is easier for me to understand. Thanks for sharing!

  • nine_k 2 hours ago
    In short, newer versions of git implemented three really frequent use cases of `git rebase --interactive` as separate lower-friction commands. Apparently they only work when there are no conflicts.
    • BobbyTables2 1 hour ago
      Wonder if the history command is all that useful.

      I prefer the interactive rebase and use it frequently.

      Would much rather “visually” move commits around than accidentally aim “git history” at an orphaned commit hash no longer in my local branch.

  • paularmstrong 1 hour ago
    `git history split` is really going to help me help juniors break up their large PRs into smaller more concise changes. If only it had the option to split an entire branch in two easily.
    • jolmg 25 minutes ago
      Maybe the issue is they think of a PR as an expensive thing. Would be best if they could just do the small thing and make a PR of that from the get-go. If they want to base future changes on the ones they just did, they can just create a new feature branch from right there, and just not create the PR of the second feature until the first is merged, or create it and add a note to the reviewer that it includes the changes of the other PR so they should review that one first. Should they want to add changes to the first feature, they just need to checkout that branch, do the changes, and merge to the second feature branch so they're available there.
    • catlifeonmars 1 hour ago
      What does it mean to split a branch in two?
      • rmunn 1 hour ago
        Probably to take a series of commits and decide "this one goes on branch A, this one on branch B", e.g. if you intermingled fixing bug A and B in the same branch, you could more easily go through and assign each commit to a new branch.

        The existing workflow for that would be (there are several possible workflows, but this is what I would do):

          git checkout intermingled-branch
          git branch bugfix-A
          git branch bugfix-B
          git checkout bugfix-A
          git rebase -i
          # Edit the file, keep commits that fix bug A, drop commits that fix bug B
          git push origin bugfix-A:bugfix-A
          git checkout bugfix-B
          git rebase -i
          # Edit the file, keep commits that fix bug B, drop commits that fix bug A
          git push origin bugfix-B:bugfix-B
  • bentt 1 hour ago
    Granted, I have a perspective of a game dev, but this kind of repo defiling just gives me the willies. IMO instead of finding a common ancestor and altering it, just make the desired change upstream and merge it to where it’s needed. If you can’t do that then you have already made a deal with the devil and might reconsider your approach. KISS
  • hahahaa 1 hour ago
    I like to be like an accountant. No editing history. Create a new "journal entry" (i.e. commit) to fix.
    • seba_dos1 1 hour ago
      Commit graph is just a data structure. Sometimes it represents a "history", sometimes other things.

      Personally, I like it when project's repository represents the history of the project rather than the history of random things developers do on their machines, but you do you.

      • hahahaa 45 minutes ago
        I am thinking of remote. Edits before pushing OK with me (they ate equivalent of recloning and redoing anyway)
        • seba_dos1 39 minutes ago
          Remotes aren't equal either. Sometimes the remote is my other machine, sometimes it's a fork on a forge used for producing CI artifacts.

          It's a good rule of thumb to consider shared branches to be append-only, but not every remote branch is "shared" and, as with any proper rule of thumb, you can always find exceptions.

    • what 1 hour ago
      You probably shouldn’t be committing things that are broken…
      • normie3000 11 minutes ago
        This is likely saying you shouldn't save a word document that contains spelling mistakes.
      • moezd 1 hour ago
        With tools like GitHub Actions and some added constraints, it's not always possible. You literally need a commit to trigger the CI workflow and it starts to trash your branch. Besides, aren't we all familiar with git commit -m "typo"?
      • hahahaa 44 minutes ago
        Commit yes you absolutely can. Merge to trunk? Probably not, depends on your strategy.
  • skydhash 1 hour ago
    > Working with lots of changes in parallel on git can be painful. You end up juggling branches and commits, and running scary rebase -i commands that can leave your tree in a half-broken state if you so much as sneeze.

    I think that only happens when you work on code as text files (i.e character streams) instead of code (i.e structured content with meaning). Like you have commit A and commit B that is in conflict, you should be glad because that's a rough signal that the intent of A and B differs. Your goal should be to think about how to compose A and B so that both intent survives (unless one supersedes the other). Which means you should be at least familiar with A and B.

    The issue I found with people that fears conflict is that they often don't understand either A or B (or both). So they are a bad candidate to actually do the operation. It's not a matter of git's cli interface, it's a matter of codebase comprehension and how well you're familiar with the changes in question.

    • thfuran 1 hour ago
      Git is the one treating code like a text file instead of code.
      • UnfitFootprint 1 hour ago
        I pull out difftastic when it’s all too hard for this. Diffs based off tree sitter
      • skydhash 1 hour ago
        Git store whole files. The diff and the merge algorithm works by line by default, but that's because line is a rough unit of code (statement, expression, and definition happens mostly within one line).
    • bulatb 1 hour ago
      I can fully understand a conflict, know it's coming, and fear it anyway because I'll have to deal with Git's behavior to fix it.
      • seba_dos1 1 hour ago
        Such as?

        When you merge a commit in that changed a file that has been already changed since the common ancestor, Git runs a tool of your choice on this file. If the tool fails, it marks the file as needing a merge and doesn't let you commit it until you unmark it to confirm that you have merged it manually. In case of octopus merges, it will just abort early. That's basically its whole behavior when it comes to conflicts.

        • bulatb 1 hour ago
          Such as moving down a fixup during interactive rebase when it's going to conflict with parents, or I've added more commits mid-rebase, or the rebase started in a tool and now I need to think about the tool's command and understand how many times the rebase point is view is backwards to interpret which is "ours" and which is "theirs" and whether it's the tool, my editor, or my own experience I should ignore because it's going to mislead me.

          None of that word salad should matter, but it does. Git will ruin everything with glee and there is always an excuse for why that's fine and it's the user's fault.

          • seba_dos1 59 minutes ago
            There's nothing "backwards" in "rebase point of view". It's just automated cherry-picking - it's like applying patches, it's as forwards as it gets. People who think it's "backwards" usually just have holes in their mental model of the repository. And yes, I find tools doing what you're asking them to do to be rather fine. In fact, Git makes it easy to notice when you're adding commits mid-rebase, which is something I often do intentionally, as it tells you what the HEAD is (and even shows a detailed state of the in-progress rebase) while authoring the commit message.
            • bulatb 31 minutes ago
              I understand how rebase works and use it many times a day. Thanks for trying to explain but I won't respond further.
              • seba_dos1 26 minutes ago
                These aren't troubles of someone who understands. If anyone else reading this has these kinds of troubles too - don't worry, understanding abstractions takes time and effort, you'll get there eventually (not if you'll keep blaming others though).
          • skydhash 40 minutes ago
            > which is "ours" and which is "theirs"

            "ours" is always HEAD, usually meaning the state of the working_tree, "theirs" is always the commit that is going to change the working_tree.

            When merging, you are taking change from another branch (theirs) to create a new commit on the current branch (ours) that ties the two together. When rebasing interactively, you switch to the new base (ours) and replay the changes of the branch (theirs) according to the edit file.

            Etymology matters. The conceptual model of git is simple, but people only focus on the operations. That's like trying to learn algorithm and data structures and focusing on the words "insert", "remove", "find", without trying to learn "list", "stack", "tree",... first.

            Instead learn about Git's glossary [0], then how the operations use and modify those concepts.

            [0] https://git-scm.com/docs/gitglossary