Encontrando um ponto de ramificação com o Git?

Eu tenho um repository com ramificações mestre e A e muita atividade de mesclagem entre os dois. Como posso encontrar o commit no meu repository quando o branch A foi criado com base no master?

Meu repository basicamente se parece com isso:

-- X -- A -- B -- C -- D -- F (master) \ / \ / \ / \ / G -- H -- I -- J (branch A) 

Eu estou olhando para a revisão A, que não é o que o git merge-base (--all) encontra.

Eu estava procurando a mesma coisa e encontrei essa pergunta. Obrigado por perguntar!

No entanto, descobri que as respostas que vejo aqui não parecem dar a resposta que você pediu (ou que eu estava procurando) – elas parecem dar o commit de G , ao invés do commit de A

Então, criei a seguinte tree (letras atribuídas em ordem cronológica), para poder testar as coisas:

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

Isso parece um pouco diferente do seu, porque eu queria ter certeza de que recebi (referindo-se a este gráfico, não ao seu) B, mas não A (e não D ou E). Aqui estão as letras anexadas aos prefixos SHA e mensagens de commit (meu repository pode ser clonado daqui , se isso é interessante para qualquer um):

 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 

Então, o objective: encontrar B. Aqui estão três maneiras que eu encontrei, depois de um pouco de ajustes:


1. visualmente, com gitk:

Você deve ver visualmente uma tree como esta (vista do mestre):

captura de tela gitk do mestre

ou aqui (como visto do tópico):

captura de tela gitk do tópico

Em ambos os casos, selecionei o commit B no meu gráfico. Uma vez que você clica nele, o seu SHA completo é apresentado em um campo de input de texto logo abaixo do gráfico.


2. visualmente, mas a partir do terminal:

git log --graph --oneline --all

(Editar / nota lateral: adicionar --decorate também pode ser interessante; adiciona uma indicação de nomes de ramificação, tags, etc. Não adicionando isso à linha de comando acima, pois a saída abaixo não reflete seu uso.)

que mostra (assumindo git config --global color.ui auto ):

saída do git log --graph --oneline --all

Ou, em texto direto:

 * a9546a2 mescla do tópico de volta ao master
 | \  
 |  * 648ca35 mesclando o master no tópico
 |  | \  
 |  * |  132ee2a primeiro commit no branch tópico
 * |  |  e7c863d confirmar no master depois que o master foi mesclado ao tópico
 |  | /  
 | / |   
 * |  37ad159 commit pós-branch no master
 | /  
 * 6aafd7f segundo commit no master antes de ramificar
 * 4112403 commit inicial no master

em ambos os casos, vemos o 6aafd7f commit como o ponto comum mais baixo, ie B no meu gráfico, ou A no seu.


3. Com magia de shell:

Você não especifica em sua pergunta se você queria algo como o acima, ou um único comando que só vai te dar uma revisão, e nada mais. Bem, aqui está o último:

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

Que você também pode colocar no seu ~ / .gitconfig como (nota: o trailing dash é importante; obrigado Brian por chamar atenção para isso) :

 [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' - 

O que poderia ser feito através da seguinte linha de comando (enrolada com aspas):

 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'\'' -' 

Nota: zsh poderia facilmente ter sido bash , mas sh não irá funcionar - a syntax < () não existe em baunilha sh . (Obrigado mais uma vez, @conny, por me informar disso em um comentário sobre outra resposta nesta página!)

Nota: Versão alternativa do acima:

Obrigado a liori por apontar que o acima poderia cair ao comparar ramos idênticos, e chegar a uma forma de diff alternativo que remove a forma sed da mistura, e torna isso "mais seguro" (ou seja, retorna um resultado (ou seja, o commit mais recente) mesmo quando você compara o master ao master):

Como uma linha .git-config:

 [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' - 

Do escudo:

 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'\'' -' 

Então, na minha tree de teste (que estava indisponível por um tempo, desculpe; está de volta), que agora funciona tanto no master quanto no topic (dando os commits G e B, respectivamente). Obrigado novamente, liori, pela forma alternativa.


Então, é isso que eu [e liori] inventamos. Parece funcionar para mim. Ele também permite um par adicional de aliases que podem ser úteis:

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

Feliz git-ing!

Você pode estar procurando por git merge-base :

O git merge-base encontra o (s) melhor (s) ancestral (ais) comum (s) entre dois commits para usar em uma fusão de três vias. Um ancestral comum é melhor que outro ancestral comum se este for um ancestral do primeiro. Um ancestral comum que não possui nenhum ancestral comum melhor é o melhor ancestral comum , isto é, uma base de mesclagem . Observe que pode haver mais de uma base de mesclagem para um par de confirmações.

Eu usei o git rev-list para esse tipo de coisa. Por exemplo, (observe os 3 pontos)

 $ git rev-list --boundary branch-a...master | grep "^-" | cut -c2- 

vai cuspir o ponto de ramificação. Agora não é perfeito; já que você mesclou o mestre na ramificação A algumas vezes, isso dividirá alguns possíveis pontos de ramificação (basicamente, o ponto de ramificação original e, em seguida, cada ponto no qual você mesclou o mestre na ramificação A). No entanto, deve pelo menos diminuir as possibilidades.

Eu adicionei esse comando aos meus aliases em ~/.gitconfig como:

 [alias] diverges = !sh -c 'git rev-list --boundary $1...$2 | grep "^-" | cut -c2-' 

então eu posso chamá-lo como:

 $ git diverges branch-a master 

Se você gosta de comandos concisos,

  git rev-list $ (git lista-rev - primeiro-pai ^ mestre do branch_name | tail -n1) ^^! 

Aqui está uma explicação.

O comando a seguir fornece a lista de todas as confirmações no mestre que ocorreram depois que o branch_name foi criado

  git rev-list - primeiro-pai ^ branch_name master 

Como você só se importa com o primeiro desses commits, você quer a última linha da saída:

  git rev-list ^ branch_name - primeiro-mestre principal |  cauda -n1 

O pai da primeira confirmação que não é um ancestral de “branch_name” é, por definição, “branch_name” e está em “master”, já que é um ancestral de algo em “master”. Então você tem o primeiro commit em ambos os branches.

O comando

  git rev-list commit ^^! 

é apenas uma maneira de mostrar a referência de confirmação pai. Você poderia usar

  git log -1 commit ^ 

como queiras.

PS: Eu não concordo com o argumento de que a ordem ancestral é irrelevante. Depende do que você quer. Por exemplo, neste caso

 _C1___C2_______ mestre
   \\ _XXXXX_ branch A (os Xs denotam cross-overs arbitrários entre master e A)
    \ _____ / ramo B

Faz todo o sentido a saída C2 como o commit “ramificação”. Foi quando o desenvolvedor se ramificou de “mestre”. Quando ele se ramificou, o ramo “B” nem foi fundido em seu ramo! Isto é o que a solução neste post dá.

Se o que você quer é o último commit C tal que todos os caminhos de origem até o último commit na ramificação “A” passem por C, então você quer ignorar a ordem de ancestralidade. Isso é puramente topológico e dá uma idéia de quando você tem duas versões do código indo ao mesmo tempo. Isso é quando você iria com abordagens baseadas em base de mesclagem, e ele retornará C1 no meu exemplo.

Dado que muitas das respostas neste tópico não fornecem a resposta que a pergunta estava pedindo, aqui está um resumo dos resultados de cada solução, juntamente com o script que usei para replicar o repository fornecido na pergunta.

O registro

Criando um repository com a estrutura fornecida, obtemos o log git de:

 $ git --no-pager log --graph --oneline --all --decorate * b80b645 (HEAD, branch_A) J - Work in branch_A branch | * 3bd4054 (master) F - Merge branch_A into branch master | |\ | |/ |/| * | a06711b I - Merge master into branch_A |\ \ * | | bcad6a3 H - Work in branch_A | | * b46632a D - Work in branch master | |/ | * 413851d C - Merge branch_A into branch master | |\ | |/ |/| * | 6e343aa G - Work in branch_A | * 89655bb B - Work in branch master |/ * 74c6405 (tag: branch_A_tag) A - Work in branch master * 7a1c939 X - Work in branch master 

Minha única adição, é a tag que explicita o ponto em que criamos o branch e, portanto, o commit que queremos encontrar.

A solução que funciona

A única solução que funciona é a fornecida por lindes corretamente retorna A :

 $ diff -u < (git rev-list --first-parent branch_A) \ <(git rev-list --first-parent master) | \ sed -ne 's/^ //p' | head -1 74c6405d17e319bd0c07c690ed876d65d89618d5 

Como Charles Bailey aponta, esta solução é muito frágil.

Se você branch_A em master e depois mescla master em branch_A sem intervir commits, então a solução de lindes só lhe dá a primeira divergência mais recente .

Isso significa que, para o meu stream de trabalho, acho que vou ter que ficar marcando o ponto de ramificação de ramificações longas, já que não posso garantir que elas possam ser encontradas com segurança mais tarde.

Isso realmente tudo se resume à falta do que hg chama de ramos nomeados . O blogueiro jhw chama essas linhagens versus famílias em seu artigo Por que eu gosto de Mercurial More Than Git e seu artigo de acompanhamento Mais sobre Mercurial vs. Git (com charts!) . Eu recomendaria que as pessoas as lessem para ver por que alguns convertidos mercuriais não percebem o nome de ramificações no git .

As soluções que não funcionam

A solução fornecida pelo mipadi retorna duas respostas, I e C :

 $ git rev-list --boundary branch_A...master | grep ^- | cut -c2- a06711b55cf7275e8c3c843748daaa0aa75aef54 413851dfecab2718a3692a4bba13b50b81e36afc 

A solução fornecida por Greg Hewgill retorna I

 $ git merge-base master branch_A a06711b55cf7275e8c3c843748daaa0aa75aef54 $ git merge-base --all master branch_A a06711b55cf7275e8c3c843748daaa0aa75aef54 

A solução fornecida por Karl retorna X :

 $ diff -u < (git log --pretty=oneline branch_A) \ <(git log --pretty=oneline master) | \ tail -1 | cut -c 2-42 7a1c939ec325515acfccb79040b2e4e1c3e7bbe5 

O roteiro

 mkdir $1 cd $1 git init git commit --allow-empty -m "X - Work in branch master" git commit --allow-empty -m "A - Work in branch master" git branch branch_A git tag branch_A_tag -m "Tag branch point of branch_A" git commit --allow-empty -m "B - Work in branch master" git checkout branch_A git commit --allow-empty -m "G - Work in branch_A" git checkout master git merge branch_A -m "C - Merge branch_A into branch master" git checkout branch_A git commit --allow-empty -m "H - Work in branch_A" git merge master -m "I - Merge master into branch_A" git checkout master git commit --allow-empty -m "D - Work in branch master" git merge branch_A -m "F - Merge branch_A into branch master" git checkout branch_A git commit --allow-empty -m "J - Work in branch_A branch" 

Eu duvido que a versão git faça muita diferença para isso, mas:

 $ git --version git version 1.7.1 

Obrigado a Charles Bailey por me mostrar uma maneira mais compacta de criar scripts no repository de exemplos.

Em geral, isso não é possível. Em um histórico de ramificações, uma ramificação e mesclagem antes de um ramificação nomeada ser ramificada e um ramo intermediário de dois ramos nomeados parece o mesmo.

No git, os branches são apenas os nomes atuais das dicas das seções da história. Eles realmente não têm uma identidade forte.

Isso geralmente não é um grande problema, já que a base de mesclagem (veja a resposta de Greg Hewgill) de dois commits é geralmente muito mais útil, dando o commit mais recente que os dois branches compartilharam.

Uma solução que depende da ordem dos pais de um commit obviamente não funcionará em situações em que um branch foi totalmente integrado em algum momento do histórico do branch.

 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" 

Essa técnica também cai se uma mesclagem de integração foi feita com os pais invertidos (por exemplo, uma ramificação temporária foi usada para realizar uma mesclagem de teste no mestre e depois encaminhada rapidamente para a ramificação de recurso para continuar a usar).

 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" 

Que tal algo como

 git log --pretty=oneline master > 1 git log --pretty=oneline branch_A > 2 git rev-parse `diff 1 2 | tail -1 | cut -c 3-42`^ 

Eu recentemente precisei resolver esse problema também e acabei escrevendo um script Ruby para isso: https://github.com/vaneyckt/git-find-branching-point

Certamente estou faltando alguma coisa, mas IMO, todos os problemas acima são causados ​​porque estamos sempre tentando encontrar o ponto de ramificação voltando no histórico, e isso causa todos os tipos de problemas por causa das combinações de mesclagem disponíveis.

Em vez disso, eu segui uma abordagem diferente, baseada no fato de que ambos os ramos compartilham muita história, exatamente toda a história antes da ramificação é 100% a mesma, então ao invés de voltar, minha proposta é sobre ir adiante commit), procurando a 1ª diferença em ambos os ramos. O ponto de ramificação será, simplesmente, o pai da primeira diferença encontrada.

Na prática:

 #!/bin/bash diff < ( git rev-list "${1:-master}" --reverse --topo-order ) \ <( git rev-list "${2:-HEAD}" --reverse --topo-order) \ --unified=1 | sed -ne 's/^ //p' | head -1 

E está resolvendo todos os meus casos habituais. Claro que existem fronteiras que não são cobertas, mas ... ciao 🙂

Depois de muita pesquisa e discussões, fica claro que não há mágica que funcione em todas as situações, pelo menos não na versão atual do Git.

É por isso que escrevi alguns patches que adicionam o conceito de um branch tail . Cada vez que uma ramificação é criada, um ponteiro para o ponto original é criado também, a tail ref. Esta referência é atualizada toda vez que a ramificação é rebaseada.

Para descobrir o ponto de ramificação da ramificação de devel, tudo que você precisa fazer é usar devel@{tail} , é isso.

https://github.com/felipec/git/commits/fc/tail

O seguinte comando irá revelar o SHA1 do Commit A

git merge-base --fork-point A

Aqui está uma versão melhorada da minha resposta anterior . Ele depende das mensagens de consolidação das mesclagens para descobrir onde a ramificação foi criada pela primeira vez.

Ele funciona em todos os repositorys mencionados aqui, e eu até resolvi alguns truques que apareceram na lista de discussão . Eu também escrevi testes para isso.

 find_merge () { local selection extra test "$2" && extra=" into $2" git rev-list --min-parents=2 --grep="Merge branch '$1'$extra" --topo-order ${3:---all} | tail -1 } branch_point () { local first_merge second_merge merge first_merge=$(find_merge $1 "" "$1 $2") second_merge=$(find_merge $2 $1 $first_merge) merge=${second_merge:-$first_merge} if [ "$merge" ]; then git merge-base $merge^1 $merge^2 else git merge-base $1 $2 fi } 

Para encontrar commits do ponto de ramificação, você poderia usar isso.

 git log --ancestry-path master..topicbranch 

Às vezes é efetivamente impossível (com algumas exceções de onde você pode ter a sorte de ter dados adicionais) e as soluções aqui não funcionam.

O Git não preserva o histórico de referências (que inclui ramificações). Apenas armazena a posição atual de cada ramificação (a cabeça). Isso significa que você pode perder algum histórico de ramificações no git ao longo do tempo. Sempre que você se ramifica por exemplo, é imediatamente perdido qual ramificação era a original. Tudo que um ramo faz é:

 git checkout branch1 # refs/branch1 -> commit1 git checkout -b branch2 # branch2 -> commit1 

Você pode assumir que o primeiro comprometido é o ramo. Isso tende a ser o caso, mas nem sempre é assim. Não há nada que o impeça de se comprometer em qualquer ramificação primeiro após a operação acima. Além disso, os timestamps do git não são confiáveis. Não é até você se comprometer com ambos que eles realmente se tornam filiais estruturalmente.

Enquanto nos diagramas tendemos a numerar os commits conceitualmente, o git não possui um conceito estável real de sequência quando a tree de commit se ramifica. Nesse caso, você pode assumir que os números (ordem de indicação) são determinados pelo registro de data e hora (pode ser divertido ver como uma git UI lida com as coisas quando você configura todos os registros de data e hora para o mesmo).

Isto é o que um ser humano espera conceitualmente:

 After branch: C1 (B1) / - \ C1 (B2) After first commit: C1 (B1) / - \ C1 - C2 (B2) 

Isso é o que você realmente recebe:

 After branch: - C1 (B1) (B2) After first commit (human): - C1 (B1) \ C2 (B2) After first commit (real): - C1 (B1) - C2 (B2) 

Você assumiria que B1 era a ramificação original, mas, na verdade, ela poderia ser simplesmente uma ramificação morta (alguém fez o checkout -b, mas nunca se comprometeu com ela). Não é até você se comprometer com ambos que você obtém uma estrutura de ramificação legítima dentro do git:

 Either: / - C2 (B1) -- C1 \ - C3 (B2) Or: / - C3 (B1) -- C1 \ - C2 (B2) 

Você sempre sabe que C1 veio antes de C2 e C3, mas você nunca sabe se o C2 veio antes do C3 ou do C3 vir antes do C2 (porque você pode definir o tempo em sua estação de trabalho para qualquer coisa, por exemplo). B1 e B2 também são enganosos, já que você não pode saber qual ramificação veio primeiro. Você pode fazer uma suposição muito boa e geralmente precisa em muitos casos. É um pouco como uma pista de corrida. Todas as coisas geralmente sendo iguais com os carros, então você pode assumir que um carro que vem em uma volta para trás começou uma volta atrás. Também temos convenções que são muito confiáveis, por exemplo, o mestre quase sempre representa os ramos mais antigos, embora, infelizmente, tenha visto casos em que nem isso é o caso.

O exemplo dado aqui é um exemplo de preservação de histórico:

 Human: - X - A - B - C - D - F (B1) \ / \ / G - H ----- I - J (B2) Real: B ----- C - D - F (B1) / / \ / - X - A / \ / \ / \ / G - H ----- I - J (B2) 

O real aqui também é enganador porque nós, como humanos, lemos da esquerda para a direita, raiz para folha (ref). Git não faz isso. Onde fazemos (A-> B) em nossas cabeças, o git faz (A < -B ou B-> A). Ele lê de ref para root. Refs podem estar em qualquer lugar, mas tendem a ser folhas, pelo menos para ramos ativos. Uma referência aponta para um commit e commits contém apenas um like para seu pai / s, não para seus filhos. Quando um commit é um commit merge, ele terá mais de um pai. O primeiro pai é sempre o commit original que foi mesclado. Os outros pais são sempre confirmações que foram mescladas no commit original.

 Paths: F->(D->(C->(B->(A->X)),(H->(G->(A->X))))),(I->(H->(G->(A->X))),(C->(B->(A->X)),(H->(G->(A->X))))) J->(I->(H->(G->(A->X))),(C->(B->(A->X)),(H->(G->(A->X))))) 

Esta não é uma representação muito eficiente, e sim uma expressão de todos os caminhos que o git pode tomar de cada referência (B1 e B2).

O armazenamento interno do Git parece mais com isso (não que A como pai aparece duas vezes):

  F->D,I | D->C | C->B,H | B->A | A->X | J->I | I->H,C | H->G | G->A 

Se você despejar um commit bruto do git, verá zero ou mais campos pai. Se houver zero, isso significa que não há pai e o commit é uma raiz (você pode ter várias raízes). Se houver, significa que não houve mesclagem e não é um commit de root. Se houver mais de um, significa que o commit é o resultado de uma mesclagem e que todos os pais após o primeiro são merge commits.

 Paths simplified: F->(D->C),I | J->I | I->H,C | C->(B->A),H | H->(G->A) | A->X Paths first parents only: F->(D->(C->(B->(A->X)))) | F->D->C->B->A->X J->(I->(H->(G->(A->X))) | J->I->H->G->A->X Or: F->D->C | J->I | I->H | C->B->A | H->G->A | A->X Paths first parents only simplified: F->D->C->B->A | J->I->->G->A | A->X Topological: - X - A - B - C - D - F (B1) \ G - H - I - J (B2) 

Quando ambos acertarem A, sua corrente será a mesma, antes que sua corrente seja completamente diferente. O primeiro commit que outros dois commits têm em comum é o ancestral comum e de onde eles divergiram. pode haver alguma confusão aqui entre os termos commit, branch e ref. Você pode, de fato, mesclar um commit. Isso é o que a mesclagem realmente faz. Um ref simplesmente aponta para um commit e um branch é nada mais que um ref na pasta .git / refs / heads, a localização da pasta é o que determina que um ref é um branch em vez de algo como uma tag.

Onde você perde a história é que a mesclagem fará uma das duas coisas dependendo das circunstâncias.

Considerar:

  / - B (B1) - A \ - C (B2) 

Nesse caso, uma mesclagem em qualquer direção criará uma nova confirmação com o primeiro pai como a confirmação apontada pela ramificação de check-out atual e o segundo pai como a confirmação na ponta da ramificação que você mesclou na sua ramificação atual. Ele precisa criar um novo commit, já que ambos os branches possuem mudanças desde o ancestral comum que deve ser combinado.

  / - B - D (B1) - A / \ --- C (B2) 

Neste ponto, D (B1) agora possui os dois conjuntos de mudanças de ambos os ramos (ele próprio e B2). No entanto, o segundo ramo não tem as alterações de B1. Se você mesclar as mudanças de B1 para B2 para que elas sejam sincronizadas, então você pode esperar algo parecido com isso (você pode forçar o git merge a fazer isso assim com –no-ff):

 Expected: / - B - D (B1) - A / \ \ --- C - E (B2) Reality: / - B - D (B1) (B2) - A / \ --- C 

Você obterá isso mesmo se B1 tiver commits adicionais. Contanto que não haja mudanças em B2 que B1 não tenha, as duas ramificações serão mescladas. Ele faz um avanço rápido que é como um rebase (rebases também comem ou lineariza o histórico), exceto ao contrário de um rebase, como apenas um branch tem um conjunto de mudanças, ele não precisa aplicar um changeset de um branch em cima do outro.

 From: / - B - D - E (B1) - A / \ --- C (B2) To: / - B - D - E (B1) (B2) - A / \ --- C 

Se você parar de trabalhar em B1, então as coisas são muito bem para preservar a história a longo prazo. Apenas B1 (que pode ser master) avançará normalmente, de modo que a localização de B2 no histórico de B2 represente com sucesso o ponto em que foi mesclado em B1. Isso é o que o git espera que você faça, ramificar B de A, então você pode mesclar A em B tanto quanto você quiser, à medida que as alterações se acumulam, mas ao fundir B de volta em A, não é esperado que você trabalhe em B e mais . If you carry on working on your branch after fast forward merging it back into the branch you were working on then your erasing B’s previous history each time. You’re really creating a new branch each time after fast forward commit to source then commit to branch. You end up with when you fast forward commit is lots of branches/merges that you can see in the history and structure but without the ability to determine what the name of that branch was or if what looks like two separate branches is really the same branch.

  0 1 2 3 4 (B1) /-\ /-\ /-\ /-\ / ---- - - - - \-/ \-/ \-/ \-/ \ 5 6 7 8 9 (B2) 

1 to 3 and 5 to 8 are structural branches that show up if you follow the history for either 4 or 9. There’s no way in git to know which of this unnamed and unreferenced structural branches belong to with of the named and references branches as the end of the structure. You might assume from this drawing that 0 to 4 belongs to B1 and 4 to 9 belongs to B2 but apart from 4 and 9 was can’t know which branch belongs to which branch, I’ve simply drawn it in a way that gives the illusion of that. 0 might belong to B2 and 5 might belong to B1. There are 16 different possibilies in this case of which named branch each of the structural branches could belong to. This is assuming that none of these structural branches came from a deleted branch or as a result of merging a branch into itself when pulling from master (the same branch name on two repos is infact two branches, a separate repository is like branching all branches).

There are a number of git strategies that work around this. You can force git merge to never fast forward and always create a merge branch. A horrible way to preserve branch history is with tags and/or branches (tags are really recommended) according to some convention of your choosing. I realy wouldn’t recommend a dummy empty commit in the branch you’re merging into. A very common convention is to not merge into an integration branch until you want to genuinely close your branch. This is a practice that people should attempt to adhere to as otherwise you’re working around the point of having branches. However in the real world the ideal is not always practical meaning doing the right thing is not viable for every situation. If what you’re doing on a branch is isolated that can work but otherwise you might be in a situation where when multiple developers are working one something they need to share their changes quickly (ideally you might really want to be working on one branch but not all situations suit that either and generally two people working on a branch is something you want to avoid).

I seem to be getting some joy with

 git rev-list branch...master 

The last line you get is the first commit on the branch, so then it’s a matter of getting the parent of that. assim

 git rev-list -1 `git rev-list branch...master | tail -1`^ 

Seems to work for me and doesn’t need diffs and so on (which is helpful as we don’t have that version of diff)

Correction: This doesn’t work if you are on the master branch, but I’m doing this in a script so that’s less of an issue

The problem appears to be to find the most recent, single-commit cut between both branches on one side, and the earliest common ancestor on the other (probably the initial commit of the repo). This matches my intuition of what the “branching off” point is.

That in mind, this is not at all easy to compute with normal git shell commands, since git rev-list — our most powerful tool — doesn’t let us restrict the path by which a commit is reached. The closest we have is git rev-list --boundary , which can give us a set of all the commits that “blocked our way”. (Note: git rev-list --ancestry-path is interesting but I don’t how to make it useful here.)

Here is the script: https://gist.github.com/abortz/d464c88923c520b79e3d . It’s relatively simple, but due to a loop it’s complicated enough to warrant a gist.

Note that most other solutions proposed here can’t possibly work in all situations for a simple reason: git rev-list --first-parent isn’t reliable in linearizing history because there can be merges with either ordering.

git rev-list --topo-order , on the other hand, is very useful — for walking commits in topographic order — but doing diffs is brittle: there are multiple possible topographic orderings for a given graph, so you are depending on a certain stability of the orderings. That said, strongk7’s solution probably works damn well most of the time. However it’s slower that mine as a result of having to walk the entire history of the repo… twice. 🙂

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

Abordagem

  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.

Notas

  • 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

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!)

You could use the following command to return the oldest commit in branch_a, which is not reachable from master:

 git rev-list branch_a ^master | tail -1 

Perhaps with an additional sanity check that the parent of that commit is actually reachable from master…

You can examine the reflog of branch A to find from which commit it was created, as well as the full history of which commits that branch pointed to. Reflogs are in .git/logs .

I believe I’ve found a way that deals with all the corner-cases mentioned here:

 branch=branch_A merge=$(git rev-list --min-parents=2 --grep="Merge.*$branch" --all | tail -1) git merge-base $merge^1 $merge^2 

Charles Bailey is quite right that solutions based on the order of ancestors have only limited value; at the end of the day you need some sort of record of “this commit came from branch X”, but such record already exists; by default ‘git merge’ would use a commit message such as “Merge branch ‘branch_A’ into master”, this tells you that all the commits from the second parent (commit^2) came from ‘branch_A’ and was merged to the first parent (commit^1), which is ‘master’.

Armed with this information you can find the first merge of ‘branch_A’ (which is when ‘branch_A’ really came into existence), and find the merge-base, which would be the branch point 🙂

I’ve tried with the repositories of Mark Booth and Charles Bailey and the solution works; how couldn’t it? The only way this wouldn’t work is if you have manually changed the default commit message for merges so that the branch information is truly lost.

For usefulness:

 [alias] branch-point = !sh -c 'merge=$(git rev-list --min-parents=2 --grep="Merge.*$1" --all | tail -1) && git merge-base $merge^1 $merge^2' 

Then you can do ‘ git branch-point branch_A ‘.

Enjoy 😉