- Основные понятия Git
- Основные команды Git
- Полезные возможности Git
- Git flow
- SSH Github & Gitlab
- Git Config
- Git Bash
Git — система контроля версий, то есть система, которая следит за изменениями файлов, позволяет фиксировать определённые состояния изменений и возвращаться к любому из этих состояний при необходимости.
В сравнении со всеми другими системами контроля версий, Git имеет уникальный подход к работе со своими данными. Все остальные системы хранят набор файлов и списки изменений (дельты) этих файлов с течением времени.
Файл | Версия I | Версия II | Версия III |
---|---|---|---|
A.txt | delta I | delta II | — |
B.txt | — | delta I | delta II |
В таблице выше delta
— список изменений файла, прочерк — отсутствие данных об изменениях.
Git рассматривает данные не как таблицы изменений конкретных файлов, а как поток снимков (stream of snapshots).
Каждый снимок (shapshot) означает сохранение определённого состояния проекта. Система запоминает, как выглядит каждый файл проекта на момент сохранения (делает снимок) и сохраняет ссылку на этот снимок. Если файл не изменился, то Git не запоминает его вновь, а создаёт ссылку на идентичную версию файла из предшествующего снимка.
Файл | Версия I | Версия II | Версия III |
---|---|---|---|
A.txt | A1 | A2 | A2 |
B.txt | B1 | B1 | B2 |
В таблице выше выделенные курсивом версии файлов не менялись в новой версии, поэтому были использованы ссылки на версии из предыдущих снимков.
Благодаря такому подходу Git чем-то похож на файловую систему.
Git делит все файлы на отслеживаемые и неотслеживаемые.
Отслеживаемые (tracked) файлы — файлы, которые были в последнем снимке состояния проекта, неотслеживаемые (untracked) файлы — все остальные.
Также имеются категория игнорируемых (ignoring) файлов. Изменения этих файлов Git игнорирует.
Чтобы сделать файл (папку) игнорируемым, необходимо его добавить в файл .gitignore
, который должен лежать в проекте (обычно в корневой папке).
Пример содержимого .gitignore
.
node_modules/
logs/
dist/
.env
- Неизменённый (Unmodified). Файл не изменён.
- Изменённый (Modified). Файл изменён локально.
- Подготовленный (Staged). Файл изменён локально и помечен для включения в следующий коммит.
- Зафиксированный (Committed). Версия файла сохранена в истории Git.
- Рабочая директория (Working Directory). Файлы распаковываются из сжатой базы данных репозитория и располагаются на локальном диске для чтения и записи. Здесь происходит работа с ними до тех пор, а затем они переходят
- Область подготовленных файлов (Staging Area). Здесь содержатся сведения о подготовленных файлах и их изменениях, которые должны попасть в следующий коммит. Эта область является частью папки .git.
- Папка .git (.git directory, Repository). Здесь хранятся метаданные (данные о данных) и Git-объекты текущего проекта, в том числе история коммитов.
Коммит (Commit) — отметка (точка, этап) в истории Git. К этой точке можно вернуться. Вся история проекта состоит из цепочки связанных друг с другом коммитов. Каждый коммит имеет родительский коммит и зависит от него.
Близкими по смыслу коммиту понятиями являются версия (version) и снимок (snapshot).
Уникальным идентификатором коммита является его хеш.
commit 54e2f882f077cb0f4a1ca0600eada25cc96c7e5a
commit ec1b065a072c545ca2849e0c05b60c520bdc39e4
Индекс (Index) — снимок следующего намеченного коммита.
Чтобы добавить файл из рабочей директории в индекс (область подготовленных файлов), используется команда add
.
/* добавить изменённый файл */
git add <filename>
/* добавить папку с изменёнными файлами */
git add <directory>
/* добавить все изменённые файлы */
git add .
Чтобы добавить файлы из области подготовленных файлов в коммит, используется команда commit
.
git commit
Команда выше открывает текстовый редактор по умолчанию для ввода сообщения коммита. Чтобы этого избежать, можно использовать флаг -m
и передать в него сообщение коммита.
git commit -m "Commit message here"
Все системы контроля версий поддерживают возможность ветвления.
Ветка (Branch) — независимая линия разработки проекта.
Ветка состоит из последовательности коммитов.
Базовый коммит ветки (Base commit) — коммит, с которого началась (была создана) ветка.
Будем обозначать латинискими буквами хеши коммитов: A
, B
, C
и т.д.
/* Ветвление */
feature | F —— G
| /
develop | C —— D —— E
| /
master (основная) | A —— B
На изображении выше базовым коммитом для ветки master
является коммит A
, для ветки B
— C
, для feature
— D
.
Изначально выбирается основная ветка, которая будет хранить в себе основную историю развития приложения (обычно master
). Это не обязательно должна быть ветка, в которой создавался первый коммит, но чаще всего это так.
От основной ветки создаются новые ветки, от них тоже при необходимости создаются ветки.
Базовым коммитом новой ветки становится последний на момент создания коммит текущей ветки.
Суть ветвления заключается в том, что ветки (содержащие новую функциональность приложения) могут развиваться независимо от основной и других веток, а затем их изменения попадают (или не попадают) в основную или другую ветку путём слияния. После слияния второстепенные ветки обычно удаляются.
Для управления ветками используется команда branch
.
/* создание новой ветки */
git branch <branch_name>
/* список всех веток */
git branch -a
/* переименование ветки */
git branch -m <old_name> <new_name>
/* удаление ветки */
git branch -D <branch_name>
Для переключения между ветками используется команда checkout
.
/* переключение на ветку branch_name */
git checkout <branch_name>
Для слияния веток используется команда merge
, о которой будет рассказано позже.
Поскольку последний коммит знает своего родителя, а тот — своего, по последнему коммиту можно восстановить всю цепочку коммитов любой ветки. Это значит, что ветка может быть представлена указателем на последний коммит, сделанный в этой ветке.
Это позволяет Git хранить ветку не как последовательность коммитов, а просто как ссылку на последний её коммит.
Верхушка ветки (Branch tip, Branch head) — последний коммит ветки. У каждой ветки есть одна вершутка.
HEAD — указатель на текущую ветку (указатель на последний коммит текущей ветки). HEAD
может быть только один в текущий момент времени для всего проекта.
Когда мы создаём новую ветку, её базовым коммитом становится тот, на который ссылается HEAD
.
Если открыть файл HEAD
в папке .git
, то можно увидеть, что он содержит ссылку на текущую ветку.
cat .git/HEAD
/* ref: refs/heads/master */
checkout develop
cat .git/HEAD
/* ref: refs/heads/develop */
Если открыть папку refs/heads
, то там можно видеть, что Git хранит для каждой ветки отдельный файл, в котором хранится хеш последнего коммита ветки.
cat ./git/refs/heads/master
/* 1ed2bf4eebbcd0515a638b48550a7eb81c7c01e5 */
Команда merge позволяет слить (смержить) истории двух веток в одну, то есть из двух последовательностей коммитов создать одну.
Целевой ветка (target branch) сливается в текущую ветку (current branch), которую также называют рабочей.
git merge <target_branch>
При слиянии изменения затрагивают только текущую ветку: целевая ветка остаётся без изменения.
Git просматривает цепочку коммитов обеих веток, пытаясь найти их общий коммит. Обычно такой коммит имеется (как минимум, начальный коммит).
Довольно редко бывают случаи, когда между двумя ветками нет ни одного общего коммита. По умолчанию Git отказывается сливать такие ветки в одну, но можно использовать флаг --allow-unrelated-histories
, который позволит это сделать.
Если общий коммит между двумя ветками найден, то возможны два случая
- Только в одной ветке были новые коммиты с момента общего коммита. В таком случае одна ветка является продолжением другой. Используется
fast-forward merge
. - Обе ветки имеют новые коммиты с момента общего коммита. Это означает, что они развивались параллельно, независимо от друга. Используется
true merge
.
Fast-forward merge (перемотка вперёд) используется, если одна ветка является продолжением другой.
Возможны два случая: целевая ветка длиннее или короче текущей.
Рассмотрим случай, когда целевая ветка впереди (длиннее) текущей. Тогда при слиянии веток указатель на последний коммит текущей ветки (верхушка ветки) сдвигается на последний коммит целевой ветки. После такого слияния истории двух веток становятся идентичными.
/* до слияния веток develop и master целевая ветка develop была
впереди на 2 коммита C и D */
develop | C —— D
| /
master (current) | A —— B
git checkout master
git merge develop
/* после слияния верхушки веток указывают на один и тот же коммит D */
develop | C —— D
| /
master (current) | A —— B —— C —— D
Если отменить последний коммит в ветке master
, то в ней отменится только коммит D
.
/* отмена последнего коммита при помощи git reset HEAD~ */
develop | C —— D
| /
master (current) | A —— B —— C
Таким образом, чтобы отменить изменения, появившиеся в текущей ветке в результате слияния, необходимо удалить ровно столько коммитов, сколько было новых коммитов в целевой ветке.
В случае, когда целевая ветка позади (короче) текущей, ничего не произойдёт, поскольку текущая ветка уже содержит все актуальные изменения (содержит все коммиты целевой ветки).
git checkout develop
git merge master
Fast-forward merge
не будет применён при использовании флага --no-ff
, применится true merge
.
git merge --no-ff branch_name
Если текущая и целевая ветки развивались независимо друг от друга в течение какого-то времени, тогда каждая из них имеет новые коммиты с момента общего коммита веток и использовать fast-merge
не получится. В этом случае применяется true merge (3-way merge), которые использует 3-х сторонний алгоритм (3-way algorithm). Алгоритм так называется, поскольку учитываются 3 стороны (состояние до изменений, изменения целевой ветки, изменения текущей ветки).
При применении true merge
создаётся особый тип коммита, имеющий сразу два родительских коммита, — merge commit. В нём содержатся новые изменения, которые были в целевой ветки, но не были в текущей.
/* до слияния веток с момента общего коммита B целевая ветка develop
имеет 2 коммита C и D, текущая ветка master имеет один коммит E */
develop | C —— D
| /
master (current) | A —— B —— E
git checkout master
git merge develop
/* после слияния в текущей ветке master появился merge commit H,
содержащий в себе все изменения коммитов C, D, E */
develop | C —— D
| / \
master (current) | A —— B —— E —— H
Как и в случае fast-forward merge
, изменения true merge
затрагивают только текущую ветку, поэтому merge commit
создаётся только в текущей ветке (коммит H
появился только в master
).
Аналогичный результат бы получился, если бы ветка develop
была текущей, а master
— целевой. Разница лишь в том, что в этом случае коммит H
хранился бы только в develop
.
Если из master
удалить последний коммит, то состояние текущей ветки станет таким, каким оно было до слияния. Таким образом, чтобы отменить все последствия слияния двух веток стратегией true merge
, достаточно удалить только merge commit
.
/* отмена последнего коммита при помощи git reset HEAD~ */
develop | C —— D
| /
master (current) | A —— B —— E
Если новые коммиты сливающихся веток затрагивают одни и те же файлы и по-разному изменяют их, возникают конфликты слияния (merge conflicts). Git не может автоматически разрешить их, поскольку изменения производились параллельно. Разработчик должен сам решить, какие изменения стоит оставить.
Для отображения конфликтов по умолчанию использует 2 версии файла и следующий синтаксис.
<<<<<<<
— изменения коммита текущей ветки.>>>>>>>
— изменения коммита целевой ветки.=======
— разделяющая полоса.
<<<<<<<
/* изменения в текущей ветке */
=======
/* изменения в целевой ветке */
>>>>>>>
Можно настроить команду merge
таким образом, чтобы также показывалось и состояние до изменений. За это отвечает свойство merge.conflictStyle
со значением diff3
. Синтаксис: |||||||
.
<<<<<<<
/* изменения в текущей ветке */
|||||||
/* до изменений */
=======
/* изменения в целевой ветке */
>>>>>>>
Чтобы разрешить конфликт, следует выбрать необходимые изменения и убрать всё лишнее, затем эти изменения добавляются в merge commit
.
Пример конфликта в одном из файлов (в коммите текущей ветки используется const
, в коммите целевой ветки — let
).
<<<<<<< HEAD (Current Change)
let name = 'Notes';
=======
const name = 'Notes';
>>>>>>> develop (Incoming Change)
В этом примере можно оставить следующую строку и добавить её в merge commit
.
let name = 'Notes';
Базовый коммит ветки (Base commit) — коммит, с которого начинается (создана) ветка.
Перебазирование (Rebasing) — перемещение последовательности коммитов к новому базовому коммиту.
git rebase <base>
Перебазирование переписывает историю текущей ветки.
Когда мы указываем ветку в качестве <base>
для команды rebase
, берётся последний коммит этой ветки.
/* до перебазирования целевая ветка develop по сравнению с master
имеет новый коммиты C и D, базовым для неё является коммит B */
develop (current) | C —— D
| /
master | A —— B —— E
git checkout develop
git rebase master
/* после перебазирования базовым коммитом для develop стал коммит E
(последний в master), C* и D* — копии коммитов C и D с новыми хешами */
develop (current) | C* —— D*
| /
master | A —— B —— E
Если бы перебазировался не develop
, а master
, то изменились бы все коммиты ветки master
с момента их общего коммита с develop
(в данном случае это только коммит E
).
git checkout master
git rebase develop
/* после перебазирования базовым коммитом для master остался коммит A
(поскольку начало историй обеих веток совпадают), E* — копия
коммита E с новым хешем */
develop | C —— D
| /
master (current) | A —— B —— C —— D —— E*
По примерам выше видно, что все новые коммиты перебазируемой ветки пересоздаются. Отменить последствия rebase
невозможно (хеш старых коммитов утерян).
При перебазировании возможны такие же конфликты, как и при true merge
. Решаются они аналогично.
Самое основное отличие заключается в том, что rebase
перезаписывает историю, а merge
только дополняет её.
Fast-forward
просто копирует коммиты из одной ветки в конец ветки.
True merge
создаёт новый merge commit
, содержащий все необходимые текущей ветке изменения целевой ветки (с необходимыми правками, если были конфликты).
Недостаток fast-forward merge
: не может работать с ветками, которые развивались параллельно.
Недостаток true merge
: merge commits
загрязняют историю приложения.
Rebase
позволяет заменять базовый коммит текущей ветки на другой коммит. Это позволяет достигнуть идеальной линейной истории путём её постоянного изменения, поскольку можно всегда размещать новые ветки в конце базовой. Тем не менее, эта история будет характерна только текущей ветке.
Если rebase
затронет коммиты, которые не были созданы в текущей ветке, то при слиянии с другой веткой Git увидит две разные истории и создаст большой merge commit, который использует обе версии. Появится дублирование одних и тех же изменений.
Таким образом, rebase
лучше использовать только для новых коммитов, если есть необходимость как-то подправить их. Если затрагиваются коммиты, которые имеются в других ветках и в будущем есть вероятность того, что эти ветки сольются вместе, rebase
лучше не использовать.
Помимо перебазирования команда rebase
имеет интерактивный режим, который позволяет полностью переписать историю определённого числа коммитов. Для перехода в интерактивный режим используется флаг -i
. Для выбора N
последнимх коммитов используется указатель HEAD~N
.
git checkout develop
git rebase -i HEAD~2 /* изменение истории двух последних коммитов текущей ветки */
В этом случае будет открыт текстовый редактор со следующим содержимым.
pick C Message for the commit C
pick D Message fof the commit D
# Rebase B..D onto B
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# d, drop <commit> = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
До символов #
показывается список всех выбранных для редактирования коммитов. По умолчанию к каждому из них принимается команда pick
(оставить как есть), но её можно заменить текстом на одну из следующих:
reword
- изменение сообщения коммита (окно для изменения сообщения появится после).squash
- совместить коммит с предыдущим (далее надо будет набрать общий текст для нового скомбинированного коммита).fixup
- совместить коммит с предыдущим (с отменой сообщения коммита).drop
- удалить коммит и его изменения из истории (это можно также сделать, удалив строку с коммитом из списка).- Также можно менять строки в списке местами, тогда соответствующие им коммиты также поменяются местами в истории.
Таким образом, при помощи интерактивного rebase
можно делать с историей практически всё, что угодно, но делать это нужно осторожно. Желательно, не затрагивая те части истории проекта, которые имеются в других ветках.
Если нужно перезаписать сообщение последнего коммита, то можно вместо rebase
использовать команду commit
с флагом --ammend
:
git commit --amend -m "Updated last commit message"
Команда git fetch используется для того, чтобы получить информацию о последних изменениях на удалённой ветке (origin/
). Таким образом можно узнать, были ли изменения вообще.
git fetch
/*
From github.com:YourName/RepositoryName
2eefe71..ac391ab develop -> origin/develop
fca7c62..c31477c feature/1 -> origin/feature/1
* [new branch] feature/2 -> origin/feature/2
* [new branch] feature/3 -> origin/feature/3
*/
Выше можно видеть, что ветки develop
и feature/1
отличаются от своих одноимённых удалённых веток хешем последних коммитов, а ветки feature/2
и feature/3
новые и их нет локально.
Команда git fetch
помимо информации об изменениях скачивает и сами изменения, но их не слияние с локальной веткой не происходит. Это можно проверить командой git diff
.
git diff origin/develop
/*
diff --git a/Git.md b/Git.md
index abe21d5..e207862 100644
+++ b/client/Dockerfile
- Hello
+ Hello, Notes!
*/
Повторный вызов команды git fetch
ничего не выведет, поскольку изменения уже были подгружены.
Команда git pull также, как и git fetch
получает изменения и информацию о них, но дополнительно сливает изменения в локальную ветку.
По сути, git pull
объединяет в себе две команды: git fetch
и git merge
.
Обычно HEAD
указывает на верхушку ветки (branch tip).
Detached HEAD — специальный тип HEAD, который может быть установлен пользователем, чтобы посмотреть, как выглядит изменения на определённом коммите в ветке.
Все три команды reset, checkout, revert позволяют откатывать (undo) изменения, но делают это по-разному.
Команда checkout перемещает указатель HEAD.
/* до checkout HEAD ветки совпадал с верхушкой (tip)
ветки master и указывал на коммит E */
(HEAD,
master)
A ——— B ——— С ——— D ——— E
git checkout B
/* после checkout */
(detached
HEAD) (master)
A ——— B ——— С ——— D ——— E
Если ввести git branch
, то он выдаст ветку (HEAD detached at B)
.
Если ввести git log
, то коммит B в текущей ветке будет последним.
/* git log */
(HEAD)
A ——— B
Таким образом, при помощи команды checkout
Git позволяет переключиться на определённый снимок проекта в прошлом, не изменяя при этом реальную ветку. Можно сделать какие-то тестовые изменения, посмотреть или скопировать какие-то файлы, а затем вернуться на полную версию ветки.
Команда revert выбирает коммит и создаёт новый коммит, который откатывате изменения выбранного коммита.
/* до revert */
(HEAD,
master)
A ——— B ——— С ——— D
git revert C
/* после revert создаётся коммит E, отменяющий все изменения коммита D,
и таким образом возвращающий состояние проекта на момент коммита B */
(HEAD,
master)
A ——— B ——— С ——— D ——— E
Если при помощи revert
откатывается не последний коммит (например, C), то могут появиться конфликты (поскольку коммит D зависеть от некоторых изменений коммита C).
Команда revert
является безопасным вариантом для отката изменений в публичном репозитории, поскольку коммиты не удаляются, а создаются — история не перезаписывается.
Команда reset сбрасывает все изменения и историю до определённого коммита.
/* до reset */
(HEAD,
master)
A ——— B ——— С ——— D
git reset B
/* после reset */
(HEAD,
master)
A ——— B
git reset HEAD~
Например, перенос N коммитов из локальной ветки A и локальную ветку B.
git checkout B
git merge A
git checkout A
git reset --hard HEAD~N # удаление N коммитов из ветки A
Фича (Feature) — новая функциональность.
- Разработка новых фич начинается с ветки
develop
, от которой создаётся новая веткаfeature/name
, гдеname
- название фичи. - Разработчик переключается на новую ветку и начинает работать с ней.
- После завершения фичи создаётся Pull Request.
- Ветка
feature/name
сливается с (merge into) веткойdevelop
. - Ветка
feature/name
удаляется. - Разработчик переключается обратно на ветку
develop
.
- От ветки
develop
создаётся веткаrelease/vX.X.X
. - Ветка
release/vX.X.X
при необходимости помечается тэгомvX.X.X
. - Разрешены мелкие исправления (minor bug fixes) в ветке
release/vX.X.X
. - Ветка
release/vX.X.X
сливается с веткойmaster
. - Ветка
release/vX.X.X
сливается обратно (back-merge) с веткойdevelop
. - Ветка
release/vX.X.X
удаляется. - Разработчик переключается обратно на ветку
develop
.
- Если в production найден серьёзный баг, который нужно быстро исправить, от ветки
master
создаётся веткаhotfix/name
, в которой делаются необходимые исправления. - Ветка
hotfix/name
сливается с веткойmaster
. - Ветка
hotfix/name
сливается с веткойdevelop
. - Ветка
hotfix/name
удаляется.
- Открыть Bash.
-
Сгенерировать ключ для Github при помощи
ssh-keygen -t rsa -C "email" -f ~/.ssh/id_rsa_github
, где нужно заменитьemail
на свой.id_rsa_github
- название файла, где будет лежать приватный ключ. Команда также автоматически генерирует публичный ключid_rsa_github.pub
в той же папке. -
Ввести ключевую фразу и повторить её.
-
Сгенерировать ключ для Gitlab при помощи
ssh-keygen -t rsa -C "email" -f ~/.ssh/id_rsa_gitlab
, где нужно заменитьemail
на свой. -
Ввести ключевую фразу и повторить её.
- Скопировать публичный ключ
~/.ssh/id_rsa_github.pub
для Github. Например, вывести его на экран при помощиcat ~/.ssh/id_rsa_github.pub
и скопировать черезCtrl + C
. - Открыть на Github Settings > SSH keys и нажать на добавление нового ключа.
- Вставить скопированный ключ, придумать название для него и сохранить.
- Скопировать публичный ключ
~/.ssh/id_rsa_gitlab
для Gitlab. - Открыть на Gitlab
Settings > SSH keys
. - Вставить скопированный ключ, придумать название для него и сохранить.
- Добавить SSH-ключ для GitHub в SSH-agent
ssh-add ~/.ssh/id_rsa_github
. Если агент не запущен, то нужно сперва его запуститьeval $(ssh-agent -s)
илиssh-agent bash
. - Добавить SSH-ключ для GitLab в SSH-agent
ssh-add ~/.ssh/id_rsa_gitlab
. - Проверить, что ключи добавлены через
ssh-add -L
.
Если для каждой новой консоли запускать агент и добавлять ключ приходиться заново, то можно настроить псевдоним.
Псевдоним (Alias) — аббревиатура, позволяющая избежать написания длинной последовательности команд.
- Создадим файл
.bashrc
в корневой папке (в Windows: `C:/Users//) и поместим там следующее.
alias sa="eval `ssh-agent -s` ssh-add ~/.ssh/id_rsa_gitlab"
Теперь каждый запуск команды sa
в любой консоли Bash будет выполнять запуск агента и добавление ключа.
- Создать файл
touch ~/.ssh/config
. - Вставить туда следующее
# config for github
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa_github
# config for gitlab
Host gitlab.com
HostName gitlab.com
User git
IdentityFile ~/.ssh/id_rsa_gitlab
- Сохранить изменения.
- Попробовать сделать
git pull
через SSH. - Дополнительная проверка для Github:
ssh -T git@github.com
(режим отладки:ssh -T git@github.com
). - Дополнительная проверка для Gitlab:
ssh -T git@gitlab.com
(режим отладки:ssh -T git@gitlab.com
).
git config -l
/* имя пользователя */
git config --global user.name "Your Username"
/* электронная почта */
git config --global user.email "your@email"
Командная оболочка (Shell) — терминальное приложение (terminal app), используемое для взаимодействия с ОС посредством письменных команд.
Bash (Bourne again shell, "Born again shell", "возрождённый" shell) — усовершенствованная версия ранней командной оболочки Bourne shell, исполняющей файлы формата .sh
в UNIX
. Bash является командной оболочкой по умолчанию для Linux и macOS.
Git Bash — пакет, устанавливающий Bash, некоторые базовые утилиты и Git на Windows.
Чтобы на определённом порте убить запущенный процесс, нужно узнать его PID (Process Identifier).
netstat -ano | findstr :PORT /* например, :3000 */
/* Скопировать PID из последнего стобца результата и вставить в команду ниже */
tskill PID