Возможно, вы сможете сделать это с помощью xpath. что-то вроде //tr[contains(@class, 'row') and position() mod 2 = 0]
может работать. Существуют другие вопросы SO, расширяющие подробности о том, как более точно сопоставлять классы.
Потерпи меня немного, прежде чем я отвечу на вопрос в том виде, как он задан. Один из ранних ответов правильный, но есть маркировка и другие относительно незначительные (но потенциально запутанные) проблемы, поэтому я хочу начать с чертежей ветвей и меток ветвей. Кроме того, люди, пришедшие из других систем, или, может быть, просто новички в управлении ревизиями и git, часто думают о ветвях как о «линиях развития», а не как «следах истории» (git реализует их как последние, а не как первые, поэтому коммит не обязательно находится на какой-либо конкретной «линии развития»).
Во-первых, есть небольшая проблема с тем, как вы нарисовали свой график:
*------*---*
Master \
*---*--*------*
A \
*-----*-----*
B (HEAD)
Вот тот же самый график, но с метками, нарисованными по-разному, и добавлены еще некоторые стрелки (и я нумеруются узлы коммитов для использования ниже):
0 <- 1 <- 2 <-------------------- master
\
3 <- 4 <- 5 <- 6 <------ A
\
7 <- 8 <- 9 <-- HEAD=B
Почему это важно, потому что git совершенно не понимает, что означает фиксация «на» некоторой ветви - или, возможно, лучше сказать, что некоторый коммит "содержится" в некотором наборе ветвей. Коммиты не могут быть перемещены или изменены, но метки ветви могут и действительно перемещаться.
В частности, имя ветви , например, master
, A
или B
, указывает на один конкретный коммит . В этом случае master
указывает на фиксацию 2, A
указывает на фиксацию 6 и B
указывает на фиксацию 9. Первые несколько коммитов с 0 по 2 содержатся во всех трех ветвях; коммиты 3, 4 и 5 содержатся как в A
, так и в B
; коммит 6 содержится только внутри A
; и коммиты с 7 по 9 содержатся только в B
. (Кстати, несколько имен могут указывать на один и тот же коммит, и это нормально, когда вы создаете новую ветвь.)
Прежде чем мы продолжим, позвольте мне перерисовать график еще одним способом:
0
\
1
\
2 <-- master
\
3 - 4 - 5
|\
| 6 <-- A
\
7
\
8
\
9 <-- HEAD=B
Это только подчеркивает, что важна не горизонтальная линия коммитов, а отношения родитель / потомок. Метка ветви указывает на начальную фиксацию, и затем (по крайней мере, как рисуются эти графики) мы перемещаемся влево, возможно, также поднимаемся или опускаемся, чтобы найти родительские коммиты.
Когда вы перебазируете коммиты, вы фактически копируете эти коммиты.
Есть одно «истинное имя» для любого коммита (или даже любого объекта в репозитории git), которое является его SHA-1: эта строка из 40 шестнадцатеричных цифр, такая как 9f317ce...
, который вы видите в git log
, например. SHA-1 представляет собой криптографическую 1 sup> контрольную сумму содержимого объекта. Содержимое содержит информацию об авторе и коммиттере (имя и адрес электронной почты), метки времени, исходное дерево и список родительских коммитов. Родителем коммита №7 всегда является коммит №5. Если вы делаете в основном точную копию коммита # 7, но устанавливаете его родительский коммит # 2 вместо коммита # 5, вы получаете другой коммит с другим идентификатором. (У меня заканчиваются однозначные цифры - обычно я использую заглавные буквы для представления идентификаторов фиксации, но с ветвями с именами A
и B
я подумал, что это сбивает с толку. Поэтому я назову копию # 7, # 7a, ниже.)
git rebase
делает Когда вы просите git перебазировать цепочку коммитов - например, коммиты # 7-8-9 выше - он имеет скопировать их , по крайней мере, если они собираются куда-то двигаться (если они не двигаются, они могут просто оставить оригиналы на месте). По умолчанию он копирует коммиты из проверенной в данный момент ветки, поэтому git rebase
требуется только две дополнительные части информации:
Когда вы запускаете git rebase <upstream>
, вы позволяете git вычислять обе части из одного фрагмента информации. , Когда вы используете --onto
, вы получаете возможность рассказать git отдельно об обеих частях: вы все еще предоставляете upstream
, но он не вычисляет цель из <upstream>
, он только вычисляет коммиты для копирования из <upstream>
. (Между прочим, я думаю, что <upstream>
не является хорошим именем, но это то, что использует rebase, и у меня нет ничего лучше, поэтому давайте придерживаться этого здесь. Rebase вызывает target <newbase>
, но Я думаю, что target - гораздо лучшее имя.)
Давайте сначала рассмотрим эти два варианта. Оба предполагают, что вы находитесь на ветке B
в первую очередь:
git rebase master
git rebase --onto master A
С первой командой аргументом <upstream>
для rebase
является master
. Со вторым это A
.
Вот как git вычисляет, который выполняет копирование: он передает текущую ветвь в git rev-list
, а также передает <upstream>
в git rev-list
, но с использованием --not
- или, точнее, с эквивалентом двухточечной нотации exclude..include
. Это означает, что нам нужно знать, как работает git rev-list
.
Хотя git rev-list
чрезвычайно сложно - большинство команд git заканчивают тем, что используют его; это движок для git log
, git bisect
, rebase
, filter-branch
и т. д. - этот конкретный случай не слишком сложен: в двухточечной нотации rev-list
перечислены все коммиты, достижимые справа. со стороны (включая этот коммит), исключая каждый коммит, достижимый с левой стороны.
В этом случае git rev-list HEAD
находит все коммиты, достижимые из HEAD
, то есть почти все коммиты: коммиты 0-5 и 7-9, а git rev-list master
находит все коммиты, достижимые из master
, который Фиксация # 0, 1 и 2. Вычитание 0-через-2 из 0-5,7-9 оставляет 3-5,7-9. Это кандидат обязуется скопировать, как указано в git rev-list master..HEAD
.
Для нашей второй команды у нас есть A..HEAD
вместо master..HEAD
, поэтому коммиты для вычитания равны 0-6. Коммит № 6 не появляется в наборе HEAD
, но это нормально: вычитая что-то, чего нет, оставляет его там. Таким образом, число получаемых кандидатов на копирование равно 7-9.
Это все еще оставляет нас с определением цели перебазирования, то есть, где должен быть скопирован коммит земли? Для второй команды ответом является «фиксация, определенная аргументом --onto
». Поскольку мы сказали --onto master
, это означает, что целью является коммит №2.
git rebase master
Однако с помощью первой команды мы не указали цель напрямую, поэтому git использует фиксацию, идентифицированную <upstream>
. <upstream>
, которое мы дали, было master
, которое указывает на коммит №2, поэтому целью является коммит №2.
Поэтому первая команда будет начинаться с копирования коммита # 3 с любыми минимальными изменениями, необходимыми для того, чтобы его родитель был коммитом № 2. Его родителем является уже коммит # 2. Ничего не должно меняться, поэтому ничего не меняется, и rebase просто повторно использует существующий коммит # 3. Затем он должен скопировать # 4, чтобы его родитель # 3, но родитель уже # 3, поэтому он просто повторно использует # 4. Точно так же # 5 это уже хорошо. Он полностью игнорирует # 6 (это не входит в набор коммитов для копирования); он проверяет # 7-9, но все они тоже хороши, так что вся перебазировка заканчивается просто повторным использованием всех оригинальных коммитов. В любом случае вы можете принудительно копировать копии с -f
, но вы этого не сделали, поэтому весь этот перебаз в конечном итоге ничего не делает.
git rebase --onto master A
Вторая команда rebase использовала --onto
для выбора # 2 в качестве своей цели, но сказала git для копирования только что зафиксировала 7-9. Родителем коммита # 7 является коммит №5, поэтому эта копия действительно должна что-то делать. 2 sup> Таким образом, git делает новый коммит - давайте назовем это # 7a - который имеет коммит № 2 в качестве родителя. Перебазировка переходит к фиксации # 8: копии теперь требуется # 7a в качестве ее родителя. Наконец, rebase переходит к коммиту # 9, которому нужен # 8a в качестве родителя. Со всеми скопированными коммитами последнее, что делает rebase, это перемещает метку (помните, метки перемещаются и меняются!). Это дает график, подобный следующему:
7a - 8a - 9a <-- HEAD=B
/
0 - 1 - 2 <-- master
\
3 - 4 - 5 - 6 <-- A
\
7 - 8 - 9 [abandoned]
git rebase --onto master A B
? Это почти то же самое, что и git rebase --onto master A
. Разница в том, что дополнительные B
в конце. К счастью, эта разница очень проста: если вы дадите git rebase
один дополнительный аргумент, сначала он запускает git checkout
для этого аргумента. 3 sup>
В своем первом наборе команд вы выполнили git rebase master
, находясь на ветке B
]. Как отмечалось выше, это большой запрет: так как ничего не нужно перемещать, git вообще ничего не копирует (если вы не используете -f
/ --force
, чего вы не делали). Затем вы проверили master
и использовали git merge B
, который - если это сказано 4 sup> - создает новый коммит с слиянием. Поэтому Ответ Дерика , по крайней мере, на тот момент, когда я его видел, является правильным: коммит слияния имеет двух родителей, один из которых является вершиной ветви B
, и эта ветвь проходит через три коммиты, которые находятся на ветке A
и, следовательно, некоторые из того, что на A
, в конечном итоге объединяются в master
.
Со своей второй последовательностью команд вы сначала извлекли B
(вы уже были на B
, так что это было избыточно, но было частью git rebase
). Затем вам пришлось перебазировать копию трех коммитов, получая последний график выше, с коммитами 7a, 8a и 9a. Затем вы проверили master
и сделали коммит слияния с B
(снова см. Сноску 4). Опять же, ответ Дерика правильный: единственное, чего не хватает, так это того, что исходные, оставленные коммиты не затянуты, и не так очевидно, что новые объединенные коммиты являются копиями.
1 sup> Это имеет значение только в том смысле, что чрезвычайно сложно нацелиться на конкретную контрольную сумму. То есть, если кто-то , которому вы доверяете, скажет вам: «Я доверяю коммиту с идентификатором 1234567 ...», для кого-то еще - кого-то, кому вы не так доверяете, почти невозможно придумать коммит, который имеет тот же идентификатор, но имеет другое содержимое. Вероятность того, что это произойдет случайно, составляет 1 к 2 160 sup>, что намного меньше вероятности того, что у вас случится сердечный приступ при ударе молнии при утоплении в цунами при похищении космическими инопланетянами. : -)
2 sup> Фактическая копия сделана с использованием эквивалента git cherry-pick
: git сравнивает дерево коммита с деревом его родителя, чтобы получить diff, затем применяет diff к дереву нового родителя.
3 sup> Это на самом деле, на самом деле, в настоящий момент буквально верно: git rebase
- это сценарий оболочки, который анализирует ваши параметры, а затем решает, какой тип внутренней перебазировки выполнить: неинтерактивный git-rebase--am
или интерактивный git-rebase--interactive
. После выяснения всех аргументов, если есть один оставшийся аргумент имени ветви, скрипт выполняет git checkout <branch-name>
перед началом внутренней ребазировки.
4 sup> Поскольку master
указывает на коммит 2, а коммит 2 является прародителем коммита 9, он обычно не делает коммит слияния в конце концов, а вместо этого делает то, что Git называет перемотка вперед . Вы можете указать Git не выполнять эти перемотки вперед, используя git merge --no-ff
. Некоторые интерфейсы, такие как веб-интерфейс GitHub и, возможно, некоторые графические интерфейсы, могут разделять различные виды операций, так что их «объединение» вызывает истинное слияние, подобное этому.
При слиянии в ускоренном режиме последний граф для первого случая выглядит следующим образом:
0 <- 1 <- 2 [master used to be here]
\
3 <- 4 <- 5 <- 6 <------ A
\
7 <- 8 <- 9 <-- master, HEAD=B
В любом случае коммиты с 1 по 9 теперь находятся на обеих ветвях , master
и B
. Разница по сравнению с истинным слиянием заключается в том, что на графике вы можете увидеть историю, которая включает слияние.
Другими словами, преимущество ускоренного слияния состоит в том, что оно не оставляет следов того, что в противном случае является тривиальной операцией. Недостаток быстрого слияния состоит в том, что он не оставляет следов. Таким образом, вопрос о том, разрешить ли быструю перемотку вперед, действительно является вопросом о том, хотите ли вы оставить явное слияние в истории, сформированной коммитами.
Перед любой из указанных операций ваш репозиторий выглядит следующим образом
o---o---o---o---o master
\
x---x---x---x---x A
\
o---o---o B
После стандартной перезагрузки (без --onto master
) структура будет:
o---o---o---o---o master
| \
| x'--x'--x'--x'--x'--o'--o'--o' B
\
x---x---x---x---x A
... где x'
- это коммиты из ветви A
. (Обратите внимание, что теперь они дублируются у основания ветви B
.)
Вместо этого, ребаз с --onto master
создаст следующую более чистую и простую структуру:
o---o---o---o---o master
| \
| o'--o'--o' B
\
x---x---x---x---x A
Различия:
(B) git rebase master
*---*---* [master]
\
*---*---*---* [A]
\
*---*---* [B](HEAD)
Ничего не произошло. В ветке master
нет новых коммитов с момента создания ветки B
.
(B) git checkout master
*---*---* [master](HEAD)
\
*---*---*---* [A]
\
*---*---* [B]
(мастер) git merge B
*---*---*-----------------------* [Master](HEAD)
\ /
*---*---*---* [A] /
\ /
*---*---* [B]
(B) git rebase --onto master A B
*---*---*-- [master]
|\
| *---*---*---* [A]
|
*---*---* [B](HEAD)
(B) git checkout master
*---*---*-- [master](HEAD)
|\
| *---*---*---* [A]
|
*---*---* [B]
(мастер) git merge B
*---*---*----------------------* [master](HEAD)
|\ /
| *---*---*---* [A] /
| /
*---*--------------* [B]
Я хочу объединить мои изменения B (и только мои изменения B, никаких изменений A) в master
Будьте осторожны с тем, что вы понимаете под «только мои изменения B».
В первом наборе ветвь B
(перед последним слиянием):
*---*---*
\
*---*---*
\
*---*---* [B]
И во втором наборе ваша ветвь B:
*---*---*
|
|
|
*---*---* [B]
Если Я правильно понимаю, что вы хотите только коммитов B, которые не находятся в ветке A . Итак, второй набор - это правильный выбор для вас до слияния.
git log --graph --decorate --oneline A B master
(или эквивалентный инструмент с графическим интерфейсом) можно использовать после каждой команды git для визуализации изменений.
Это начальное состояние хранилища, с B
в качестве текущей ветви.
(B) git log --graph --oneline --decorate A B master
* 5a84c72 (A) C6
| * 9a90b7c (HEAD -> B) C9
| * 2968483 C8
| * 187c9c8 C7
|/
* 769014a C5
* 6b8147c C4
* 9166c60 C3
* 0aaf90b (master) C2
* 8c46dcd C1
* 4d74b57 C0
Вот скрипт для создания хранилища в этом состоянии.
#!/bin/bash
commit () {
for i in $(seq $1 $2); do
echo article $i > $i
git add $i
git commit -m C$i
done
}
git init
commit 0 2
git checkout -b A
commit 3 6
git checkout -b B HEAD~
commit 7 9
Первая команда rebase ничего не делает.
(B) git rebase master
Current branch B is up to date.
Проверка master
и слияние B
просто указывают master
на тот же коммит, что и B
(т.е. 9a90b7c
). Новых коммитов не создано.
(B) git checkout master
Switched to branch 'master'
(master) git merge B
Updating 0aaf90b..9a90b7c
Fast-forward
<... snipped diffstat ...>
(master) git log --graph --oneline --decorate A B master
* 5a84c72 (A) C6
| * 9a90b7c (HEAD -> master, B) C9
| * 2968483 C8
| * 187c9c8 C7
|/
* 769014a C5
* 6b8147c C4
* 9166c60 C3
* 0aaf90b C2
* 8c46dcd C1
* 4d74b57 C0
Вторая команда rebase копирует коммиты в диапазоне A..B
и указывает их на master
. Три коммитов в этом диапазоне: 9a90b7c C9, 2968483 C8, and 187c9c8 C7
. Копии являются новыми коммитами со своими собственными идентификаторами коммитов; 7c0e241
, 40b105d
и 5b0bda1
. Ветви master
и A
неизменны.
(B) git rebase --onto master A B
First, rewinding head to replay your work on top of it...
Applying: C7
Applying: C8
Applying: C9
(B) log --graph --oneline --decorate A B master
* 7c0e241 (HEAD -> B) C9
* 40b105d C8
* 5b0bda1 C7
| * 5a84c72 (A) C6
| * 769014a C5
| * 6b8147c C4
| * 9166c60 C3
|/
* 0aaf90b (master) C2
* 8c46dcd C1
* 4d74b57 C0
Как и прежде, проверка master
и слияние B
просто указывают master
на тот же коммит, что и B
(т.е. 7c0e241
). Новых коммитов не создано.
Первоначальная цепочка коммитов, на которую указывал B
, все еще существует.
git log --graph --oneline --decorate A B master 9a90b7c
* 7c0e241 (HEAD -> master, B) C9
* 40b105d C8
* 5b0bda1 C7
| * 5a84c72 (A) C6
| | * 9a90b7c C9 <- NOTE: This is what B used to be
| | * 2968483 C8
| | * 187c9c8 C7
| |/
| * 769014a C5
| * 6b8147c C4
| * 9166c60 C3
|/
* 0aaf90b C2
* 8c46dcd C1
* 4d74b57 C0
Вы можете попробовать это сами и посмотреть. Вы можете создать локальный репозиторий git для игры:
#! /bin/bash
set -e
mkdir repo
cd repo
git init
touch file
git add file
git commit -m 'init'
echo a > file0
git add file0
git commit -m 'added a to file'
git checkout -b A
echo b >> fileA
git add fileA
git commit -m 'b added to file'
echo c >> fileA
git add fileA
git commit -m 'c added to file'
git checkout -b B
echo x >> fileB
git add fileB
git commit -m 'x added to file'
echo y >> fileB
git add fileB
git commit -m 'y added to file'
cd ..
git clone repo rebase
cd rebase
git checkout master
git checkout A
git checkout B
git rebase master
cd ..
git clone repo onto
cd onto
git checkout master
git checkout A
git checkout B
git rebase --onto master A B
cd ..
diff <(cd rebase; git log --graph --all) <(cd onto; git log --graph --all)