Hacker News new | ask | show | jobs
by roryokane 87 days ago
Even if you don’t use p4merge, you can set Git’s merge.conflictStyle config to "diff3" or "zdiff3" (https://git-scm.com/docs/git-config#Documentation/git-config...). If you do that, Git’s conflict markers show the base version as well:

  <<<<<<< left
  ||||||| base
  def calculate(x):
      a = x * 2
      b = a + 1
      return b
  =======
  def calculate(x):
      a = x * 2
      logger.debug(f"a={a}")
      b = a + 1
      return b
  >>>>>>> right
With this configuration, a developer reading the raw conflict markers could infer the same information provided by Manyana’s conflict markers: that the right side added the logging line.
5 comments

That still have an issue with the vocabulary. Things like "theirs/our" is still out of touch but it's already better than a loose spatial analogy on some representation of the DAG.

Something like base, that is "common base", looks far more apt to my mind. In the same vein, endogenous/exogenous would be far more precise, or at least aligned with the concern at stake. Maybe "local/alien" might be a less pompous vocabulary to convey the same idea.

After 15 years i still cant remember which is which. I get annoyed every time. Maybe I should invest 15 minutes finally to remember properly
Seriously!

Why not show the names of the branch + short Id (and when is not direct name, at least "this is from NAME")

doesn't it? Next to the conflict markers, it'll display HEAD, the ref name, or the short commit hash.
I'll be honest, as a fairly skilled and experienced programmer who isn't a git expert, I know what HEAD means, but when I'm rebasing I really have no idea. It all seems to work out in the end because my collaborative work is simple and usually 2–3 people only, so I'm never rebasing against a ton of commits I lack context for (because 90% of them are my commits since I'm usually dealing with PRs to my open source projects rather than someone else's).

HEAD is "the thing we're editing now" but that's not terribly useful when rebasing since you're repeatedly editing a fake history.

Git leaks a lot of implementation details into its UX. Rebasing is meant to be equivalent to checking out the "base" branch and cherry picking commits onto it. Therefore "ours" during a rebase is the base branch.

The meaning of "ours" and "theirs" is always the same, but the "base" of the operation is reversed compared to what you might be used to during merge.

Rebasing can be confusing and hard and messy, but once I learned that rule and took the time to internalize it, I at least never got confused on this particular detail again.

> fake history

That's the thing, it's not actually fake history. Git really is doing the things it looks like it's doing during a rebase. That's why you can do all kinds of weird tricks like stopping in the middle to reset back a commit in order to make a new intervening commit. The reason you can abort at any time with (almost) no risk is because the old history is still hanging around in the database and won't be removed until GC runs, usually long after the rebase is settled.

> HEAD is "the thing we're editing now" but that's not terribly useful when rebasing since you're repeatedly editing a fake history.

You got two things wrong here. Firstly, HEAD isn't 'the thing you're editing now'. HEAD is what you have already committed. If you want to edit the HEAD, you have to either amend the commit or reset and redo the commit. (To make the situation even more complex, the amended or overridden commit remains in the repo unchanged, but orphaned.)

The actual thing being edited is a 'patch' that will eventually be converted into a new commit (snapshot). If you're doing a rebase and want to see the next patch in the pipeline that you're editing now, try this:

  git rebase --show-current-patch
Secondly, rebase is not editing a fake history. Rebase is creating a new (and real) history by repeatedly cherry picking commits from the old history based on the rebase plan. HEAD is the tip commit of the new history under construction. On completion of the rebase, the branch ref of the old history is switched to the new history, where HEAD is now at. Meanwhile, the old history remains in the repo unchanged, but again orphaned.

All the orphaned commits are still visible in the HEAD's reflog. You can use it to undo the rebase if you wish.

I agree that the entire thing is confusing as hell. But I have a bunch of aliases and scripts that show you the process graphically in realtime. You can use that awareness to make the right call every time. I'm thinking about converting it into a TUI application and publishing it.

I avoid this problem by not rebasing.
It does now by default, since v2.12 (released 2017). Prior to that you had to set the log.decorate config. Good times.
It does
This is one of my pain points, and one time I googled and got the real answer (which is why it's such a pain point).

That answer is "It depends on the context"

> The reason the "ours" and "theirs" notions get swapped around during rebase is that rebase works by doing a series of cherry-picks, into an anonymous branch (detached HEAD mode). The target branch is the anonymous branch, and the merge-from branch is your original (pre-rebase) branch: so "--ours" means the anonymous one rebase is building while "--theirs" means "our branch being rebased".[0]

[0] https://stackoverflow.com/questions/25576415/what-is-the-pre...

I ended up creating a personal vim plugin for merges one night because of a frustrating merge experience and never being able to remember what is what. It presents just two diff panes at top to reduce the cognitive load and a navigation list in a third split below to switch between diffs or final buffer (local/remote, base/local, base/remote and final). The list has branch names next to local/remote so you always know what is what. And most of the time the local/remote diff is what I am interested in so that’s what it shows first.
Let’s see if I get this wrong after 25 years of git:

ours means what is in my local codebase.

theirs means what is being merged into my local codebase.

I find it best to avoid merge conflicts than to try to resolve them. Strategies that keep branches short lived and frequently merging main into them helps a lot.

That's kind of the simplest case, though, where "theirs" and "ours" makes obvious sense.

What if I'm rebasing a branch onto another? Is "ours" the branch being rebased, or the other one? Or if I'm applying a stash?

"Ours" and "theirs" make sense in most cases (since "ours" refers to the HEAD you're merging into).

Rebases are the sole exception (in typical use) because ours/theirs is reversed, since you're merging HEAD into the other branch. Personally, I prefer merge commits over rebases if possible; they make PRs harder for others to review by breaking the "see changes since last review" feature. Git generally works better without rebases and squash commits.

Wow, interesting to see such a diametrically opposed view. We’ve banned merge commits internally and our entire workflow is rebase driven. Generally, I find that rebases are far better at keeping Git history clean and clearly allowing you to see the diff between the base you’re merging into and the changes you’ve made.
> Git generally works better without rebases and squash commits.

If squash commits make Git harder for you, that's a tell that your branches are trying to do too many things before merging back into main.

> What if I'm rebasing a branch onto another?

Just checkout the branch you are merging/rebasing into before doing it.

> Or if I'm applying a stash?

The stash is in that case effectively a remote branch you are merging into your local codebase. ours is your local, theirs is the stash.

The thing is, you'll typically switch to master to merge your own branch. This makes your own branch 'theirs', which is where the confusion comes from.
Not me. I typically merge main onto a feature branch where all the conflicts are resolved in a sane way. Then I checkout main and merge the feature branch into it with no conflicts.

As a bonus I can then also merge the feature branch into main as a squash commit, ditching the history of a feature branch for one large commit that implements the feature. There is no point in having half implemented and/or buggy commits from the feature branch clogging up my main history. Nobody should ever need to revert main to that state and if I really really need to look at that particular code commit I can still find it in the feature branch history.

Yep. This is the only model that has worked well for me for more than a decade.
This is what I do, and I was taught by an experienced Git user over a decade ago. I've been doing it ever since. All my merges into main are fast forwards.
> ours means what is in my local codebase

Since it's always one person doing a merge, why isn't it "mine" instead of "ours"? There aren't five of us at my computer collaboratively merging in a PR. There is one person doing it.

"Ours" makes it sound like some branch everyone who's working on the repo already has access to, not the active branch on my machine.

That's between you and git.
a better (more confusing) example:

i have a branch and i want to merge that branch into main.

is ours the branch and main theirs? or is ours main, and the branch theirs?

I always checkout the branch I am merging something into. I was vaguely aware I could have main checked out but merge foo into bar but have never once done that.

  git checkout mybranch
  git rebase main
A conflict happens. Now "ours" is main and "theirs" is mybranch, even though from your perspective you're still on mybranch. Git isn't, however.
> Let’s see if I get this wrong after 25 years of git

You used it 5 years before Linus? Impressive!

Haha yes. You caught me :)

I was wondering when someone was going to point it out. I actually have only been using it since about 2009 after a brief flirtation with SVN and a horrible breakup with CVS.

I was thinking about creating a TUI application that points out what each part in the conflict indicator corresponds to. This idea is primarily meant for rebases where the HEAD and the ID of the updated commits change constantly. Think of it as a map view of the rebase process, that improves your situational awareness by presenting all the relevant information simultaneously. That could trivially work for merges too.
iirc ours is always the commit the merge is starting from. the issue is that with a merge your current commit is the merging commit while with a rebase it is reversed.

I suspect that this could be because the rebase command is implemented as a serie of merges/cherry-picks from the target branch.

  git checkout mybranch
  git rebase main
Now git takes main and starts cloning (cherry-picking, as you said) commits from mybranch on top of it. From git's viewpoint it's working on top of main, so if a conflict occurs, main is "ours" and mybranch is "theirs". But from your viewpoint you're still on mybranch, and indeed are left on mybranch when the rebase is complete. (It's a different mybranch, of course; once the rebase is completed, git moves mybranch to point to the new (detached) HEAD.) Which makes "ours" and "theirs" exactly the opposite of what the user expects.
I had to make an alias for rebasing, because I kept doing the opposite:

    git checkout master #check out the branch to apply commits to
    git rebase mybranch #Apply all commits from mybranch
Now I just write

    rebase-current-branch
and it does what I want: fetches origin/master and rebases my working branch on top of it.

But "ours"/"theirs" still keeps tripping me up.

You can use the --onto flag for git rebase

  git rebase --onto origin/master
It will checkout origin/master and replay the current branch on top.

P.S. I had to check the man page as I use Magit. In the latter I tap r, then u. In magit my upstream is usually the main trunk. You can also tap e instead of u to choose the base branch.

Tip, you may want to use origin/HEAD over origin/master
Man do I hate this behavior because it would be really some by just using the branch names rather then "ours" and "theirs"
Agreed. Even when the branch is the same, it would always be distinguishable by <remote-name>/<branch-name> vs. just <branch-name>.
It doesn't matter which is which. The resolution will be the same regardless.
>Maybe "local/alien" might be a less pompous vocabulary to convey the same idea.

That is more alien and just as contrived. If you merge branches that you made, they're both local and "ours". You just have to remember that "ours" is the branch you are on, and "theirs" is the other one. I have no idea what happens in an octopus merge but anyway, the option exists to show commit titles along with markers to help you keep it straight.

Indeed, thanks.

Something that carry the meaning "the branch we want to use as a starting point" and "the other branch we want to integrate in the previous one" is what I had in mind, but it might not fit all situations in git merge/rebase.

Assuming you have to start on one of the branches being merged, "current" is a good name for the one you started on. "Other" is good enough for the other one. By the way I found out that octopus merges never succeed in case of conflicts. I'm not even sure if prerecorded resolutions work in that case. You're supposed to do a series of normal merges instead if you have conflicts.
This is better but it still doesn't really help when the conflict is 1000 lines and one side changed one character and the other deleted the whole thing. That isn't theoretical - it happens quite regularly.

What you really need is the ability to diff the base and "ours" or "theirs". I've found most different UIs can't do this. VSCode can, but it's difficult to get to.

I haven't tried p4merge though - if it can do that I'm sold!

If I understood your point correctly, I believe that Meld can do that. And then you get a windows as [1]. You can configure git to choose which version goes where. Something like:

    [mergetool "meld"]                                                                                  
        cmd = meld "$LOCAL" "$MERGED" "$REMOTE" --output "$MERGED"                                      
        #cmd = meld "$LOCAL" "$BASE" "$REMOTE" --output "$MERGED"

[1] https://linuxkamarada.com/files/2019/11/git-mergetool-meld-e...
I tried p4merge a while ago and it didn't do it ubfortunately, still stuck copying the base and ours to seperate files and diffing them.
So the way you can do it in VSCode is to open the conflict in their smart merge editor... Often it is actually smart enough to highlight the relevant change but if not each of the left/right editors has a button in its toolbar to diff it against the base.

Not the easiest to access but better than copying/pasting (which is what I also used to do).

I'm on my phone right now so I'm not going to dig too hard for this, but you can also configure a "merge tool" (or something like that) so you can use Meld or Kompare to make the process easier. This has helped me in a pinch to work out some confusing merge conflicts.
I started using Meld years ago and continue to find people who've never heard of it. It's a pretty good tool.
Huge meld fan here, recommended by a professor about a year ago. Game changer
roryokane thanks for this example. I tend to stick with git CLI and this was new to me.
I still find this shit unreadable, even after years of practice.
I've trained myself to avoid this entirely by avoiding changing lines unnecessarily. With LLMs, I also force them to stay concise and ONLY change what is absolutely necessary.