Comment by pitaj

Comment by pitaj 2 days ago

72 replies

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
lalaithion 2 days ago

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~
  • Certhas 2 days ago

    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.

    • lalaithion 2 days ago

      None of these seem to preclude a command to make an arbitrary branch point to an arbitrary commit without changing anything else.

      • karatinversion a day ago

        You are looking for

          git update-ref <branch-name> <commit-sha>
        • DiggyJohnson 18 hours ago

          Wouldn't the fail or break under any circumstance where they don't immediately share a history?

      • fragmede 2 days ago

        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
  • neild 2 days ago

    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".

    • rav 2 days ago

      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

    • lalaithion 2 days ago

      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.

  • Terr_ 2 days ago

    > 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.

    • JadeNB a day ago

      > 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.

      • dragonwriter a day ago

        > 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.

  • pitaj 2 days ago

    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~
    • DangitBobby 2 days ago

      You should also be able to do

        git branch -f master origin/master
      • pitaj 2 days ago

        This doesn't work if your local master was already ahead of origin

        • DangitBobby 2 days ago

          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.

  • jimbokun 2 days ago

    Are there alternative git command lines that keep the beautiful internals, but implement a more elegant and intuitive set of commands to manage it?

    • dalia-reds 2 days ago

      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

    • maleldil a day ago

      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_.

    • stouset 2 days ago

      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.

  • lilyball 2 days ago

    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.

    • zahlman a day ago

      > 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?

  • rav 2 days ago

    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.

  • assbuttbuttass 18 hours ago

    > 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~

mrshu 2 days ago

Not trying to defend the choice of `git checkout` over `git switch` (and `git restore`) but they were introduced in v2.23 of Git [0], which was released about 5 years ago [1]. If you take a look at their help pages, they still include a warning that says

> THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.

Granted, it has been in there for basically as long as the command(s) existed [2] and after 5 years perhaps it might be time to no longer call it experimental.

Still, it does seem like `git checkout` might be a bit more backwards compatible (and also reflective of the time when this website was originally created).

[0] https://github.com/git/git/blob/757161efcca150a9a96b312d9e78...

[1] https://github.com/git/git/releases/tag/v2.23.0

[2] https://github.com/git/git/commit/4e43b7ff1ea4b6f16b93a432b6...

baobun 2 days ago

5. Teaching `git add .` as default to add changes to the staging area is not ideal. Show adding specific files instead has less room for subsequent "oh shit" and better.

  • zahlman a day ago

    Learning about the `-p` option for `git add` was one of two things that revolutionized my Git usage. (The other was figuring out how to write effective commit messages.)

    • wodenokoto a day ago

      This is the main reason to use a GUI imho.

      • baobun a day ago

        Tig is a great one for the terminal fwiw.

        gitg for something simple, graphical and widely available.

  • ajross a day ago

    True enough, but it does make for good practice with the index and splitting workflows later on when you need to clean it up.

    I think there's space for "git add ." as a didactic step. It maps cleanly to the most obvious way to understand a commit, as "here's what I've done". Bootstrapping from that to an understanding of "commits as communication with other developers" will naturally happen over time.

    • baobun a day ago

      Is not very compatible with printlog-debugging. I'd rather encourage devs to prod around as they go if it benefits them, which causes grief for either them or reviewers in the end if they've internalized what you just said.

      Explicitly adding internalizes a personal review process as inherent part of the push process, instead of something you attempt to force on top later.

      It's better with a collaboration workflow that limits the span of time with expected discipline, imo.

      • recursive a day ago

        You can have both. Make sure the whole diff is what you want it to be before invoking `add .`

jaapz 2 days ago

Could you motivate why you suggest these? Why is `switch` better than `checkout`? And why not use `reset --hard`?

  • jopicornell a day ago

    Not comment OP, but checkout has two very different uses merged into one: restoring files and switching branches. To not break compatibility, git has now switch and restore commands that make commands more readable and understandable.

    You should avoid reset --hard because it will delete all your uncommited, and you could end up in situations where that's really bad. Using reset --keep will keep uncommited changes, and failing if any uncommited change cannot be kept.

johnisgood a day ago

What do you mean avoid "reset --hard"? Why or why is it not enough in practice? I use it quite often, along with "alias git-restore-file='git restore --source=HEAD --'". It seems to work.

CharlieDigital 2 days ago

What's the problem with `reset --hard`?

  • pitaj 2 days ago

    It leaves behind tracked files that were moved or deleted between revisions.

xk3 18 hours ago

> 2. Avoid `reset --hard` at all costs

Sounds like you might be looking for `git reset --keep`

  • [removed] 16 hours ago
    [deleted]
stouset 2 days ago

Rewriting these for jj users. I'm prefering long option names and full command names for clarity here, but all the commands have shortened aliases and all the option names have single-letter alternatives. `@` means "the current revision", `x+` means "the revision just after `x`", `x-` means "the revision just before `x`".

2. "Accidentally committed something to master that should have been on a brand new branch".

This doesn't really have an analogue. Branches ("bookmarks") only move when you tell them to. If you make a new commit on top of master, it doesn't point master to it, it just lives one past the tip of master. But let's say you accidentally moved master to include the new commit you shouldn't have:

    # set master to the previous commit (and reaffirm that
    # you're okay moving a bookmark backward)
    $ jj bookmark set master --allow-backwards --revision @- 

    # there is no step two, you're still editing the change you already were
3. Move a commit from one branch to another.

    # move the revision one-past-master on to our desired bookmark
    $ jj rebase --revision master+ --destination name-of-the-correct-bookmark

    # there is also no step two; technically we're not updating the bookmark
    # to point to the new commit yet, but this isn't something you'd do as rote
    # habit in jujutsu anyway
4. Fuck this noise, I give up:

    # list all the operations I've performed against the repo
    $ jj op log

    # restore to some previous known-good state
    $ jj op restore {id}
Bonus content, translated from the article:

> Oh shit, I committed and immediately realized I need to make one small change!

    # move the current edits into the previous revision
    $ jj squash
> Oh shit, I need to change the message on my last commit!

    # re-describe the previous revision
    $ jj describe --revision @-
> Oh shit, I tried to run a diff but nothing happened?!

    # there is no staging area, all your changes are part of the repo and there is no
    # staging area pseudo-commit; please understand that this still works elegantly
    # with "patch-add" workflows and does not imply that large change sets can't be
    # easily broken up into small commits
> Oh shit, I need to undo a commit from like 5 commits ago!

    # find the commit
    $ jj log

    # back it out
    $ jj backout {id}
> Oh shit, I need to undo my changes to a file!

    # find the commit
    $ jj log

    # restore the paths provided to their contents in the given revision
    $ jj restore --from {id} [paths...]
And finally there are a few things that are super easy/obvious in jujutsu that are far more annoying in git.

> Oh shit, I committed and many commits later realized I need to make one small change!

    # moves the changes in the current working copy into the revision provided
    $ jj squash --into {id}
> Oh shit, I committed and many commits later realized I need to make extensive changes!

    # sets your working copy to the commit provided; later commits will be
    # auto-rebased on top live as you make modifications
    $ jj edit {id}
> Oh shit, I need to reorder two commits!

    # does what it says on the tin
    $ jj rebase --revision {a} --insert-before {b}
> Oh shit, I haven't committed anything in hours but I need something from an interim change from like thirty minutes ago

    # look in the "obsolete log" for earlier iterations of the current revision
    $ jj obslog

    # restore the contents
    $ jj restore --from {id} [paths...]
> Oh shit, I made a bunch of changes but want them to be in multiple commits (e.g., patch-add workflow)

    # choose the parts to move out; you'll end up with two revisions, one with each half
    $ jj split
> Oh shit, I need to break out a change from my current work into a new branch off master

    # choose the parts to move out; you'll end up with two revisions, one with each half
    $ jj split

    # move the stuff I pulled out onto master
    $ jj rebase --revision @- --destination master

    # optional: name it; most of the time you wouldn't bother
    $ jj bookmark create new-name --revision master+
> Oh shit, I need to make three sequential changes but roll them out one-by-one. I also might need to make fixes to previous ones before later ones are rolled out.

    # author a new change on top of master and name it a
    $ jj new master
    …
    $ jj bookmark create a

    # author a new change on top of a and name it b
    $ jj new
    …
    $ jj bookmark create b

    # author a new change on top of b and name it c
    $ jj new
    …
    $ jj bookmark create c

    # edit a; nothing else is necessary to ensure b and c remain as descendants of
    # revision a
    jj edit a
    …

    # author a new change as part of b; nothing else is necessary to ensure c remains
    # up to date on top of b
    $ jj new --insert-before c
    …

    # point c at the new change
    $ jj bookmark set b
  • amanwithnoplan a day ago

    Please kindly write one for a jj-specific issue: "my build vomitted out a bunch of files and I used any jj command before editing my .gitignore"

    I've found myself using git to fix the mess in this particular instance.

    • stouset a day ago

          $ jj file untrack {paths-or-pattern}
      
      Alternatively if you have a bunch of files spewed everywhere with no rhyme or reason which can't be globbed or enumerated reasonably:

          $ jj status | grep '^A' | awk '{print $2}' | xargs jj file untrack
  • hooper a day ago

    One thing I really appreciate is that you can run `jj new master` at _any_ time to drop what you're doing and start a new change. The way jj handles the working copy, conflicts, and visible heads means there's just no need to think about uncommitted changes, unfinished conflict resolution, detached head, etc.. So many things that would get in your way just can't happen.

    • stouset a day ago

      I haven’t thought about it at all but you’re right. It’s surprising how nice it is that I can enter a repo and `jj new main` without needing to remember any context whatsoever.

      My post was a pretty naked attempt to showcase how much less convoluted basic operations are in jj vs. git and hopefully drum up some interest. Hopefully someone bites.

      • steveklabnik 15 hours ago

        `jj new trunk()` is even better than `jj new main`, I just realized, ha!

        • stouset 15 hours ago

          It is! I've fully migrated my repos over to `main` at this point so it's rare I have to think about the difference. You could also make an alias to `jj n` or something to make it even easier.

Am4TIfIsER0ppos 21 hours ago

git switch is too new and its man page says "THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE."

dustingetz 2 days ago

millennial boomer here where is the gen z cheat sheet for this git switch thing that i keep hearing about

ajross a day ago

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

Even harder: always use "git reset --hard".

Basically don't use local branches. The correct workflow for almost every task these days is "all branches are remote". Fetch from remotes. Reset to whatever remote branch you want to work above. Do your work. Push back to a remote branch (usually a pull request branch in common usage) when you're done.

If you need to manage local state, do it manually with tags (or stash, but IMHO I never remember what I stashed and will always make a dummy commit and tag it).

Don't ever try to manually manage a branch locally unless you (1) absolutely have to and (2) absolutely know what you're doing. And even then, don't, just use a hosted upstream like github or whatever.

  • smrq a day ago

    This sounds like the correct Git workflow if you think the correct VCS to use is SVN.

    • ajross a day ago

      And that sounds like you failed to understand me. I didn't say "don't use branches". I said "all branches are remote". Pushing to a branch is communication with other human beings. Mixing your own private state into that is confusing and needless in 99% of situations (and the remaining 1% is isomorphic to "you're a maintainer curating branches for pushing to other people at a well-known location").

      All branches are public.

      • ryandrake a day ago

        I have quite a few projects that do not have a "remote" and will probably never have a remote repo. Should I not be using branches at all?

      • simoncion a day ago

        > All branches are public.

        What actual problem does this solve? For me, WIP branches only ever get pushed up if at least one of two things are true about them:

        1) They're actually worth preserving, and not some experimental garbage that ended up being totally pointless.

        2) I need to get them off of my local machine for disaster-recovery purposes.

        > If you need to manage local state, do it manually with tags (or stash, but IMHO I never remember what I stashed and will always make a dummy commit and tag it).

        I don't see the benefit one gets from putting work that's not fit for publication in a dummy commit on a public branch. That's just asking for garbage that noone should concern themselves with to accidentally get pushed up at the end of a long-ass day.

        • ajross a day ago

          > 1) They're actually worth preserving, and not some experimental garbage that ended up being totally pointless.

          That seems naive. You don't know what's pointless for years, usually. Can I tell you how many times I've gone back to stale pull requests and topic branches to recover "How did I do this?" code?

          > 2) I need to get them off of my local machine for disaster-recovery purposes.

          That's called a "backup", and yes, data robustness is a big advantage of this workflow. You're acting like this is some kind of rare event. I push my local work to a branch (or three) on github every hour!

          A corrolary is hardware independence, btw. Working off remote branches means I can also stand up a replacement development environment with a simple clone. (And the corrolary to that means that I can trivially document this such that other people can stand up development environments for my stuff, too!)

  • snafferty a day ago

    This is a workflow I’ve never seen on any team or project I’ve worked on. Another commenter already mentioned the remote branch for everything preference, but usage of tags is especially interesting to me. I think that’s how most people use branches, and tags tend to be more permanent. What do you do when you come back to the commit with the tag, cherry pick it over and delete the tag? It sounds like an overly complicated process compared to having a branch and rebasing onto the current branch when you finally go to make the change for real.

    • ajross a day ago

      Local branches aren't names for anything other humans beings care about. All "branches" discussed in a team are remote. But because branches have "history" and "state", keeping your local names around is just inviting them to get out of sync with identically or similarly-named branches out there in the rest of the world.

      > It sounds like an overly complicated process compared to having a branch and rebasing onto the current branch when you finally go to make the change for real.

      Not sure I understand the problem here? The rebase is the hard part. It doesn't help you to have a name for the code you're coming "from". If it collides it collides and you have to resolve it.

      What I said about tags was just as short term memory "This commit right here worked on this date", stored in a way that (unless I delete or force-update the tag) I can't forget or pollute. Branches don't have that property. And again local branches don't have any advantages.

    • [removed] a day ago
      [deleted]
  • krick a day ago

    At first I was put aback by this, but it actually kinda makes sense. I mean, if people are giving off unwarranted advises about "the right way" here, yeah, you should start with a remote branch, and push all your work ASAP. Especially when you are closing the lid of your laptop to change location.

    ...Not that I am gonna follow that advice, of course. Same as I'm not gonna use git switch for a task git checkout does perfectly well.