Search code examples
gitbranchgit-branchgit-log

git log showing source of a branch


I have a repos with several branches and I would like to be able to tell the initial source branch from which each branch was started. For example:

  1. Initial checkin of master
  2. make changes to master
  3. branch master to featureA
  4. make changes to featureA
  5. branch featureA to featureB
  6. make changes to featureB
  7. merge featureB into master

or if you want the commands:

git clone <url> test
cd test

echo "Test">test.txt
git add .
git commit -m "Initial checkin"
git push

echo "Updates">>test.txt
git add .
git commit -m "Updates"
git push

git branch featureA
git checkout featureA
git push --set-upstream origin featureA
echo "Updates featureA">>test.txt
git add .
git commit -m "Updates to featureA"
git push

git branch featureB
git checkout featureB
git push --set-upstream origin featureB
echo "Updates featureB">>test.txt
git add .
git commit -m "Updates to featureB"
git push

git checkout master
git merge origin/featureB
git push 

but when I run a git log --all --source I don't see where featureB came from:

commit d5f1a9d511ff349a35befbe7aa4f41aca75a0e5a refs/heads/featureB
Author: itsme mario
Date:   Tue Oct 11 15:16:49 2022 -0400

    Updates to featureB

commit f66006c5d87ee2a507da39aa8a8d6f354b454bb8 refs/heads/featureA
Author: itsme mario
Date:   Tue Oct 11 15:15:28 2022 -0400

    Updates to featureA

commit 9f433234c228029b5efba118001f0afc8ab5c4ee refs/heads/featureA
Author: itsme mario
Date:   Tue Oct 11 15:13:52 2022 -0400

    Updates

commit 57d41e78fea121977aa7e52177901ac77109b8bb refs/heads/featureA
Author: itsme mario
Date:   Tue Oct 11 15:13:23 2022 -0400

    Initial checkin

if I a graph it doesn't show the different branches either git log --all --source --graph:

* commit d5f1a9d511ff349a35befbe7aa4f41aca75a0e5a   refs/heads/featureB
| Author: itsme mario
| Date:   Tue Oct 11 15:16:49 2022 -0400
| 
|     Updates to featureB
| 
* commit f66006c5d87ee2a507da39aa8a8d6f354b454bb8   refs/heads/featureA
| Author: itsme mario
| Date:   Tue Oct 11 15:15:28 2022 -0400
| 
|     Updates to featureA
| 
* commit 9f433234c228029b5efba118001f0afc8ab5c4ee   refs/heads/featureA
| Author: itsme mario
| Date:   Tue Oct 11 15:13:52 2022 -0400
| 
|     Updates
| 
* commit 57d41e78fea121977aa7e52177901ac77109b8bb   refs/heads/featureA
  Author: itsme mario
  Date:   Tue Oct 11 15:13:23 2022 -0400
  
      Initial checkin

What can I do to have the git log show where featureB came from (aka featureA via master)? Thank you!


Solution

  • https://jvns.ca/blog/2023/11/23/branches-intuition-reality/ blog post is very relevant.


    Git branches are just tip pointers, and move over time.
    Git doesn't remember "on what branch I (HEAD) was back when I made the commit". It can only tell you where branches point now, and maybe help you reconstruct some semantics from that.

    1. Use --decorate flag to at least see where branches point now:

      > git log --all --graph --oneline --decorate
      * 391f2b9 (HEAD -> master, featureB) Updates to featureB
      * 0517f37 (featureA) Updates to featureA
      * 6e4aef1 Updates
      * 3598c0b Initial checkin
      
    2. Your example is particularly confusing because the merge back to master was a trivial "fast-forward" — git simply advanced master to point to same place as featureB. But in most "merge request => merge" workflows, the fact of even trivial merges is retained by using git merge --no-ff (I think that's the default in both GitHub and GitLab, configurable).
      Let's rewind history and try that:

      > git switch -C master HEAD~2
      > git merge --no-ff featureB
      > git log --all --graph --oneline --decorate
      *   c7bb409 (HEAD -> master) Merge branch 'featureB'
      |\  
      | * 391f2b9 (featureB) Updates to featureB
      | * 0517f37 (featureA) Updates to featureA
      |/  
      * 6e4aef1 Updates
      * 3598c0b Initial checkin
      
    3. When a commit like "Updates" (9f433234c228029b5efba118001f0afc8ab5c4ee in your output, 6e4aef1 in mine) is reachable both from master and featureA (and featureB too in this case), you intuitively say that it "is on master" because master is a long-lived branch, special to your workflow.
      Git doesn't know that. All it knows is that commit is an ancestor of all those branches. But, if we have a real merge commit like we forced with --no-ff, that commit has 2 parents:

      > git show c7bb409 | head -n2
      commit c7bb4099b802e4ebf5b75307a7ebe7a9028f3eb7
      Merge: 6e4aef1 391f2b9
      

      and the order merge parents matters a little! It matters to how most tools draw the graph, and conceptually:

      • parent c7bb409^1 == 6e4aef1 is drawn on the left and is assumed to be "the branch into which we merged" and
      • parent c7bb409^2 == 391f2b9 is drawn on the right and is "the branch that was merged". It's also clear from the default commit message git merge used: "Merge branch 'featureB'" but that's for you, Git doesn't parse English.

    So, what information we use to try to infer an answer to your question?

    I would like to be able to tell the initial source branch from which each branch was started

    We can follow the left-most parents from master to see the commits that "belong to master":

    c7bb409 (HEAD -> master) Merge branch 'featureB'
    6e4aef1 Updates
    3598c0b Initial checkin
    

    We know featureA is directly descended from one of those (6e4aef1 Updates), so that was clearly "branched from master".
    We know featureB was merged back to master, "from the right" (2nd parent), so we tend to think of it as a secondary branch in the sense "it is descended from master" rather than "master is descended from featureB". However, (as same blog mentions) many people also merge from master into feature branches, and if that happened, we'd have to overrule that based on our human intuition about workflows, based on "master" vs. "featureB" names.

    But you also expect us to say "featureB is branched from featureA" and not "from master" right? Well, you can see that in the graph because featureA still points to that point. It would become unclear if further work happened on A:

    > git switch featureA
    > echo "Updates featureA">>test.txt
    > git add .
    > git commit -m "More featureA work"
    > git log --all --graph --oneline --decorate
    * 9e92771 (HEAD -> featureA) More featureA work
    | *   c7bb409 (master) Merge branch 'featureB'
    | |\  
    | | * 391f2b9 (featureB) Updates to featureB
    | |/  
    |/|   
    * | 0517f37 Updates to featureA
    |/  
    * 6e4aef1 Updates
    * 3598c0b Initial checkin
    

    Somewhat confusing because now featureA is on left side of the diagram (as newest commit). Oh well.

    The point is, now both featureA & featureB are descended from same commit (0517f37 Updates to featureA) and you can't be certain from the graph whether "B from branched from A", "A was branched from B", or both were "branched from C" which was deleted (or never pushed). You only get a hint from commit message "Updates to featureA" that author thought of that as part of A work.

    Here is the confusing part about reading graphs:

    Merges are ordered, but _forks are symmetric 😕

    Looking at "fork point" 0517f37 in above graph, the fact A continues on the left and B branches to the right is an accident of the order git walked the graph!
    The is no "order of children" stored anywhere. In other situations, or with other tools, you can see that fork differently!

    If there was latter work on master, or if that "More featureA work" happened earlier, we'd see this:

    > git switch featureA
    > env GIT_AUTHOR_DATE='2023-12-07 13:20:00 +0200' GIT_COMMITTER_DATE='2023-12-07 13:20:00 +0200' git commit --amend
    > git log --all --graph --oneline --decorate
    *   c7bb409 (master) Merge branch 'featureB'
    |\  
    | * 391f2b9 (featureB) Updates to featureB
    | | * 7f339fa (HEAD -> featureA) More featureA work
    | |/  
    | * 0517f37 Updates to featureA
    |/  
    * 6e4aef1 Updates
    * 3598c0b Initial checkin
    

    See? Now featureA is to the right of featureB. 🔁

    git show-branch

    You've used --source in your question, which is handy flag but ambiguous — when same commit is reachable from multiple refs (esp. common with --all), git will pick one arbitrarily.

    I just learned here about show-branch which makes the symmetry clearer:

    > git switch master
    > git show-branch --sparse --sha1-name --more=1000 master featureA featureB
    * [master] Merge branch 'featureB'
     ! [featureA] More featureA work
      ! [featureB] Updates to featureB
    ---
    -   [c7bb409] Merge branch 'featureB'
    * + [391f2b9] Updates to featureB
     +  [7f339fa] More featureA work
    *++ [0517f37] Updates to featureA
    *++ [6e4aef1] Updates
    *++ [3598c0b] Initial checkin
    

    (The order of the headers and columns is exactly what I give on command line. The * chars highlight HEAD, master in this case.)
    Note how our fork point between A and B — 0517f37 "Updates to featureA" — is marked as "belonging" to both featureA and featureB (++). And to master as well (*) — it's reachable because featureB was merged into master.

    What I'm trying to show here again, Git doesn't keep "this commit was made on branch A". It only knows which branch(es) descend from it — which can be more than one!
    And show-branch can reflect those "more than one" situations.