Most active commenters
  • seba_dos1(5)
  • lalaithion(4)
  • pitaj(3)
  • Certhas(3)

←back to thread

Oh Shit, Git?

(ohshitgit.com)
464 points Anon84 | 37 comments | | HN request time: 0.002s | source | bottom
Show context
pitaj ◴[] No.42729155[source]
Some changes I would make:

1. Always use `git switch` instead of `git checkout`

2. Avoid `reset --hard` at all costs. So for the "accidentally committed something to master that should have been on a brand new branch" issue, I would do this instead:

    # create a new branch from the current state of master
    git branch some-new-branch-name
    # switch to the previous commit
    git switch -d HEAD~
    # overwrite master branch to that commit instead
    git switch -C master
    # switch to the work branch you created
    git switch some-new-branch-name
    # your commit lives in this branch now :)
3. I'd apply the same to the `cherry-pick` version of "accidentally committed to the wrong branch":

    git switch name-of-the-correct-branch
    # grab the last commit to master
    git cherry-pick master
    # delete it from master
    git switch -d master~
    git switch -C master
4. And also to the "git-approved" way for "Fuck this noise, I give up.":

    # get the lastest state of origin
    git fetch origin
    # reset tracked files
    git restore -WS .
    # delete untracked files and directories
    git clean -d --force
    # reset master to remote version
    git switch -d origin/master
    git switch -C master
    # repeat for each borked branch
replies(11): >>42729838 #>>42729902 #>>42730974 #>>42731510 #>>42731713 #>>42731798 #>>42731885 #>>42732130 #>>42734933 #>>42737820 #>>42740404 #
1. lalaithion ◴[] No.42729838[source]
The disconnect between git's beautiful internal model of blobs, a tree of commits, and pointers to commits, and the command line interface is so wild. All of these recipes are unintuitive even if you have a firm grasp of git's model; you also need to know the quirks of the commands! To just look at the first one... wouldn't it be more intuitive for the command line interface to be:

    # this command exists already;
    $ git switch -c some-new-branch-name
    # is there a command that simply moves a branch from one commit to another without changing anything else? It feels like it should be possible given how git works.
    $ git move-branch master HEAD~
replies(8): >>42729951 #>>42729992 #>>42730629 #>>42730721 #>>42730733 #>>42730978 #>>42731467 #>>42740395 #
2. pitaj ◴[] No.42729951[source]
I prefer just using `git switch` because it's easy to remember the flags (and the position of arguments), but you're right, there is a simpler way:

    git switch -c some-new-branch-name
    git branch -f master HEAD~
replies(2): >>42730595 #>>42731415 #
3. neild ◴[] No.42729992[source]
The "move a branch from one commit to another without changing anything" command is "git reset".

"git reset --hard" is "...and also change all the files in the working directory to match the new branch commit".

"git reset --soft" is "...but leave the working directory alone".

replies(2): >>42730734 #>>42731399 #
4. DangitBobby ◴[] No.42730595[source]
You should also be able to do

  git branch -f master origin/master
replies(1): >>42730761 #
5. rav ◴[] No.42730629[source]
For move-branch: Use `git branch -f master HEAD~` if you're currently on another branch, or `git reset --soft HEAD~` if you're currently on master.
6. Certhas ◴[] No.42730721[source]
The real "internal model" of git contains much more data/moving parts.

There isn't one tree of commits, there are typically at least two: local and remote

Branches are not just pointers to commits, but also possibly related to pointers in the other tree via tracking.

Stash and index and the actual contents of the working directory are additional data that live outside the tree of commits. When op says "avoid git reset hard" it's because of how all these interact.

Files can be tracked, untracked and ignored not ignored. All four combinations are possible.

replies(1): >>42731353 #
7. jimbokun ◴[] No.42730733[source]
Are there alternative git command lines that keep the beautiful internals, but implement a more elegant and intuitive set of commands to manage it?
replies(4): >>42731017 #>>42731570 #>>42732079 #>>42736470 #
8. rav ◴[] No.42730734[source]
Actually, "git reset --soft" moves the current branch to another commit, without moving the index (aka staging area) along with it, whereas "git reset" (aka "git reset --mixed") moves the current branch AND the index to another commit. I really couldn't wrap my head around it before I had gone through "Reset demystified" [1] a couple times - it's not a quick read but I can strongly recommend it.

[1] https://git-scm.com/book/en/v2/Git-Tools-Reset-Demystified

9. pitaj ◴[] No.42730761{3}[source]
This doesn't work if your local master was already ahead of origin
replies(1): >>42730777 #
10. DangitBobby ◴[] No.42730777{4}[source]
Indeed, as with all of these examples exceptions will apply and, it's a good idea to check the log before taking any such action. I believe your example also depends on exactly how many commits you've made that need to be moved. In any case, it depends on me remembering exactly what `~` signifies.
11. Terr_ ◴[] No.42730978[source]
> The disconnect between git's beautiful internal model of blobs, a tree of commits, and pointers to commits, and the command line interface is so wild

Something I heard somewhere that stuck with me: git is less less of a Version Control System, and more of a toolkit for assembling your own flavor of one.

replies(1): >>42732279 #
12. dalia-reds ◴[] No.42731017[source]
Check out jujutsu or jj (same thing). It's its own VCS, but it uses git as a backend, so it works with GitHub and other git integrations
13. lalaithion ◴[] No.42731353[source]
None of these seem to preclude a command to make an arbitrary branch point to an arbitrary commit without changing anything else.
replies(2): >>42731671 #>>42732411 #
14. lalaithion ◴[] No.42731399[source]
git reset only works if you're on the branch you want to move, which is why every one of these example flows has you create your new branch, then do the reset, then switch to the new branch, instead of just allowing you to move a branch you're not on.
15. lalaithion ◴[] No.42731415[source]
Good to know! Thanks for the tip.
16. lilyball ◴[] No.42731467[source]
The "move a branch" command is `git push .`. Yes, you can push to the current repo. I have a script called git-update-branch which just does some preflight checks and then runs `git push --no-verify . +$branch@{upstream}:$branch` to reset a branch back to its upstream version.
replies(1): >>42733696 #
17. stouset ◴[] No.42731570[source]
Seconded jujutsu. It's 100% git-compatible and one of those rare birds that is both more powerful and simpler to use in practice due to rethinking some of the core ideas.
18. fragmede ◴[] No.42731671{3}[source]
This works if the branch exists or creates it if it doesn't exist, but not if it's checked out.

    git branch -f branch_name commit
if it's checked out:

    git reset --hard commit
replies(1): >>42734347 #
19. maleldil ◴[] No.42732079[source]
Another vote for jujutsu. No one else needs to know you're using it. You can think of it as just a different CLI for git (although you shouldn't mix them). I used to use third-party interfaces like lazygit, but I don't need them anymore because jujutsu _just makes sense_.
20. JadeNB ◴[] No.42732279[source]
> Something I heard somewhere that stuck with me: git is less less of a Version Control System, and more of a toolkit for assembling your own flavor of one.

That's how it is in principle, but it seems to me that there aren't that many different CLI "porcelains" in practice. Kind of like how Knuth figured people would essentially write their DSLs on top of plain TeX, not spend most of their time in giant macro packages like LaTeX.

replies(1): >>42732925 #
21. karatinversion ◴[] No.42732411{3}[source]
You are looking for

  git update-ref <branch-name> <commit-sha>
replies(1): >>42740833 #
22. dragonwriter ◴[] No.42732925{3}[source]
> That's how it is in principle, but it seems to me that there aren't that many different CLI "porcelains" in practice.

I think that's because most of the people that make custom tooling to support particular workflows build it into graphical (including IDE extensions, web-based. etc.) frontends, not CLIs.

23. zahlman ◴[] No.42733696[source]
> The "move a branch" command is `git push .`. Yes, you can push to the current repo.

Wouldn't that copy a branch rather than moving it?

replies(1): >>42764674 #
24. seba_dos1 ◴[] No.42734347{4}[source]
> but not if it's checked out

...and for a good reason that should be apparent to anyone who understands git's model (HEAD points to a ref in this case, so if you suddenly change what that ref points to without updating the working tree you create an inconsistency).

You can do that manually of course (with `git update-ref` or even a text editor), but then you get to clean up the mess yourself.

replies(2): >>42734445 #>>42735314 #
25. thfuran ◴[] No.42734445{5}[source]
Couldn't head just detach without any consistency issue?
replies(1): >>42734522 #
26. seba_dos1 ◴[] No.42734522{6}[source]
Theoretically it could, but that would be a rather surprising side effect. You could also check the new revision out and leave HEAD intact. Which one of those outcomes you would expect and why?

"error: ref in use by higher layers" makes much more sense to me in this case.

replies(2): >>42735349 #>>42742361 #
27. Certhas ◴[] No.42735314{5}[source]
To me that looks like git is leaking implementation details left and right.

So much for "a branch is simply a pointer to a commit"...

replies(1): >>42744011 #
28. Certhas ◴[] No.42735349{7}[source]
If you buy the "git is just a tree of commits and pointers" mental model it's absolutely not a surprising side effect but would be the logical thing to expect. I moved a pointer to a commit around, why would that change where HEAD is pointed.

Turns out it's a tree of commits and pointers to within that tree and a master pointer that come in two versions: pointing towards the pointers or pointing towards the tree. And pointers behave very differently when the master pointer is pointing to them...

Elegant. Simple. :P

replies(1): >>42743939 #
29. jonasced ◴[] No.42736470[source]
Lazygit has a terminal UI but might otherwise be what you're looking for: https://github.com/jesseduffield/lazygit
30. assbuttbuttass ◴[] No.42740395[source]
> is there a command that simply moves a branch from one commit to another without changing anything else? It feels like it should be possible given how git works.

git switch -C master HEAD~

31. DiggyJohnson ◴[] No.42740833{4}[source]
Wouldn't the fail or break under any circumstance where they don't immediately share a history?
replies(1): >>42770192 #
32. thfuran ◴[] No.42742361{7}[source]
It doesn't seem surprising to me. It probably ought to print ought a warning that head has detached though, like some other commands already do. That error message on the other hand seems very unhelpful. It's lingo that only makes sense if you're neck deep in the plumbing.
replies(1): >>42743972 #
33. seba_dos1 ◴[] No.42743939{8}[source]
> I moved a pointer to a commit around, why would that change where HEAD is pointed.

...because HEAD points to what's checked out. This pointer does not just exist and hang around, it has its semantics. Not understanding that reveals flaws in your mental model.

Besides, the side affect you find "not surprising" here is... rewriting HEAD to change what it points to. Then you ask "why would that change where HEAD is pointed". Sounds like you may be confused. Are you forgetting that a ref may point not just to a commit, but also to another ref? This is the whole idea behind branches after all, having HEAD point to a ref is exactly what makes branches semantically different from tags - if you don't understand it then no wonder you're confused.

(protip: if you find git's "pointers to pointers" confusing, perhaps because in C a "pointer" and "pointer to pointer" are separate types that make multiple dereferencing steps explicit, think of them as symlinks instead and it should become clearer - that's in fact how symrefs used to be implemented in the past)

When a pointer is in use by higher layers, a good UI will prevent you from making direct changes underneath it unless you force it or go low-level enough for it to not matter. The only sin of git I can see here is that `git` command provides you both high-level and low-level interfaces to manipulate the data structure you're working on with no clear distinction for the user.

34. seba_dos1 ◴[] No.42743972{8}[source]
There's no such message there, it was a description of a situation written by me and it doesn't even actually match the git's lingo. Should have made it clearer I guess.

It is surprising. You wanted to edit the value of `main` ref, yet suddenly you now edited `HEAD` too without meaning it. Bailing out and letting you actually decide whether you want to do it or not is the correct thing to do for a high-level command like `git branch` (alternatively it could ask you what to do interactively). If you don't want such safeguards and you know what you're doing, use `git update-ref` which will happily let you break whatever you want.

35. seba_dos1 ◴[] No.42744011{6}[source]
Do you react the same way when an OS prevents you from writing to a file with an exclusive lock placed on it? So much for "a file is simply a collection of data stored as a single object"...

If a git repo was purely a collection of meaningless pointers and graph nodes, git would be a graph manipulation utility, not a version control system. The fact that some of those pointers have a meaning is what makes it useful and it doesn't contradict the fact that what you're working on is still just a bunch of pointers and nodes.

36. lilyball ◴[] No.42764674{3}[source]
"move a branch" means changing the commit the branch points to. `git push . $sha:$branch` will update $branch to point to $sha (you'll probably want to force this, unless you're just fast-forwarding the branch).
37. karatinversion ◴[] No.42770192{5}[source]
I just tested it by creating a repo with two branches without a common ancestor, and I was able to move a branch pointer to either history with update-ref, so no, I don't think so