Finding a branch point with Git?

前端 未结 22 1341
小鲜肉
小鲜肉 2020-11-22 10:11

I have a repository with branches master and A and lots of merge activity between the two. How can I find the commit in my repository when branch A was created based on mast

相关标签:
22条回答
  • 2020-11-22 10:50

    In general, this is not possible. In a branch history a branch-and-merge before a named branch was branched off and an intermediate branch of two named branches look the same.

    In git, branches are just the current names of the tips of sections of history. They don't really have a strong identity.

    This isn't usually a big issue as the merge-base (see Greg Hewgill's answer) of two commits is usually much more useful, giving the most recent commit which the two branches shared.

    A solution relying on the order of parents of a commit obviously won't work in situations where a branch has been fully integrated at some point in the branch's history.

    git commit --allow-empty -m root # actual branch commit
    git checkout -b branch_A
    git commit --allow-empty -m  "branch_A commit"
    git checkout master
    git commit --allow-empty -m "More work on master"
    git merge -m "Merge branch_A into master" branch_A # identified as branch point
    git checkout branch_A
    git merge --ff-only master
    git commit --allow-empty -m "More work on branch_A"
    git checkout master
    git commit --allow-empty -m "More work on master"
    

    This technique also falls down if an integration merge has been made with the parents reversed (e.g. a temporary branch was used to perform a test merge into master and then fast-forwarded into the feature branch to build on further).

    git commit --allow-empty -m root # actual branch point
    git checkout -b branch_A
    git commit --allow-empty -m  "branch_A commit"
    git checkout master
    git commit --allow-empty -m "More work on master"
    git merge -m "Merge branch_A into master" branch_A # identified as branch point
    git checkout branch_A
    git commit --allow-empty -m "More work on branch_A"
    
    git checkout -b tmp-branch master
    git merge -m "Merge branch_A into tmp-branch (master copy)" branch_A
    git checkout branch_A
    git merge --ff-only tmp-branch
    git branch -d tmp-branch
    
    git checkout master
    git commit --allow-empty -m "More work on master"
    
    0 讨论(0)
  • 2020-11-22 10:50

    Not quite a solution to the question but I thought it was worth noting the the approach I use when I have a long-living branch:

    At the same time I create the branch, I also create a tag with the same name but with an -init suffix, for example feature-branch and feature-branch-init.

    (It is kind of bizarre that this is such a hard question to answer!)

    0 讨论(0)
  • 2020-11-22 10:51

    I was looking for the same thing, and I found this question. Thank you for asking it!

    However, I found that the answers I see here don't seem to quite give the answer you asked for (or that I was looking for) -- they seem to give the G commit, instead of the A commit.

    So, I've created the following tree (letters assigned in chronological order), so I could test things out:

    A - B - D - F - G   <- "master" branch (at G)
         \   \     /
          C - E --'     <- "topic" branch (still at E)
    

    This looks a little different than yours, because I wanted to make sure that I got (referring to this graph, not yours) B, but not A (and not D or E). Here are the letters attached to SHA prefixes and commit messages (my repo can be cloned from here, if that's interesting to anyone):

    G: a9546a2 merge from topic back to master
    F: e7c863d commit on master after master was merged to topic
    E: 648ca35 merging master onto topic
    D: 37ad159 post-branch commit on master
    C: 132ee2a first commit on topic branch
    B: 6aafd7f second commit on master before branching
    A: 4112403 initial commit on master
    

    So, the goal: find B. Here are three ways that I found, after a bit of tinkering:


    1. visually, with gitk:

    You should visually see a tree like this (as viewed from master):

    gitk screen capture from master

    or here (as viewed from topic):

    gitk screen capture from topic

    in both cases, I've selected the commit that is B in my graph. Once you click on it, its full SHA is presented in a text input field just below the graph.


    2. visually, but from the terminal:

    git log --graph --oneline --all

    (Edit/side-note: adding --decorate can also be interesting; it adds an indication of branch names, tags, etc. Not adding this to the command-line above since the output below doesn't reflect its use.)

    which shows (assuming git config --global color.ui auto):

    output of git log --graph --oneline --all

    Or, in straight text:

    *   a9546a2 merge from topic back to master
    |\  
    | *   648ca35 merging master onto topic
    | |\  
    | * | 132ee2a first commit on topic branch
    * | | e7c863d commit on master after master was merged to topic
    | |/  
    |/|   
    * | 37ad159 post-branch commit on master
    |/  
    * 6aafd7f second commit on master before branching
    * 4112403 initial commit on master
    

    in either case, we see the 6aafd7f commit as the lowest common point, i.e. B in my graph, or A in yours.


    3. With shell magic:

    You don't specify in your question whether you wanted something like the above, or a single command that'll just get you the one revision, and nothing else. Well, here's the latter:

    diff -u <(git rev-list --first-parent topic) \
                 <(git rev-list --first-parent master) | \
         sed -ne 's/^ //p' | head -1
    6aafd7ff98017c816033df18395c5c1e7829960d
    

    Which you can also put into your ~/.gitconfig as (note: trailing dash is important; thanks Brian for bringing attention to that):

    [alias]
        oldest-ancestor = !zsh -c 'diff -u <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | sed -ne \"s/^ //p\" | head -1' -
    

    Which could be done via the following (convoluted with quoting) command-line:

    git config --global alias.oldest-ancestor '!zsh -c '\''diff -u <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | sed -ne "s/^ //p" | head -1'\'' -'
    

    Note: zsh could just as easily have been bash, but sh will not work -- the <() syntax doesn't exist in vanilla sh. (Thank you again, @conny, for making me aware of it in a comment on another answer on this page!)

    Note: Alternate version of the above:

    Thanks to liori for pointing out that the above could fall down when comparing identical branches, and coming up with an alternate diff form which removes the sed form from the mix, and makes this "safer" (i.e. it returns a result (namely, the most recent commit) even when you compare master to master):

    As a .git-config line:

    [alias]
        oldest-ancestor = !zsh -c 'diff --old-line-format='' --new-line-format='' <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | head -1' -
    

    From the shell:

    git config --global alias.oldest-ancestor '!zsh -c '\''diff --old-line-format='' --new-line-format='' <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | head -1'\'' -'
    

    So, in my test tree (which was unavailable for a while, sorry; it's back), that now works on both master and topic (giving commits G and B, respectively). Thanks again, liori, for the alternate form.


    So, that's what I [and liori] came up with. It seems to work for me. It also allows an additional couple of aliases that might prove handy:

    git config --global alias.branchdiff '!sh -c "git diff `git oldest-ancestor`.."'
    git config --global alias.branchlog '!sh -c "git log `git oldest-ancestor`.."'
    

    Happy git-ing!

    0 讨论(0)
  • 2020-11-22 10:51

    The following implements git equivalent of svn log --stop-on-copy and can also be used to find branch origin.

    Approach

    1. Get head for all branches
    2. collect mergeBase for target branch each other branch
    3. git.log and iterate
    4. Stop at first commit that appears in the mergeBase list

    Like all rivers run to the sea, all branches run to master and therefore we find merge-base between seemingly unrelated branches. As we walk back from branch head through ancestors, we can stop at the first potential merge base since in theory it should be origin point of this branch.

    Notes

    • I haven't tried this approach where sibling and cousin branches merged between each other.
    • I know there must be a better solution.

    details: https://stackoverflow.com/a/35353202/9950

    0 讨论(0)
  • 2020-11-22 10:52

    If you like terse commands,

    git rev-list $(git rev-list --first-parent ^branch_name master | tail -n1)^^! 

    Here's an explanation.

    The following command gives you the list of all commits in master that occurred after branch_name was created

    git rev-list --first-parent ^branch_name master 

    Since you only care about the earliest of those commits you want the last line of the output:

    git rev-list ^branch_name --first-parent master | tail -n1

    The parent of the earliest commit that's not an ancestor of "branch_name" is, by definition, in "branch_name," and is in "master" since it's an ancestor of something in "master." So you've got the earliest commit that's in both branches.

    The command

    git rev-list commit^^!

    is just a way to show the parent commit reference. You could use

    git log -1 commit^

    or whatever.

    PS: I disagree with the argument that ancestor order is irrelevant. It depends on what you want. For example, in this case

    _C1___C2_______ master
      \    \_XXXXX_ branch A (the Xs denote arbitrary cross-overs between master and A)
       \_____/ branch B
    

    it makes perfect sense to output C2 as the "branching" commit. This is when the developer branched out from "master." When he branched, branch "B" wasn't even merged in his branch! This is what the solution in this post gives.

    If what you want is the last commit C such that all paths from origin to the last commit on branch "A" go through C, then you want to ignore ancestry order. That's purely topological and gives you an idea of since when you have two versions of the code going at the same time. That's when you'd go with merge-base based approaches, and it will return C1 in my example.

    0 讨论(0)
  • 2020-11-22 10:52

    To find commits from the branching point, you could use this.

    git log --ancestry-path master..topicbranch
    
    0 讨论(0)
提交回复
热议问题