mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-03 20:46:47 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b146c05578 | ||
|
|
6c7f893f7a | ||
|
|
8b15f22379 | ||
|
|
9c7db3a37b | ||
|
|
c961a5833b | ||
|
|
6509875009 | ||
|
|
3bf1be65b3 | ||
|
|
8e6967c702 | ||
|
|
0e0458f355 | ||
|
|
05bfa96936 | ||
|
|
be6acf2fbe | ||
|
|
bf092f76d6 | ||
|
|
9c384c5267 | ||
|
|
d772c9f1d4 | ||
|
|
e5534d9781 | ||
|
|
34755285a1 | ||
|
|
e0ecc9e835 | ||
|
|
add1de4138 | ||
|
|
ed496deeca | ||
|
|
1f8e838052 | ||
|
|
f212e19efd | ||
|
|
fb0931e1a1 | ||
|
|
ee1597415d | ||
|
|
63dc07fded | ||
|
|
70e473b25d | ||
|
|
1a4cf84b58 | ||
|
|
ad72a1f5a3 | ||
|
|
59379b45da | ||
|
|
2d4ca2b54f | ||
|
|
ec5075104a | ||
|
|
6f535d71c9 | ||
|
|
ec3a28df43 | ||
|
|
5b933762c2 | ||
|
|
3eed997161 | ||
|
|
526d8a8a76 | ||
|
|
e1fc90615d | ||
|
|
460a166e16 | ||
|
|
2e66d87b94 | ||
|
|
820f7b9404 | ||
|
|
3cddd7cfa5 | ||
|
|
2e0d0a92ee | ||
|
|
ed857d1e07 | ||
|
|
b30ec538fb | ||
|
|
ee11046d35 | ||
|
|
25f8b0337e | ||
|
|
63ddc52a6b | ||
|
|
b07c4fc001 | ||
|
|
2304ffc804 | ||
|
|
401610c0ef | ||
|
|
64b2685c2d | ||
|
|
7d4bfb6621 | ||
|
|
a2778f01c6 | ||
|
|
d161afe37f | ||
|
|
3dd96a8010 | ||
|
|
681a9bf20d | ||
|
|
0606b7a43b | ||
|
|
33da56eeb4 | ||
|
|
46b93bba0e | ||
|
|
d2d50aedd0 | ||
|
|
ea5fb6a364 | ||
|
|
a82134f41c | ||
|
|
00b03079d8 | ||
|
|
9592686629 | ||
|
|
114ad52ff6 | ||
|
|
9514284f8e | ||
|
|
ece14844bb | ||
|
|
9910a7c308 | ||
|
|
910a61dc46 | ||
|
|
e5dd4d3110 | ||
|
|
595c7ee73e | ||
|
|
051af6a066 | ||
|
|
e2d7e461a8 | ||
|
|
7c66ca83c1 | ||
|
|
007b406b14 | ||
|
|
2b30085dba | ||
|
|
5c95d23169 | ||
|
|
e156e090cc | ||
|
|
0accb07dcc | ||
|
|
3cac14c76e | ||
|
|
1636931c2b | ||
|
|
6e027f42da | ||
|
|
5149b24ab3 | ||
|
|
c520c5cfc3 | ||
|
|
c88ecdf87c | ||
|
|
d607b366cb | ||
|
|
c8f26aca68 | ||
|
|
30656b5ac6 | ||
|
|
5dacbb6293 | ||
|
|
88d4313970 | ||
|
|
4160fa518c | ||
|
|
fba1a2b5ac | ||
|
|
c6c4346d48 | ||
|
|
ee9ae8f07f | ||
|
|
8d68ab41b6 | ||
|
|
af97bf484c | ||
|
|
a57310df24 | ||
|
|
9d68b287db | ||
|
|
a5c72d056d | ||
|
|
8a86de85c8 | ||
|
|
dd31f8ecea | ||
|
|
5a7b2ab6d0 | ||
|
|
68a9d7fd77 | ||
|
|
19cbafcdfc | ||
|
|
f2c85c5b19 | ||
|
|
0faa41e6f8 | ||
|
|
2e32e55957 | ||
|
|
037cd99138 | ||
|
|
6388885699 | ||
|
|
d3e9bc2185 | ||
|
|
ea4587a3b8 | ||
|
|
2da300f2fb | ||
|
|
7848958326 | ||
|
|
2cba98e3fe | ||
|
|
f8c9ce33c2 | ||
|
|
71753770ad | ||
|
|
820b1e811d | ||
|
|
4a33fede7b | ||
|
|
db12853bbe | ||
|
|
711674f6cd | ||
|
|
fc91ef6a59 | ||
|
|
43251e7275 | ||
|
|
f081358943 | ||
|
|
0c6ab4b43e | ||
|
|
1b2fb34ffd | ||
|
|
0e5a4c7a36 | ||
|
|
47b91f1ef5 | ||
|
|
e2db6a1732 | ||
|
|
509e3efa70 | ||
|
|
8edad826ca | ||
|
|
3ec416047f | ||
|
|
8ed29f2b7d | ||
|
|
9adbef40de | ||
|
|
0cd5257523 | ||
|
|
49da7b482d | ||
|
|
d675117289 | ||
|
|
e63858215e | ||
|
|
3fe4db9316 | ||
|
|
185bbf0c75 | ||
|
|
a8586ba57e | ||
|
|
dad7a70bf8 | ||
|
|
3791f0b2fa | ||
|
|
b8fbe9756e | ||
|
|
ab25600ccb | ||
|
|
d50c58b4c6 | ||
|
|
5645a662de | ||
|
|
a41218551d | ||
|
|
d210107caa | ||
|
|
826128a8e0 | ||
|
|
aec46942a8 | ||
|
|
aa70723e3a | ||
|
|
7db54a948a | ||
|
|
32556480da | ||
|
|
2553015029 | ||
|
|
6c010a788c | ||
|
|
b17c38befd | ||
|
|
79dc1d9052 | ||
|
|
28d2b1432b | ||
|
|
ed98e11fa0 | ||
|
|
07a22e69e7 | ||
|
|
bf3dd79b7a | ||
|
|
21072226d2 | ||
|
|
b09000194a | ||
|
|
3546ab8f21 | ||
|
|
9a13447b97 | ||
|
|
1efb565b22 | ||
|
|
08a445eb9d | ||
|
|
28501fbf77 | ||
|
|
8f1f712841 | ||
|
|
b82b6a2992 | ||
|
|
ddcd6be245 | ||
|
|
711be78811 | ||
|
|
3422b1e218 | ||
|
|
a3fdf91714 | ||
|
|
368f9c8cb3 | ||
|
|
e18b4a4cc3 | ||
|
|
46718e25ca | ||
|
|
d675eb6507 | ||
|
|
dc4e88f8a4 | ||
|
|
120dd1530a | ||
|
|
860a8d102b | ||
|
|
a304fed68c | ||
|
|
227b0b781c | ||
|
|
740474c10c | ||
|
|
a0d179b6dc | ||
|
|
c53c5e47ef | ||
|
|
188773511e | ||
|
|
62c5c32fbb | ||
|
|
981ba4c66c | ||
|
|
7f5465a27b | ||
|
|
275ed12486 | ||
|
|
6a340ec840 | ||
|
|
3535cd0f94 | ||
|
|
b1a56249f5 | ||
|
|
82c54ed3d2 | ||
|
|
bd62b519de | ||
|
|
04e0a9bb45 | ||
|
|
2b4ac986a2 | ||
|
|
caedf57484 | ||
|
|
046b0d9daa | ||
|
|
e3c5e07b20 | ||
|
|
e4e16fa38e | ||
|
|
b7e029adc7 | ||
|
|
08d679c3a8 | ||
|
|
2947b56134 | ||
|
|
659d668e16 | ||
|
|
7bbcec965b | ||
|
|
24de156592 | ||
|
|
8d3cce4a49 | ||
|
|
95757ceb3b | ||
|
|
2087a02f01 | ||
|
|
6ffe98abac | ||
|
|
046cb942c2 | ||
|
|
fc2f8b7b20 | ||
|
|
298dae23e8 | ||
|
|
a02b54e1b7 | ||
|
|
0af4e5a843 | ||
|
|
e0503b9922 | ||
|
|
ef239c04fb | ||
|
|
d508badd62 | ||
|
|
e7d0116312 | ||
|
|
85fdb700ba | ||
|
|
7513d77567 | ||
|
|
c757063264 | ||
|
|
b24955063c | ||
|
|
605bc026a1 | ||
|
|
c6930e0538 | ||
|
|
3a59aba46d | ||
|
|
213ae8dd07 | ||
|
|
4780953cef | ||
|
|
11bc8b87fa | ||
|
|
3bfbc9d255 | ||
|
|
8121a0cc74 | ||
|
|
4adca84d68 | ||
|
|
b7c61aa883 | ||
|
|
40f6767cfc | ||
|
|
776d8f4d2e | ||
|
|
4b67a45a16 | ||
|
|
a82d952f48 | ||
|
|
549ce09f71 | ||
|
|
cef804f27a | ||
|
|
60f902f026 | ||
|
|
7ce3165afa | ||
|
|
c28e25524a | ||
|
|
73c7dc9c5d | ||
|
|
b542579db3 | ||
|
|
e6274af015 | ||
|
|
4f57bf22c9 | ||
|
|
8dbd7d44ff | ||
|
|
4b4dccfd7d | ||
|
|
81ea3107ed | ||
|
|
5c8bc790ff | ||
|
|
b5d612e6d6 | ||
|
|
dea279920c | ||
|
|
5834440767 | ||
|
|
55fb3ef4e6 | ||
|
|
f314cb3763 | ||
|
|
516e963392 | ||
|
|
804a134aa5 | ||
|
|
82fc6fb111 | ||
|
|
0bda93d4c3 | ||
|
|
6735bf4c89 | ||
|
|
4bd1322941 | ||
|
|
45cf993982 | ||
|
|
e4659145e8 | ||
|
|
bf6e9a1bd3 | ||
|
|
a68cd6af9c | ||
|
|
4ca012dbfb | ||
|
|
6bd1c1d068 | ||
|
|
5d692e8961 | ||
|
|
9cc33c479b | ||
|
|
c79e360584 | ||
|
|
f76cc27956 | ||
|
|
5a50bfd179 | ||
|
|
c36333af3d | ||
|
|
4d78d76a44 | ||
|
|
d08a9849c3 | ||
|
|
3791b5057a | ||
|
|
dfe5c805c4 | ||
|
|
e94ff63bc5 | ||
|
|
723504a290 | ||
|
|
40a29fd622 | ||
|
|
648748781d | ||
|
|
63e8b8c01c | ||
|
|
ed47529604 | ||
|
|
ff5d0155db | ||
|
|
48acf3818f | ||
|
|
de3e4838ad | ||
|
|
6af8f278d0 | ||
|
|
e3c6887e5d | ||
|
|
c34e0deca7 | ||
|
|
75b2c50a55 | ||
|
|
af7c71d1ee | ||
|
|
161bb684fa | ||
|
|
a624e0457f | ||
|
|
f2aa7e7e28 | ||
|
|
75aa339b4e | ||
|
|
45d45d2397 | ||
|
|
f7e8b2dd71 | ||
|
|
8b5d59c238 | ||
|
|
f82f4f6dbc | ||
|
|
ff3c5d331e | ||
|
|
adef3bd4ca | ||
|
|
33f9f81c8c | ||
|
|
f6fafc65ee | ||
|
|
9c645088bf | ||
|
|
dd1bf629b8 | ||
|
|
c80a94aa7a | ||
|
|
6c0b805137 | ||
|
|
8a69d994c0 | ||
|
|
037e957282 | ||
|
|
db011d8e34 | ||
|
|
752526c880 | ||
|
|
c63fed2074 | ||
|
|
1b52a0d83f | ||
|
|
909a3b6791 | ||
|
|
558ceb64c7 | ||
|
|
1034962c7e | ||
|
|
eabe7f462a | ||
|
|
22c10479d5 | ||
|
|
f0572238cb | ||
|
|
78f3a7a478 | ||
|
|
90772e1eaa | ||
|
|
526c9dea9b | ||
|
|
c244c8f231 | ||
|
|
bfde06d049 | ||
|
|
6b8abb7887 | ||
|
|
2b6a109e38 | ||
|
|
71a30155dc | ||
|
|
6ee20840b2 | ||
|
|
56424eb1aa | ||
|
|
38c7030b0f | ||
|
|
31b8524fe6 | ||
|
|
0ae34aeeb7 | ||
|
|
e1c376ef54 | ||
|
|
c13f550d63 | ||
|
|
06351c1adf | ||
|
|
0b55eaca1d | ||
|
|
ee8c31880c | ||
|
|
daf8176dd7 | ||
|
|
76109a4c44 | ||
|
|
082d342bf8 | ||
|
|
39c56553b3 | ||
|
|
c5cd217a65 | ||
|
|
9e2a3a87dd | ||
|
|
ac580ae6a0 | ||
|
|
9e1e20fef2 | ||
|
|
4b49bd406f | ||
|
|
7351907474 | ||
|
|
a349e886ce | ||
|
|
dd61c49a15 | ||
|
|
1c3db24e44 | ||
|
|
15962e489d | ||
|
|
ac9515d8c7 | ||
|
|
72a92d748f | ||
|
|
36c2b00336 | ||
|
|
67b08ac239 | ||
|
|
67b8ef449c | ||
|
|
bb856ad7c6 | ||
|
|
b54b8ae746 | ||
|
|
65bd0ab431 | ||
|
|
93b9e1bd19 | ||
|
|
a51f64814c | ||
|
|
b5e325b0a4 | ||
|
|
88c76868ba | ||
|
|
e471567b5d | ||
|
|
13ee0f0a5d | ||
|
|
76a1b501f2 | ||
|
|
7201a91b69 | ||
|
|
31d796ee75 | ||
|
|
0ac869a415 | ||
|
|
9da9143aed | ||
|
|
b0383ba73a | ||
|
|
a3096e720c | ||
|
|
c599aaed51 | ||
|
|
f999bbce7c | ||
|
|
bff076c70a | ||
|
|
979c3d6278 | ||
|
|
1fc8823825 | ||
|
|
eeb9c02836 | ||
|
|
4d39668743 | ||
|
|
627cc285f7 | ||
|
|
bee338f93b | ||
|
|
01bf7f21e6 | ||
|
|
c517d1e0a2 | ||
|
|
8cad8cda8f | ||
|
|
6b81e6adca | ||
|
|
b499eba1a8 | ||
|
|
97daec7228 | ||
|
|
ff2a799200 | ||
|
|
31fcec16d9 | ||
|
|
0d395e4051 | ||
|
|
09178a1276 | ||
|
|
b66aa42ee5 | ||
|
|
3cfdae4116 | ||
|
|
d7956c481d | ||
|
|
7a3291a1f7 | ||
|
|
08c2b46d04 | ||
|
|
1c48842277 | ||
|
|
010f6d7f6e | ||
|
|
39c20bc634 | ||
|
|
823d95a8c6 | ||
|
|
225cd3cc60 | ||
|
|
91d62da577 | ||
|
|
9f71ed6c41 | ||
|
|
984eb95cb7 | ||
|
|
1da762c295 | ||
|
|
e357c00d4d | ||
|
|
1be6c522d8 | ||
|
|
8af59c3e6e | ||
|
|
c713d19383 | ||
|
|
4f7324bad0 | ||
|
|
aed3d56840 | ||
|
|
3745ba506b | ||
|
|
5e2254395a | ||
|
|
b243f30f48 | ||
|
|
e57843d947 | ||
|
|
5bb6198219 | ||
|
|
bbaeab68e1 | ||
|
|
946c1dff99 | ||
|
|
d838965a41 | ||
|
|
469938ee9b | ||
|
|
f7af2b991d | ||
|
|
75c3ab8a4a | ||
|
|
7bd0c779c7 | ||
|
|
bc7873144e |
+25
-34
@@ -43,17 +43,23 @@ jobs:
|
||||
restore-keys: |
|
||||
${{runner.os}}-go-
|
||||
- name: Test code
|
||||
# we're passing -short so that we skip the integration tests, which will be run in parallel below
|
||||
# we're passing -short so that we skip the integration tests, which will be run below
|
||||
run: |
|
||||
go test ./... -short
|
||||
integration-tests-old:
|
||||
runs-on: ubuntu-latest
|
||||
integration-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
parallelism: [5]
|
||||
index: [0,1,2,3,4]
|
||||
name: "Integration Tests (Old pattern) (${{ matrix.index }}/${{ matrix.parallelism }})"
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
cache_path: ~/.cache/go-build
|
||||
- os: windows-latest
|
||||
cache_path: ~\AppData\Local\go-build
|
||||
runs-on: ${{matrix.os}}
|
||||
name: "Integration Tests - ${{matrix.os}}"
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
steps:
|
||||
@@ -64,42 +70,20 @@ jobs:
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
- name: Cache build
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
${{matrix.cache_path}}
|
||||
~/go/pkg/mod
|
||||
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
|
||||
restore-keys: |
|
||||
${{runner.os}}-go-
|
||||
- name: Test code
|
||||
# for file.allow thing see https://vielmetti.typepad.com/logbook/2022/10/git-security-fixes-lead-to-fatal-transport-file-not-allowed-error-in-ci-systems-cve-2022-39253.html
|
||||
env:
|
||||
# LONG_WAIT_BEFORE_FAIL means that for a given test assertion, we'll wait longer before failing
|
||||
LONG_WAIT_BEFORE_FAIL: true
|
||||
run: |
|
||||
git config --global protocol.file.allow always && PARALLEL_TOTAL=${{ matrix.parallelism }} PARALLEL_INDEX=${{ matrix.index }} go test pkg/integration/deprecated/*.go
|
||||
integration-tests:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Integration Tests"
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
- name: Cache build
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
|
||||
restore-keys: |
|
||||
${{runner.os}}-go-
|
||||
- name: Test code
|
||||
run: |
|
||||
go test pkg/integration/clients/*.go
|
||||
go test ./pkg/integration/clients
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
@@ -164,6 +148,13 @@ jobs:
|
||||
# ensure our vendor directory matches up with our go modules
|
||||
run: |
|
||||
go mod vendor && git diff --exit-code || (echo "Unexpected change to vendor directory. Run 'go mod vendor' locally and commit the changes" && exit 1)
|
||||
- name: Check Integration Test List
|
||||
# ensure our integration test list is up to date
|
||||
run: |
|
||||
go generate pkg/integration/tests/tests.go && git diff --exit-code || (echo "Integration test list not up to date. Run 'go generate pkg/integration/tests/tests.go' locally and commit the changes" && exit 1)
|
||||
shell: bash # needed so that we get "-o pipefail"
|
||||
- name: Check Filenames
|
||||
run: scripts/check_filenames.sh
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
|
||||
@@ -20,12 +20,12 @@ jobs:
|
||||
run: echo "UUID_APP_TAG=$(uuidgen)" >> $GITHUB_ENV
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: registry.uffizzi.com/${{ env.UUID_APP_TAG }}
|
||||
tags: type=raw,value=60d
|
||||
- name: Build and Push Image to registry.uffizzi.com ephemeral registry
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
context: ./
|
||||
@@ -60,17 +60,11 @@ jobs:
|
||||
name: preview-spec
|
||||
path: docker-compose.rendered.yml
|
||||
retention-days: 2
|
||||
- name: Serialize PR Event to File
|
||||
run: |
|
||||
cat << EOF > event.json
|
||||
${{ toJSON(github.event) }}
|
||||
|
||||
EOF
|
||||
- name: Upload PR Event as Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: preview-spec
|
||||
path: event.json
|
||||
path: ${{ github.event_path }}
|
||||
retention-days: 2
|
||||
|
||||
delete-preview:
|
||||
@@ -79,11 +73,9 @@ jobs:
|
||||
if: ${{ github.event.action == 'closed' }}
|
||||
steps:
|
||||
# If this PR is closing, we will not render a compose file nor pass it to the next workflow.
|
||||
- name: Serialize PR Event to File
|
||||
run: echo '${{ toJSON(github.event) }}' > event.json
|
||||
- name: Upload PR Event as Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: preview-spec
|
||||
path: event.json
|
||||
path: ${{ github.event_path }}
|
||||
retention-days: 2
|
||||
|
||||
@@ -7,11 +7,11 @@ on:
|
||||
types:
|
||||
- completed
|
||||
|
||||
|
||||
jobs:
|
||||
cache-compose-file:
|
||||
name: Cache Compose File
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
outputs:
|
||||
compose-file-cache-key: ${{ env.COMPOSE_FILE_HASH }}
|
||||
pr-number: ${{ env.PR_NUMBER }}
|
||||
@@ -29,6 +29,9 @@ jobs:
|
||||
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "preview-spec"
|
||||
})[0];
|
||||
if (matchArtifact === undefined) {
|
||||
throw TypeError('Build Artifact not found!');
|
||||
}
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
@@ -39,16 +42,18 @@ jobs:
|
||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
|
||||
- name: 'Unzip artifact'
|
||||
run: unzip preview-spec.zip
|
||||
|
||||
- name: Read Event into ENV
|
||||
run: |
|
||||
echo 'EVENT_JSON<<EOF' >> $GITHUB_ENV
|
||||
cat event.json >> $GITHUB_ENV
|
||||
echo 'EOF' >> $GITHUB_ENV
|
||||
echo -e '\nEOF' >> $GITHUB_ENV
|
||||
- name: Hash Rendered Compose File
|
||||
id: hash
|
||||
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
|
||||
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
|
||||
run: echo "COMPOSE_FILE_HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache Rendered Compose File
|
||||
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
|
||||
uses: actions/cache@v3
|
||||
@@ -70,7 +75,8 @@ jobs:
|
||||
name: Use Remote Workflow to Preview on Uffizzi
|
||||
needs:
|
||||
- cache-compose-file
|
||||
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2.6.1
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2
|
||||
with:
|
||||
# If this workflow was triggered by a PR close event, cache-key will be an empty string
|
||||
# and this reusable workflow will delete the preview deployment.
|
||||
@@ -81,4 +87,4 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
id-token: write
|
||||
|
||||
+1
-8
@@ -35,14 +35,7 @@ lazygit.exe
|
||||
|
||||
test/git_server/data
|
||||
|
||||
# we'll scrap these lines once we've fully moved over to the new integration test approach
|
||||
test/integration/*/actual/
|
||||
test/integration/*/used_config/
|
||||
# these sample hooks waste too much space
|
||||
test/integration/*/expected/**/hooks/
|
||||
test/integration/*/expected_remote/**/hooks/
|
||||
|
||||
test/integration_new/**
|
||||
test/results/**
|
||||
|
||||
oryxBuildBinary
|
||||
__debug_bin
|
||||
|
||||
Vendored
+2
-2
@@ -7,7 +7,7 @@
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "main.go",
|
||||
"args": ["--debug", "--use-config-file=.vscode/debugger_config.yml"],
|
||||
"args": ["--debug", "--use-config-file=${workspaceFolder}/.vscode/debugger_config.yml"],
|
||||
"console": "integratedTerminal",
|
||||
"presentation": {
|
||||
"hidden": true
|
||||
@@ -19,7 +19,7 @@
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "main.go",
|
||||
"args": ["--logs", "--use-config-file=.vscode/debugger_config.yml"],
|
||||
"args": ["--logs", "--use-config-file=${workspaceFolder}/.vscode/debugger_config.yml"],
|
||||
"console": "integratedTerminal",
|
||||
"presentation": {
|
||||
"hidden": true
|
||||
|
||||
Vendored
+15
-3
@@ -5,8 +5,8 @@
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Generate cheatsheet",
|
||||
"type": "process",
|
||||
"command": "go run scripts/cheatsheet/main.go ",
|
||||
"type": "shell",
|
||||
"command": "go run scripts/cheatsheet/main.go generate",
|
||||
"problemMatcher": [],
|
||||
},
|
||||
{
|
||||
@@ -69,6 +69,18 @@
|
||||
"presentation": {
|
||||
"focus": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Sync tests list",
|
||||
"type": "shell",
|
||||
"command": "go generate pkg/integration/tests/tests.go",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
},
|
||||
"presentation": {
|
||||
"focus": true
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
+5
-22
@@ -99,36 +99,19 @@ go install mvdan.cc/gofumpt@latest && gofumpt -l -w .
|
||||
|
||||
## Internationalisation
|
||||
|
||||
Boy that's a hard word to spell. Anyway, lazygit is translated into several languages within the pkg/i18n package. If you need to render text to the user, you should add a new field to the TranslationSet struct in `pkg/i18n/english.go` and add the actual content within the `EnglishTranslationSet()` method in the same file. Then you can access via `gui.Tr.YourNewText` (or `app.Tr.YourNewText`, etc). Although it is appreciated if you translate the text into other languages, it's not expected of you (google translate will likely do a bad job anyway!).
|
||||
Boy that's a hard word to spell. Anyway, lazygit is translated into several languages within the pkg/i18n package. If you need to render text to the user, you should add a new field to the TranslationSet struct in `pkg/i18n/english.go` and add the actual content within the `EnglishTranslationSet()` method in the same file. Then you can access via `gui.Tr.YourNewText` (or `self.c.Tr.YourNewText`, etc). Although it is appreciated if you translate the text into other languages, it's not expected of you (google translate will likely do a bad job anyway!).
|
||||
|
||||
Note, we use 'Sentence case' for everything (so no 'Title Case' or 'whatever-it's-called-when-there's-no-capital-letters-case')
|
||||
|
||||
## Debugging
|
||||
|
||||
The easiest way to debug lazygit is to have two terminal tabs open at once: one for running lazygit (via `go run main.go -debug` in the project root) and one for viewing lazygit's logs (which can be done via `go run main.go --logs` or just `lazygit --logs`).
|
||||
|
||||
From most places in the codebase you have access to a logger e.g. `gui.Log.Warn("blah")`.
|
||||
From most places in the codebase you have access to a logger e.g. `gui.Log.Warn("blah")` or `self.c.Log.Warn("blah")`.
|
||||
|
||||
If you find that the existing logs are too noisy, you can set the log level with e.g. `LOG_LEVEL=warn go run main.go -debug` and then only use `Warn` logs yourself.
|
||||
|
||||
If you need to log from code in the vendor directory (e.g. the `gocui` package), you won't have access to the logger, but you can easily add logging support by adding the following:
|
||||
|
||||
```go
|
||||
func newLogger() *logrus.Entry {
|
||||
// REPLACE THE BELOW PATH WITH YOUR ACTUAL LOG PATH (YOU'LL SEE THIS PRINTED WHEN YOU RUN `lazygit --logs`
|
||||
logPath := "/Users/jesseduffield/Library/Application Support/jesseduffield/lazygit/development.log"
|
||||
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
panic("unable to log to file")
|
||||
}
|
||||
logger := logrus.New()
|
||||
logger.SetLevel(logrus.WarnLevel)
|
||||
logger.SetOutput(file)
|
||||
return logger.WithFields(logrus.Fields{})
|
||||
}
|
||||
|
||||
var Log = newLogger()
|
||||
...
|
||||
Log.Warn("blah")
|
||||
```
|
||||
If you need to log from code in the vendor directory (e.g. the `gocui` package), you won't have access to the logger, but you can easily add logging support by setting the `LAZYGIT_LOG_PATH` environment variable and using `logs.Global.Warn("blah")`. This is a global logger that's only intended for development purposes.
|
||||
|
||||
If you keep having to do some setup steps to reproduce an issue, read the Testing section below to see how to create an integration test by recording a lazygit session. It's pretty easy!
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/clients"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
var usage = `
|
||||
@@ -17,11 +19,6 @@ Usage:
|
||||
If you pass no test names, it runs all tests
|
||||
Accepted environment variables:
|
||||
KEY_PRESS_DELAY (e.g. 200): the number of milliseconds to wait between keypresses
|
||||
MODE:
|
||||
* ask (default): if a snapshot test fails, asks if you want to update the snapshot
|
||||
* check: if a snapshot test fails, exits with an error
|
||||
* update: if a snapshot test fails, updates the snapshot
|
||||
* sandbox: uses the test's setup step to run the test in a sandbox where you can do whatever you want
|
||||
|
||||
TUI mode:
|
||||
> go run cmd/integration_test/main.go tui
|
||||
@@ -54,6 +51,11 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// allowing full test paths to be passed for convenience
|
||||
testNames = lo.Map(testNames, func(name string, _ int) string {
|
||||
return components.TestNameFromFilePath(name)
|
||||
})
|
||||
|
||||
clients.RunCLI(testNames, slow, sandbox)
|
||||
case "tui":
|
||||
clients.RunTUI()
|
||||
|
||||
+43
-44
@@ -9,7 +9,7 @@ Default path for the config file:
|
||||
For old installations (slightly embarrassing: I didn't realise at the time that you didn't need to supply a vendor name to the path so I just used my name):
|
||||
|
||||
- Linux: `~/.config/jesseduffield/lazygit/config.yml`
|
||||
- MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
|
||||
- MacOS: `~/Library/Application\ Support/jesseduffield/lazygit/config.yml`
|
||||
- Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
|
||||
|
||||
If you want to change the config directory:
|
||||
@@ -28,7 +28,8 @@ gui:
|
||||
expandFocusedSidePanel: false
|
||||
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
|
||||
language: 'auto' # one of 'auto' | 'en' | 'zh' | 'pl' | 'nl' | 'ja' | 'ko'
|
||||
timeFormat: '02 Jan 06 15:04 MST' # https://pkg.go.dev/time#Time.Format
|
||||
timeFormat: '02 Jan 06' # https://pkg.go.dev/time#Time.Format
|
||||
shortTimeFormat: '3:04PM'
|
||||
theme:
|
||||
activeBorderColor:
|
||||
- green
|
||||
@@ -57,12 +58,14 @@ gui:
|
||||
showFileTree: true # for rendering changes files in a tree format
|
||||
showListFooter: true # for seeing the '5 of 20' message in list panels
|
||||
showRandomTip: true
|
||||
experimentalShowBranchHeads: false # visualize branch heads with (*) in commits list
|
||||
showBottomLine: true # for hiding the bottom information line (unless it has important information to tell you)
|
||||
showCommandLog: true
|
||||
showIcons: false
|
||||
commandLogSize: 8
|
||||
splitDiff: 'auto' # one of 'auto' | 'always'
|
||||
skipRewordInEditorWarning: false # for skipping the confirmation before launching the reword editor
|
||||
border: 'single' # one of 'single' | 'double' | 'rounded' | 'hidden'
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
@@ -86,6 +89,9 @@ git:
|
||||
# displays the whole git graph by default in the commits panel (equivalent to passing the `--all` argument to `git log`)
|
||||
showWholeGraph: false
|
||||
skipHookPrefix: WIP
|
||||
# The main branches. We colour commits green if they belong to one of these branches,
|
||||
# so that you can easily see which commits are unique to your branch (coloured in yellow)
|
||||
mainBranches: [master, main]
|
||||
autoFetch: true
|
||||
autoRefresh: true
|
||||
branchLogCmd: 'git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --'
|
||||
@@ -95,9 +101,12 @@ git:
|
||||
parseEmoji: false
|
||||
diffContextSize: 3 # how many lines of context are shown around a change in diffs
|
||||
os:
|
||||
editCommand: '' # see 'Configuring File Editing' section
|
||||
editCommandTemplate: ''
|
||||
openCommand: ''
|
||||
editPreset: '' # see 'Configuring File Editing' section
|
||||
edit: ''
|
||||
editAtLine: ''
|
||||
editAtLineAndWait: ''
|
||||
open: ''
|
||||
openLink: ''
|
||||
refresher:
|
||||
refreshInterval: 10 # File/submodule refresh interval in seconds. Auto-refresh can be disabled via option 'git.autoRefresh'.
|
||||
fetchInterval: 60 # Re-fetch interval in seconds. Auto-fetch can be disabled via option 'git.autoFetch'.
|
||||
@@ -115,8 +124,6 @@ keybinding:
|
||||
quit: 'q'
|
||||
quit-alt1: '<c-c>' # alternative/alias of quit
|
||||
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
|
||||
# When set to a printable character, this will work for returning from non-prompt panels
|
||||
return-alt1: null
|
||||
quitWithoutChangingDirectory: 'Q'
|
||||
togglePanel: '<tab>' # goto the next panel
|
||||
prevItem: '<up>' # go one line up
|
||||
@@ -136,13 +143,12 @@ keybinding:
|
||||
jumpToBlock: ['1', '2', '3', '4', '5'] # goto the Nth block / panel
|
||||
nextMatch: 'n'
|
||||
prevMatch: 'N'
|
||||
optionMenu: 'x' # show help menu
|
||||
optionMenu: null # show help menu
|
||||
optionMenu-alt1: '?' # show help menu
|
||||
select: '<space>'
|
||||
goInto: '<enter>'
|
||||
openRecentRepos: '<c-r>'
|
||||
confirm: '<enter>'
|
||||
confirm-alt1: 'y'
|
||||
remove: 'd'
|
||||
new: 'n'
|
||||
edit: 'e'
|
||||
@@ -170,7 +176,6 @@ keybinding:
|
||||
diffingMenu-alt: '<c-e>' # deprecated
|
||||
copyToClipboard: '<c-o>'
|
||||
submitEditorText: '<enter>'
|
||||
appendNewline: '<a-enter>'
|
||||
extrasMenu: '@'
|
||||
toggleWhitespaceInDiffView: '<c-w>'
|
||||
increaseContextInDiffView: '}'
|
||||
@@ -203,6 +208,7 @@ keybinding:
|
||||
mergeIntoCurrentBranch: 'M'
|
||||
viewGitFlowOptions: 'i'
|
||||
fastForward: 'f' # fast-forward this branch from its upstream
|
||||
createTag: 'T'
|
||||
pushTag: 'P'
|
||||
setUpstream: 'u' # set as upstream of checked-out branch
|
||||
fetchRemote: 'f'
|
||||
@@ -250,59 +256,60 @@ keybinding:
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'start "" {{filename}}'
|
||||
open: 'start "" {{filename}}'
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'xdg-open {{filename}} >/dev/null'
|
||||
open: 'xdg-open {{filename}} >/dev/null'
|
||||
```
|
||||
|
||||
### OSX
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'open {{filename}}'
|
||||
open: 'open {{filename}}'
|
||||
```
|
||||
|
||||
### Configuring File Editing
|
||||
|
||||
Lazygit will edit a file with the first set editor in the following:
|
||||
There are two commands for opening files, `o` for "open" and `e` for "edit". `o`
|
||||
acts as if the file was double-clicked in the Finder/Explorer, so it also works
|
||||
for non-text files, whereas `e` opens the file in an editor. `e` can also jump
|
||||
to the right line in the file if you invoke it from the staging panel, for
|
||||
example.
|
||||
|
||||
1. config.yaml
|
||||
To tell lazygit which editor to use for the `e` command, the easiest way to do
|
||||
that is to provide an editPreset config, e.g.
|
||||
|
||||
```yaml
|
||||
os:
|
||||
editCommand: 'vim' # as an example
|
||||
editPreset: 'vscode'
|
||||
```
|
||||
|
||||
2. \$(git config core.editor)
|
||||
3. \$GIT_EDITOR
|
||||
4. \$VISUAL
|
||||
5. \$EDITOR
|
||||
6. \$(which vi)
|
||||
Supported presets are `vim`, `nvim`, `emacs`, `nano`, `vscode`, `sublime`, `bbedit`,
|
||||
`kakoune`, `helix`, and `xcode`. In many cases lazygit will be able to guess the right preset
|
||||
from your $(git config core.editor), or an environment variable such as $VISUAL or $EDITOR.
|
||||
|
||||
Lazygit will log an error if none of these options are set.
|
||||
|
||||
You can specify the current line number when you're in the patch explorer.
|
||||
If for some reason you are not happy with the default commands from a preset, or
|
||||
there simply is no preset for your editor, you can customize the commands by
|
||||
setting the `edit`, `editAtLine`, and `editAtLineAndWait` options, e.g.:
|
||||
|
||||
```yaml
|
||||
os:
|
||||
editCommand: 'vim'
|
||||
editCommandTemplate: '{{editor}} +{{line}} -- {{filename}}'
|
||||
edit: 'myeditor {{filename}}'
|
||||
editAtLine: 'myeditor --line={{line}} {{filename}}'
|
||||
editAtLineAndWait: 'myeditor --block --line={{line}} {{filename}}'
|
||||
editInTerminal: true
|
||||
```
|
||||
|
||||
or
|
||||
The `editInTerminal` option is used to decide whether lazygit needs to suspend
|
||||
itself to the background before calling the editor.
|
||||
|
||||
```yaml
|
||||
os:
|
||||
editCommand: 'code'
|
||||
editCommandTemplate: '{{editor}} --goto -- {{filename}}:{{line}}'
|
||||
```
|
||||
|
||||
`{{editor}}` in `editCommandTemplate` is replaced with the value of `editCommand`.
|
||||
Contributions of new editor presets are welcome; see the `getPreset` function in
|
||||
[`editor_presets.go`](https://github.com/jesseduffield/lazygit/blob/master/pkg/config/editor_presets.go).
|
||||
|
||||
### Overriding default config file location
|
||||
|
||||
@@ -318,15 +325,6 @@ or
|
||||
LG_CONFIG_FILE="$HOME/.base_lg_conf,$HOME/.light_theme_lg_conf" lazygit
|
||||
```
|
||||
|
||||
### Recommended Config Values
|
||||
|
||||
for users of VSCode
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'code -rg {{filename}}'
|
||||
```
|
||||
|
||||
## Color Attributes
|
||||
|
||||
For color attributes you can choose an array of attributes (with max one color attribute)
|
||||
@@ -350,6 +348,7 @@ The available attributes are:
|
||||
- default
|
||||
- reverse # useful for high-contrast
|
||||
- underline
|
||||
- strikethrough
|
||||
|
||||
## Highlighting the selected line
|
||||
|
||||
@@ -480,7 +479,7 @@ services:
|
||||
Where:
|
||||
|
||||
- `gitDomain` stands for the domain used by git itself (i.e. the one present on clone URLs), e.g. `git.work.com`
|
||||
- `provider` is one of `github`, `bitbucket`, `bitbucketServer`, `azuredevops` or `gitlab`
|
||||
- `provider` is one of `github`, `bitbucket`, `bitbucketServer`, `azuredevops`, `gitlab` or `gitea`
|
||||
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
|
||||
|
||||
## Predefined commit message prefix
|
||||
|
||||
+234
-209
@@ -2,294 +2,319 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
# Lazygit Keybindings
|
||||
|
||||
## Global Keybindings
|
||||
_Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
|
||||
## Global keybindings
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+r</kbd>: switch to a recent repo
|
||||
<kbd>pgup</kbd>: scroll up main panel (fn+up/shift+k)
|
||||
<kbd>pgdown</kbd>: scroll down main panel (fn+down/shift+j)
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
<kbd>ctrl+p</kbd>: view custom patch options
|
||||
<kbd>R</kbd>: refresh
|
||||
<kbd>x</kbd>: open menu
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
<kbd>@</kbd>: open command log menu
|
||||
<kbd><c-r></kbd>: Switch to a recent repo
|
||||
<kbd><pgup></kbd>: Scroll up main panel (fn+up/shift+k)
|
||||
<kbd><pgdown></kbd>: Scroll down main panel (fn+down/shift+j)
|
||||
<kbd>@</kbd>: Open command log menu
|
||||
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
|
||||
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd>z</kbd>: undo (via reflog) (experimental)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd>:</kbd>: Execute custom command
|
||||
<kbd><c-p></kbd>: View custom patch options
|
||||
<kbd>m</kbd>: View merge/rebase options
|
||||
<kbd>R</kbd>: Refresh
|
||||
<kbd>+</kbd>: Next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: Prev screen mode
|
||||
<kbd>?</kbd>: Open menu
|
||||
<kbd><c-s></kbd>: View filter-by-path options
|
||||
<kbd>W</kbd>: Open diff menu
|
||||
<kbd><c-e></kbd>: Open diff menu
|
||||
<kbd><c-w></kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
<kbd>z</kbd>: Undo
|
||||
<kbd><c-z></kbd>: Redo
|
||||
<kbd>P</kbd>: Push
|
||||
<kbd>p</kbd>: Pull
|
||||
</pre>
|
||||
|
||||
## List Panel Navigation
|
||||
## List panel navigation
|
||||
|
||||
<pre>
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
<kbd>]</kbd>: next tab
|
||||
<kbd>[</kbd>: previous tab
|
||||
<kbd>,</kbd>: Previous page
|
||||
<kbd>.</kbd>: Next page
|
||||
<kbd><</kbd>: Scroll to top
|
||||
<kbd>/</kbd>: Start search
|
||||
<kbd>></kbd>: Scroll to bottom
|
||||
<kbd>H</kbd>: Scroll left
|
||||
<kbd>L</kbd>: Scroll right
|
||||
<kbd>]</kbd>: Next tab
|
||||
<kbd>[</kbd>: Previous tab
|
||||
</pre>
|
||||
|
||||
## Commit Files
|
||||
## Commit files
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy the committed file name to the clipboard
|
||||
<kbd>c</kbd>: checkout file
|
||||
<kbd>d</kbd>: discard this commit's changes to this file
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>space</kbd>: toggle file included in patch
|
||||
<kbd>a</kbd>: toggle all files included in patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
<kbd><c-o></kbd>: Copy the committed file name to the clipboard
|
||||
<kbd>c</kbd>: Checkout file
|
||||
<kbd>d</kbd>: Discard this commit's changes to this file
|
||||
<kbd>o</kbd>: Open file
|
||||
<kbd>e</kbd>: Edit file
|
||||
<kbd><space></kbd>: Toggle file included in patch
|
||||
<kbd>a</kbd>: Toggle all files included in patch
|
||||
<kbd><enter></kbd>: Enter file to add selected lines to the patch (or toggle directory collapsed)
|
||||
<kbd>`</kbd>: Toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Commit summary
|
||||
|
||||
<pre>
|
||||
<kbd><enter></kbd>: Confirm
|
||||
<kbd><esc></kbd>: Close
|
||||
</pre>
|
||||
|
||||
## Commits
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>b</kbd>: view bisect options
|
||||
<kbd>s</kbd>: squash down
|
||||
<kbd>f</kbd>: fixup commit
|
||||
<kbd>r</kbd>: reword commit
|
||||
<kbd>R</kbd>: reword commit with editor
|
||||
<kbd>d</kbd>: delete commit
|
||||
<kbd>e</kbd>: edit commit
|
||||
<kbd>p</kbd>: pick commit (when mid-rebase)
|
||||
<kbd>F</kbd>: create fixup commit for this commit
|
||||
<kbd>S</kbd>: squash all 'fixup!' commits above selected commit (autosquash)
|
||||
<kbd>ctrl+j</kbd>: move commit down one
|
||||
<kbd>ctrl+k</kbd>: move commit up one
|
||||
<kbd>v</kbd>: paste commits (cherry-pick)
|
||||
<kbd>A</kbd>: amend commit with staged changes
|
||||
<kbd>a</kbd>: reset commit author
|
||||
<kbd>t</kbd>: revert commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+l</kbd>: open log menu
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>c</kbd>: copy commit (cherry-pick)
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
<kbd><c-o></kbd>: Copy commit SHA to clipboard
|
||||
<kbd><c-r></kbd>: Reset cherry-picked (copied) commits selection
|
||||
<kbd>b</kbd>: View bisect options
|
||||
<kbd>s</kbd>: Squash down
|
||||
<kbd>f</kbd>: Fixup commit
|
||||
<kbd>r</kbd>: Reword commit
|
||||
<kbd>R</kbd>: Reword commit with editor
|
||||
<kbd>d</kbd>: Delete commit
|
||||
<kbd>e</kbd>: Edit commit
|
||||
<kbd>p</kbd>: Pick commit (when mid-rebase)
|
||||
<kbd>F</kbd>: Create fixup commit for this commit
|
||||
<kbd>S</kbd>: Squash all 'fixup!' commits above selected commit (autosquash)
|
||||
<kbd><c-j></kbd>: Move commit down one
|
||||
<kbd><c-k></kbd>: Move commit up one
|
||||
<kbd>v</kbd>: Paste commits (cherry-pick)
|
||||
<kbd>A</kbd>: Amend commit with staged changes
|
||||
<kbd>a</kbd>: Set/Reset commit author
|
||||
<kbd>t</kbd>: Revert commit
|
||||
<kbd>T</kbd>: Tag commit
|
||||
<kbd><c-l></kbd>: Open log menu
|
||||
<kbd><space></kbd>: Checkout commit
|
||||
<kbd>y</kbd>: Copy commit attribute
|
||||
<kbd>o</kbd>: Open commit in browser
|
||||
<kbd>n</kbd>: Create new branch off of commit
|
||||
<kbd>g</kbd>: View reset options
|
||||
<kbd>c</kbd>: Copy commit (cherry-pick)
|
||||
<kbd>C</kbd>: Copy commit range (cherry-pick)
|
||||
<kbd><enter></kbd>: View selected item's files
|
||||
</pre>
|
||||
|
||||
## Confirmation panel
|
||||
|
||||
<pre>
|
||||
<kbd><enter></kbd>: Confirm
|
||||
<kbd><esc></kbd>: Close/Cancel
|
||||
</pre>
|
||||
|
||||
## Files
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
<kbd>d</kbd>: view 'discard changes' options
|
||||
<kbd>space</kbd>: toggle staged
|
||||
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
|
||||
<kbd>c</kbd>: commit changes
|
||||
<kbd>w</kbd>: commit changes without pre-commit hook
|
||||
<kbd>A</kbd>: amend last commit
|
||||
<kbd>C</kbd>: commit changes using git editor
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>i</kbd>: ignore or exclude file
|
||||
<kbd>r</kbd>: refresh files
|
||||
<kbd>s</kbd>: stash all changes
|
||||
<kbd>S</kbd>: view stash options
|
||||
<kbd>a</kbd>: stage/unstage all
|
||||
<kbd>enter</kbd>: stage individual hunks/lines for file, or collapse/expand for directory
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>D</kbd>: view reset options
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd><c-o></kbd>: Copy the file name to the clipboard
|
||||
<kbd>d</kbd>: View 'discard changes' options
|
||||
<kbd><space></kbd>: Toggle staged
|
||||
<kbd><c-b></kbd>: Filter files (staged/unstaged)
|
||||
<kbd>c</kbd>: Commit changes
|
||||
<kbd>w</kbd>: Commit changes without pre-commit hook
|
||||
<kbd>A</kbd>: Amend last commit
|
||||
<kbd>C</kbd>: Commit changes using git editor
|
||||
<kbd>e</kbd>: Edit file
|
||||
<kbd>o</kbd>: Open file
|
||||
<kbd>i</kbd>: Ignore or exclude file
|
||||
<kbd>r</kbd>: Refresh files
|
||||
<kbd>s</kbd>: Stash all changes
|
||||
<kbd>S</kbd>: View stash options
|
||||
<kbd>a</kbd>: Stage/unstage all
|
||||
<kbd><enter></kbd>: Stage individual hunks/lines for file, or collapse/expand for directory
|
||||
<kbd>g</kbd>: View upstream reset options
|
||||
<kbd>D</kbd>: View reset options
|
||||
<kbd>`</kbd>: Toggle file tree view
|
||||
<kbd>M</kbd>: Open external merge tool (git mergetool)
|
||||
<kbd>f</kbd>: Fetch
|
||||
</pre>
|
||||
|
||||
## Local Branches
|
||||
## Local branches
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy branch name to clipboard
|
||||
<kbd>i</kbd>: show git-flow options
|
||||
<kbd>space</kbd>: checkout
|
||||
<kbd>n</kbd>: new branch
|
||||
<kbd>o</kbd>: create pull request
|
||||
<kbd>O</kbd>: create pull request options
|
||||
<kbd>ctrl+y</kbd>: copy pull request URL to clipboard
|
||||
<kbd>c</kbd>: checkout by name
|
||||
<kbd>F</kbd>: force checkout
|
||||
<kbd>d</kbd>: delete branch
|
||||
<kbd>r</kbd>: rebase checked-out branch onto this branch
|
||||
<kbd>M</kbd>: merge into currently checked out branch
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>R</kbd>: rename branch
|
||||
<kbd>u</kbd>: set/unset upstream
|
||||
<kbd>enter</kbd>: view commits
|
||||
<kbd><c-o></kbd>: Copy branch name to clipboard
|
||||
<kbd>i</kbd>: Show git-flow options
|
||||
<kbd><space></kbd>: Checkout
|
||||
<kbd>n</kbd>: New branch
|
||||
<kbd>o</kbd>: Create pull request
|
||||
<kbd>O</kbd>: Create pull request options
|
||||
<kbd><c-y></kbd>: Copy pull request URL to clipboard
|
||||
<kbd>c</kbd>: Checkout by name
|
||||
<kbd>F</kbd>: Force checkout
|
||||
<kbd>d</kbd>: Delete branch
|
||||
<kbd>r</kbd>: Rebase checked-out branch onto this branch
|
||||
<kbd>M</kbd>: Merge into currently checked out branch
|
||||
<kbd>f</kbd>: Fast-forward this branch from its upstream
|
||||
<kbd>T</kbd>: Create tag
|
||||
<kbd>g</kbd>: View reset options
|
||||
<kbd>R</kbd>: Rename branch
|
||||
<kbd>u</kbd>: Set/Unset upstream
|
||||
<kbd><enter></kbd>: View commits
|
||||
</pre>
|
||||
|
||||
## Main Panel (Merging)
|
||||
## Main panel (merging)
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>◄</kbd>: select previous conflict
|
||||
<kbd>►</kbd>: select next conflict
|
||||
<kbd>▲</kbd>: select previous hunk
|
||||
<kbd>▼</kbd>: select next hunk
|
||||
<kbd>z</kbd>: undo
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick all hunks
|
||||
<kbd>esc</kbd>: return to files panel
|
||||
<kbd>e</kbd>: Edit file
|
||||
<kbd>o</kbd>: Open file
|
||||
<kbd><left></kbd>: Select previous conflict
|
||||
<kbd><right></kbd>: Select next conflict
|
||||
<kbd><up></kbd>: Select previous hunk
|
||||
<kbd><down></kbd>: Select next hunk
|
||||
<kbd>z</kbd>: Undo
|
||||
<kbd>M</kbd>: Open external merge tool (git mergetool)
|
||||
<kbd><space></kbd>: Pick hunk
|
||||
<kbd>b</kbd>: Pick all hunks
|
||||
<kbd><esc></kbd>: Return to files panel
|
||||
</pre>
|
||||
|
||||
## Main Panel (Normal)
|
||||
## Main panel (normal)
|
||||
|
||||
<pre>
|
||||
<kbd>mouse wheel ▼</kbd>: scroll down (fn+up)
|
||||
<kbd>mouse wheel ▲</kbd>: scroll up (fn+down)
|
||||
<kbd>mouse wheel down</kbd>: Scroll down (fn+up)
|
||||
<kbd>mouse wheel up</kbd>: Scroll up (fn+down)
|
||||
</pre>
|
||||
|
||||
## Main Panel (Patch Building)
|
||||
## Main panel (patch building)
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
<kbd>►</kbd>: select next hunk
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>space</kbd>: add/remove line(s) to patch
|
||||
<kbd>esc</kbd>: exit custom patch builder
|
||||
<kbd><left></kbd>: Select previous hunk
|
||||
<kbd><right></kbd>: Select next hunk
|
||||
<kbd>v</kbd>: Toggle drag select
|
||||
<kbd>V</kbd>: Toggle drag select
|
||||
<kbd>a</kbd>: Toggle select hunk
|
||||
<kbd><c-o></kbd>: Copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: Open file
|
||||
<kbd>e</kbd>: Edit file
|
||||
<kbd><space></kbd>: Add/Remove line(s) to patch
|
||||
<kbd><esc></kbd>: Exit custom patch builder
|
||||
</pre>
|
||||
|
||||
## Main Panel (Staging)
|
||||
## Main panel (staging)
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
<kbd>►</kbd>: select next hunk
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>esc</kbd>: return to files panel
|
||||
<kbd>tab</kbd>: switch to other panel (staged/unstaged changes)
|
||||
<kbd>space</kbd>: toggle line staged / unstaged
|
||||
<kbd>d</kbd>: delete change (git reset)
|
||||
<kbd>E</kbd>: edit hunk
|
||||
<kbd>c</kbd>: commit changes
|
||||
<kbd>w</kbd>: commit changes without pre-commit hook
|
||||
<kbd>C</kbd>: commit changes using git editor
|
||||
<kbd><left></kbd>: Select previous hunk
|
||||
<kbd><right></kbd>: Select next hunk
|
||||
<kbd>v</kbd>: Toggle drag select
|
||||
<kbd>V</kbd>: Toggle drag select
|
||||
<kbd>a</kbd>: Toggle select hunk
|
||||
<kbd><c-o></kbd>: Copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: Open file
|
||||
<kbd>e</kbd>: Edit file
|
||||
<kbd><esc></kbd>: Return to files panel
|
||||
<kbd><tab></kbd>: Switch to other panel (staged/unstaged changes)
|
||||
<kbd><space></kbd>: Toggle line staged / unstaged
|
||||
<kbd>d</kbd>: Delete change (git reset)
|
||||
<kbd>E</kbd>: Edit hunk
|
||||
<kbd>c</kbd>: Commit changes
|
||||
<kbd>w</kbd>: Commit changes without pre-commit hook
|
||||
<kbd>C</kbd>: Commit changes using git editor
|
||||
</pre>
|
||||
|
||||
## Menu
|
||||
|
||||
<pre>
|
||||
<kbd><enter></kbd>: Execute
|
||||
<kbd><esc></kbd>: Close
|
||||
</pre>
|
||||
|
||||
## Reflog
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>c</kbd>: copy commit (cherry-pick)
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>enter</kbd>: view commits
|
||||
<kbd><c-o></kbd>: Copy commit SHA to clipboard
|
||||
<kbd><space></kbd>: Checkout commit
|
||||
<kbd>y</kbd>: Copy commit attribute
|
||||
<kbd>o</kbd>: Open commit in browser
|
||||
<kbd>n</kbd>: Create new branch off of commit
|
||||
<kbd>g</kbd>: View reset options
|
||||
<kbd>c</kbd>: Copy commit (cherry-pick)
|
||||
<kbd>C</kbd>: Copy commit range (cherry-pick)
|
||||
<kbd><c-r></kbd>: Reset cherry-picked (copied) commits selection
|
||||
<kbd><enter></kbd>: View commits
|
||||
</pre>
|
||||
|
||||
## Remote Branches
|
||||
## Remote branches
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: checkout
|
||||
<kbd>n</kbd>: new branch
|
||||
<kbd>M</kbd>: merge into currently checked out branch
|
||||
<kbd>r</kbd>: rebase checked-out branch onto this branch
|
||||
<kbd>d</kbd>: delete branch
|
||||
<kbd>u</kbd>: set as upstream of checked-out branch
|
||||
<kbd>esc</kbd>: Return to remotes list
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>enter</kbd>: view commits
|
||||
<kbd><c-o></kbd>: Copy branch name to clipboard
|
||||
<kbd><space></kbd>: Checkout
|
||||
<kbd>n</kbd>: New branch
|
||||
<kbd>M</kbd>: Merge into currently checked out branch
|
||||
<kbd>r</kbd>: Rebase checked-out branch onto this branch
|
||||
<kbd>d</kbd>: Delete branch
|
||||
<kbd>u</kbd>: Set as upstream of checked-out branch
|
||||
<kbd><esc></kbd>: Return to remotes list
|
||||
<kbd>g</kbd>: View reset options
|
||||
<kbd><enter></kbd>: View commits
|
||||
</pre>
|
||||
|
||||
## Remotes
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: fetch remote
|
||||
<kbd>n</kbd>: add new remote
|
||||
<kbd>d</kbd>: remove remote
|
||||
<kbd>e</kbd>: edit remote
|
||||
<kbd>f</kbd>: Fetch remote
|
||||
<kbd>n</kbd>: Add new remote
|
||||
<kbd>d</kbd>: Remove remote
|
||||
<kbd>e</kbd>: Edit remote
|
||||
</pre>
|
||||
|
||||
## Stash
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: apply
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
<kbd>n</kbd>: new branch
|
||||
<kbd>r</kbd>: rename stash
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
<kbd><space></kbd>: Apply
|
||||
<kbd>g</kbd>: Pop
|
||||
<kbd>d</kbd>: Drop
|
||||
<kbd>n</kbd>: New branch
|
||||
<kbd>r</kbd>: Rename stash
|
||||
<kbd><enter></kbd>: View selected item's files
|
||||
</pre>
|
||||
|
||||
## Status
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edit config file
|
||||
<kbd>o</kbd>: open config file
|
||||
<kbd>u</kbd>: check for update
|
||||
<kbd>enter</kbd>: switch to a recent repo
|
||||
<kbd>a</kbd>: show all branch logs
|
||||
<kbd>o</kbd>: Open config file
|
||||
<kbd>e</kbd>: Edit config file
|
||||
<kbd>u</kbd>: Check for update
|
||||
<kbd><enter></kbd>: Switch to a recent repo
|
||||
<kbd>a</kbd>: Show all branch logs
|
||||
</pre>
|
||||
|
||||
## Sub-commits
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>c</kbd>: copy commit (cherry-pick)
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
<kbd><c-o></kbd>: Copy commit SHA to clipboard
|
||||
<kbd><space></kbd>: Checkout commit
|
||||
<kbd>y</kbd>: Copy commit attribute
|
||||
<kbd>o</kbd>: Open commit in browser
|
||||
<kbd>n</kbd>: Create new branch off of commit
|
||||
<kbd>g</kbd>: View reset options
|
||||
<kbd>c</kbd>: Copy commit (cherry-pick)
|
||||
<kbd>C</kbd>: Copy commit range (cherry-pick)
|
||||
<kbd><c-r></kbd>: Reset cherry-picked (copied) commits selection
|
||||
<kbd><enter></kbd>: View selected item's files
|
||||
</pre>
|
||||
|
||||
## Submodules
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
|
||||
<kbd>enter</kbd>: enter submodule
|
||||
<kbd>d</kbd>: remove submodule
|
||||
<kbd>u</kbd>: update submodule
|
||||
<kbd>n</kbd>: add new submodule
|
||||
<kbd>e</kbd>: update submodule URL
|
||||
<kbd>i</kbd>: initialize submodule
|
||||
<kbd>b</kbd>: view bulk submodule options
|
||||
<kbd><c-o></kbd>: Copy submodule name to clipboard
|
||||
<kbd><enter></kbd>: Enter submodule
|
||||
<kbd>d</kbd>: Remove submodule
|
||||
<kbd>u</kbd>: Update submodule
|
||||
<kbd>n</kbd>: Add new submodule
|
||||
<kbd>e</kbd>: Update submodule URL
|
||||
<kbd>i</kbd>: Initialize submodule
|
||||
<kbd>b</kbd>: View bulk submodule options
|
||||
</pre>
|
||||
|
||||
## Tags
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: checkout
|
||||
<kbd>d</kbd>: delete tag
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: create tag
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>enter</kbd>: view commits
|
||||
<kbd><space></kbd>: Checkout
|
||||
<kbd>d</kbd>: Delete tag
|
||||
<kbd>P</kbd>: Push tag
|
||||
<kbd>n</kbd>: Create tag
|
||||
<kbd>g</kbd>: View reset options
|
||||
<kbd><enter></kbd>: View commits
|
||||
</pre>
|
||||
|
||||
+137
-112
@@ -2,29 +2,32 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
# Lazygit キーバインド
|
||||
|
||||
_Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
|
||||
## グローバルキーバインド
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+r</kbd>: 最近使用したリポジトリに切り替え
|
||||
<kbd>pgup</kbd>: メインパネルを上にスクロール (fn+up/shift+k)
|
||||
<kbd>pgdown</kbd>: メインパネルを下にスクロール (fn+down/shift+j)
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
<kbd>ctrl+p</kbd>: view custom patch options
|
||||
<kbd>R</kbd>: リフレッシュ
|
||||
<kbd>x</kbd>: メニューを開く
|
||||
<kbd>+</kbd>: 次のスクリーンモード (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: 前のスクリーンモード
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: 差分メニューを開く
|
||||
<kbd>ctrl+e</kbd>: 差分メニューを開く
|
||||
<kbd><c-r></kbd>: 最近使用したリポジトリに切り替え
|
||||
<kbd><pgup></kbd>: メインパネルを上にスクロール (fn+up/shift+k)
|
||||
<kbd><pgdown></kbd>: メインパネルを下にスクロール (fn+down/shift+j)
|
||||
<kbd>@</kbd>: コマンドログメニューを開く
|
||||
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
|
||||
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
|
||||
<kbd>:</kbd>: カスタムコマンドを実行
|
||||
<kbd><c-p></kbd>: View custom patch options
|
||||
<kbd>m</kbd>: View merge/rebase options
|
||||
<kbd>R</kbd>: リフレッシュ
|
||||
<kbd>+</kbd>: 次のスクリーンモード (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: 前のスクリーンモード
|
||||
<kbd>?</kbd>: メニューを開く
|
||||
<kbd><c-s></kbd>: View filter-by-path options
|
||||
<kbd>W</kbd>: 差分メニューを開く
|
||||
<kbd><c-e></kbd>: 差分メニューを開く
|
||||
<kbd><c-w></kbd>: 空白文字の差分の表示有無を切り替え
|
||||
<kbd>z</kbd>: アンドゥ (via reflog) (experimental)
|
||||
<kbd>ctrl+z</kbd>: リドゥ (via reflog) (experimental)
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd><c-z></kbd>: リドゥ (via reflog) (experimental)
|
||||
<kbd>P</kbd>: Push
|
||||
<kbd>p</kbd>: Pull
|
||||
</pre>
|
||||
|
||||
## 一覧パネルの操作
|
||||
@@ -32,9 +35,9 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<pre>
|
||||
<kbd>,</kbd>: 前のページ
|
||||
<kbd>.</kbd>: 次のページ
|
||||
<kbd><</kbd>: 最上部までスクロール
|
||||
<kbd><</kbd>: 最上部までスクロール
|
||||
<kbd>/</kbd>: 検索を開始
|
||||
<kbd>></kbd>: 最下部までスクロール
|
||||
<kbd>></kbd>: 最下部までスクロール
|
||||
<kbd>H</kbd>: 左スクロール
|
||||
<kbd>L</kbd>: 右スクロール
|
||||
<kbd>]</kbd>: 次のタブ
|
||||
@@ -44,118 +47,124 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## Stash
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: 適用
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
<kbd><space></kbd>: 適用
|
||||
<kbd>g</kbd>: Pop
|
||||
<kbd>d</kbd>: Drop
|
||||
<kbd>n</kbd>: 新しいブランチを作成
|
||||
<kbd>r</kbd>: Stashを変更
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
<kbd><enter></kbd>: View selected item's files
|
||||
</pre>
|
||||
|
||||
## Sub-commits
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: コミットのSHAをクリップボードにコピー
|
||||
<kbd>space</kbd>: コミットをチェックアウト
|
||||
<kbd><c-o></kbd>: コミットのSHAをクリップボードにコピー
|
||||
<kbd><space></kbd>: コミットをチェックアウト
|
||||
<kbd>y</kbd>: コミットの情報をコピー
|
||||
<kbd>o</kbd>: ブラウザでコミットを開く
|
||||
<kbd>n</kbd>: コミットにブランチを作成
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>g</kbd>: View reset options
|
||||
<kbd>c</kbd>: コミットをコピー (cherry-pick)
|
||||
<kbd>C</kbd>: コミットを範囲コピー (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
<kbd><c-r></kbd>: Reset cherry-picked (copied) commits selection
|
||||
<kbd><enter></kbd>: View selected item's files
|
||||
</pre>
|
||||
|
||||
## コミット
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: コミットのSHAをクリップボードにコピー
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>b</kbd>: view bisect options
|
||||
<kbd>s</kbd>: squash down
|
||||
<kbd>f</kbd>: fixup commit
|
||||
<kbd><c-o></kbd>: コミットのSHAをクリップボードにコピー
|
||||
<kbd><c-r></kbd>: Reset cherry-picked (copied) commits selection
|
||||
<kbd>b</kbd>: View bisect options
|
||||
<kbd>s</kbd>: Squash down
|
||||
<kbd>f</kbd>: Fixup commit
|
||||
<kbd>r</kbd>: コミットメッセージを変更
|
||||
<kbd>R</kbd>: エディタでコミットメッセージを編集
|
||||
<kbd>d</kbd>: コミットを削除
|
||||
<kbd>e</kbd>: コミットを編集
|
||||
<kbd>p</kbd>: pick commit (when mid-rebase)
|
||||
<kbd>p</kbd>: Pick commit (when mid-rebase)
|
||||
<kbd>F</kbd>: このコミットに対するfixupコミットを作成
|
||||
<kbd>S</kbd>: squash all 'fixup!' commits above selected commit (autosquash)
|
||||
<kbd>ctrl+j</kbd>: コミットを1つ下に移動
|
||||
<kbd>ctrl+k</kbd>: コミットを1つ上に移動
|
||||
<kbd>S</kbd>: Squash all 'fixup!' commits above selected commit (autosquash)
|
||||
<kbd><c-j></kbd>: コミットを1つ下に移動
|
||||
<kbd><c-k></kbd>: コミットを1つ上に移動
|
||||
<kbd>v</kbd>: コミットを貼り付け (cherry-pick)
|
||||
<kbd>A</kbd>: ステージされた変更でamendコミット
|
||||
<kbd>a</kbd>: reset commit author
|
||||
<kbd>a</kbd>: Set/Reset commit author
|
||||
<kbd>t</kbd>: コミットをrevert
|
||||
<kbd>T</kbd>: タグを作成
|
||||
<kbd>ctrl+l</kbd>: ログメニューを開く
|
||||
<kbd>space</kbd>: コミットをチェックアウト
|
||||
<kbd><c-l></kbd>: ログメニューを開く
|
||||
<kbd><space></kbd>: コミットをチェックアウト
|
||||
<kbd>y</kbd>: コミットの情報をコピー
|
||||
<kbd>o</kbd>: ブラウザでコミットを開く
|
||||
<kbd>n</kbd>: コミットにブランチを作成
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>g</kbd>: View reset options
|
||||
<kbd>c</kbd>: コミットをコピー (cherry-pick)
|
||||
<kbd>C</kbd>: コミットを範囲コピー (cherry-pick)
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
<kbd><enter></kbd>: View selected item's files
|
||||
</pre>
|
||||
|
||||
## コミットファイル
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: コミットされたファイル名をクリップボードにコピー
|
||||
<kbd>c</kbd>: checkout file
|
||||
<kbd>d</kbd>: discard this commit's changes to this file
|
||||
<kbd><c-o></kbd>: コミットされたファイル名をクリップボードにコピー
|
||||
<kbd>c</kbd>: Checkout file
|
||||
<kbd>d</kbd>: Discard this commit's changes to this file
|
||||
<kbd>o</kbd>: ファイルを開く
|
||||
<kbd>e</kbd>: ファイルを編集
|
||||
<kbd>space</kbd>: toggle file included in patch
|
||||
<kbd>a</kbd>: toggle all files included in patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
|
||||
<kbd><space></kbd>: Toggle file included in patch
|
||||
<kbd>a</kbd>: Toggle all files included in patch
|
||||
<kbd><enter></kbd>: Enter file to add selected lines to the patch (or toggle directory collapsed)
|
||||
<kbd>`</kbd>: ファイルツリーの表示を切り替え
|
||||
</pre>
|
||||
|
||||
## コミットメッセージ
|
||||
|
||||
<pre>
|
||||
<kbd><enter></kbd>: 確認
|
||||
<kbd><esc></kbd>: 閉じる
|
||||
</pre>
|
||||
|
||||
## サブモジュール
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: サブモジュール名をクリップボードにコピー
|
||||
<kbd>enter</kbd>: サブモジュールを開く
|
||||
<kbd><c-o></kbd>: サブモジュール名をクリップボードにコピー
|
||||
<kbd><enter></kbd>: サブモジュールを開く
|
||||
<kbd>d</kbd>: サブモジュールを削除
|
||||
<kbd>u</kbd>: サブモジュールを更新
|
||||
<kbd>n</kbd>: サブモジュールを新規追加
|
||||
<kbd>e</kbd>: サブモジュールのURLを更新
|
||||
<kbd>i</kbd>: サブモジュールを初期化
|
||||
<kbd>b</kbd>: view bulk submodule options
|
||||
<kbd>b</kbd>: View bulk submodule options
|
||||
</pre>
|
||||
|
||||
## ステータス
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: 設定ファイルを編集
|
||||
<kbd>o</kbd>: 設定ファイルを開く
|
||||
<kbd>e</kbd>: 設定ファイルを編集
|
||||
<kbd>u</kbd>: 更新を確認
|
||||
<kbd>enter</kbd>: 最近使用したリポジトリに切り替え
|
||||
<kbd><enter></kbd>: 最近使用したリポジトリに切り替え
|
||||
<kbd>a</kbd>: すべてのブランチログを表示
|
||||
</pre>
|
||||
|
||||
## タグ
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: チェックアウト
|
||||
<kbd><space></kbd>: チェックアウト
|
||||
<kbd>d</kbd>: タグを削除
|
||||
<kbd>P</kbd>: タグをpush
|
||||
<kbd>n</kbd>: タグを作成
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>enter</kbd>: コミットを閲覧
|
||||
<kbd>g</kbd>: View reset options
|
||||
<kbd><enter></kbd>: コミットを閲覧
|
||||
</pre>
|
||||
|
||||
## ファイル
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: ファイル名をクリップボードにコピー
|
||||
<kbd>ctrl+w</kbd>: 空白文字の差分の表示有無を切り替え
|
||||
<kbd>d</kbd>: view 'discard changes' options
|
||||
<kbd>space</kbd>: ステージ/アンステージ
|
||||
<kbd>ctrl+b</kbd>: ファイルをフィルタ (ステージ/アンステージ)
|
||||
<kbd><c-o></kbd>: ファイル名をクリップボードにコピー
|
||||
<kbd>d</kbd>: View 'discard changes' options
|
||||
<kbd><space></kbd>: ステージ/アンステージ
|
||||
<kbd><c-b></kbd>: ファイルをフィルタ (ステージ/アンステージ)
|
||||
<kbd>c</kbd>: 変更をコミット
|
||||
<kbd>w</kbd>: pre-commitフックを実行せずに変更をコミット
|
||||
<kbd>A</kbd>: 最新のコミットにamend
|
||||
@@ -165,36 +174,37 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>i</kbd>: ファイルをignore
|
||||
<kbd>r</kbd>: ファイルをリフレッシュ
|
||||
<kbd>s</kbd>: 変更をstash
|
||||
<kbd>S</kbd>: view stash options
|
||||
<kbd>S</kbd>: View stash options
|
||||
<kbd>a</kbd>: すべての変更をステージ/アンステージ
|
||||
<kbd>enter</kbd>: stage individual hunks/lines for file, or collapse/expand for directory
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>D</kbd>: view reset options
|
||||
<kbd><enter></kbd>: Stage individual hunks/lines for file, or collapse/expand for directory
|
||||
<kbd>g</kbd>: View upstream reset options
|
||||
<kbd>D</kbd>: View reset options
|
||||
<kbd>`</kbd>: ファイルツリーの表示を切り替え
|
||||
<kbd>M</kbd>: git mergetoolを開く
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>M</kbd>: Git mergetoolを開く
|
||||
<kbd>f</kbd>: Fetch
|
||||
</pre>
|
||||
|
||||
## ブランチ
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: ブランチ名をクリップボードにコピー
|
||||
<kbd>i</kbd>: show git-flow options
|
||||
<kbd>space</kbd>: チェックアウト
|
||||
<kbd><c-o></kbd>: ブランチ名をクリップボードにコピー
|
||||
<kbd>i</kbd>: Show git-flow options
|
||||
<kbd><space></kbd>: チェックアウト
|
||||
<kbd>n</kbd>: 新しいブランチを作成
|
||||
<kbd>o</kbd>: Pull Requestを作成
|
||||
<kbd>O</kbd>: create pull request options
|
||||
<kbd>ctrl+y</kbd>: Pull RequestのURLをクリップボードにコピー
|
||||
<kbd>c</kbd>: checkout by name
|
||||
<kbd>F</kbd>: force checkout
|
||||
<kbd>O</kbd>: Create pull request options
|
||||
<kbd><c-y></kbd>: Pull RequestのURLをクリップボードにコピー
|
||||
<kbd>c</kbd>: Checkout by name
|
||||
<kbd>F</kbd>: Force checkout
|
||||
<kbd>d</kbd>: ブランチを削除
|
||||
<kbd>r</kbd>: rebase checked-out branch onto this branch
|
||||
<kbd>r</kbd>: Rebase checked-out branch onto this branch
|
||||
<kbd>M</kbd>: 現在のブランチにマージ
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>f</kbd>: Fast-forward this branch from its upstream
|
||||
<kbd>T</kbd>: タグを作成
|
||||
<kbd>g</kbd>: View reset options
|
||||
<kbd>R</kbd>: ブランチ名を変更
|
||||
<kbd>u</kbd>: set/unset upstream
|
||||
<kbd>enter</kbd>: コミットを閲覧
|
||||
<kbd>u</kbd>: Set/Unset upstream
|
||||
<kbd><enter></kbd>: コミットを閲覧
|
||||
</pre>
|
||||
|
||||
## メインパネル (Merging)
|
||||
@@ -202,60 +212,67 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<pre>
|
||||
<kbd>e</kbd>: ファイルを編集
|
||||
<kbd>o</kbd>: ファイルを開く
|
||||
<kbd>◄</kbd>: 前のコンフリクトを選択
|
||||
<kbd>►</kbd>: 次のコンフリクトを選択
|
||||
<kbd>▲</kbd>: 前のhunkを選択
|
||||
<kbd>▼</kbd>: 次のhunkを選択
|
||||
<kbd><left></kbd>: 前のコンフリクトを選択
|
||||
<kbd><right></kbd>: 次のコンフリクトを選択
|
||||
<kbd><up></kbd>: 前のhunkを選択
|
||||
<kbd><down></kbd>: 次のhunkを選択
|
||||
<kbd>z</kbd>: アンドゥ
|
||||
<kbd>M</kbd>: git mergetoolを開く
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick all hunks
|
||||
<kbd>esc</kbd>: ファイル一覧に戻る
|
||||
<kbd>M</kbd>: Git mergetoolを開く
|
||||
<kbd><space></kbd>: Pick hunk
|
||||
<kbd>b</kbd>: Pick all hunks
|
||||
<kbd><esc></kbd>: ファイル一覧に戻る
|
||||
</pre>
|
||||
|
||||
## メインパネル (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd>mouse wheel ▼</kbd>: 下にスクロール (fn+up)
|
||||
<kbd>mouse wheel ▲</kbd>: 上にスクロール (fn+down)
|
||||
<kbd>mouse wheel down</kbd>: 下にスクロール (fn+up)
|
||||
<kbd>mouse wheel up</kbd>: 上にスクロール (fn+down)
|
||||
</pre>
|
||||
|
||||
## メインパネル (Patch Building)
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: 前のhunkを選択
|
||||
<kbd>►</kbd>: 次のhunkを選択
|
||||
<kbd><left></kbd>: 前のhunkを選択
|
||||
<kbd><right></kbd>: 次のhunkを選択
|
||||
<kbd>v</kbd>: 範囲選択を切り替え
|
||||
<kbd>V</kbd>: 範囲選択を切り替え
|
||||
<kbd>a</kbd>: hunk選択を切り替え
|
||||
<kbd>ctrl+o</kbd>: 選択されたテキストをクリップボードにコピー
|
||||
<kbd>a</kbd>: Hunk選択を切り替え
|
||||
<kbd><c-o></kbd>: 選択されたテキストをクリップボードにコピー
|
||||
<kbd>o</kbd>: ファイルを開く
|
||||
<kbd>e</kbd>: ファイルを編集
|
||||
<kbd>space</kbd>: 行をパッチに追加/削除
|
||||
<kbd>esc</kbd>: exit custom patch builder
|
||||
<kbd><space></kbd>: 行をパッチに追加/削除
|
||||
<kbd><esc></kbd>: Exit custom patch builder
|
||||
</pre>
|
||||
|
||||
## メインパネル (Staging)
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: 前のhunkを選択
|
||||
<kbd>►</kbd>: 次のhunkを選択
|
||||
<kbd><left></kbd>: 前のhunkを選択
|
||||
<kbd><right></kbd>: 次のhunkを選択
|
||||
<kbd>v</kbd>: 範囲選択を切り替え
|
||||
<kbd>V</kbd>: 範囲選択を切り替え
|
||||
<kbd>a</kbd>: hunk選択を切り替え
|
||||
<kbd>ctrl+o</kbd>: 選択されたテキストをクリップボードにコピー
|
||||
<kbd>a</kbd>: Hunk選択を切り替え
|
||||
<kbd><c-o></kbd>: 選択されたテキストをクリップボードにコピー
|
||||
<kbd>o</kbd>: ファイルを開く
|
||||
<kbd>e</kbd>: ファイルを編集
|
||||
<kbd>esc</kbd>: ファイル一覧に戻る
|
||||
<kbd>tab</kbd>: パネルを切り替え
|
||||
<kbd>space</kbd>: 選択行をステージ/アンステージ
|
||||
<kbd><esc></kbd>: ファイル一覧に戻る
|
||||
<kbd><tab></kbd>: パネルを切り替え
|
||||
<kbd><space></kbd>: 選択行をステージ/アンステージ
|
||||
<kbd>d</kbd>: 変更を削除 (git reset)
|
||||
<kbd>E</kbd>: edit hunk
|
||||
<kbd>E</kbd>: Edit hunk
|
||||
<kbd>c</kbd>: 変更をコミット
|
||||
<kbd>w</kbd>: pre-commitフックを実行せずに変更をコミット
|
||||
<kbd>C</kbd>: gitエディタを使用して変更をコミット
|
||||
</pre>
|
||||
|
||||
## メニュー
|
||||
|
||||
<pre>
|
||||
<kbd><enter></kbd>: 実行
|
||||
<kbd><esc></kbd>: 閉じる
|
||||
</pre>
|
||||
|
||||
## リモート
|
||||
|
||||
<pre>
|
||||
@@ -268,28 +285,36 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## リモートブランチ
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: チェックアウト
|
||||
<kbd><c-o></kbd>: ブランチ名をクリップボードにコピー
|
||||
<kbd><space></kbd>: チェックアウト
|
||||
<kbd>n</kbd>: 新しいブランチを作成
|
||||
<kbd>M</kbd>: 現在のブランチにマージ
|
||||
<kbd>r</kbd>: rebase checked-out branch onto this branch
|
||||
<kbd>r</kbd>: Rebase checked-out branch onto this branch
|
||||
<kbd>d</kbd>: ブランチを削除
|
||||
<kbd>u</kbd>: set as upstream of checked-out branch
|
||||
<kbd>esc</kbd>: リモート一覧に戻る
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>enter</kbd>: コミットを閲覧
|
||||
<kbd>u</kbd>: Set as upstream of checked-out branch
|
||||
<kbd><esc></kbd>: リモート一覧に戻る
|
||||
<kbd>g</kbd>: View reset options
|
||||
<kbd><enter></kbd>: コミットを閲覧
|
||||
</pre>
|
||||
|
||||
## 参照ログ
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: コミットのSHAをクリップボードにコピー
|
||||
<kbd>space</kbd>: コミットをチェックアウト
|
||||
<kbd><c-o></kbd>: コミットのSHAをクリップボードにコピー
|
||||
<kbd><space></kbd>: コミットをチェックアウト
|
||||
<kbd>y</kbd>: コミットの情報をコピー
|
||||
<kbd>o</kbd>: ブラウザでコミットを開く
|
||||
<kbd>n</kbd>: コミットにブランチを作成
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>g</kbd>: View reset options
|
||||
<kbd>c</kbd>: コミットをコピー (cherry-pick)
|
||||
<kbd>C</kbd>: コミットを範囲コピー (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>enter</kbd>: コミットを閲覧
|
||||
<kbd><c-r></kbd>: Reset cherry-picked (copied) commits selection
|
||||
<kbd><enter></kbd>: コミットを閲覧
|
||||
</pre>
|
||||
|
||||
## 確認パネル
|
||||
|
||||
<pre>
|
||||
<kbd><enter></kbd>: 確認
|
||||
<kbd><esc></kbd>: 閉じる/キャンセル
|
||||
</pre>
|
||||
|
||||
+137
-112
@@ -2,39 +2,42 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
# Lazygit 키 바인딩
|
||||
|
||||
_Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
|
||||
## 글로벌 키 바인딩
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+r</kbd>: 최근에 사용한 저장소로 전환
|
||||
<kbd>pgup</kbd>: 메인 패널을 위로 스크롤 (fn+up/shift+k)
|
||||
<kbd>pgdown</kbd>: 메인 패널을 아래로로 스크롤 (fn+down/shift+j)
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
<kbd>ctrl+p</kbd>: 커스텀 Patch 옵션 보기
|
||||
<kbd><c-r></kbd>: 최근에 사용한 저장소로 전환
|
||||
<kbd><pgup></kbd>: 메인 패널을 위로 스크롤 (fn+up/shift+k)
|
||||
<kbd><pgdown></kbd>: 메인 패널을 아래로로 스크롤 (fn+down/shift+j)
|
||||
<kbd>@</kbd>: 명령어 로그 메뉴 열기
|
||||
<kbd>}</kbd>: Diff 보기의 변경 사항 주위에 표시되는 컨텍스트의 크기를 늘리기
|
||||
<kbd>{</kbd>: Diff 보기의 변경 사항 주위에 표시되는 컨텍스트 크기 줄이기
|
||||
<kbd>:</kbd>: Execute custom command
|
||||
<kbd><c-p></kbd>: 커스텀 Patch 옵션 보기
|
||||
<kbd>m</kbd>: View merge/rebase options
|
||||
<kbd>R</kbd>: 새로고침
|
||||
<kbd>x</kbd>: 매뉴 열기
|
||||
<kbd>+</kbd>: 다음 스크린 모드 (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: 이전 스크린 모드
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>?</kbd>: 매뉴 열기
|
||||
<kbd><c-s></kbd>: View filter-by-path options
|
||||
<kbd>W</kbd>: Diff 메뉴 열기
|
||||
<kbd>ctrl+e</kbd>: Diff 메뉴 열기
|
||||
<kbd>@</kbd>: 명령어 로그 메뉴 열기
|
||||
<kbd>}</kbd>: diff 보기의 변경 사항 주위에 표시되는 컨텍스트의 크기를 늘리기
|
||||
<kbd>{</kbd>: diff 보기의 변경 사항 주위에 표시되는 컨텍스트 크기 줄이기
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd><c-e></kbd>: Diff 메뉴 열기
|
||||
<kbd><c-w></kbd>: 공백문자를 Diff 뷰에서 표시 여부 전환
|
||||
<kbd>z</kbd>: 되돌리기 (reflog) (실험적)
|
||||
<kbd>ctrl+z</kbd>: 다시 실행 (reflog) (실험적)
|
||||
<kbd><c-z></kbd>: 다시 실행 (reflog) (실험적)
|
||||
<kbd>P</kbd>: 푸시
|
||||
<kbd>p</kbd>: 업데이트
|
||||
</pre>
|
||||
|
||||
## List Panel Navigation
|
||||
## List panel navigation
|
||||
|
||||
<pre>
|
||||
<kbd>,</kbd>: 이전 페이지
|
||||
<kbd>.</kbd>: 다음 페이지
|
||||
<kbd><</kbd>: 맨 위로 스크롤
|
||||
<kbd><</kbd>: 맨 위로 스크롤
|
||||
<kbd>/</kbd>: 검색 시작
|
||||
<kbd>></kbd>: 맨 아래로 스크롤
|
||||
<kbd>></kbd>: 맨 아래로 스크롤
|
||||
<kbd>H</kbd>: 우 스크롤
|
||||
<kbd>L</kbd>: 좌 스크롤
|
||||
<kbd>]</kbd>: 이전 탭
|
||||
@@ -44,42 +47,49 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## Reflog
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 커밋 SHA를 클립보드에 복사
|
||||
<kbd>space</kbd>: 커밋을 체크아웃
|
||||
<kbd><c-o></kbd>: 커밋 SHA를 클립보드에 복사
|
||||
<kbd><space></kbd>: 커밋을 체크아웃
|
||||
<kbd>y</kbd>: 커밋 attribute 복사
|
||||
<kbd>o</kbd>: 브라우저에서 커밋 열기
|
||||
<kbd>n</kbd>: 커밋에서 새 브랜치를 만듭니다.
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>g</kbd>: View reset options
|
||||
<kbd>c</kbd>: 커밋을 복사 (cherry-pick)
|
||||
<kbd>C</kbd>: 커밋을 범위로 복사 (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>enter</kbd>: 커밋 보기
|
||||
<kbd><c-r></kbd>: Reset cherry-picked (copied) commits selection
|
||||
<kbd><enter></kbd>: 커밋 보기
|
||||
</pre>
|
||||
|
||||
## Stash
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: 적용
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
<kbd><space></kbd>: 적용
|
||||
<kbd>g</kbd>: Pop
|
||||
<kbd>d</kbd>: Drop
|
||||
<kbd>n</kbd>: 새 브랜치 생성
|
||||
<kbd>r</kbd>: rename stash
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
<kbd>r</kbd>: Rename stash
|
||||
<kbd><enter></kbd>: View selected item's files
|
||||
</pre>
|
||||
|
||||
## Sub-commits
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 커밋 SHA를 클립보드에 복사
|
||||
<kbd>space</kbd>: 커밋을 체크아웃
|
||||
<kbd><c-o></kbd>: 커밋 SHA를 클립보드에 복사
|
||||
<kbd><space></kbd>: 커밋을 체크아웃
|
||||
<kbd>y</kbd>: 커밋 attribute 복사
|
||||
<kbd>o</kbd>: 브라우저에서 커밋 열기
|
||||
<kbd>n</kbd>: 커밋에서 새 브랜치를 만듭니다.
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>g</kbd>: View reset options
|
||||
<kbd>c</kbd>: 커밋을 복사 (cherry-pick)
|
||||
<kbd>C</kbd>: 커밋을 범위로 복사 (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
<kbd><c-r></kbd>: Reset cherry-picked (copied) commits selection
|
||||
<kbd><enter></kbd>: View selected item's files
|
||||
</pre>
|
||||
|
||||
## 메뉴
|
||||
|
||||
<pre>
|
||||
<kbd><enter></kbd>: 실행
|
||||
<kbd><esc></kbd>: 닫기
|
||||
</pre>
|
||||
|
||||
## 메인 패널 (Merging)
|
||||
@@ -87,103 +97,104 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<pre>
|
||||
<kbd>e</kbd>: 파일 편집
|
||||
<kbd>o</kbd>: 파일 닫기
|
||||
<kbd>◄</kbd>: 이전 충돌을 선택
|
||||
<kbd>►</kbd>: 다음 충돌을 선택
|
||||
<kbd>▲</kbd>: 이전 hunk를 선택
|
||||
<kbd>▼</kbd>: 다음 hunk를 선택
|
||||
<kbd><left></kbd>: 이전 충돌을 선택
|
||||
<kbd><right></kbd>: 다음 충돌을 선택
|
||||
<kbd><up></kbd>: 이전 hunk를 선택
|
||||
<kbd><down></kbd>: 다음 hunk를 선택
|
||||
<kbd>z</kbd>: 되돌리기
|
||||
<kbd>M</kbd>: git mergetool를 열기
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick all hunks
|
||||
<kbd>esc</kbd>: 파일 목록으로 돌아가기
|
||||
<kbd>M</kbd>: Git mergetool를 열기
|
||||
<kbd><space></kbd>: Pick hunk
|
||||
<kbd>b</kbd>: Pick all hunks
|
||||
<kbd><esc></kbd>: 파일 목록으로 돌아가기
|
||||
</pre>
|
||||
|
||||
## 메인 패널 (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd>mouse wheel ▼</kbd>: 아래로 스크롤 (fn+up)
|
||||
<kbd>mouse wheel ▲</kbd>: 위로 스크롤 (fn+down)
|
||||
<kbd>mouse wheel down</kbd>: 아래로 스크롤 (fn+up)
|
||||
<kbd>mouse wheel up</kbd>: 위로 스크롤 (fn+down)
|
||||
</pre>
|
||||
|
||||
## 메인 패널 (Patch Building)
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: 이전 hunk를 선택
|
||||
<kbd>►</kbd>: 다음 hunk를 선택
|
||||
<kbd><left></kbd>: 이전 hunk를 선택
|
||||
<kbd><right></kbd>: 다음 hunk를 선택
|
||||
<kbd>v</kbd>: 드래그 선택 전환
|
||||
<kbd>V</kbd>: 드래그 선택 전환
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>ctrl+o</kbd>: 선택한 텍스트를 클립보드에 복사
|
||||
<kbd>a</kbd>: Toggle select hunk
|
||||
<kbd><c-o></kbd>: 선택한 텍스트를 클립보드에 복사
|
||||
<kbd>o</kbd>: 파일 닫기
|
||||
<kbd>e</kbd>: 파일 편집
|
||||
<kbd>space</kbd>: line(s)을 패치에 추가/삭제
|
||||
<kbd>esc</kbd>: exit custom patch builder
|
||||
<kbd><space></kbd>: Line(s)을 패치에 추가/삭제
|
||||
<kbd><esc></kbd>: Exit custom patch builder
|
||||
</pre>
|
||||
|
||||
## 메인 패널 (Staging)
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: 이전 hunk를 선택
|
||||
<kbd>►</kbd>: 다음 hunk를 선택
|
||||
<kbd><left></kbd>: 이전 hunk를 선택
|
||||
<kbd><right></kbd>: 다음 hunk를 선택
|
||||
<kbd>v</kbd>: 드래그 선택 전환
|
||||
<kbd>V</kbd>: 드래그 선택 전환
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>ctrl+o</kbd>: 선택한 텍스트를 클립보드에 복사
|
||||
<kbd>a</kbd>: Toggle select hunk
|
||||
<kbd><c-o></kbd>: 선택한 텍스트를 클립보드에 복사
|
||||
<kbd>o</kbd>: 파일 닫기
|
||||
<kbd>e</kbd>: 파일 편집
|
||||
<kbd>esc</kbd>: 파일 목록으로 돌아가기
|
||||
<kbd>tab</kbd>: 패널 전환
|
||||
<kbd>space</kbd>: 선택한 행을 staged / unstaged
|
||||
<kbd><esc></kbd>: 파일 목록으로 돌아가기
|
||||
<kbd><tab></kbd>: 패널 전환
|
||||
<kbd><space></kbd>: 선택한 행을 staged / unstaged
|
||||
<kbd>d</kbd>: 변경을 삭제 (git reset)
|
||||
<kbd>E</kbd>: edit hunk
|
||||
<kbd>E</kbd>: Edit hunk
|
||||
<kbd>c</kbd>: 커밋 변경내용
|
||||
<kbd>w</kbd>: commit changes without pre-commit hook
|
||||
<kbd>w</kbd>: Commit changes without pre-commit hook
|
||||
<kbd>C</kbd>: Git 편집기를 사용하여 변경 내용을 커밋합니다.
|
||||
</pre>
|
||||
|
||||
## 브랜치
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 브랜치명을 클립보드에 복사
|
||||
<kbd>i</kbd>: git-flow 옵션 보기
|
||||
<kbd>space</kbd>: 체크아웃
|
||||
<kbd><c-o></kbd>: 브랜치명을 클립보드에 복사
|
||||
<kbd>i</kbd>: Git-flow 옵션 보기
|
||||
<kbd><space></kbd>: 체크아웃
|
||||
<kbd>n</kbd>: 새 브랜치 생성
|
||||
<kbd>o</kbd>: 풀 리퀘스트 생성
|
||||
<kbd>O</kbd>: 풀 리퀘스트 생성 옵션
|
||||
<kbd>ctrl+y</kbd>: 풀 리퀘스트 URL을 클립보드에 복사
|
||||
<kbd><c-y></kbd>: 풀 리퀘스트 URL을 클립보드에 복사
|
||||
<kbd>c</kbd>: 이름으로 체크아웃
|
||||
<kbd>F</kbd>: 강제 체크아웃
|
||||
<kbd>d</kbd>: 브랜치 삭제
|
||||
<kbd>r</kbd>: 체크아웃된 브랜치를 이 브랜치에 리베이스
|
||||
<kbd>M</kbd>: 현재 브랜치에 병합
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>f</kbd>: Fast-forward this branch from its upstream
|
||||
<kbd>T</kbd>: 태그를 생성
|
||||
<kbd>g</kbd>: View reset options
|
||||
<kbd>R</kbd>: 브랜치 이름 변경
|
||||
<kbd>u</kbd>: set/unset upstream
|
||||
<kbd>enter</kbd>: 커밋 보기
|
||||
<kbd>u</kbd>: Set/Unset upstream
|
||||
<kbd><enter></kbd>: 커밋 보기
|
||||
</pre>
|
||||
|
||||
## 상태
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: 설정 파일 수정
|
||||
<kbd>o</kbd>: 설정 파일 열기
|
||||
<kbd>e</kbd>: 설정 파일 수정
|
||||
<kbd>u</kbd>: 업데이트 확인
|
||||
<kbd>enter</kbd>: 최근에 사용한 저장소로 전환
|
||||
<kbd><enter></kbd>: 최근에 사용한 저장소로 전환
|
||||
<kbd>a</kbd>: 모든 브랜치 로그 표시
|
||||
</pre>
|
||||
|
||||
## 서브모듈
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 서브모듈 이름을 클립보드에 복사
|
||||
<kbd>enter</kbd>: 서브모듈 열기
|
||||
<kbd><c-o></kbd>: 서브모듈 이름을 클립보드에 복사
|
||||
<kbd><enter></kbd>: 서브모듈 열기
|
||||
<kbd>d</kbd>: 서브모듈 삭제
|
||||
<kbd>u</kbd>: 서브모듈 업데이트
|
||||
<kbd>n</kbd>: 새로운 서브모듈 추가
|
||||
<kbd>e</kbd>: 서브모듈의 URL을 수정
|
||||
<kbd>i</kbd>: 서브모듈 초기화
|
||||
<kbd>b</kbd>: view bulk submodule options
|
||||
<kbd>b</kbd>: View bulk submodule options
|
||||
</pre>
|
||||
|
||||
## 원격
|
||||
@@ -198,98 +209,112 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## 원격 브랜치
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: 체크아웃
|
||||
<kbd><c-o></kbd>: 브랜치명을 클립보드에 복사
|
||||
<kbd><space></kbd>: 체크아웃
|
||||
<kbd>n</kbd>: 새 브랜치 생성
|
||||
<kbd>M</kbd>: 현재 브랜치에 병합
|
||||
<kbd>r</kbd>: 체크아웃된 브랜치를 이 브랜치에 리베이스
|
||||
<kbd>d</kbd>: 브랜치 삭제
|
||||
<kbd>u</kbd>: set as upstream of checked-out branch
|
||||
<kbd>esc</kbd>: 원격목록으로 돌아가기
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>enter</kbd>: 커밋 보기
|
||||
<kbd>u</kbd>: Set as upstream of checked-out branch
|
||||
<kbd><esc></kbd>: 원격목록으로 돌아가기
|
||||
<kbd>g</kbd>: View reset options
|
||||
<kbd><enter></kbd>: 커밋 보기
|
||||
</pre>
|
||||
|
||||
## 커밋
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 커밋 SHA를 클립보드에 복사
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>b</kbd>: bisect 옵션 보기
|
||||
<kbd>s</kbd>: squash down
|
||||
<kbd>f</kbd>: fixup commit
|
||||
<kbd><c-o></kbd>: 커밋 SHA를 클립보드에 복사
|
||||
<kbd><c-r></kbd>: Reset cherry-picked (copied) commits selection
|
||||
<kbd>b</kbd>: Bisect 옵션 보기
|
||||
<kbd>s</kbd>: Squash down
|
||||
<kbd>f</kbd>: Fixup commit
|
||||
<kbd>r</kbd>: 커밋메시지 변경
|
||||
<kbd>R</kbd>: 에디터에서 커밋메시지 수정
|
||||
<kbd>d</kbd>: 커밋 삭제
|
||||
<kbd>e</kbd>: 커밋을 편집
|
||||
<kbd>p</kbd>: pick commit (when mid-rebase)
|
||||
<kbd>F</kbd>: create fixup commit for this commit
|
||||
<kbd>S</kbd>: squash all 'fixup!' commits above selected commit (autosquash)
|
||||
<kbd>ctrl+j</kbd>: 커밋을 1개 아래로 이동
|
||||
<kbd>ctrl+k</kbd>: 커밋을 1개 위로 이동
|
||||
<kbd>p</kbd>: Pick commit (when mid-rebase)
|
||||
<kbd>F</kbd>: Create fixup commit for this commit
|
||||
<kbd>S</kbd>: Squash all 'fixup!' commits above selected commit (autosquash)
|
||||
<kbd><c-j></kbd>: 커밋을 1개 아래로 이동
|
||||
<kbd><c-k></kbd>: 커밋을 1개 위로 이동
|
||||
<kbd>v</kbd>: 커밋을 붙여넣기 (cherry-pick)
|
||||
<kbd>A</kbd>: amend commit with staged changes
|
||||
<kbd>a</kbd>: reset commit author
|
||||
<kbd>A</kbd>: Amend commit with staged changes
|
||||
<kbd>a</kbd>: Set/Reset commit author
|
||||
<kbd>t</kbd>: 커밋 되돌리기
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+l</kbd>: 로그 메뉴 열기
|
||||
<kbd>space</kbd>: 커밋을 체크아웃
|
||||
<kbd>T</kbd>: Tag commit
|
||||
<kbd><c-l></kbd>: 로그 메뉴 열기
|
||||
<kbd><space></kbd>: 커밋을 체크아웃
|
||||
<kbd>y</kbd>: 커밋 attribute 복사
|
||||
<kbd>o</kbd>: 브라우저에서 커밋 열기
|
||||
<kbd>n</kbd>: 커밋에서 새 브랜치를 만듭니다.
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>g</kbd>: View reset options
|
||||
<kbd>c</kbd>: 커밋을 복사 (cherry-pick)
|
||||
<kbd>C</kbd>: 커밋을 범위로 복사 (cherry-pick)
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
<kbd><enter></kbd>: View selected item's files
|
||||
</pre>
|
||||
|
||||
## 커밋 파일
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 커밋한 파일명을 클립보드에 복사
|
||||
<kbd>c</kbd>: checkout file
|
||||
<kbd>d</kbd>: discard this commit's changes to this file
|
||||
<kbd><c-o></kbd>: 커밋한 파일명을 클립보드에 복사
|
||||
<kbd>c</kbd>: Checkout file
|
||||
<kbd>d</kbd>: Discard this commit's changes to this file
|
||||
<kbd>o</kbd>: 파일 닫기
|
||||
<kbd>e</kbd>: 파일 편집
|
||||
<kbd>space</kbd>: toggle file included in patch
|
||||
<kbd>a</kbd>: toggle all files included in patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
|
||||
<kbd><space></kbd>: Toggle file included in patch
|
||||
<kbd>a</kbd>: Toggle all files included in patch
|
||||
<kbd><enter></kbd>: Enter file to add selected lines to the patch (or toggle directory collapsed)
|
||||
<kbd>`</kbd>: 파일 트리뷰로 전환
|
||||
</pre>
|
||||
|
||||
## 커밋메시지
|
||||
|
||||
<pre>
|
||||
<kbd><enter></kbd>: 확인
|
||||
<kbd><esc></kbd>: 닫기
|
||||
</pre>
|
||||
|
||||
## 태그
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: 체크아웃
|
||||
<kbd><space></kbd>: 체크아웃
|
||||
<kbd>d</kbd>: 태그 삭제
|
||||
<kbd>P</kbd>: 태그를 push
|
||||
<kbd>n</kbd>: 태그를 생성
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>enter</kbd>: 커밋 보기
|
||||
<kbd>g</kbd>: View reset options
|
||||
<kbd><enter></kbd>: 커밋 보기
|
||||
</pre>
|
||||
|
||||
## 파일
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 파일명을 클립보드에 복사
|
||||
<kbd>ctrl+w</kbd>: 공백문자를 Diff 뷰에서 표시 여부 전환
|
||||
<kbd>d</kbd>: view 'discard changes' options
|
||||
<kbd>space</kbd>: Staged 전환
|
||||
<kbd>ctrl+b</kbd>: 파일을 필터하기 (Staged/unstaged)
|
||||
<kbd><c-o></kbd>: 파일명을 클립보드에 복사
|
||||
<kbd>d</kbd>: View 'discard changes' options
|
||||
<kbd><space></kbd>: Staged 전환
|
||||
<kbd><c-b></kbd>: 파일을 필터하기 (Staged/unstaged)
|
||||
<kbd>c</kbd>: 커밋 변경내용
|
||||
<kbd>w</kbd>: commit changes without pre-commit hook
|
||||
<kbd>w</kbd>: Commit changes without pre-commit hook
|
||||
<kbd>A</kbd>: 마지맛 커밋 수정
|
||||
<kbd>C</kbd>: Git 편집기를 사용하여 변경 내용을 커밋합니다.
|
||||
<kbd>e</kbd>: 파일 편집
|
||||
<kbd>o</kbd>: 파일 닫기
|
||||
<kbd>i</kbd>: ignore file
|
||||
<kbd>i</kbd>: Ignore file
|
||||
<kbd>r</kbd>: 파일 새로고침
|
||||
<kbd>s</kbd>: 변경사항을 Stash
|
||||
<kbd>S</kbd>: Stash 옵션 보기
|
||||
<kbd>a</kbd>: 모든 변경을 Staged/unstaged으로 전환
|
||||
<kbd>enter</kbd>: stage individual hunks/lines for file, or collapse/expand for directory
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>D</kbd>: view reset options
|
||||
<kbd><enter></kbd>: Stage individual hunks/lines for file, or collapse/expand for directory
|
||||
<kbd>g</kbd>: View upstream reset options
|
||||
<kbd>D</kbd>: View reset options
|
||||
<kbd>`</kbd>: 파일 트리뷰로 전환
|
||||
<kbd>M</kbd>: git mergetool를 열기
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>M</kbd>: Git mergetool를 열기
|
||||
<kbd>f</kbd>: Fetch
|
||||
</pre>
|
||||
|
||||
## 확인 패널
|
||||
|
||||
<pre>
|
||||
<kbd><enter></kbd>: 확인
|
||||
<kbd><esc></kbd>: 닫기/취소
|
||||
</pre>
|
||||
|
||||
+228
-203
@@ -2,294 +2,319 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
# Lazygit Sneltoetsen
|
||||
|
||||
## Globale Sneltoetsen
|
||||
_Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
|
||||
## Globale sneltoetsen
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+r</kbd>: wissel naar een recente repo
|
||||
<kbd>pgup</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+up/shift+k)
|
||||
<kbd>pgdown</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+down/shift+j)
|
||||
<kbd>m</kbd>: bekijk merge/rebase opties
|
||||
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
|
||||
<kbd>R</kbd>: verversen
|
||||
<kbd>x</kbd>: open menu
|
||||
<kbd>+</kbd>: volgende scherm modus (normaal/half/groot)
|
||||
<kbd>_</kbd>: vorige scherm modus
|
||||
<kbd>ctrl+s</kbd>: bekijk scoping opties
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
<kbd>@</kbd>: open command log menu
|
||||
<kbd><c-r></kbd>: Wissel naar een recente repo
|
||||
<kbd><pgup></kbd>: Scroll naar beneden vanaf hoofdpaneel (fn+up/shift+k)
|
||||
<kbd><pgdown></kbd>: Scroll naar beneden vanaf hoofdpaneel (fn+down/shift+j)
|
||||
<kbd>@</kbd>: Open command log menu
|
||||
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
|
||||
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
|
||||
<kbd>:</kbd>: voer aangepaste commando uit
|
||||
<kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel)
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd>:</kbd>: Voer aangepaste commando uit
|
||||
<kbd><c-p></kbd>: Bekijk aangepaste patch opties
|
||||
<kbd>m</kbd>: Bekijk merge/rebase opties
|
||||
<kbd>R</kbd>: Verversen
|
||||
<kbd>+</kbd>: Volgende scherm modus (normaal/half/groot)
|
||||
<kbd>_</kbd>: Vorige scherm modus
|
||||
<kbd>?</kbd>: Open menu
|
||||
<kbd><c-s></kbd>: Bekijk scoping opties
|
||||
<kbd>W</kbd>: Open diff menu
|
||||
<kbd><c-e></kbd>: Open diff menu
|
||||
<kbd><c-w></kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
<kbd>z</kbd>: Ongedaan maken (via reflog) (experimenteel)
|
||||
<kbd><c-z></kbd>: Redo (via reflog) (experimenteel)
|
||||
<kbd>P</kbd>: Push
|
||||
<kbd>p</kbd>: Pull
|
||||
</pre>
|
||||
|
||||
## Lijstpaneel Navigatie
|
||||
## Lijstpaneel navigatie
|
||||
|
||||
<pre>
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar boven
|
||||
<kbd>/</kbd>: start met zoeken
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
<kbd>]</kbd>: volgende tabblad
|
||||
<kbd>[</kbd>: vorige tabblad
|
||||
<kbd>,</kbd>: Vorige pagina
|
||||
<kbd>.</kbd>: Volgende pagina
|
||||
<kbd><</kbd>: Scroll naar boven
|
||||
<kbd>/</kbd>: Start met zoeken
|
||||
<kbd>></kbd>: Scroll naar beneden
|
||||
<kbd>H</kbd>: Scroll left
|
||||
<kbd>L</kbd>: Scroll right
|
||||
<kbd>]</kbd>: Volgende tabblad
|
||||
<kbd>[</kbd>: Vorige tabblad
|
||||
</pre>
|
||||
|
||||
## Bestanden
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
<kbd>d</kbd>: bekijk 'veranderingen ongedaan maken' opties
|
||||
<kbd>space</kbd>: toggle staged
|
||||
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
|
||||
<kbd>c</kbd>: commit veranderingen
|
||||
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
|
||||
<kbd>A</kbd>: wijzig laatste commit
|
||||
<kbd>C</kbd>: commit veranderingen met de git editor
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>i</kbd>: ignore or exclude file
|
||||
<kbd>r</kbd>: refresh bestanden
|
||||
<kbd>s</kbd>: stash-bestanden
|
||||
<kbd>S</kbd>: bekijk stash opties
|
||||
<kbd>a</kbd>: toggle staged alle
|
||||
<kbd>enter</kbd>: stage individuele hunks/lijnen
|
||||
<kbd>g</kbd>: bekijk upstream reset opties
|
||||
<kbd>D</kbd>: bekijk reset opties
|
||||
<kbd>`</kbd>: toggle bestandsboom weergave
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd><c-o></kbd>: Kopieer de bestandsnaam naar het klembord
|
||||
<kbd>d</kbd>: Bekijk 'veranderingen ongedaan maken' opties
|
||||
<kbd><space></kbd>: Toggle staged
|
||||
<kbd><c-b></kbd>: Filter files (staged/unstaged)
|
||||
<kbd>c</kbd>: Commit veranderingen
|
||||
<kbd>w</kbd>: Commit veranderingen zonder pre-commit hook
|
||||
<kbd>A</kbd>: Wijzig laatste commit
|
||||
<kbd>C</kbd>: Commit veranderingen met de git editor
|
||||
<kbd>e</kbd>: Verander bestand
|
||||
<kbd>o</kbd>: Open bestand
|
||||
<kbd>i</kbd>: Ignore or exclude file
|
||||
<kbd>r</kbd>: Refresh bestanden
|
||||
<kbd>s</kbd>: Stash-bestanden
|
||||
<kbd>S</kbd>: Bekijk stash opties
|
||||
<kbd>a</kbd>: Toggle staged alle
|
||||
<kbd><enter></kbd>: Stage individuele hunks/lijnen
|
||||
<kbd>g</kbd>: Bekijk upstream reset opties
|
||||
<kbd>D</kbd>: Bekijk reset opties
|
||||
<kbd>`</kbd>: Toggle bestandsboom weergave
|
||||
<kbd>M</kbd>: Open external merge tool (git mergetool)
|
||||
<kbd>f</kbd>: Fetch
|
||||
</pre>
|
||||
|
||||
## Bevestigingspaneel
|
||||
|
||||
<pre>
|
||||
<kbd><enter></kbd>: Bevestig
|
||||
<kbd><esc></kbd>: Sluiten
|
||||
</pre>
|
||||
|
||||
## Branches
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: kopieer branch name naar klembord
|
||||
<kbd>i</kbd>: laat git-flow opties zien
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>o</kbd>: maak een pull-request
|
||||
<kbd>O</kbd>: bekijk opties voor pull-aanvraag
|
||||
<kbd>ctrl+y</kbd>: kopieer de URL van het pull-verzoek naar het klembord
|
||||
<kbd>c</kbd>: uitchecken bij naam
|
||||
<kbd>F</kbd>: forceer checkout
|
||||
<kbd>d</kbd>: verwijder branch
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>M</kbd>: merge in met huidige checked out branch
|
||||
<kbd>f</kbd>: fast-forward deze branch vanaf zijn upstream
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>R</kbd>: hernoem branch
|
||||
<kbd>u</kbd>: set/unset upstream
|
||||
<kbd>enter</kbd>: bekijk commits
|
||||
<kbd><c-o></kbd>: Kopieer branch name naar klembord
|
||||
<kbd>i</kbd>: Laat git-flow opties zien
|
||||
<kbd><space></kbd>: Uitchecken
|
||||
<kbd>n</kbd>: Nieuwe branch
|
||||
<kbd>o</kbd>: Maak een pull-request
|
||||
<kbd>O</kbd>: Bekijk opties voor pull-aanvraag
|
||||
<kbd><c-y></kbd>: Kopieer de URL van het pull-verzoek naar het klembord
|
||||
<kbd>c</kbd>: Uitchecken bij naam
|
||||
<kbd>F</kbd>: Forceer checkout
|
||||
<kbd>d</kbd>: Verwijder branch
|
||||
<kbd>r</kbd>: Rebase branch
|
||||
<kbd>M</kbd>: Merge in met huidige checked out branch
|
||||
<kbd>f</kbd>: Fast-forward deze branch vanaf zijn upstream
|
||||
<kbd>T</kbd>: Creëer tag
|
||||
<kbd>g</kbd>: Bekijk reset opties
|
||||
<kbd>R</kbd>: Hernoem branch
|
||||
<kbd>u</kbd>: Set/Unset upstream
|
||||
<kbd><enter></kbd>: Bekijk commits
|
||||
</pre>
|
||||
|
||||
## Commit bericht
|
||||
|
||||
<pre>
|
||||
<kbd><enter></kbd>: Bevestig
|
||||
<kbd><esc></kbd>: Sluiten
|
||||
</pre>
|
||||
|
||||
## Commit bestanden
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: kopieer de vastgelegde bestandsnaam naar het klembord
|
||||
<kbd>c</kbd>: bestand uitchecken
|
||||
<kbd>d</kbd>: uitsluit deze commit zijn veranderingen aan dit bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>space</kbd>: toggle bestand inbegrepen in patch
|
||||
<kbd>a</kbd>: toggle all files included in patch
|
||||
<kbd>enter</kbd>: enter bestand om geselecteerde regels toe te voegen aan de patch
|
||||
<kbd>`</kbd>: toggle bestandsboom weergave
|
||||
<kbd><c-o></kbd>: Kopieer de vastgelegde bestandsnaam naar het klembord
|
||||
<kbd>c</kbd>: Bestand uitchecken
|
||||
<kbd>d</kbd>: Uitsluit deze commit zijn veranderingen aan dit bestand
|
||||
<kbd>o</kbd>: Open bestand
|
||||
<kbd>e</kbd>: Verander bestand
|
||||
<kbd><space></kbd>: Toggle bestand inbegrepen in patch
|
||||
<kbd>a</kbd>: Toggle all files included in patch
|
||||
<kbd><enter></kbd>: Enter bestand om geselecteerde regels toe te voegen aan de patch
|
||||
<kbd>`</kbd>: Toggle bestandsboom weergave
|
||||
</pre>
|
||||
|
||||
## Commits
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
|
||||
<kbd>b</kbd>: view bisect options
|
||||
<kbd>s</kbd>: squash beneden
|
||||
<kbd><c-o></kbd>: Kopieer commit SHA naar klembord
|
||||
<kbd><c-r></kbd>: Reset cherry-picked (gekopieerde) commits selectie
|
||||
<kbd>b</kbd>: View bisect options
|
||||
<kbd>s</kbd>: Squash beneden
|
||||
<kbd>f</kbd>: Fixup commit
|
||||
<kbd>r</kbd>: hernoem commit
|
||||
<kbd>R</kbd>: hernoem commit met editor
|
||||
<kbd>d</kbd>: verwijder commit
|
||||
<kbd>e</kbd>: wijzig commit
|
||||
<kbd>p</kbd>: kies commit (wanneer midden in rebase)
|
||||
<kbd>F</kbd>: creëer fixup commit voor deze commit
|
||||
<kbd>S</kbd>: squash bovenstaande commits
|
||||
<kbd>ctrl+j</kbd>: verplaats commit 1 naar beneden
|
||||
<kbd>ctrl+k</kbd>: verplaats commit 1 naar boven
|
||||
<kbd>v</kbd>: plak commits (cherry-pick)
|
||||
<kbd>A</kbd>: wijzig commit met staged veranderingen
|
||||
<kbd>a</kbd>: reset commit author
|
||||
<kbd>t</kbd>: commit ongedaan maken
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+l</kbd>: open log menu
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>n</kbd>: creëer nieuwe branch van commit
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>c</kbd>: kopieer commit (cherry-pick)
|
||||
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
<kbd>r</kbd>: Hernoem commit
|
||||
<kbd>R</kbd>: Hernoem commit met editor
|
||||
<kbd>d</kbd>: Verwijder commit
|
||||
<kbd>e</kbd>: Wijzig commit
|
||||
<kbd>p</kbd>: Kies commit (wanneer midden in rebase)
|
||||
<kbd>F</kbd>: Creëer fixup commit
|
||||
<kbd>S</kbd>: Squash bovenstaande commits
|
||||
<kbd><c-j></kbd>: Verplaats commit 1 naar beneden
|
||||
<kbd><c-k></kbd>: Verplaats commit 1 naar boven
|
||||
<kbd>v</kbd>: Plak commits (cherry-pick)
|
||||
<kbd>A</kbd>: Wijzig commit met staged veranderingen
|
||||
<kbd>a</kbd>: Set/Reset commit author
|
||||
<kbd>t</kbd>: Commit ongedaan maken
|
||||
<kbd>T</kbd>: Tag commit
|
||||
<kbd><c-l></kbd>: Open log menu
|
||||
<kbd><space></kbd>: Checkout commit
|
||||
<kbd>y</kbd>: Copy commit attribute
|
||||
<kbd>o</kbd>: Open commit in browser
|
||||
<kbd>n</kbd>: Creëer nieuwe branch van commit
|
||||
<kbd>g</kbd>: Bekijk reset opties
|
||||
<kbd>c</kbd>: Kopieer commit (cherry-pick)
|
||||
<kbd>C</kbd>: Kopieer commit reeks (cherry-pick)
|
||||
<kbd><enter></kbd>: Bekijk gecommite bestanden
|
||||
</pre>
|
||||
|
||||
## Menu
|
||||
|
||||
<pre>
|
||||
<kbd><enter></kbd>: Uitvoeren
|
||||
<kbd><esc></kbd>: Sluiten
|
||||
</pre>
|
||||
|
||||
## Mergen
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>◄</kbd>: selecteer voorgaand conflict
|
||||
<kbd>►</kbd>: selecteer volgende conflict
|
||||
<kbd>▲</kbd>: selecteer bovenste hunk
|
||||
<kbd>▼</kbd>: selecteer onderste hunk
|
||||
<kbd>z</kbd>: ongedaan maken
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>space</kbd>: kies hunk
|
||||
<kbd>b</kbd>: kies bijde hunks
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
<kbd>e</kbd>: Verander bestand
|
||||
<kbd>o</kbd>: Open bestand
|
||||
<kbd><left></kbd>: Selecteer voorgaand conflict
|
||||
<kbd><right></kbd>: Selecteer volgende conflict
|
||||
<kbd><up></kbd>: Selecteer bovenste hunk
|
||||
<kbd><down></kbd>: Selecteer onderste hunk
|
||||
<kbd>z</kbd>: Ongedaan maken
|
||||
<kbd>M</kbd>: Open external merge tool (git mergetool)
|
||||
<kbd><space></kbd>: Kies stuk
|
||||
<kbd>b</kbd>: Kies beide stukken
|
||||
<kbd><esc></kbd>: Ga terug naar het bestanden paneel
|
||||
</pre>
|
||||
|
||||
## Normaal
|
||||
|
||||
<pre>
|
||||
<kbd>mouse wheel ▼</kbd>: scroll omlaag (fn+up)
|
||||
<kbd>mouse wheel ▲</kbd>: scroll omhoog (fn+down)
|
||||
<kbd>mouse wheel down</kbd>: Scroll omlaag (fn+up)
|
||||
<kbd>mouse wheel up</kbd>: Scroll omhoog (fn+down)
|
||||
</pre>
|
||||
|
||||
## Patch Bouwen
|
||||
## Patch bouwen
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: selecteer de vorige hunk
|
||||
<kbd>►</kbd>: selecteer de volgende hunk
|
||||
<kbd>v</kbd>: toggle drag selecteer
|
||||
<kbd>V</kbd>: toggle drag selecteer
|
||||
<kbd>a</kbd>: toggle selecteer hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>space</kbd>: voeg toe/verwijder lijn(en) in patch
|
||||
<kbd>esc</kbd>: sluit lijn-bij-lijn modus
|
||||
<kbd><left></kbd>: Selecteer de vorige hunk
|
||||
<kbd><right></kbd>: Selecteer de volgende hunk
|
||||
<kbd>v</kbd>: Toggle drag selecteer
|
||||
<kbd>V</kbd>: Toggle drag selecteer
|
||||
<kbd>a</kbd>: Toggle selecteer hunk
|
||||
<kbd><c-o></kbd>: Copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: Open bestand
|
||||
<kbd>e</kbd>: Verander bestand
|
||||
<kbd><space></kbd>: Voeg toe/verwijder lijn(en) in patch
|
||||
<kbd><esc></kbd>: Sluit lijn-bij-lijn modus
|
||||
</pre>
|
||||
|
||||
## Reflog
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>n</kbd>: creëer nieuwe branch van commit
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>c</kbd>: kopieer commit (cherry-pick)
|
||||
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
|
||||
<kbd>enter</kbd>: bekijk commits
|
||||
<kbd><c-o></kbd>: Kopieer commit SHA naar klembord
|
||||
<kbd><space></kbd>: Checkout commit
|
||||
<kbd>y</kbd>: Copy commit attribute
|
||||
<kbd>o</kbd>: Open commit in browser
|
||||
<kbd>n</kbd>: Creëer nieuwe branch van commit
|
||||
<kbd>g</kbd>: Bekijk reset opties
|
||||
<kbd>c</kbd>: Kopieer commit (cherry-pick)
|
||||
<kbd>C</kbd>: Kopieer commit reeks (cherry-pick)
|
||||
<kbd><c-r></kbd>: Reset cherry-picked (gekopieerde) commits selectie
|
||||
<kbd><enter></kbd>: Bekijk commits
|
||||
</pre>
|
||||
|
||||
## Remote Branches
|
||||
## Remote branches
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>M</kbd>: merge in met huidige checked out branch
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>d</kbd>: verwijder branch
|
||||
<kbd>u</kbd>: stel in als upstream van uitgecheckte branch
|
||||
<kbd>esc</kbd>: ga terug naar remotes lijst
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>enter</kbd>: bekijk commits
|
||||
<kbd><c-o></kbd>: Kopieer branch name naar klembord
|
||||
<kbd><space></kbd>: Uitchecken
|
||||
<kbd>n</kbd>: Nieuwe branch
|
||||
<kbd>M</kbd>: Merge in met huidige checked out branch
|
||||
<kbd>r</kbd>: Rebase branch
|
||||
<kbd>d</kbd>: Verwijder branch
|
||||
<kbd>u</kbd>: Stel in als upstream van uitgecheckte branch
|
||||
<kbd><esc></kbd>: Ga terug naar remotes lijst
|
||||
<kbd>g</kbd>: Bekijk reset opties
|
||||
<kbd><enter></kbd>: Bekijk commits
|
||||
</pre>
|
||||
|
||||
## Remotes
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: fetch remote
|
||||
<kbd>n</kbd>: voeg een nieuwe remote toe
|
||||
<kbd>d</kbd>: verwijder remote
|
||||
<kbd>e</kbd>: wijzig remote
|
||||
<kbd>f</kbd>: Fetch remote
|
||||
<kbd>n</kbd>: Voeg een nieuwe remote toe
|
||||
<kbd>d</kbd>: Verwijder remote
|
||||
<kbd>e</kbd>: Wijzig remote
|
||||
</pre>
|
||||
|
||||
## Staging
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: selecteer de vorige hunk
|
||||
<kbd>►</kbd>: selecteer de volgende hunk
|
||||
<kbd>v</kbd>: toggle drag selecteer
|
||||
<kbd>V</kbd>: toggle drag selecteer
|
||||
<kbd>a</kbd>: toggle selecteer hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
<kbd>tab</kbd>: ga naar een ander paneel
|
||||
<kbd>space</kbd>: toggle lijnen staged / unstaged
|
||||
<kbd>d</kbd>: verwijdert change (git reset)
|
||||
<kbd>E</kbd>: edit hunk
|
||||
<kbd>c</kbd>: commit veranderingen
|
||||
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
|
||||
<kbd>C</kbd>: commit veranderingen met de git editor
|
||||
<kbd><left></kbd>: Selecteer de vorige hunk
|
||||
<kbd><right></kbd>: Selecteer de volgende hunk
|
||||
<kbd>v</kbd>: Toggle drag selecteer
|
||||
<kbd>V</kbd>: Toggle drag selecteer
|
||||
<kbd>a</kbd>: Toggle selecteer hunk
|
||||
<kbd><c-o></kbd>: Copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: Open bestand
|
||||
<kbd>e</kbd>: Verander bestand
|
||||
<kbd><esc></kbd>: Ga terug naar het bestanden paneel
|
||||
<kbd><tab></kbd>: Ga naar een ander paneel
|
||||
<kbd><space></kbd>: Toggle lijnen staged / unstaged
|
||||
<kbd>d</kbd>: Verwijdert change (git reset)
|
||||
<kbd>E</kbd>: Edit hunk
|
||||
<kbd>c</kbd>: Commit veranderingen
|
||||
<kbd>w</kbd>: Commit veranderingen zonder pre-commit hook
|
||||
<kbd>C</kbd>: Commit veranderingen met de git editor
|
||||
</pre>
|
||||
|
||||
## Stash
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: toepassen
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: laten vallen
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>r</kbd>: rename stash
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
<kbd><space></kbd>: Toepassen
|
||||
<kbd>g</kbd>: Pop
|
||||
<kbd>d</kbd>: Laten vallen
|
||||
<kbd>n</kbd>: Nieuwe branch
|
||||
<kbd>r</kbd>: Rename stash
|
||||
<kbd><enter></kbd>: Bekijk gecommite bestanden
|
||||
</pre>
|
||||
|
||||
## Status
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: verander config bestand
|
||||
<kbd>o</kbd>: open config bestand
|
||||
<kbd>u</kbd>: check voor updates
|
||||
<kbd>enter</kbd>: wissel naar een recente repo
|
||||
<kbd>a</kbd>: alle logs van de branch laten zien
|
||||
<kbd>o</kbd>: Open config bestand
|
||||
<kbd>e</kbd>: Verander config bestand
|
||||
<kbd>u</kbd>: Check voor updates
|
||||
<kbd><enter></kbd>: Wissel naar een recente repo
|
||||
<kbd>a</kbd>: Alle logs van de branch laten zien
|
||||
</pre>
|
||||
|
||||
## Sub-commits
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>n</kbd>: creëer nieuwe branch van commit
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>c</kbd>: kopieer commit (cherry-pick)
|
||||
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
<kbd><c-o></kbd>: Kopieer commit SHA naar klembord
|
||||
<kbd><space></kbd>: Checkout commit
|
||||
<kbd>y</kbd>: Copy commit attribute
|
||||
<kbd>o</kbd>: Open commit in browser
|
||||
<kbd>n</kbd>: Creëer nieuwe branch van commit
|
||||
<kbd>g</kbd>: Bekijk reset opties
|
||||
<kbd>c</kbd>: Kopieer commit (cherry-pick)
|
||||
<kbd>C</kbd>: Kopieer commit reeks (cherry-pick)
|
||||
<kbd><c-r></kbd>: Reset cherry-picked (gekopieerde) commits selectie
|
||||
<kbd><enter></kbd>: Bekijk gecommite bestanden
|
||||
</pre>
|
||||
|
||||
## Submodules
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: kopieer submodule naam naar klembord
|
||||
<kbd>enter</kbd>: enter submodule
|
||||
<kbd>d</kbd>: remove submodule
|
||||
<kbd>u</kbd>: update submodule
|
||||
<kbd>n</kbd>: voeg nieuwe submodule toe
|
||||
<kbd>e</kbd>: update submodule URL
|
||||
<kbd>i</kbd>: initialiseer submodule
|
||||
<kbd>b</kbd>: bekijk bulk submodule opties
|
||||
<kbd><c-o></kbd>: Kopieer submodule naam naar klembord
|
||||
<kbd><enter></kbd>: Enter submodule
|
||||
<kbd>d</kbd>: Remove submodule
|
||||
<kbd>u</kbd>: Update submodule
|
||||
<kbd>n</kbd>: Voeg nieuwe submodule toe
|
||||
<kbd>e</kbd>: Update submodule URL
|
||||
<kbd>i</kbd>: Initialiseer submodule
|
||||
<kbd>b</kbd>: Bekijk bulk submodule opties
|
||||
</pre>
|
||||
|
||||
## Tags
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>d</kbd>: verwijder tag
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: creëer tag
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>enter</kbd>: bekijk commits
|
||||
<kbd><space></kbd>: Uitchecken
|
||||
<kbd>d</kbd>: Verwijder tag
|
||||
<kbd>P</kbd>: Push tag
|
||||
<kbd>n</kbd>: Creëer tag
|
||||
<kbd>g</kbd>: Bekijk reset opties
|
||||
<kbd><enter></kbd>: Bekijk commits
|
||||
</pre>
|
||||
|
||||
+224
-199
@@ -2,294 +2,319 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
# Lazygit Keybindings
|
||||
|
||||
_Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
|
||||
## Globalne
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+r</kbd>: switch to a recent repo
|
||||
<kbd>pgup</kbd>: scroll up main panel (fn+up/shift+k)
|
||||
<kbd>pgdown</kbd>: scroll down main panel (fn+down/shift+j)
|
||||
<kbd>m</kbd>: widok scalenia/opcje zmiany bazy
|
||||
<kbd>ctrl+p</kbd>: view custom patch options
|
||||
<kbd>R</kbd>: odśwież
|
||||
<kbd>x</kbd>: open menu
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
<kbd>@</kbd>: open command log menu
|
||||
<kbd><c-r></kbd>: Switch to a recent repo
|
||||
<kbd><pgup></kbd>: Scroll up main panel (fn+up/shift+k)
|
||||
<kbd><pgdown></kbd>: Scroll down main panel (fn+down/shift+j)
|
||||
<kbd>@</kbd>: Open command log menu
|
||||
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
|
||||
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
|
||||
<kbd>:</kbd>: wykonaj własną komendę
|
||||
<kbd>z</kbd>: undo (via reflog) (experimental)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd>:</kbd>: Wykonaj własną komendę
|
||||
<kbd><c-p></kbd>: View custom patch options
|
||||
<kbd>m</kbd>: Widok scalenia/opcje zmiany bazy
|
||||
<kbd>R</kbd>: Odśwież
|
||||
<kbd>+</kbd>: Next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: Prev screen mode
|
||||
<kbd>?</kbd>: Open menu
|
||||
<kbd><c-s></kbd>: View filter-by-path options
|
||||
<kbd>W</kbd>: Open diff menu
|
||||
<kbd><c-e></kbd>: Open diff menu
|
||||
<kbd><c-w></kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
<kbd>z</kbd>: Undo
|
||||
<kbd><c-z></kbd>: Redo
|
||||
<kbd>P</kbd>: Push
|
||||
<kbd>p</kbd>: Pull
|
||||
</pre>
|
||||
|
||||
## List Panel Navigation
|
||||
## List panel navigation
|
||||
|
||||
<pre>
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
<kbd>]</kbd>: next tab
|
||||
<kbd>[</kbd>: previous tab
|
||||
<kbd>,</kbd>: Previous page
|
||||
<kbd>.</kbd>: Next page
|
||||
<kbd><</kbd>: Scroll to top
|
||||
<kbd>/</kbd>: Start search
|
||||
<kbd>></kbd>: Scroll to bottom
|
||||
<kbd>H</kbd>: Scroll left
|
||||
<kbd>L</kbd>: Scroll right
|
||||
<kbd>]</kbd>: Next tab
|
||||
<kbd>[</kbd>: Previous tab
|
||||
</pre>
|
||||
|
||||
## Commit summary
|
||||
|
||||
<pre>
|
||||
<kbd><enter></kbd>: Potwierdź
|
||||
<kbd><esc></kbd>: Zamknij
|
||||
</pre>
|
||||
|
||||
## Commity
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>b</kbd>: view bisect options
|
||||
<kbd>s</kbd>: ściśnij
|
||||
<kbd>f</kbd>: napraw commit
|
||||
<kbd>r</kbd>: zmień nazwę commita
|
||||
<kbd>R</kbd>: zmień nazwę commita w edytorze
|
||||
<kbd>d</kbd>: usuń commit
|
||||
<kbd>e</kbd>: edytuj commit
|
||||
<kbd>p</kbd>: wybierz commit (podczas zmiany bazy)
|
||||
<kbd>F</kbd>: utwórz commit naprawczy dla tego commita
|
||||
<kbd>S</kbd>: spłaszcz wszystkie commity naprawcze powyżej zaznaczonych commitów (autosquash)
|
||||
<kbd>ctrl+j</kbd>: przenieś commit 1 w dół
|
||||
<kbd>ctrl+k</kbd>: przenieś commit 1 w górę
|
||||
<kbd>v</kbd>: wklej commity (przebieranie)
|
||||
<kbd>A</kbd>: popraw commit zmianami z poczekalni
|
||||
<kbd>a</kbd>: reset commit author
|
||||
<kbd>t</kbd>: odwróć commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+l</kbd>: open log menu
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>g</kbd>: wyświetl opcje resetu
|
||||
<kbd>c</kbd>: kopiuj commit (przebieranie)
|
||||
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
|
||||
<kbd>enter</kbd>: przeglądaj pliki commita
|
||||
<kbd><c-o></kbd>: Copy commit SHA to clipboard
|
||||
<kbd><c-r></kbd>: Reset cherry-picked (copied) commits selection
|
||||
<kbd>b</kbd>: View bisect options
|
||||
<kbd>s</kbd>: Ściśnij
|
||||
<kbd>f</kbd>: Napraw commit
|
||||
<kbd>r</kbd>: Zmień nazwę commita
|
||||
<kbd>R</kbd>: Zmień nazwę commita w edytorze
|
||||
<kbd>d</kbd>: Usuń commit
|
||||
<kbd>e</kbd>: Edytuj commit
|
||||
<kbd>p</kbd>: Wybierz commit (podczas zmiany bazy)
|
||||
<kbd>F</kbd>: Utwórz commit naprawczy dla tego commita
|
||||
<kbd>S</kbd>: Spłaszcz wszystkie commity naprawcze powyżej zaznaczonych commitów (autosquash)
|
||||
<kbd><c-j></kbd>: Przenieś commit 1 w dół
|
||||
<kbd><c-k></kbd>: Przenieś commit 1 w górę
|
||||
<kbd>v</kbd>: Wklej commity (przebieranie)
|
||||
<kbd>A</kbd>: Popraw commit zmianami z poczekalni
|
||||
<kbd>a</kbd>: Set/Reset commit author
|
||||
<kbd>t</kbd>: Odwróć commit
|
||||
<kbd>T</kbd>: Tag commit
|
||||
<kbd><c-l></kbd>: Open log menu
|
||||
<kbd><space></kbd>: Checkout commit
|
||||
<kbd>y</kbd>: Copy commit attribute
|
||||
<kbd>o</kbd>: Open commit in browser
|
||||
<kbd>n</kbd>: Create new branch off of commit
|
||||
<kbd>g</kbd>: Wyświetl opcje resetu
|
||||
<kbd>c</kbd>: Kopiuj commit (przebieranie)
|
||||
<kbd>C</kbd>: Kopiuj zakres commitów (przebieranie)
|
||||
<kbd><enter></kbd>: Przeglądaj pliki commita
|
||||
</pre>
|
||||
|
||||
## Local Branches
|
||||
## Confirmation panel
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy branch name to clipboard
|
||||
<kbd>i</kbd>: show git-flow options
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
<kbd>o</kbd>: utwórz żądanie pobrania
|
||||
<kbd>O</kbd>: utwórz opcje żądania ściągnięcia
|
||||
<kbd>ctrl+y</kbd>: skopiuj adres URL żądania pobrania do schowka
|
||||
<kbd>c</kbd>: przełącz używając nazwy
|
||||
<kbd>F</kbd>: wymuś przełączenie
|
||||
<kbd>d</kbd>: usuń gałąź
|
||||
<kbd>r</kbd>: zmiana bazy gałęzi
|
||||
<kbd>M</kbd>: scal do obecnej gałęzi
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>g</kbd>: wyświetl opcje resetu
|
||||
<kbd>R</kbd>: rename branch
|
||||
<kbd>u</kbd>: set/unset upstream
|
||||
<kbd>enter</kbd>: view commits
|
||||
<kbd><enter></kbd>: Potwierdź
|
||||
<kbd><esc></kbd>: Zamknij
|
||||
</pre>
|
||||
|
||||
## Main Panel (Patch Building)
|
||||
## Local branches
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: poprzedni kawałek
|
||||
<kbd>►</kbd>: następny kawałek
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>space</kbd>: add/remove line(s) to patch
|
||||
<kbd>esc</kbd>: wyście z trybu "linia po linii"
|
||||
<kbd><c-o></kbd>: Copy branch name to clipboard
|
||||
<kbd>i</kbd>: Show git-flow options
|
||||
<kbd><space></kbd>: Przełącz
|
||||
<kbd>n</kbd>: Nowa gałąź
|
||||
<kbd>o</kbd>: Utwórz żądanie pobrania
|
||||
<kbd>O</kbd>: Utwórz opcje żądania ściągnięcia
|
||||
<kbd><c-y></kbd>: Skopiuj adres URL żądania pobrania do schowka
|
||||
<kbd>c</kbd>: Przełącz używając nazwy
|
||||
<kbd>F</kbd>: Wymuś przełączenie
|
||||
<kbd>d</kbd>: Usuń gałąź
|
||||
<kbd>r</kbd>: Zmiana bazy gałęzi
|
||||
<kbd>M</kbd>: Scal do obecnej gałęzi
|
||||
<kbd>f</kbd>: Fast-forward this branch from its upstream
|
||||
<kbd>T</kbd>: Create tag
|
||||
<kbd>g</kbd>: Wyświetl opcje resetu
|
||||
<kbd>R</kbd>: Rename branch
|
||||
<kbd>u</kbd>: Set/Unset upstream
|
||||
<kbd><enter></kbd>: View commits
|
||||
</pre>
|
||||
|
||||
## Main panel (patch building)
|
||||
|
||||
<pre>
|
||||
<kbd><left></kbd>: Poprzedni kawałek
|
||||
<kbd><right></kbd>: Następny kawałek
|
||||
<kbd>v</kbd>: Toggle drag select
|
||||
<kbd>V</kbd>: Toggle drag select
|
||||
<kbd>a</kbd>: Toggle select hunk
|
||||
<kbd><c-o></kbd>: Copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: Otwórz plik
|
||||
<kbd>e</kbd>: Edytuj plik
|
||||
<kbd><space></kbd>: Add/Remove line(s) to patch
|
||||
<kbd><esc></kbd>: Wyście z trybu "linia po linii"
|
||||
</pre>
|
||||
|
||||
## Menu
|
||||
|
||||
<pre>
|
||||
<kbd><enter></kbd>: Wykonaj
|
||||
<kbd><esc></kbd>: Zamknij
|
||||
</pre>
|
||||
|
||||
## Pliki
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
<kbd>d</kbd>: pokaż opcje porzucania zmian
|
||||
<kbd>space</kbd>: przełącz stan poczekalni
|
||||
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
|
||||
<kbd><c-o></kbd>: Copy the file name to the clipboard
|
||||
<kbd>d</kbd>: Pokaż opcje porzucania zmian
|
||||
<kbd><space></kbd>: Przełącz stan poczekalni
|
||||
<kbd><c-b></kbd>: Filter files (staged/unstaged)
|
||||
<kbd>c</kbd>: Zatwierdź zmiany
|
||||
<kbd>w</kbd>: zatwierdź zmiany bez skryptu pre-commit
|
||||
<kbd>w</kbd>: Zatwierdź zmiany bez skryptu pre-commit
|
||||
<kbd>A</kbd>: Zmień ostatni commit
|
||||
<kbd>C</kbd>: Zatwierdź zmiany używając edytora
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>i</kbd>: ignore or exclude file
|
||||
<kbd>r</kbd>: odśwież pliki
|
||||
<kbd>s</kbd>: przechowaj zmiany
|
||||
<kbd>S</kbd>: wyświetl opcje schowka
|
||||
<kbd>a</kbd>: przełącz stan poczekalni wszystkich
|
||||
<kbd>enter</kbd>: zatwierdź pojedyncze linie
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>D</kbd>: wyświetl opcje resetu
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>f</kbd>: pobierz
|
||||
<kbd>e</kbd>: Edytuj plik
|
||||
<kbd>o</kbd>: Otwórz plik
|
||||
<kbd>i</kbd>: Ignore or exclude file
|
||||
<kbd>r</kbd>: Odśwież pliki
|
||||
<kbd>s</kbd>: Przechowaj zmiany
|
||||
<kbd>S</kbd>: Wyświetl opcje schowka
|
||||
<kbd>a</kbd>: Przełącz stan poczekalni wszystkich
|
||||
<kbd><enter></kbd>: Zatwierdź pojedyncze linie
|
||||
<kbd>g</kbd>: View upstream reset options
|
||||
<kbd>D</kbd>: Wyświetl opcje resetu
|
||||
<kbd>`</kbd>: Toggle file tree view
|
||||
<kbd>M</kbd>: Open external merge tool (git mergetool)
|
||||
<kbd>f</kbd>: Pobierz
|
||||
</pre>
|
||||
|
||||
## Pliki commita
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy the committed file name to the clipboard
|
||||
<kbd>c</kbd>: plik wybierania
|
||||
<kbd>d</kbd>: porzuć zmiany commita dla tego pliku
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>space</kbd>: toggle file included in patch
|
||||
<kbd>a</kbd>: toggle all files included in patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
<kbd><c-o></kbd>: Copy the committed file name to the clipboard
|
||||
<kbd>c</kbd>: Plik wybierania
|
||||
<kbd>d</kbd>: Porzuć zmiany commita dla tego pliku
|
||||
<kbd>o</kbd>: Otwórz plik
|
||||
<kbd>e</kbd>: Edytuj plik
|
||||
<kbd><space></kbd>: Toggle file included in patch
|
||||
<kbd>a</kbd>: Toggle all files included in patch
|
||||
<kbd><enter></kbd>: Enter file to add selected lines to the patch (or toggle directory collapsed)
|
||||
<kbd>`</kbd>: Toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Poczekalnia
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: poprzedni kawałek
|
||||
<kbd>►</kbd>: następny kawałek
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>esc</kbd>: wróć do panelu plików
|
||||
<kbd>tab</kbd>: switch to other panel (staged/unstaged changes)
|
||||
<kbd>space</kbd>: toggle line staged / unstaged
|
||||
<kbd>d</kbd>: delete change (git reset)
|
||||
<kbd>E</kbd>: edit hunk
|
||||
<kbd><left></kbd>: Poprzedni kawałek
|
||||
<kbd><right></kbd>: Następny kawałek
|
||||
<kbd>v</kbd>: Toggle drag select
|
||||
<kbd>V</kbd>: Toggle drag select
|
||||
<kbd>a</kbd>: Toggle select hunk
|
||||
<kbd><c-o></kbd>: Copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: Otwórz plik
|
||||
<kbd>e</kbd>: Edytuj plik
|
||||
<kbd><esc></kbd>: Wróć do panelu plików
|
||||
<kbd><tab></kbd>: Switch to other panel (staged/unstaged changes)
|
||||
<kbd><space></kbd>: Toggle line staged / unstaged
|
||||
<kbd>d</kbd>: Delete change (git reset)
|
||||
<kbd>E</kbd>: Edit hunk
|
||||
<kbd>c</kbd>: Zatwierdź zmiany
|
||||
<kbd>w</kbd>: zatwierdź zmiany bez skryptu pre-commit
|
||||
<kbd>w</kbd>: Zatwierdź zmiany bez skryptu pre-commit
|
||||
<kbd>C</kbd>: Zatwierdź zmiany używając edytora
|
||||
</pre>
|
||||
|
||||
## Reflog
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>g</kbd>: wyświetl opcje resetu
|
||||
<kbd>c</kbd>: kopiuj commit (przebieranie)
|
||||
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>enter</kbd>: view commits
|
||||
<kbd><c-o></kbd>: Copy commit SHA to clipboard
|
||||
<kbd><space></kbd>: Checkout commit
|
||||
<kbd>y</kbd>: Copy commit attribute
|
||||
<kbd>o</kbd>: Open commit in browser
|
||||
<kbd>n</kbd>: Create new branch off of commit
|
||||
<kbd>g</kbd>: Wyświetl opcje resetu
|
||||
<kbd>c</kbd>: Kopiuj commit (przebieranie)
|
||||
<kbd>C</kbd>: Kopiuj zakres commitów (przebieranie)
|
||||
<kbd><c-r></kbd>: Reset cherry-picked (copied) commits selection
|
||||
<kbd><enter></kbd>: View commits
|
||||
</pre>
|
||||
|
||||
## Remote Branches
|
||||
## Remote branches
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
<kbd>M</kbd>: scal do obecnej gałęzi
|
||||
<kbd>r</kbd>: zmiana bazy gałęzi
|
||||
<kbd>d</kbd>: usuń gałąź
|
||||
<kbd>u</kbd>: set as upstream of checked-out branch
|
||||
<kbd>esc</kbd>: wróć do listy repozytoriów zdalnych
|
||||
<kbd>g</kbd>: wyświetl opcje resetu
|
||||
<kbd>enter</kbd>: view commits
|
||||
<kbd><c-o></kbd>: Copy branch name to clipboard
|
||||
<kbd><space></kbd>: Przełącz
|
||||
<kbd>n</kbd>: Nowa gałąź
|
||||
<kbd>M</kbd>: Scal do obecnej gałęzi
|
||||
<kbd>r</kbd>: Zmiana bazy gałęzi
|
||||
<kbd>d</kbd>: Usuń gałąź
|
||||
<kbd>u</kbd>: Set as upstream of checked-out branch
|
||||
<kbd><esc></kbd>: Wróć do listy repozytoriów zdalnych
|
||||
<kbd>g</kbd>: Wyświetl opcje resetu
|
||||
<kbd><enter></kbd>: View commits
|
||||
</pre>
|
||||
|
||||
## Remotes
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: fetch remote
|
||||
<kbd>n</kbd>: add new remote
|
||||
<kbd>d</kbd>: remove remote
|
||||
<kbd>e</kbd>: edit remote
|
||||
<kbd>f</kbd>: Fetch remote
|
||||
<kbd>n</kbd>: Add new remote
|
||||
<kbd>d</kbd>: Remove remote
|
||||
<kbd>e</kbd>: Edit remote
|
||||
</pre>
|
||||
|
||||
## Scalanie
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>◄</kbd>: poprzedni konflikt
|
||||
<kbd>►</kbd>: następny konflikt
|
||||
<kbd>▲</kbd>: wybierz poprzedni kawałek
|
||||
<kbd>▼</kbd>: wybierz następny kawałek
|
||||
<kbd>z</kbd>: cofnij
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>space</kbd>: wybierz kawałek
|
||||
<kbd>b</kbd>: wybierz wszystkie kawałki
|
||||
<kbd>esc</kbd>: wróć do panelu plików
|
||||
<kbd>e</kbd>: Edytuj plik
|
||||
<kbd>o</kbd>: Otwórz plik
|
||||
<kbd><left></kbd>: Poprzedni konflikt
|
||||
<kbd><right></kbd>: Następny konflikt
|
||||
<kbd><up></kbd>: Wybierz poprzedni kawałek
|
||||
<kbd><down></kbd>: Wybierz następny kawałek
|
||||
<kbd>z</kbd>: Cofnij
|
||||
<kbd>M</kbd>: Open external merge tool (git mergetool)
|
||||
<kbd><space></kbd>: Wybierz kawałek
|
||||
<kbd>b</kbd>: Wybierz oba kawałki
|
||||
<kbd><esc></kbd>: Wróć do panelu plików
|
||||
</pre>
|
||||
|
||||
## Schowek
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: zastosuj
|
||||
<kbd>g</kbd>: wyciągnij
|
||||
<kbd>d</kbd>: porzuć
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
<kbd>r</kbd>: rename stash
|
||||
<kbd>enter</kbd>: przeglądaj pliki commita
|
||||
<kbd><space></kbd>: Zastosuj
|
||||
<kbd>g</kbd>: Wyciągnij
|
||||
<kbd>d</kbd>: Porzuć
|
||||
<kbd>n</kbd>: Nowa gałąź
|
||||
<kbd>r</kbd>: Rename stash
|
||||
<kbd><enter></kbd>: Przeglądaj pliki commita
|
||||
</pre>
|
||||
|
||||
## Status
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edytuj konfigurację
|
||||
<kbd>o</kbd>: otwórz konfigurację
|
||||
<kbd>u</kbd>: sprawdź aktualizacje
|
||||
<kbd>enter</kbd>: switch to a recent repo
|
||||
<kbd>a</kbd>: pokaż wszystkie logi gałęzi
|
||||
<kbd>o</kbd>: Otwórz konfigurację
|
||||
<kbd>e</kbd>: Edytuj konfigurację
|
||||
<kbd>u</kbd>: Sprawdź aktualizacje
|
||||
<kbd><enter></kbd>: Switch to a recent repo
|
||||
<kbd>a</kbd>: Pokaż wszystkie logi gałęzi
|
||||
</pre>
|
||||
|
||||
## Sub-commits
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>g</kbd>: wyświetl opcje resetu
|
||||
<kbd>c</kbd>: kopiuj commit (przebieranie)
|
||||
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>enter</kbd>: przeglądaj pliki commita
|
||||
<kbd><c-o></kbd>: Copy commit SHA to clipboard
|
||||
<kbd><space></kbd>: Checkout commit
|
||||
<kbd>y</kbd>: Copy commit attribute
|
||||
<kbd>o</kbd>: Open commit in browser
|
||||
<kbd>n</kbd>: Create new branch off of commit
|
||||
<kbd>g</kbd>: Wyświetl opcje resetu
|
||||
<kbd>c</kbd>: Kopiuj commit (przebieranie)
|
||||
<kbd>C</kbd>: Kopiuj zakres commitów (przebieranie)
|
||||
<kbd><c-r></kbd>: Reset cherry-picked (copied) commits selection
|
||||
<kbd><enter></kbd>: Przeglądaj pliki commita
|
||||
</pre>
|
||||
|
||||
## Submodules
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
|
||||
<kbd>enter</kbd>: enter submodule
|
||||
<kbd>d</kbd>: remove submodule
|
||||
<kbd>u</kbd>: update submodule
|
||||
<kbd>n</kbd>: add new submodule
|
||||
<kbd>e</kbd>: update submodule URL
|
||||
<kbd>i</kbd>: initialize submodule
|
||||
<kbd>b</kbd>: view bulk submodule options
|
||||
<kbd><c-o></kbd>: Copy submodule name to clipboard
|
||||
<kbd><enter></kbd>: Enter submodule
|
||||
<kbd>d</kbd>: Remove submodule
|
||||
<kbd>u</kbd>: Update submodule
|
||||
<kbd>n</kbd>: Add new submodule
|
||||
<kbd>e</kbd>: Update submodule URL
|
||||
<kbd>i</kbd>: Initialize submodule
|
||||
<kbd>b</kbd>: View bulk submodule options
|
||||
</pre>
|
||||
|
||||
## Tags
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>d</kbd>: delete tag
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: create tag
|
||||
<kbd>g</kbd>: wyświetl opcje resetu
|
||||
<kbd>enter</kbd>: view commits
|
||||
<kbd><space></kbd>: Przełącz
|
||||
<kbd>d</kbd>: Delete tag
|
||||
<kbd>P</kbd>: Push tag
|
||||
<kbd>n</kbd>: Create tag
|
||||
<kbd>g</kbd>: Wyświetl opcje resetu
|
||||
<kbd><enter></kbd>: View commits
|
||||
</pre>
|
||||
|
||||
## Zwykłe
|
||||
|
||||
<pre>
|
||||
<kbd>mouse wheel ▼</kbd>: przewiń w dół (fn+up)
|
||||
<kbd>mouse wheel ▲</kbd>: przewiń w górę (fn+down)
|
||||
<kbd>mouse wheel down</kbd>: Przewiń w dół (fn+up)
|
||||
<kbd>mouse wheel up</kbd>: Przewiń w górę (fn+down)
|
||||
</pre>
|
||||
|
||||
@@ -2,27 +2,30 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
# Lazygit 按键绑定
|
||||
|
||||
_Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
|
||||
## 全局键绑定
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+r</kbd>: 切换到最近的仓库
|
||||
<kbd>pgup</kbd>: 向上滚动主面板 (fn+up/shift+k)
|
||||
<kbd>pgdown</kbd>: 向下滚动主面板 (fn+down/shift+j)
|
||||
<kbd>m</kbd>: 查看 合并/变基 选项
|
||||
<kbd>ctrl+p</kbd>: 查看自定义补丁选项
|
||||
<kbd>R</kbd>: 刷新
|
||||
<kbd>x</kbd>: 打开菜单
|
||||
<kbd>+</kbd>: 下一屏模式(正常/半屏/全屏)
|
||||
<kbd>_</kbd>: 上一屏模式
|
||||
<kbd>ctrl+s</kbd>: 查看按路径过滤选项
|
||||
<kbd>W</kbd>: 打开 diff 菜单
|
||||
<kbd>ctrl+e</kbd>: 打开 diff 菜单
|
||||
<kbd><c-r></kbd>: 切换到最近的仓库
|
||||
<kbd><pgup></kbd>: 向上滚动主面板 (fn+up/shift+k)
|
||||
<kbd><pgdown></kbd>: 向下滚动主面板 (fn+down/shift+j)
|
||||
<kbd>@</kbd>: 打开命令日志菜单
|
||||
<kbd>}</kbd>: 扩大差异视图中显示的上下文范围
|
||||
<kbd>{</kbd>: 缩小差异视图中显示的上下文范围
|
||||
<kbd>:</kbd>: 执行自定义命令
|
||||
<kbd><c-p></kbd>: 查看自定义补丁选项
|
||||
<kbd>m</kbd>: 查看 合并/变基 选项
|
||||
<kbd>R</kbd>: 刷新
|
||||
<kbd>+</kbd>: 下一屏模式(正常/半屏/全屏)
|
||||
<kbd>_</kbd>: 上一屏模式
|
||||
<kbd>?</kbd>: 打开菜单
|
||||
<kbd><c-s></kbd>: 查看按路径过滤选项
|
||||
<kbd>W</kbd>: 打开 diff 菜单
|
||||
<kbd><c-e></kbd>: 打开 diff 菜单
|
||||
<kbd><c-w></kbd>: 切换是否在差异视图中显示空白字符差异
|
||||
<kbd>z</kbd>: (通过 reflog)撤销「实验功能」
|
||||
<kbd>ctrl+z</kbd>: (通过 reflog)重做「实验功能」
|
||||
<kbd><c-z></kbd>: (通过 reflog)重做「实验功能」
|
||||
<kbd>P</kbd>: 推送
|
||||
<kbd>p</kbd>: 拉取
|
||||
</pre>
|
||||
@@ -32,9 +35,9 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<pre>
|
||||
<kbd>,</kbd>: 上一页
|
||||
<kbd>.</kbd>: 下一页
|
||||
<kbd><</kbd>: 滚动到顶部
|
||||
<kbd><</kbd>: 滚动到顶部
|
||||
<kbd>/</kbd>: 开始搜索
|
||||
<kbd>></kbd>: 滚动到底部
|
||||
<kbd>></kbd>: 滚动到底部
|
||||
<kbd>H</kbd>: 向左滚动
|
||||
<kbd>L</kbd>: 向右滚动
|
||||
<kbd>]</kbd>: 下一个标签
|
||||
@@ -44,60 +47,61 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## Reflog 页面
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板
|
||||
<kbd>space</kbd>: 检出提交
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd><c-o></kbd>: 将提交的 SHA 复制到剪贴板
|
||||
<kbd><space></kbd>: 检出提交
|
||||
<kbd>y</kbd>: Copy commit attribute
|
||||
<kbd>o</kbd>: 在浏览器中打开提交
|
||||
<kbd>n</kbd>: 从提交创建新分支
|
||||
<kbd>g</kbd>: 查看重置选项
|
||||
<kbd>c</kbd>: 复制提交(拣选)
|
||||
<kbd>C</kbd>: 复制提交范围(拣选)
|
||||
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
|
||||
<kbd>enter</kbd>: 查看提交
|
||||
<kbd><c-r></kbd>: 重置已拣选(复制)的提交
|
||||
<kbd><enter></kbd>: 查看提交
|
||||
</pre>
|
||||
|
||||
## 分支页面
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 将分支名称复制到剪贴板
|
||||
<kbd><c-o></kbd>: 将分支名称复制到剪贴板
|
||||
<kbd>i</kbd>: 显示 git-flow 选项
|
||||
<kbd>space</kbd>: 检出
|
||||
<kbd><space></kbd>: 检出
|
||||
<kbd>n</kbd>: 新分支
|
||||
<kbd>o</kbd>: 创建抓取请求
|
||||
<kbd>O</kbd>: 创建抓取请求选项
|
||||
<kbd>ctrl+y</kbd>: 将抓取请求 URL 复制到剪贴板
|
||||
<kbd><c-y></kbd>: 将抓取请求 URL 复制到剪贴板
|
||||
<kbd>c</kbd>: 按名称检出
|
||||
<kbd>F</kbd>: 强制检出
|
||||
<kbd>d</kbd>: 删除分支
|
||||
<kbd>r</kbd>: 将已检出的分支变基到该分支
|
||||
<kbd>M</kbd>: 合并到当前检出的分支
|
||||
<kbd>f</kbd>: 从上游快进此分支
|
||||
<kbd>T</kbd>: 创建标签
|
||||
<kbd>g</kbd>: 查看重置选项
|
||||
<kbd>R</kbd>: 重命名分支
|
||||
<kbd>u</kbd>: set/unset upstream
|
||||
<kbd>enter</kbd>: 查看提交
|
||||
<kbd>u</kbd>: Set/Unset upstream
|
||||
<kbd><enter></kbd>: 查看提交
|
||||
</pre>
|
||||
|
||||
## 子提交
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板
|
||||
<kbd>space</kbd>: 检出提交
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd><c-o></kbd>: 将提交的 SHA 复制到剪贴板
|
||||
<kbd><space></kbd>: 检出提交
|
||||
<kbd>y</kbd>: Copy commit attribute
|
||||
<kbd>o</kbd>: 在浏览器中打开提交
|
||||
<kbd>n</kbd>: 从提交创建新分支
|
||||
<kbd>g</kbd>: 查看重置选项
|
||||
<kbd>c</kbd>: 复制提交(拣选)
|
||||
<kbd>C</kbd>: 复制提交范围(拣选)
|
||||
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
|
||||
<kbd>enter</kbd>: 查看提交的文件
|
||||
<kbd><c-r></kbd>: 重置已拣选(复制)的提交
|
||||
<kbd><enter></kbd>: 查看提交的文件
|
||||
</pre>
|
||||
|
||||
## 子模块
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 将子模块名称复制到剪贴板
|
||||
<kbd>enter</kbd>: 输入子模块
|
||||
<kbd><c-o></kbd>: 将子模块名称复制到剪贴板
|
||||
<kbd><enter></kbd>: 输入子模块
|
||||
<kbd>d</kbd>: 删除子模块
|
||||
<kbd>u</kbd>: 更新子模块
|
||||
<kbd>n</kbd>: 添加新的子模块
|
||||
@@ -109,8 +113,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## 提交
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板
|
||||
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
|
||||
<kbd><c-o></kbd>: 将提交的 SHA 复制到剪贴板
|
||||
<kbd><c-r></kbd>: 重置已拣选(复制)的提交
|
||||
<kbd>b</kbd>: 查看二分查找选项
|
||||
<kbd>s</kbd>: 向下压缩
|
||||
<kbd>f</kbd>: 修正提交(fixup)
|
||||
@@ -119,48 +123,54 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>d</kbd>: 删除提交
|
||||
<kbd>e</kbd>: 编辑提交
|
||||
<kbd>p</kbd>: 选择提交(变基过程中)
|
||||
<kbd>F</kbd>: 为此提交创建修正
|
||||
<kbd>F</kbd>: 创建修正提交
|
||||
<kbd>S</kbd>: 压缩在所选提交之上的所有“fixup!”提交(自动压缩)
|
||||
<kbd>ctrl+j</kbd>: 下移提交
|
||||
<kbd>ctrl+k</kbd>: 上移提交
|
||||
<kbd><c-j></kbd>: 下移提交
|
||||
<kbd><c-k></kbd>: 上移提交
|
||||
<kbd>v</kbd>: 粘贴提交(拣选)
|
||||
<kbd>A</kbd>: 用已暂存的更改来修补提交
|
||||
<kbd>a</kbd>: reset commit author
|
||||
<kbd>a</kbd>: Set/Reset commit author
|
||||
<kbd>t</kbd>: 还原提交
|
||||
<kbd>T</kbd>: 标签提交
|
||||
<kbd>ctrl+l</kbd>: 打开日志菜单
|
||||
<kbd>space</kbd>: 检出提交
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd><c-l></kbd>: 打开日志菜单
|
||||
<kbd><space></kbd>: 检出提交
|
||||
<kbd>y</kbd>: Copy commit attribute
|
||||
<kbd>o</kbd>: 在浏览器中打开提交
|
||||
<kbd>n</kbd>: 从提交创建新分支
|
||||
<kbd>g</kbd>: 查看重置选项
|
||||
<kbd>c</kbd>: 复制提交(拣选)
|
||||
<kbd>C</kbd>: 复制提交范围(拣选)
|
||||
<kbd>enter</kbd>: 查看提交的文件
|
||||
<kbd><enter></kbd>: 查看提交的文件
|
||||
</pre>
|
||||
|
||||
## 提交文件
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 将提交的文件名复制到剪贴板
|
||||
<kbd><c-o></kbd>: 将提交的文件名复制到剪贴板
|
||||
<kbd>c</kbd>: 检出文件
|
||||
<kbd>d</kbd>: 放弃对此文件的提交更改
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>e</kbd>: 编辑文件
|
||||
<kbd>space</kbd>: 补丁中包含的切换文件
|
||||
<kbd>a</kbd>: toggle all files included in patch
|
||||
<kbd>enter</kbd>: 输入文件以将所选行添加到补丁中(或切换目录折叠)
|
||||
<kbd><space></kbd>: 补丁中包含的切换文件
|
||||
<kbd>a</kbd>: Toggle all files included in patch
|
||||
<kbd><enter></kbd>: 输入文件以将所选行添加到补丁中(或切换目录折叠)
|
||||
<kbd>`</kbd>: 切换文件树视图
|
||||
</pre>
|
||||
|
||||
## 提交讯息
|
||||
|
||||
<pre>
|
||||
<kbd><enter></kbd>: 确认
|
||||
<kbd><esc></kbd>: 关闭
|
||||
</pre>
|
||||
|
||||
## 文件
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 将文件名复制到剪贴板
|
||||
<kbd>ctrl+w</kbd>: 切换是否在差异视图中显示空白字符差异
|
||||
<kbd><c-o></kbd>: 将文件名复制到剪贴板
|
||||
<kbd>d</kbd>: 查看'放弃更改'选项
|
||||
<kbd>space</kbd>: 切换暂存状态
|
||||
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
|
||||
<kbd><space></kbd>: 切换暂存状态
|
||||
<kbd><c-b></kbd>: Filter files (staged/unstaged)
|
||||
<kbd>c</kbd>: 提交更改
|
||||
<kbd>w</kbd>: 提交更改而无需预先提交钩子
|
||||
<kbd>A</kbd>: 修补最后一次提交
|
||||
@@ -172,7 +182,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>s</kbd>: 将所有更改加入贮藏
|
||||
<kbd>S</kbd>: 查看贮藏选项
|
||||
<kbd>a</kbd>: 切换所有文件的暂存状态
|
||||
<kbd>enter</kbd>: 暂存单个 块/行 用于文件, 或 折叠/展开 目录
|
||||
<kbd><enter></kbd>: 暂存单个 块/行 用于文件, 或 折叠/展开 目录
|
||||
<kbd>g</kbd>: 查看上游重置选项
|
||||
<kbd>D</kbd>: 查看重置选项
|
||||
<kbd>`</kbd>: 切换文件树视图
|
||||
@@ -183,27 +193,27 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## 构建补丁中
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: 选择上一个区块
|
||||
<kbd>►</kbd>: 选择下一个区块
|
||||
<kbd><left></kbd>: 选择上一个区块
|
||||
<kbd><right></kbd>: 选择下一个区块
|
||||
<kbd>v</kbd>: 切换拖动选择
|
||||
<kbd>V</kbd>: 切换拖动选择
|
||||
<kbd>a</kbd>: 切换选择区块
|
||||
<kbd>ctrl+o</kbd>: 将选中文本复制到剪贴板
|
||||
<kbd><c-o></kbd>: 将选中文本复制到剪贴板
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>e</kbd>: 编辑文件
|
||||
<kbd>space</kbd>: 添加/移除 行到补丁
|
||||
<kbd>esc</kbd>: 退出逐行模式
|
||||
<kbd><space></kbd>: 添加/移除 行到补丁
|
||||
<kbd><esc></kbd>: 退出逐行模式
|
||||
</pre>
|
||||
|
||||
## 标签页面
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: 检出
|
||||
<kbd><space></kbd>: 检出
|
||||
<kbd>d</kbd>: 删除标签
|
||||
<kbd>P</kbd>: 推送标签
|
||||
<kbd>n</kbd>: 创建标签
|
||||
<kbd>g</kbd>: 查看重置选项
|
||||
<kbd>enter</kbd>: 查看提交
|
||||
<kbd><enter></kbd>: 查看提交
|
||||
</pre>
|
||||
|
||||
## 正在合并
|
||||
@@ -211,33 +221,33 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<pre>
|
||||
<kbd>e</kbd>: 编辑文件
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>◄</kbd>: 选择上一个冲突
|
||||
<kbd>►</kbd>: 选择下一个冲突
|
||||
<kbd>▲</kbd>: 选择顶部块
|
||||
<kbd>▼</kbd>: 选择底部块
|
||||
<kbd><left></kbd>: 选择上一个冲突
|
||||
<kbd><right></kbd>: 选择下一个冲突
|
||||
<kbd><up></kbd>: 选择顶部块
|
||||
<kbd><down></kbd>: 选择底部块
|
||||
<kbd>z</kbd>: 撤销
|
||||
<kbd>M</kbd>: 打开外部合并工具 (git mergetool)
|
||||
<kbd>space</kbd>: 选中区块
|
||||
<kbd><space></kbd>: 选中区块
|
||||
<kbd>b</kbd>: 选中所有区块
|
||||
<kbd>esc</kbd>: 返回文件面板
|
||||
<kbd><esc></kbd>: 返回文件面板
|
||||
</pre>
|
||||
|
||||
## 正在暂存
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: 选择上一个区块
|
||||
<kbd>►</kbd>: 选择下一个区块
|
||||
<kbd><left></kbd>: 选择上一个区块
|
||||
<kbd><right></kbd>: 选择下一个区块
|
||||
<kbd>v</kbd>: 切换拖动选择
|
||||
<kbd>V</kbd>: 切换拖动选择
|
||||
<kbd>a</kbd>: 切换选择区块
|
||||
<kbd>ctrl+o</kbd>: 将选中文本复制到剪贴板
|
||||
<kbd><c-o></kbd>: 将选中文本复制到剪贴板
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>e</kbd>: 编辑文件
|
||||
<kbd>esc</kbd>: 返回文件面板
|
||||
<kbd>tab</kbd>: 切换到其他面板
|
||||
<kbd>space</kbd>: 切换行暂存状态
|
||||
<kbd><esc></kbd>: 返回文件面板
|
||||
<kbd><tab></kbd>: 切换到其他面板
|
||||
<kbd><space></kbd>: 切换行暂存状态
|
||||
<kbd>d</kbd>: 取消变更 (git reset)
|
||||
<kbd>E</kbd>: edit hunk
|
||||
<kbd>E</kbd>: Edit hunk
|
||||
<kbd>c</kbd>: 提交更改
|
||||
<kbd>w</kbd>: 提交更改而无需预先提交钩子
|
||||
<kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息)
|
||||
@@ -246,43 +256,58 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## 正常
|
||||
|
||||
<pre>
|
||||
<kbd>mouse wheel ▼</kbd>: 向下滚动 (fn+up)
|
||||
<kbd>mouse wheel ▲</kbd>: 向上滚动 (fn+down)
|
||||
<kbd>mouse wheel down</kbd>: 向下滚动 (fn+up)
|
||||
<kbd>mouse wheel up</kbd>: 向上滚动 (fn+down)
|
||||
</pre>
|
||||
|
||||
## 状态
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: 编辑配置文件
|
||||
<kbd>o</kbd>: 打开配置文件
|
||||
<kbd>e</kbd>: 编辑配置文件
|
||||
<kbd>u</kbd>: 检查更新
|
||||
<kbd>enter</kbd>: 切换到最近的仓库
|
||||
<kbd><enter></kbd>: 切换到最近的仓库
|
||||
<kbd>a</kbd>: 显示所有分支的日志
|
||||
</pre>
|
||||
|
||||
## 确认面板
|
||||
|
||||
<pre>
|
||||
<kbd><enter></kbd>: 确认
|
||||
<kbd><esc></kbd>: 关闭
|
||||
</pre>
|
||||
|
||||
## 菜单
|
||||
|
||||
<pre>
|
||||
<kbd><enter></kbd>: 执行
|
||||
<kbd><esc></kbd>: 关闭
|
||||
</pre>
|
||||
|
||||
## 贮藏
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: 应用
|
||||
<kbd><space></kbd>: 应用
|
||||
<kbd>g</kbd>: 应用并删除
|
||||
<kbd>d</kbd>: 删除
|
||||
<kbd>n</kbd>: 新分支
|
||||
<kbd>r</kbd>: rename stash
|
||||
<kbd>enter</kbd>: 查看提交的文件
|
||||
<kbd>r</kbd>: Rename stash
|
||||
<kbd><enter></kbd>: 查看提交的文件
|
||||
</pre>
|
||||
|
||||
## 远程分支
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: 检出
|
||||
<kbd><c-o></kbd>: 将分支名称复制到剪贴板
|
||||
<kbd><space></kbd>: 检出
|
||||
<kbd>n</kbd>: 新分支
|
||||
<kbd>M</kbd>: 合并到当前检出的分支
|
||||
<kbd>r</kbd>: 将已检出的分支变基到该分支
|
||||
<kbd>d</kbd>: 删除分支
|
||||
<kbd>u</kbd>: 设置为检出分支的上游
|
||||
<kbd>esc</kbd>: 返回远程仓库列表
|
||||
<kbd><esc></kbd>: 返回远程仓库列表
|
||||
<kbd>g</kbd>: 查看重置选项
|
||||
<kbd>enter</kbd>: 查看提交
|
||||
<kbd><enter></kbd>: 查看提交
|
||||
</pre>
|
||||
|
||||
## 远程页面
|
||||
|
||||
@@ -4,21 +4,21 @@ go 1.18
|
||||
|
||||
require (
|
||||
github.com/OpenPeeDeeP/xdg v1.0.0
|
||||
github.com/atotto/clipboard v0.1.2
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/aybabtme/humanlog v0.4.1
|
||||
github.com/cli/safeexec v1.0.0
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||
github.com/creack/pty v1.1.11
|
||||
github.com/fsmiamoto/git-todo-parser v0.0.2
|
||||
github.com/fsmiamoto/git-todo-parser v0.0.4
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/gdamore/tcell/v2 v2.5.4
|
||||
github.com/gdamore/tcell/v2 v2.6.0
|
||||
github.com/go-errors/errors v1.4.2
|
||||
github.com/gookit/color v1.4.2
|
||||
github.com/imdario/mergo v0.3.11
|
||||
github.com/integrii/flaggy v1.4.0
|
||||
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20230105224502-214a0df08da7
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20230324073941-36f2e87458fa
|
||||
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
|
||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
|
||||
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
|
||||
@@ -62,14 +62,16 @@ require (
|
||||
github.com/onsi/ginkgo v1.10.3 // indirect
|
||||
github.com/onsi/gomega v1.7.1 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
|
||||
github.com/rivo/uniseg v0.4.3 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.2.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
|
||||
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 // indirect
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
|
||||
golang.org/x/sys v0.3.0 // indirect
|
||||
golang.org/x/term v0.3.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
||||
replace github.com/creack/pty => github.com/photostorm/pty v1.1.19-0.20230324012736-6794a5ba0ba0
|
||||
|
||||
@@ -6,8 +6,8 @@ github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
|
||||
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aybabtme/humanlog v0.4.1 h1:D8d9um55rrthJsP8IGSHBcti9lTb/XknmDAX6Zy8tek=
|
||||
github.com/aybabtme/humanlog v0.4.1/go.mod h1:B0bnQX4FTSU3oftPMTTPvENCy8LqixLDvYJA9TUCAGo=
|
||||
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
|
||||
@@ -15,9 +15,6 @@ github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
|
||||
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
|
||||
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -28,15 +25,15 @@ github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fsmiamoto/git-todo-parser v0.0.2 h1:l6Y+9q7jbM+yK/w6kASpHO7ejL9ARCErm3tCEqOT278=
|
||||
github.com/fsmiamoto/git-todo-parser v0.0.2/go.mod h1:B+AgTbNE2BARvJqzXygThzqxLIaEWvwr2sxKYYb0Fas=
|
||||
github.com/fsmiamoto/git-todo-parser v0.0.4 h1:fzcGaoAFDHWzJRKw//CSZFrXucsLKplIvOSab3FtWWM=
|
||||
github.com/fsmiamoto/git-todo-parser v0.0.4/go.mod h1:B+AgTbNE2BARvJqzXygThzqxLIaEWvwr2sxKYYb0Fas=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
||||
github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
|
||||
github.com/gdamore/tcell/v2 v2.5.4/go.mod h1:dZgRy5v4iMobMEcWNYBtREnDZAT9DYmfqIkrgEMxLyw=
|
||||
github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg=
|
||||
github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
@@ -72,8 +69,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
|
||||
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20230105224502-214a0df08da7 h1:a7T87NGCrmFhzWBWvaaPQppjbY9/w0Vl4hoB7RnFaug=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20230105224502-214a0df08da7/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20230324073941-36f2e87458fa h1:E9G1mj94rMal1YLaABwdxLUUgKq+xGbElFjHRNaDJUg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20230324073941-36f2e87458fa/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
|
||||
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
|
||||
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
|
||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
|
||||
@@ -129,6 +126,8 @@ github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
|
||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
|
||||
github.com/photostorm/pty v1.1.19-0.20230324012736-6794a5ba0ba0 h1:ahg2J7KgBYMH1KJNvde12jjJZGXwGq0AdRPztBgX2Rc=
|
||||
github.com/photostorm/pty v1.1.19-0.20230324012736-6794a5ba0ba0/go.mod h1:KO+FcPtyLAiRC0hJwreJVvfwc7vnNz77UxBTIGHdPVk=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -136,8 +135,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
|
||||
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
|
||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/samber/lo v1.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM=
|
||||
@@ -204,20 +204,24 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220721230656-c6bc011c0c49/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
|
||||
+18
-6
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/logs"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
)
|
||||
|
||||
@@ -33,7 +35,6 @@ type App struct {
|
||||
Config config.AppConfigurer
|
||||
OSCommand *oscommands.OSCommand
|
||||
Gui *gui.Gui
|
||||
Updater *updates.Updater // may only need this on the Gui
|
||||
}
|
||||
|
||||
func Run(
|
||||
@@ -77,6 +78,18 @@ func NewCommon(config config.AppConfigurer) (*common.Common, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newLogger(cfg config.AppConfigurer) *logrus.Entry {
|
||||
if cfg.GetDebug() {
|
||||
logPath, err := config.LogPath()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return logs.NewDevelopmentLogger(logPath)
|
||||
} else {
|
||||
return logs.NewProductionLogger()
|
||||
}
|
||||
}
|
||||
|
||||
// NewApp bootstrap a new application
|
||||
func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) {
|
||||
app := &App{
|
||||
@@ -87,8 +100,7 @@ func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) {
|
||||
|
||||
app.OSCommand = oscommands.NewOSCommand(common, config, oscommands.GetPlatform(), oscommands.NewNullGuiIO(app.Log))
|
||||
|
||||
var err error
|
||||
app.Updater, err = updates.NewUpdater(common, config, app.OSCommand)
|
||||
updater, err := updates.NewUpdater(common, config, app.OSCommand)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
@@ -108,7 +120,7 @@ func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) {
|
||||
return app, err
|
||||
}
|
||||
|
||||
app.Gui, err = gui.NewGui(common, config, gitVersion, app.Updater, showRecentRepos, dirName)
|
||||
app.Gui, err = gui.NewGui(common, config, gitVersion, updater, showRecentRepos, dirName)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
@@ -123,7 +135,7 @@ func (app *App) validateGitVersion() (*git_commands.GitVersion, error) {
|
||||
return nil, minVersionError
|
||||
}
|
||||
|
||||
if version.IsOlderThan(2, 0, 0) {
|
||||
if version.IsOlderThan(2, 20, 0) {
|
||||
return nil, minVersionError
|
||||
}
|
||||
|
||||
@@ -193,7 +205,7 @@ func (app *App) setupRepo() (bool, error) {
|
||||
}
|
||||
|
||||
if shouldInitRepo {
|
||||
if err := app.OSCommand.Cmd.New("git init " + initialBranchArg).Run(); err != nil {
|
||||
if err := app.OSCommand.Cmd.New([]string{"git", "init", initialBranchArg}).Run(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
|
||||
+252
-52
@@ -1,13 +1,17 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"strconv"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// Sometimes lazygit will be invoked in daemon mode from a parent lazygit process.
|
||||
@@ -15,33 +19,58 @@ import (
|
||||
// For example, if we want to ensure that a git command doesn't hang due to
|
||||
// waiting for an editor to save a commit message, we can tell git to invoke lazygit
|
||||
// as the editor via 'GIT_EDITOR=lazygit', and use the env var
|
||||
// 'LAZYGIT_DAEMON_KIND=EXIT_IMMEDIATELY' to specify that we want to run lazygit
|
||||
// as a daemon which simply exits immediately. Any additional arguments we want
|
||||
// to pass to a daemon can be done via other env vars.
|
||||
// 'LAZYGIT_DAEMON_KIND=1' (exit immediately) to specify that we want to run lazygit
|
||||
// as a daemon which simply exits immediately.
|
||||
//
|
||||
// 'Daemon' is not the best name for this, because it's not a persistent background
|
||||
// process, but it's close enough.
|
||||
|
||||
type DaemonKind string
|
||||
type DaemonKind int
|
||||
|
||||
const (
|
||||
InteractiveRebase DaemonKind = "INTERACTIVE_REBASE"
|
||||
ExitImmediately DaemonKind = "EXIT_IMMEDIATELY"
|
||||
// for when we fail to parse the daemon kind
|
||||
DaemonKindUnknown DaemonKind = iota
|
||||
|
||||
DaemonKindExitImmediately
|
||||
DaemonKindCherryPick
|
||||
DaemonKindMoveTodoUp
|
||||
DaemonKindMoveTodoDown
|
||||
DaemonKindInsertBreak
|
||||
DaemonKindChangeTodoActions
|
||||
DaemonKindMoveFixupCommitDown
|
||||
)
|
||||
|
||||
const (
|
||||
DaemonKindEnvKey string = "LAZYGIT_DAEMON_KIND"
|
||||
RebaseTODOEnvKey string = "LAZYGIT_REBASE_TODO"
|
||||
|
||||
// Contains json-encoded arguments to the daemon
|
||||
DaemonInstructionEnvKey string = "LAZYGIT_DAEMON_INSTRUCTION"
|
||||
)
|
||||
|
||||
type Daemon interface {
|
||||
Run() error
|
||||
func getInstruction() Instruction {
|
||||
jsonData := os.Getenv(DaemonInstructionEnvKey)
|
||||
|
||||
mapping := map[DaemonKind]func(string) Instruction{
|
||||
DaemonKindExitImmediately: deserializeInstruction[*ExitImmediatelyInstruction],
|
||||
DaemonKindCherryPick: deserializeInstruction[*CherryPickCommitsInstruction],
|
||||
DaemonKindChangeTodoActions: deserializeInstruction[*ChangeTodoActionsInstruction],
|
||||
DaemonKindMoveFixupCommitDown: deserializeInstruction[*MoveFixupCommitDownInstruction],
|
||||
DaemonKindMoveTodoUp: deserializeInstruction[*MoveTodoUpInstruction],
|
||||
DaemonKindMoveTodoDown: deserializeInstruction[*MoveTodoDownInstruction],
|
||||
DaemonKindInsertBreak: deserializeInstruction[*InsertBreakInstruction],
|
||||
}
|
||||
|
||||
return mapping[getDaemonKind()](jsonData)
|
||||
}
|
||||
|
||||
func Handle(common *common.Common) {
|
||||
d := getDaemon(common)
|
||||
if d == nil {
|
||||
if !InDaemonMode() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := d.Run(); err != nil {
|
||||
instruction := getInstruction()
|
||||
|
||||
if err := instruction.run(common); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -49,58 +78,229 @@ func Handle(common *common.Common) {
|
||||
}
|
||||
|
||||
func InDaemonMode() bool {
|
||||
return getDaemonKind() != ""
|
||||
}
|
||||
|
||||
func getDaemon(common *common.Common) Daemon {
|
||||
switch getDaemonKind() {
|
||||
case InteractiveRebase:
|
||||
return &rebaseDaemon{c: common}
|
||||
case ExitImmediately:
|
||||
return &exitImmediatelyDaemon{c: common}
|
||||
}
|
||||
|
||||
return nil
|
||||
return getDaemonKind() != DaemonKindUnknown
|
||||
}
|
||||
|
||||
func getDaemonKind() DaemonKind {
|
||||
return DaemonKind(os.Getenv(DaemonKindEnvKey))
|
||||
intValue, err := strconv.Atoi(os.Getenv(DaemonKindEnvKey))
|
||||
if err != nil {
|
||||
return DaemonKindUnknown
|
||||
}
|
||||
|
||||
return DaemonKind(intValue)
|
||||
}
|
||||
|
||||
type rebaseDaemon struct {
|
||||
c *common.Common
|
||||
// An Instruction is a command to be run by lazygit in daemon mode.
|
||||
// It is serialized to json and passed to lazygit via environment variables
|
||||
type Instruction interface {
|
||||
Kind() DaemonKind
|
||||
SerializedInstructions() string
|
||||
|
||||
// runs the instruction
|
||||
run(common *common.Common) error
|
||||
}
|
||||
|
||||
func (self *rebaseDaemon) Run() error {
|
||||
self.c.Log.Info("Lazygit invoked as interactive rebase demon")
|
||||
self.c.Log.Info("args: ", os.Args)
|
||||
func serializeInstruction[T any](instruction T) string {
|
||||
jsonData, err := json.Marshal(instruction)
|
||||
if err != nil {
|
||||
// this should never happen
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if strings.HasSuffix(os.Args[1], "git-rebase-todo") {
|
||||
if err := os.WriteFile(os.Args[1], []byte(os.Getenv(RebaseTODOEnvKey)), 0o644); err != nil {
|
||||
return err
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
func deserializeInstruction[T Instruction](jsonData string) Instruction {
|
||||
var instruction T
|
||||
err := json.Unmarshal([]byte(jsonData), &instruction)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return instruction
|
||||
}
|
||||
|
||||
func ToEnvVars(instruction Instruction) []string {
|
||||
return []string{
|
||||
fmt.Sprintf("%s=%d", DaemonKindEnvKey, instruction.Kind()),
|
||||
fmt.Sprintf("%s=%s", DaemonInstructionEnvKey, instruction.SerializedInstructions()),
|
||||
}
|
||||
}
|
||||
|
||||
type ExitImmediatelyInstruction struct{}
|
||||
|
||||
func (self *ExitImmediatelyInstruction) Kind() DaemonKind {
|
||||
return DaemonKindExitImmediately
|
||||
}
|
||||
|
||||
func (self *ExitImmediatelyInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *ExitImmediatelyInstruction) run(common *common.Common) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewExitImmediatelyInstruction() Instruction {
|
||||
return &ExitImmediatelyInstruction{}
|
||||
}
|
||||
|
||||
type CherryPickCommitsInstruction struct {
|
||||
Todo string
|
||||
}
|
||||
|
||||
func NewCherryPickCommitsInstruction(commits []*models.Commit) Instruction {
|
||||
todoLines := lo.Map(commits, func(commit *models.Commit, _ int) TodoLine {
|
||||
return TodoLine{
|
||||
Action: "pick",
|
||||
Commit: commit,
|
||||
}
|
||||
} else if strings.HasSuffix(os.Args[1], filepath.Join(gitDir(), "COMMIT_EDITMSG")) { // TODO: test
|
||||
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
|
||||
// but in this case we don't need to edit it, so we'll just return
|
||||
} else {
|
||||
self.c.Log.Info("Lazygit demon did not match on any use cases")
|
||||
})
|
||||
|
||||
todo := TodoLinesToString(todoLines)
|
||||
|
||||
return &CherryPickCommitsInstruction{
|
||||
Todo: todo,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func gitDir() string {
|
||||
dir := env.GetGitDirEnv()
|
||||
if dir == "" {
|
||||
return ".git"
|
||||
func (self *CherryPickCommitsInstruction) Kind() DaemonKind {
|
||||
return DaemonKindCherryPick
|
||||
}
|
||||
|
||||
func (self *CherryPickCommitsInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *CherryPickCommitsInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
return utils.PrependStrToTodoFile(path, []byte(self.Todo))
|
||||
})
|
||||
}
|
||||
|
||||
type ChangeTodoActionsInstruction struct {
|
||||
Changes []ChangeTodoAction
|
||||
}
|
||||
|
||||
func NewChangeTodoActionsInstruction(changes []ChangeTodoAction) Instruction {
|
||||
return &ChangeTodoActionsInstruction{
|
||||
Changes: changes,
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
type exitImmediatelyDaemon struct {
|
||||
c *common.Common
|
||||
func (self *ChangeTodoActionsInstruction) Kind() DaemonKind {
|
||||
return DaemonKindChangeTodoActions
|
||||
}
|
||||
|
||||
func (self *exitImmediatelyDaemon) Run() error {
|
||||
return nil
|
||||
func (self *ChangeTodoActionsInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *ChangeTodoActionsInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
for _, c := range self.Changes {
|
||||
if err := utils.EditRebaseTodo(path, c.Sha, todo.Pick, c.NewAction); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Takes the sha of some commit, and the sha of a fixup commit that was created
|
||||
// at the end of the branch, then moves the fixup commit down to right after the
|
||||
// original commit, changing its type to "fixup"
|
||||
type MoveFixupCommitDownInstruction struct {
|
||||
OriginalSha string
|
||||
FixupSha string
|
||||
}
|
||||
|
||||
func NewMoveFixupCommitDownInstruction(originalSha string, fixupSha string) Instruction {
|
||||
return &MoveFixupCommitDownInstruction{
|
||||
OriginalSha: originalSha,
|
||||
FixupSha: fixupSha,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *MoveFixupCommitDownInstruction) Kind() DaemonKind {
|
||||
return DaemonKindMoveFixupCommitDown
|
||||
}
|
||||
|
||||
func (self *MoveFixupCommitDownInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *MoveFixupCommitDownInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
return utils.MoveFixupCommitDown(path, self.OriginalSha, self.FixupSha)
|
||||
})
|
||||
}
|
||||
|
||||
type MoveTodoUpInstruction struct {
|
||||
Sha string
|
||||
}
|
||||
|
||||
func NewMoveTodoUpInstruction(sha string) Instruction {
|
||||
return &MoveTodoUpInstruction{
|
||||
Sha: sha,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *MoveTodoUpInstruction) Kind() DaemonKind {
|
||||
return DaemonKindMoveTodoUp
|
||||
}
|
||||
|
||||
func (self *MoveTodoUpInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *MoveTodoUpInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
return utils.MoveTodoUp(path, self.Sha, todo.Pick)
|
||||
})
|
||||
}
|
||||
|
||||
type MoveTodoDownInstruction struct {
|
||||
Sha string
|
||||
}
|
||||
|
||||
func NewMoveTodoDownInstruction(sha string) Instruction {
|
||||
return &MoveTodoDownInstruction{
|
||||
Sha: sha,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *MoveTodoDownInstruction) Kind() DaemonKind {
|
||||
return DaemonKindMoveTodoDown
|
||||
}
|
||||
|
||||
func (self *MoveTodoDownInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *MoveTodoDownInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
return utils.MoveTodoDown(path, self.Sha, todo.Pick)
|
||||
})
|
||||
}
|
||||
|
||||
type InsertBreakInstruction struct{}
|
||||
|
||||
func NewInsertBreakInstruction() Instruction {
|
||||
return &InsertBreakInstruction{}
|
||||
}
|
||||
|
||||
func (self *InsertBreakInstruction) Kind() DaemonKind {
|
||||
return DaemonKindInsertBreak
|
||||
}
|
||||
|
||||
func (self *InsertBreakInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *InsertBreakInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
return utils.PrependStrToTodoFile(path, []byte("break\n"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
)
|
||||
|
||||
type TodoLine struct {
|
||||
Action string
|
||||
Commit *models.Commit
|
||||
}
|
||||
|
||||
func (self *TodoLine) ToString() string {
|
||||
if self.Action == "break" {
|
||||
return self.Action + "\n"
|
||||
} else {
|
||||
return self.Action + " " + self.Commit.Sha + " " + self.Commit.Name + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
func TodoLinesToString(todoLines []TodoLine) string {
|
||||
lines := slices.Map(todoLines, func(todoLine TodoLine) string {
|
||||
return todoLine.ToString()
|
||||
})
|
||||
|
||||
return strings.Join(slices.Reverse(lines), "")
|
||||
}
|
||||
|
||||
type ChangeTodoAction struct {
|
||||
Sha string
|
||||
NewAction todo.TodoCommand
|
||||
}
|
||||
|
||||
func handleInteractiveRebase(common *common.Common, f func(path string) error) error {
|
||||
common.Log.Info("Lazygit invoked as interactive rebase demon")
|
||||
common.Log.Info("args: ", os.Args)
|
||||
path := os.Args[1]
|
||||
|
||||
if strings.HasSuffix(path, "git-rebase-todo") {
|
||||
return f(path)
|
||||
} else if strings.HasSuffix(path, filepath.Join(gitDir(), "COMMIT_EDITMSG")) { // TODO: test
|
||||
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
|
||||
// but in this case we don't need to edit it, so we'll just return
|
||||
} else {
|
||||
common.Log.Info("Lazygit demon did not match on any use cases")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func gitDir() string {
|
||||
dir := env.GetGitDirEnv()
|
||||
if dir == "" {
|
||||
return ".git"
|
||||
}
|
||||
return dir
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/logs"
|
||||
"github.com/jesseduffield/lazygit/pkg/logs/tail"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
@@ -106,7 +106,12 @@ func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTes
|
||||
}
|
||||
|
||||
if cliArgs.TailLogs {
|
||||
logs.TailLogs()
|
||||
logPath, err := config.LogPath()
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
tail.TailLogs(logPath)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func newLogger(config config.AppConfigurer) *logrus.Entry {
|
||||
var log *logrus.Logger
|
||||
if config.GetDebug() {
|
||||
log = newDevelopmentLogger()
|
||||
} else {
|
||||
log = newProductionLogger()
|
||||
}
|
||||
|
||||
// highly recommended: tail -f development.log | humanlog
|
||||
// https://github.com/aybabtme/humanlog
|
||||
log.Formatter = &logrus.JSONFormatter{}
|
||||
|
||||
return log.WithFields(logrus.Fields{})
|
||||
}
|
||||
|
||||
func newProductionLogger() *logrus.Logger {
|
||||
log := logrus.New()
|
||||
log.Out = io.Discard
|
||||
log.SetLevel(logrus.ErrorLevel)
|
||||
return log
|
||||
}
|
||||
|
||||
func newDevelopmentLogger() *logrus.Logger {
|
||||
logger := logrus.New()
|
||||
logger.SetLevel(getLogLevel())
|
||||
logPath, err := config.LogPath()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to log to log file: %v", err)
|
||||
}
|
||||
logger.SetOutput(file)
|
||||
return logger
|
||||
}
|
||||
|
||||
func getLogLevel() logrus.Level {
|
||||
strLevel := os.Getenv("LOG_LEVEL")
|
||||
level, err := logrus.ParseLevel(strLevel)
|
||||
if err != nil {
|
||||
return logrus.DebugLevel
|
||||
}
|
||||
return level
|
||||
}
|
||||
+46
-34
@@ -12,6 +12,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/maps"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
@@ -87,33 +88,34 @@ func writeString(file *os.File, str string) {
|
||||
|
||||
func localisedTitle(tr *i18n.TranslationSet, str string) string {
|
||||
contextTitleMap := map[string]string{
|
||||
"global": tr.GlobalTitle,
|
||||
"navigation": tr.NavigationTitle,
|
||||
"branches": tr.BranchesTitle,
|
||||
"localBranches": tr.LocalBranchesTitle,
|
||||
"files": tr.FilesTitle,
|
||||
"status": tr.StatusTitle,
|
||||
"submodules": tr.SubmodulesTitle,
|
||||
"subCommits": tr.SubCommitsTitle,
|
||||
"remoteBranches": tr.RemoteBranchesTitle,
|
||||
"remotes": tr.RemotesTitle,
|
||||
"reflogCommits": tr.ReflogCommitsTitle,
|
||||
"tags": tr.TagsTitle,
|
||||
"commitFiles": tr.CommitFilesTitle,
|
||||
"commitMessage": tr.CommitMessageTitle,
|
||||
"commits": tr.CommitsTitle,
|
||||
"confirmation": tr.ConfirmationTitle,
|
||||
"information": tr.InformationTitle,
|
||||
"main": tr.NormalTitle,
|
||||
"patchBuilding": tr.PatchBuildingTitle,
|
||||
"mergeConflicts": tr.MergingTitle,
|
||||
"staging": tr.StagingTitle,
|
||||
"menu": tr.MenuTitle,
|
||||
"search": tr.SearchTitle,
|
||||
"secondary": tr.SecondaryTitle,
|
||||
"stash": tr.StashTitle,
|
||||
"suggestions": tr.SuggestionsCheatsheetTitle,
|
||||
"extras": tr.ExtrasTitle,
|
||||
"global": tr.GlobalTitle,
|
||||
"navigation": tr.NavigationTitle,
|
||||
"branches": tr.BranchesTitle,
|
||||
"localBranches": tr.LocalBranchesTitle,
|
||||
"files": tr.FilesTitle,
|
||||
"status": tr.StatusTitle,
|
||||
"submodules": tr.SubmodulesTitle,
|
||||
"subCommits": tr.SubCommitsTitle,
|
||||
"remoteBranches": tr.RemoteBranchesTitle,
|
||||
"remotes": tr.RemotesTitle,
|
||||
"reflogCommits": tr.ReflogCommitsTitle,
|
||||
"tags": tr.TagsTitle,
|
||||
"commitFiles": tr.CommitFilesTitle,
|
||||
"commitMessage": tr.CommitMessageTitle,
|
||||
"commitDescription": tr.CommitDescriptionTitle,
|
||||
"commits": tr.CommitsTitle,
|
||||
"confirmation": tr.ConfirmationTitle,
|
||||
"information": tr.InformationTitle,
|
||||
"main": tr.NormalTitle,
|
||||
"patchBuilding": tr.PatchBuildingTitle,
|
||||
"mergeConflicts": tr.MergingTitle,
|
||||
"staging": tr.StagingTitle,
|
||||
"menu": tr.MenuTitle,
|
||||
"search": tr.SearchTitle,
|
||||
"secondary": tr.SecondaryTitle,
|
||||
"stash": tr.StashTitle,
|
||||
"suggestions": tr.SuggestionsCheatsheetTitle,
|
||||
"extras": tr.ExtrasTitle,
|
||||
}
|
||||
|
||||
title, ok := contextTitleMap[str]
|
||||
@@ -182,6 +184,8 @@ func getHeader(binding *types.Binding, tr *i18n.TranslationSet) header {
|
||||
func formatSections(tr *i18n.TranslationSet, bindingSections []*bindingSection) string {
|
||||
content := fmt.Sprintf("# Lazygit %s\n", tr.Keybindings)
|
||||
|
||||
content += fmt.Sprintf("\n%s\n", italicize(tr.KeybindingsLegend))
|
||||
|
||||
for _, section := range bindingSections {
|
||||
content += formatTitle(section.title)
|
||||
content += "<pre>\n"
|
||||
@@ -199,13 +203,21 @@ func formatTitle(title string) string {
|
||||
}
|
||||
|
||||
func formatBinding(binding *types.Binding) string {
|
||||
result := fmt.Sprintf(" <kbd>%s</kbd>: %s", escapeAngleBrackets(keybindings.LabelFromKey(binding.Key)), binding.Description)
|
||||
if binding.Alternative != "" {
|
||||
return fmt.Sprintf(
|
||||
" <kbd>%s</kbd>: %s (%s)\n",
|
||||
keybindings.LabelFromKey(binding.Key),
|
||||
binding.Description,
|
||||
binding.Alternative,
|
||||
)
|
||||
result += fmt.Sprintf(" (%s)", binding.Alternative)
|
||||
}
|
||||
return fmt.Sprintf(" <kbd>%s</kbd>: %s\n", keybindings.LabelFromKey(binding.Key), binding.Description)
|
||||
result += "\n"
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func escapeAngleBrackets(str string) string {
|
||||
result := strings.ReplaceAll(str, ">", ">")
|
||||
result = strings.ReplaceAll(result, "<", "<")
|
||||
return result
|
||||
}
|
||||
|
||||
func italicize(str string) string {
|
||||
return fmt.Sprintf("_%s_", str)
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestGetBindingSections(t *testing.T) {
|
||||
},
|
||||
expected: []*bindingSection{
|
||||
{
|
||||
title: "Global Keybindings",
|
||||
title: "Global keybindings",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "",
|
||||
@@ -139,7 +139,7 @@ func TestGetBindingSections(t *testing.T) {
|
||||
},
|
||||
expected: []*bindingSection{
|
||||
{
|
||||
title: "List Panel Navigation",
|
||||
title: "List panel navigation",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
@@ -215,7 +215,7 @@ func TestGetBindingSections(t *testing.T) {
|
||||
},
|
||||
expected: []*bindingSection{
|
||||
{
|
||||
title: "List Panel Navigation",
|
||||
title: "List panel navigation",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
|
||||
+13
-6
@@ -24,6 +24,7 @@ type GitCommand struct {
|
||||
Commit *git_commands.CommitCommands
|
||||
Config *git_commands.ConfigCommands
|
||||
Custom *git_commands.CustomCommands
|
||||
Diff *git_commands.DiffCommands
|
||||
File *git_commands.FileCommands
|
||||
Flow *git_commands.FlowCommands
|
||||
Patch *git_commands.PatchCommands
|
||||
@@ -112,29 +113,35 @@ func NewGitCommandAux(
|
||||
tagCommands := git_commands.NewTagCommands(gitCommon)
|
||||
commitCommands := git_commands.NewCommitCommands(gitCommon)
|
||||
customCommands := git_commands.NewCustomCommands(gitCommon)
|
||||
diffCommands := git_commands.NewDiffCommands(gitCommon)
|
||||
fileCommands := git_commands.NewFileCommands(gitCommon)
|
||||
submoduleCommands := git_commands.NewSubmoduleCommands(gitCommon)
|
||||
workingTreeCommands := git_commands.NewWorkingTreeCommands(gitCommon, submoduleCommands, fileLoader)
|
||||
rebaseCommands := git_commands.NewRebaseCommands(gitCommon, commitCommands, workingTreeCommands)
|
||||
stashCommands := git_commands.NewStashCommands(gitCommon, fileLoader, workingTreeCommands)
|
||||
// TODO: have patch manager take workingTreeCommands in its entirety
|
||||
patchManager := patch.NewPatchManager(cmn.Log, workingTreeCommands.ApplyPatch, workingTreeCommands.ShowFileDiff)
|
||||
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchManager)
|
||||
patchBuilder := patch.NewPatchBuilder(cmn.Log,
|
||||
func(from string, to string, reverse bool, filename string, plain bool) (string, error) {
|
||||
// TODO: make patch builder take Gui.IgnoreWhitespaceInDiffView into
|
||||
// account. For now we just pass false.
|
||||
return workingTreeCommands.ShowFileDiff(from, to, reverse, filename, plain, false)
|
||||
})
|
||||
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
|
||||
bisectCommands := git_commands.NewBisectCommands(gitCommon)
|
||||
|
||||
branchLoader := git_commands.NewBranchLoader(cmn, branchCommands.GetRawBranches, branchCommands.CurrentBranchInfo, configCommands)
|
||||
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
|
||||
commitLoader := git_commands.NewCommitLoader(cmn, cmd, dotGitDir, branchCommands.CurrentBranchInfo, statusCommands.RebaseMode)
|
||||
commitLoader := git_commands.NewCommitLoader(cmn, cmd, dotGitDir, statusCommands.RebaseMode)
|
||||
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
|
||||
remoteLoader := git_commands.NewRemoteLoader(cmn, cmd, repo.Remotes)
|
||||
stashLoader := git_commands.NewStashLoader(cmn, cmd)
|
||||
tagLoader := git_commands.NewTagLoader(cmn, version, cmd)
|
||||
tagLoader := git_commands.NewTagLoader(cmn, cmd)
|
||||
|
||||
return &GitCommand{
|
||||
Branch: branchCommands,
|
||||
Commit: commitCommands,
|
||||
Config: configCommands,
|
||||
Custom: customCommands,
|
||||
Diff: diffCommands,
|
||||
File: fileCommands,
|
||||
Flow: flowCommands,
|
||||
Patch: patchCommands,
|
||||
@@ -270,5 +277,5 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam
|
||||
}
|
||||
|
||||
func VerifyInGitRepo(osCommand *oscommands.OSCommand) error {
|
||||
return osCommand.Cmd.New("git rev-parse --git-dir").DontLog().Run()
|
||||
return osCommand.Cmd.New(git_commands.NewGitCmd("rev-parse").Arg("--git-dir").ToArgv()).DontLog().Run()
|
||||
}
|
||||
|
||||
@@ -30,12 +30,8 @@ func NewGitCmdObjBuilder(log *logrus.Entry, innerBuilder *oscommands.CmdObjBuild
|
||||
|
||||
var defaultEnvVar = "GIT_OPTIONAL_LOCKS=0"
|
||||
|
||||
func (self *gitCmdObjBuilder) New(cmdStr string) oscommands.ICmdObj {
|
||||
return self.innerBuilder.New(cmdStr).AddEnvVars(defaultEnvVar)
|
||||
}
|
||||
|
||||
func (self *gitCmdObjBuilder) NewFromArgs(args []string) oscommands.ICmdObj {
|
||||
return self.innerBuilder.NewFromArgs(args).AddEnvVars(defaultEnvVar)
|
||||
func (self *gitCmdObjBuilder) New(args []string) oscommands.ICmdObj {
|
||||
return self.innerBuilder.New(args).AddEnvVars(defaultEnvVar)
|
||||
}
|
||||
|
||||
func (self *gitCmdObjBuilder) NewShell(cmdStr string) oscommands.ICmdObj {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -98,13 +97,15 @@ func (self *BisectCommands) GetInfo() *BisectInfo {
|
||||
}
|
||||
|
||||
func (self *BisectCommands) Reset() error {
|
||||
return self.cmd.New("git bisect reset").StreamOutput().Run()
|
||||
cmdArgs := NewGitCmd("bisect").Arg("reset").ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).StreamOutput().Run()
|
||||
}
|
||||
|
||||
func (self *BisectCommands) Mark(ref string, term string) error {
|
||||
return self.cmd.New(
|
||||
fmt.Sprintf("git bisect %s %s", term, ref),
|
||||
).
|
||||
cmdArgs := NewGitCmd("bisect").Arg(term, ref).ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).
|
||||
IgnoreEmptyError().
|
||||
StreamOutput().
|
||||
Run()
|
||||
@@ -115,7 +116,9 @@ func (self *BisectCommands) Skip(ref string) error {
|
||||
}
|
||||
|
||||
func (self *BisectCommands) Start() error {
|
||||
return self.cmd.New("git bisect start").StreamOutput().Run()
|
||||
cmdArgs := NewGitCmd("bisect").Arg("start").ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).StreamOutput().Run()
|
||||
}
|
||||
|
||||
// tells us whether we've found our problem commit(s). We return a string slice of
|
||||
@@ -137,7 +140,8 @@ func (self *BisectCommands) IsDone() (bool, []string, error) {
|
||||
done := false
|
||||
candidates := []string{}
|
||||
|
||||
err := self.cmd.New(fmt.Sprintf("git rev-list %s", newSha)).RunAndProcessLines(func(line string) (bool, error) {
|
||||
cmdArgs := NewGitCmd("rev-list").Arg(newSha).ToArgv()
|
||||
err := self.cmd.New(cmdArgs).RunAndProcessLines(func(line string) (bool, error) {
|
||||
sha := strings.TrimSpace(line)
|
||||
|
||||
if status, ok := info.statusMap[sha]; ok {
|
||||
@@ -167,9 +171,11 @@ func (self *BisectCommands) IsDone() (bool, []string, error) {
|
||||
// bisecting is actually a descendant of our current bisect commit. If it's not, we need to
|
||||
// render the commits from the bad commit.
|
||||
func (self *BisectCommands) ReachableFromStart(bisectInfo *BisectInfo) bool {
|
||||
err := self.cmd.New(
|
||||
fmt.Sprintf("git merge-base --is-ancestor %s %s", bisectInfo.GetNewSha(), bisectInfo.GetStartSha()),
|
||||
).DontLog().Run()
|
||||
cmdArgs := NewGitCmd("merge-base").
|
||||
Arg("--is-ancestor", bisectInfo.GetNewSha(), bisectInfo.GetStartSha()).
|
||||
ToArgv()
|
||||
|
||||
err := self.cmd.New(cmdArgs).DontLog().Run()
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/mgutz/str"
|
||||
)
|
||||
|
||||
type BranchCommands struct {
|
||||
@@ -20,12 +21,20 @@ func NewBranchCommands(gitCommon *GitCommon) *BranchCommands {
|
||||
|
||||
// New creates a new branch
|
||||
func (self *BranchCommands) New(name string, base string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git checkout -b %s %s", self.cmd.Quote(name), self.cmd.Quote(base))).Run()
|
||||
cmdArgs := NewGitCmd("checkout").
|
||||
Arg("-b", name, base).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// CurrentBranchInfo get the current branch information.
|
||||
func (self *BranchCommands) CurrentBranchInfo() (BranchInfo, error) {
|
||||
branchName, err := self.cmd.New("git symbolic-ref --short HEAD").DontLog().RunWithOutput()
|
||||
branchName, err := self.cmd.New(
|
||||
NewGitCmd("symbolic-ref").
|
||||
Arg("--short", "HEAD").
|
||||
ToArgv(),
|
||||
).DontLog().RunWithOutput()
|
||||
if err == nil && branchName != "HEAD\n" {
|
||||
trimmedBranchName := strings.TrimSpace(branchName)
|
||||
return BranchInfo{
|
||||
@@ -34,7 +43,11 @@ func (self *BranchCommands) CurrentBranchInfo() (BranchInfo, error) {
|
||||
DetachedHead: false,
|
||||
}, nil
|
||||
}
|
||||
output, err := self.cmd.New(`git branch --points-at=HEAD --format="%(HEAD)%00%(objectname)%00%(refname)"`).DontLog().RunWithOutput()
|
||||
output, err := self.cmd.New(
|
||||
NewGitCmd("branch").
|
||||
Arg("--points-at=HEAD", "--format=%(HEAD)%00%(objectname)%00%(refname)").
|
||||
ToArgv(),
|
||||
).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return BranchInfo{}, err
|
||||
}
|
||||
@@ -59,13 +72,12 @@ func (self *BranchCommands) CurrentBranchInfo() (BranchInfo, error) {
|
||||
|
||||
// Delete delete branch
|
||||
func (self *BranchCommands) Delete(branch string, force bool) error {
|
||||
command := "git branch -d"
|
||||
cmdArgs := NewGitCmd("branch").
|
||||
ArgIfElse(force, "-D", "-d").
|
||||
Arg(branch).
|
||||
ToArgv()
|
||||
|
||||
if force {
|
||||
command = "git branch -D"
|
||||
}
|
||||
|
||||
return self.cmd.New(fmt.Sprintf("%s %s", command, self.cmd.Quote(branch))).Run()
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// Checkout checks out a branch (or commit), with --force if you set the force arg to true
|
||||
@@ -75,12 +87,12 @@ type CheckoutOptions struct {
|
||||
}
|
||||
|
||||
func (self *BranchCommands) Checkout(branch string, options CheckoutOptions) error {
|
||||
forceArg := ""
|
||||
if options.Force {
|
||||
forceArg = " --force"
|
||||
}
|
||||
cmdArgs := NewGitCmd("checkout").
|
||||
ArgIf(options.Force, "--force").
|
||||
Arg(branch).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(fmt.Sprintf("git checkout%s %s", forceArg, self.cmd.Quote(branch))).
|
||||
return self.cmd.New(cmdArgs).
|
||||
// prevents git from prompting us for input which would freeze the program
|
||||
// TODO: see if this is actually needed here
|
||||
AddEnvVars("GIT_TERMINAL_PROMPT=0").
|
||||
@@ -100,19 +112,34 @@ func (self *BranchCommands) GetGraphCmdObj(branchName string) oscommands.ICmdObj
|
||||
templateValues := map[string]string{
|
||||
"branchName": self.cmd.Quote(branchName),
|
||||
}
|
||||
return self.cmd.New(utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)).DontLog()
|
||||
|
||||
resolvedTemplate := utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)
|
||||
|
||||
return self.cmd.New(str.ToArgv(resolvedTemplate)).DontLog()
|
||||
}
|
||||
|
||||
func (self *BranchCommands) SetCurrentBranchUpstream(remoteName string, remoteBranchName string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git branch --set-upstream-to=%s/%s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName))).Run()
|
||||
cmdArgs := NewGitCmd("branch").
|
||||
Arg(fmt.Sprintf("--set-upstream-to=%s/%s", remoteName, remoteBranchName)).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *BranchCommands) SetUpstream(remoteName string, remoteBranchName string, branchName string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git branch --set-upstream-to=%s/%s %s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName), self.cmd.Quote(branchName))).Run()
|
||||
cmdArgs := NewGitCmd("branch").
|
||||
Arg(fmt.Sprintf("--set-upstream-to=%s/%s", remoteName, remoteBranchName)).
|
||||
Arg(branchName).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *BranchCommands) UnsetUpstream(branchName string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git branch --unset-upstream %s", self.cmd.Quote(branchName))).Run()
|
||||
cmdArgs := NewGitCmd("branch").Arg("--unset-upstream", branchName).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *BranchCommands) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
|
||||
@@ -126,29 +153,49 @@ func (self *BranchCommands) GetUpstreamDifferenceCount(branchName string) (strin
|
||||
// GetCommitDifferences checks how many pushables/pullables there are for the
|
||||
// current branch
|
||||
func (self *BranchCommands) GetCommitDifferences(from, to string) (string, string) {
|
||||
command := "git rev-list %s..%s --count"
|
||||
pushableCount, err := self.cmd.New(fmt.Sprintf(command, to, from)).DontLog().RunWithOutput()
|
||||
pushableCount, err := self.countDifferences(to, from)
|
||||
if err != nil {
|
||||
return "?", "?"
|
||||
}
|
||||
pullableCount, err := self.cmd.New(fmt.Sprintf(command, from, to)).DontLog().RunWithOutput()
|
||||
pullableCount, err := self.countDifferences(from, to)
|
||||
if err != nil {
|
||||
return "?", "?"
|
||||
}
|
||||
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
|
||||
}
|
||||
|
||||
func (self *BranchCommands) countDifferences(from, to string) (string, error) {
|
||||
cmdArgs := NewGitCmd("rev-list").
|
||||
Arg(fmt.Sprintf("%s..%s", from, to)).
|
||||
Arg("--count").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
}
|
||||
|
||||
func (self *BranchCommands) IsHeadDetached() bool {
|
||||
err := self.cmd.New("git symbolic-ref -q HEAD").DontLog().Run()
|
||||
cmdArgs := NewGitCmd("symbolic-ref").Arg("-q", "HEAD").ToArgv()
|
||||
|
||||
err := self.cmd.New(cmdArgs).DontLog().Run()
|
||||
return err != nil
|
||||
}
|
||||
|
||||
func (self *BranchCommands) Rename(oldName string, newName string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git branch --move %s %s", self.cmd.Quote(oldName), self.cmd.Quote(newName))).Run()
|
||||
cmdArgs := NewGitCmd("branch").
|
||||
Arg("--move", oldName, newName).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *BranchCommands) GetRawBranches() (string, error) {
|
||||
return self.cmd.New(`git for-each-ref --sort=-committerdate --format="%(HEAD)%00%(refname:short)%00%(upstream:short)%00%(upstream:track)" refs/heads`).DontLog().RunWithOutput()
|
||||
cmdArgs := NewGitCmd("for-each-ref").
|
||||
Arg("--sort=-committerdate").
|
||||
Arg(`--format=%(HEAD)%00%(refname:short)%00%(upstream:short)%00%(upstream:track)`).
|
||||
Arg("refs/heads").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
}
|
||||
|
||||
type MergeOpts struct {
|
||||
@@ -156,19 +203,16 @@ type MergeOpts struct {
|
||||
}
|
||||
|
||||
func (self *BranchCommands) Merge(branchName string, opts MergeOpts) error {
|
||||
mergeArg := ""
|
||||
if self.UserConfig.Git.Merging.Args != "" {
|
||||
mergeArg = " " + self.UserConfig.Git.Merging.Args
|
||||
}
|
||||
cmdArgs := NewGitCmd("merge").
|
||||
Arg("--no-edit").
|
||||
ArgIf(self.UserConfig.Git.Merging.Args != "", self.UserConfig.Git.Merging.Args).
|
||||
ArgIf(opts.FastForwardOnly, "--ff-only").
|
||||
Arg(branchName).
|
||||
ToArgv()
|
||||
|
||||
command := fmt.Sprintf("git merge --no-edit%s %s", mergeArg, self.cmd.Quote(branchName))
|
||||
if opts.FastForwardOnly {
|
||||
command = fmt.Sprintf("%s --ff-only", command)
|
||||
}
|
||||
|
||||
return self.cmd.New(command).Run()
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *BranchCommands) AllBranchesLogCmdObj() oscommands.ICmdObj {
|
||||
return self.cmd.New(self.UserConfig.Git.AllBranchesLogCmd).DontLog()
|
||||
return self.cmd.New(str.ToArgv(self.UserConfig.Git.AllBranchesLogCmd)).DontLog()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -20,21 +21,21 @@ func TestBranchGetCommitDifferences(t *testing.T) {
|
||||
{
|
||||
"Can't retrieve pushable count",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect("git rev-list @{u}..HEAD --count", "", errors.New("error")),
|
||||
ExpectGitArgs([]string{"rev-list", "@{u}..HEAD", "--count"}, "", errors.New("error")),
|
||||
"?", "?",
|
||||
},
|
||||
{
|
||||
"Can't retrieve pullable count",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect("git rev-list @{u}..HEAD --count", "1\n", nil).
|
||||
Expect("git rev-list HEAD..@{u} --count", "", errors.New("error")),
|
||||
ExpectGitArgs([]string{"rev-list", "@{u}..HEAD", "--count"}, "1\n", nil).
|
||||
ExpectGitArgs([]string{"rev-list", "HEAD..@{u}", "--count"}, "", errors.New("error")),
|
||||
"?", "?",
|
||||
},
|
||||
{
|
||||
"Retrieve pullable and pushable count",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect("git rev-list @{u}..HEAD --count", "1\n", nil).
|
||||
Expect("git rev-list HEAD..@{u} --count", "2\n", nil),
|
||||
ExpectGitArgs([]string{"rev-list", "@{u}..HEAD", "--count"}, "1\n", nil).
|
||||
ExpectGitArgs([]string{"rev-list", "HEAD..@{u}", "--count"}, "2\n", nil),
|
||||
"1", "2",
|
||||
},
|
||||
}
|
||||
@@ -53,7 +54,7 @@ func TestBranchGetCommitDifferences(t *testing.T) {
|
||||
|
||||
func TestBranchNewBranch(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
Expect(`git checkout -b "test" "refs/heads/master"`, "", nil)
|
||||
ExpectGitArgs([]string{"checkout", "-b", "test", "refs/heads/master"}, "", nil)
|
||||
instance := buildBranchCommands(commonDeps{runner: runner})
|
||||
|
||||
assert.NoError(t, instance.New("test", "refs/heads/master"))
|
||||
@@ -72,7 +73,7 @@ func TestBranchDeleteBranch(t *testing.T) {
|
||||
{
|
||||
"Delete a branch",
|
||||
false,
|
||||
oscommands.NewFakeRunner(t).Expect(`git branch -d "test"`, "", nil),
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"branch", "-d", "test"}, "", nil),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -80,7 +81,7 @@ func TestBranchDeleteBranch(t *testing.T) {
|
||||
{
|
||||
"Force delete a branch",
|
||||
true,
|
||||
oscommands.NewFakeRunner(t).Expect(`git branch -D "test"`, "", nil),
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"branch", "-D", "test"}, "", nil),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -99,12 +100,53 @@ func TestBranchDeleteBranch(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBranchMerge(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
Expect(`git merge --no-edit "test"`, "", nil)
|
||||
instance := buildBranchCommands(commonDeps{runner: runner})
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
userConfig *config.UserConfig
|
||||
opts MergeOpts
|
||||
branchName string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
testName: "basic",
|
||||
userConfig: &config.UserConfig{},
|
||||
opts: MergeOpts{},
|
||||
branchName: "mybranch",
|
||||
expected: []string{"merge", "--no-edit", "mybranch"},
|
||||
},
|
||||
{
|
||||
testName: "merging args",
|
||||
userConfig: &config.UserConfig{
|
||||
Git: config.GitConfig{
|
||||
Merging: config.MergingConfig{
|
||||
Args: "--merging-args", // it's up to the user what they put here
|
||||
},
|
||||
},
|
||||
},
|
||||
opts: MergeOpts{},
|
||||
branchName: "mybranch",
|
||||
expected: []string{"merge", "--no-edit", "--merging-args", "mybranch"},
|
||||
},
|
||||
{
|
||||
testName: "fast forward only",
|
||||
userConfig: &config.UserConfig{},
|
||||
opts: MergeOpts{FastForwardOnly: true},
|
||||
branchName: "mybranch",
|
||||
expected: []string{"merge", "--no-edit", "--ff-only", "mybranch"},
|
||||
},
|
||||
}
|
||||
|
||||
assert.NoError(t, instance.Merge("test", MergeOpts{}))
|
||||
runner.CheckForMissingCalls()
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs(s.expected, "", nil)
|
||||
instance := buildBranchCommands(commonDeps{runner: runner, userConfig: s.userConfig})
|
||||
|
||||
assert.NoError(t, instance.Merge(s.branchName, s.opts))
|
||||
runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchCheckout(t *testing.T) {
|
||||
@@ -118,7 +160,7 @@ func TestBranchCheckout(t *testing.T) {
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Checkout",
|
||||
oscommands.NewFakeRunner(t).Expect(`git checkout "test"`, "", nil),
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"checkout", "test"}, "", nil),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -126,7 +168,7 @@ func TestBranchCheckout(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"Checkout forced",
|
||||
oscommands.NewFakeRunner(t).Expect(`git checkout --force "test"`, "", nil),
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"checkout", "--force", "test"}, "", nil),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -172,7 +214,7 @@ func TestBranchCurrentBranchInfo(t *testing.T) {
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"says we are on the master branch if we are",
|
||||
oscommands.NewFakeRunner(t).Expect(`git symbolic-ref --short HEAD`, "master", nil),
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"symbolic-ref", "--short", "HEAD"}, "master", nil),
|
||||
func(info BranchInfo, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "master", info.RefName)
|
||||
@@ -183,8 +225,9 @@ func TestBranchCurrentBranchInfo(t *testing.T) {
|
||||
{
|
||||
"falls back to git `git branch --points-at=HEAD` if symbolic-ref fails",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
|
||||
Expect(`git branch --points-at=HEAD --format="%(HEAD)%00%(objectname)%00%(refname)"`, "*\x006f71c57a8d4bd6c11399c3f55f42c815527a73a4\x00(HEAD detached at 6f71c57a)\n", nil),
|
||||
ExpectGitArgs([]string{"symbolic-ref", "--short", "HEAD"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{"branch", "--points-at=HEAD", "--format=%(HEAD)%00%(objectname)%00%(refname)"},
|
||||
"*\x006f71c57a8d4bd6c11399c3f55f42c815527a73a4\x00(HEAD detached at 6f71c57a)\n", nil),
|
||||
func(info BranchInfo, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "6f71c57a8d4bd6c11399c3f55f42c815527a73a4", info.RefName)
|
||||
@@ -195,9 +238,9 @@ func TestBranchCurrentBranchInfo(t *testing.T) {
|
||||
{
|
||||
"handles a detached head (LANG=zh_CN.UTF-8)",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
|
||||
Expect(
|
||||
`git branch --points-at=HEAD --format="%(HEAD)%00%(objectname)%00%(refname)"`,
|
||||
ExpectGitArgs([]string{"symbolic-ref", "--short", "HEAD"}, "", errors.New("error")).
|
||||
ExpectGitArgs(
|
||||
[]string{"branch", "--points-at=HEAD", "--format=%(HEAD)%00%(objectname)%00%(refname)"},
|
||||
"*\x00679b0456f3db7c505b398def84e7d023e5b55a8d\x00(头指针在 679b0456 分离)\n"+
|
||||
" \x00679b0456f3db7c505b398def84e7d023e5b55a8d\x00refs/heads/master\n",
|
||||
nil),
|
||||
@@ -211,8 +254,8 @@ func TestBranchCurrentBranchInfo(t *testing.T) {
|
||||
{
|
||||
"bubbles up error if there is one",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
|
||||
Expect(`git branch --points-at=HEAD --format="%(HEAD)%00%(objectname)%00%(refname)"`, "", errors.New("error")),
|
||||
ExpectGitArgs([]string{"symbolic-ref", "--short", "HEAD"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{"branch", "--points-at=HEAD", "--format=%(HEAD)%00%(objectname)%00%(refname)"}, "", errors.New("error")),
|
||||
func(info BranchInfo, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualValues(t, "", info.RefName)
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
||||
var ErrInvalidCommitIndex = errors.New("invalid commit index")
|
||||
|
||||
type CommitCommands struct {
|
||||
*GitCommon
|
||||
}
|
||||
@@ -18,25 +20,29 @@ func NewCommitCommands(gitCommon *GitCommon) *CommitCommands {
|
||||
}
|
||||
}
|
||||
|
||||
// RewordLastCommit rewords the topmost commit with the given message
|
||||
func (self *CommitCommands) RewordLastCommit(message string) error {
|
||||
return self.cmd.New("git commit --allow-empty --amend --only -m " + self.cmd.Quote(message)).Run()
|
||||
}
|
||||
|
||||
// ResetAuthor resets the author of the topmost commit
|
||||
func (self *CommitCommands) ResetAuthor() error {
|
||||
return self.cmd.New("git commit --allow-empty --only --no-edit --amend --reset-author").Run()
|
||||
cmdArgs := NewGitCmd("commit").
|
||||
Arg("--allow-empty", "--only", "--no-edit", "--amend", "--reset-author").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// Sets the commit's author to the supplied value. Value is expected to be of the form 'Name <Email>'
|
||||
func (self *CommitCommands) SetAuthor(value string) error {
|
||||
commandStr := fmt.Sprintf("git commit --allow-empty --only --no-edit --amend --author=%s", self.cmd.Quote(value))
|
||||
return self.cmd.New(commandStr).Run()
|
||||
cmdArgs := NewGitCmd("commit").
|
||||
Arg("--allow-empty", "--only", "--no-edit", "--amend", "--author="+value).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// ResetToCommit reset to commit
|
||||
func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars []string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git reset --%s %s", strength, sha)).
|
||||
cmdArgs := NewGitCmd("reset").Arg("--"+strength, sha).ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).
|
||||
// prevents git from prompting us for input which would freeze the program
|
||||
// TODO: see if this is actually needed here
|
||||
AddEnvVars("GIT_TERMINAL_PROMPT=0").
|
||||
@@ -45,29 +51,59 @@ func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars [
|
||||
}
|
||||
|
||||
func (self *CommitCommands) CommitCmdObj(message string) oscommands.ICmdObj {
|
||||
splitMessage := strings.Split(message, "\n")
|
||||
lineArgs := ""
|
||||
for _, line := range splitMessage {
|
||||
lineArgs += fmt.Sprintf(" -m %s", self.cmd.Quote(line))
|
||||
}
|
||||
messageArgs := self.commitMessageArgs(message)
|
||||
|
||||
skipHookPrefix := self.UserConfig.Git.SkipHookPrefix
|
||||
noVerifyFlag := ""
|
||||
if skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix) {
|
||||
noVerifyFlag = " --no-verify"
|
||||
|
||||
cmdArgs := NewGitCmd("commit").
|
||||
ArgIf(skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix), "--no-verify").
|
||||
ArgIf(self.signoffFlag() != "", self.signoffFlag()).
|
||||
Arg(messageArgs...).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *CommitCommands) RewordLastCommitInEditorCmdObj() oscommands.ICmdObj {
|
||||
return self.cmd.New(NewGitCmd("commit").Arg("--allow-empty", "--amend", "--only").ToArgv())
|
||||
}
|
||||
|
||||
// RewordLastCommit rewords the topmost commit with the given message
|
||||
func (self *CommitCommands) RewordLastCommit(message string) error {
|
||||
messageArgs := self.commitMessageArgs(message)
|
||||
|
||||
cmdArgs := NewGitCmd("commit").
|
||||
Arg("--allow-empty", "--amend", "--only").
|
||||
Arg(messageArgs...).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *CommitCommands) commitMessageArgs(message string) []string {
|
||||
msg, description, _ := strings.Cut(message, "\n")
|
||||
args := []string{"-m", msg}
|
||||
|
||||
if description != "" {
|
||||
args = append(args, "-m", description)
|
||||
}
|
||||
|
||||
return self.cmd.New(fmt.Sprintf("git commit%s%s%s", noVerifyFlag, self.signoffFlag(), lineArgs))
|
||||
return args
|
||||
}
|
||||
|
||||
// runs git commit without the -m argument meaning it will invoke the user's editor
|
||||
func (self *CommitCommands) CommitEditorCmdObj() oscommands.ICmdObj {
|
||||
return self.cmd.New(fmt.Sprintf("git commit%s%s", self.signoffFlag(), self.verboseFlag()))
|
||||
cmdArgs := NewGitCmd("commit").
|
||||
ArgIf(self.signoffFlag() != "", self.signoffFlag()).
|
||||
ArgIf(self.verboseFlag() != "", self.verboseFlag()).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *CommitCommands) signoffFlag() string {
|
||||
if self.UserConfig.Git.Commit.SignOff {
|
||||
return " --signoff"
|
||||
return "--signoff"
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
@@ -76,9 +112,9 @@ func (self *CommitCommands) signoffFlag() string {
|
||||
func (self *CommitCommands) verboseFlag() string {
|
||||
switch self.config.UserConfig.Git.Commit.Verbose {
|
||||
case "always":
|
||||
return " --verbose"
|
||||
return "--verbose"
|
||||
case "never":
|
||||
return " --no-verbose"
|
||||
return "--no-verbose"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -86,20 +122,26 @@ func (self *CommitCommands) verboseFlag() string {
|
||||
|
||||
// Get the subject of the HEAD commit
|
||||
func (self *CommitCommands) GetHeadCommitMessage() (string, error) {
|
||||
message, err := self.cmd.New("git log -1 --pretty=%s").DontLog().RunWithOutput()
|
||||
cmdArgs := NewGitCmd("log").Arg("-1", "--pretty=%s").ToArgv()
|
||||
|
||||
message, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
return strings.TrimSpace(message), err
|
||||
}
|
||||
|
||||
func (self *CommitCommands) GetCommitMessage(commitSha string) (string, error) {
|
||||
cmdStr := "git rev-list --format=%B --max-count=1 " + commitSha
|
||||
messageWithHeader, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
|
||||
cmdArgs := NewGitCmd("rev-list").
|
||||
Arg("--format=%B", "--max-count=1", commitSha).
|
||||
ToArgv()
|
||||
|
||||
messageWithHeader, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "")
|
||||
return strings.TrimSpace(message), err
|
||||
}
|
||||
|
||||
func (self *CommitCommands) GetCommitDiff(commitSha string) (string, error) {
|
||||
cmdStr := "git show --no-color " + commitSha
|
||||
diff, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
|
||||
cmdArgs := NewGitCmd("show").Arg("--no-color", commitSha).ToArgv()
|
||||
|
||||
diff, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
return diff, err
|
||||
}
|
||||
|
||||
@@ -109,8 +151,11 @@ type Author struct {
|
||||
}
|
||||
|
||||
func (self *CommitCommands) GetCommitAuthor(commitSha string) (Author, error) {
|
||||
cmdStr := "git show --no-patch --pretty=format:'%an%x00%ae' " + commitSha
|
||||
output, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
|
||||
cmdArgs := NewGitCmd("show").
|
||||
Arg("--no-patch", "--pretty=format:'%an%x00%ae'", commitSha).
|
||||
ToArgv()
|
||||
|
||||
output, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return Author{}, err
|
||||
}
|
||||
@@ -129,15 +174,21 @@ func (self *CommitCommands) GetCommitMessageFirstLine(sha string) (string, error
|
||||
}
|
||||
|
||||
func (self *CommitCommands) GetCommitMessagesFirstLine(shas []string) (string, error) {
|
||||
return self.cmd.New(
|
||||
fmt.Sprintf("git show --no-patch --pretty=format:%%s %s", strings.Join(shas, " ")),
|
||||
).DontLog().RunWithOutput()
|
||||
cmdArgs := NewGitCmd("show").
|
||||
Arg("--no-patch", "--pretty=format:%s").
|
||||
Arg(shas...).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
}
|
||||
|
||||
func (self *CommitCommands) GetCommitsOneline(shas []string) (string, error) {
|
||||
return self.cmd.New(
|
||||
fmt.Sprintf("git show --no-patch --oneline %s", strings.Join(shas, " ")),
|
||||
).DontLog().RunWithOutput()
|
||||
cmdArgs := NewGitCmd("show").
|
||||
Arg("--no-patch", "--oneline").
|
||||
Arg(shas...).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
}
|
||||
|
||||
// AmendHead amends HEAD with whatever is staged in your working tree
|
||||
@@ -146,30 +197,60 @@ func (self *CommitCommands) AmendHead() error {
|
||||
}
|
||||
|
||||
func (self *CommitCommands) AmendHeadCmdObj() oscommands.ICmdObj {
|
||||
return self.cmd.New("git commit --amend --no-edit --allow-empty")
|
||||
cmdArgs := NewGitCmd("commit").
|
||||
Arg("--amend", "--no-edit", "--allow-empty").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *CommitCommands) ShowCmdObj(sha string, filterPath string) oscommands.ICmdObj {
|
||||
func (self *CommitCommands) ShowCmdObj(sha string, filterPath string, ignoreWhitespace bool) oscommands.ICmdObj {
|
||||
contextSize := self.UserConfig.Git.DiffContextSize
|
||||
filterPathArg := ""
|
||||
if filterPath != "" {
|
||||
filterPathArg = fmt.Sprintf(" -- %s", self.cmd.Quote(filterPath))
|
||||
}
|
||||
|
||||
cmdStr := fmt.Sprintf("git show --submodule --color=%s --unified=%d --no-renames --stat -p %s %s", self.UserConfig.Git.Paging.ColorArg, contextSize, sha, filterPathArg)
|
||||
return self.cmd.New(cmdStr).DontLog()
|
||||
cmdArgs := NewGitCmd("show").
|
||||
Arg("--submodule").
|
||||
Arg("--color="+self.UserConfig.Git.Paging.ColorArg).
|
||||
Arg(fmt.Sprintf("--unified=%d", contextSize)).
|
||||
Arg("--stat").
|
||||
Arg("-p").
|
||||
Arg(sha).
|
||||
ArgIf(ignoreWhitespace, "--ignore-all-space").
|
||||
ArgIf(filterPath != "", "--", filterPath).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).DontLog()
|
||||
}
|
||||
|
||||
// Revert reverts the selected commit by sha
|
||||
func (self *CommitCommands) Revert(sha string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git revert %s", sha)).Run()
|
||||
cmdArgs := NewGitCmd("revert").Arg(sha).ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *CommitCommands) RevertMerge(sha string, parentNumber int) error {
|
||||
return self.cmd.New(fmt.Sprintf("git revert %s -m %d", sha, parentNumber)).Run()
|
||||
cmdArgs := NewGitCmd("revert").Arg(sha, "-m", fmt.Sprintf("%d", parentNumber)).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// CreateFixupCommit creates a commit that fixes up a previous commit
|
||||
func (self *CommitCommands) CreateFixupCommit(sha string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git commit --fixup=%s", sha)).Run()
|
||||
cmdArgs := NewGitCmd("commit").Arg("--fixup=" + sha).ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// a value of 0 means the head commit, 1 is the parent commit, etc
|
||||
func (self *CommitCommands) GetCommitMessageFromHistory(value int) (string, error) {
|
||||
cmdArgs := NewGitCmd("log").Arg("-1", fmt.Sprintf("--skip=%d", value), "--pretty=%H").
|
||||
ToArgv()
|
||||
|
||||
hash, _ := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
formattedHash := strings.TrimSpace(hash)
|
||||
if len(formattedHash) == 0 {
|
||||
return "", ErrInvalidCommitIndex
|
||||
}
|
||||
return self.GetCommitMessage(formattedHash)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
@@ -25,12 +24,18 @@ func NewCommitFileLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *
|
||||
|
||||
// GetFilesInDiff get the specified commit files
|
||||
func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) {
|
||||
reverseFlag := ""
|
||||
if reverse {
|
||||
reverseFlag = " -R "
|
||||
}
|
||||
cmdArgs := NewGitCmd("diff").
|
||||
Arg("--submodule").
|
||||
Arg("--no-ext-diff").
|
||||
Arg("--name-status").
|
||||
Arg("-z").
|
||||
Arg("--no-renames").
|
||||
ArgIf(reverse, "-R").
|
||||
Arg(from).
|
||||
Arg(to).
|
||||
ToArgv()
|
||||
|
||||
filenames, err := self.cmd.New(fmt.Sprintf("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)).DontLog().RunWithOutput()
|
||||
filenames, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// context:
|
||||
@@ -29,11 +29,15 @@ type CommitLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
|
||||
getCurrentBranchInfo func() (BranchInfo, error)
|
||||
getRebaseMode func() (enums.RebaseMode, error)
|
||||
readFile func(filename string) ([]byte, error)
|
||||
walkFiles func(root string, fn filepath.WalkFunc) error
|
||||
dotGitDir string
|
||||
getRebaseMode func() (enums.RebaseMode, error)
|
||||
readFile func(filename string) ([]byte, error)
|
||||
walkFiles func(root string, fn filepath.WalkFunc) error
|
||||
dotGitDir string
|
||||
// List of main branches that exist in the repo.
|
||||
// We use these to obtain the merge base of the branch.
|
||||
// When nil, we're yet to obtain the list of existing main branches.
|
||||
// When an empty slice, we've obtained the list and it's empty.
|
||||
mainBranches []string
|
||||
}
|
||||
|
||||
// making our dependencies explicit for the sake of easier testing
|
||||
@@ -41,17 +45,16 @@ func NewCommitLoader(
|
||||
cmn *common.Common,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
dotGitDir string,
|
||||
getCurrentBranchInfo func() (BranchInfo, error),
|
||||
getRebaseMode func() (enums.RebaseMode, error),
|
||||
) *CommitLoader {
|
||||
return &CommitLoader{
|
||||
Common: cmn,
|
||||
cmd: cmd,
|
||||
getCurrentBranchInfo: getCurrentBranchInfo,
|
||||
getRebaseMode: getRebaseMode,
|
||||
readFile: os.ReadFile,
|
||||
walkFiles: filepath.Walk,
|
||||
dotGitDir: dotGitDir,
|
||||
Common: cmn,
|
||||
cmd: cmd,
|
||||
getRebaseMode: getRebaseMode,
|
||||
readFile: os.ReadFile,
|
||||
walkFiles: filepath.Walk,
|
||||
dotGitDir: dotGitDir,
|
||||
mainBranches: nil,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,10 +71,6 @@ type GetCommitsOptions struct {
|
||||
func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
|
||||
commits := []*models.Commit{}
|
||||
var rebasingCommits []*models.Commit
|
||||
rebaseMode, err := self.getRebaseMode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
|
||||
var err error
|
||||
@@ -94,7 +93,7 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
|
||||
if commit.Sha == firstPushedCommit {
|
||||
passedFirstPushedCommit = true
|
||||
}
|
||||
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
|
||||
commit.Status = map[bool]models.CommitStatus{true: models.StatusUnpushed, false: models.StatusPushed}[!passedFirstPushedCommit]
|
||||
commits = append(commits, commit)
|
||||
return false, nil
|
||||
})
|
||||
@@ -106,16 +105,7 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
if rebaseMode != enums.REBASE_MODE_NONE {
|
||||
currentCommit := commits[len(rebasingCommits)]
|
||||
youAreHere := style.FgYellow.Sprintf("<-- %s ---", self.Tr.YouAreHere)
|
||||
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
|
||||
}
|
||||
|
||||
commits, err = self.setCommitMergedStatuses(opts.RefName, commits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commits = self.setCommitMergedStatuses(opts.RefName, commits)
|
||||
|
||||
return commits, nil
|
||||
}
|
||||
@@ -124,7 +114,7 @@ func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*mod
|
||||
// chances are we have as many commits as last time so we'll set the capacity to be the old length
|
||||
result := make([]*models.Commit, 0, len(commits))
|
||||
for i, commit := range commits {
|
||||
if commit.Status != "rebasing" { // removing the existing rebase commits so we can add the refreshed ones
|
||||
if !commit.IsTODO() { // removing the existing rebase commits so we can add the refreshed ones
|
||||
result = append(result, commits[i:]...)
|
||||
break
|
||||
}
|
||||
@@ -205,35 +195,40 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commitShas := slices.Map(commits, func(commit *models.Commit) string {
|
||||
return commit.Sha
|
||||
commitShas := slices.FilterMap(commits, func(commit *models.Commit) (string, bool) {
|
||||
return commit.Sha, commit.Sha != ""
|
||||
})
|
||||
|
||||
// note that we're not filtering these as we do non-rebasing commits just because
|
||||
// I suspect that will cause some damage
|
||||
cmdObj := self.cmd.New(
|
||||
fmt.Sprintf(
|
||||
"git -c log.showSignature=false show %s --no-patch --oneline %s --abbrev=%d",
|
||||
strings.Join(commitShas, " "),
|
||||
prettyFormat,
|
||||
20,
|
||||
),
|
||||
NewGitCmd("show").
|
||||
Config("log.showSignature=false").
|
||||
Arg("--no-patch", "--oneline", "--abbrev=20", prettyFormat).
|
||||
Arg(commitShas...).
|
||||
ToArgv(),
|
||||
).DontLog()
|
||||
|
||||
hydratedCommits := make([]*models.Commit, 0, len(commits))
|
||||
i := 0
|
||||
fullCommits := map[string]*models.Commit{}
|
||||
err = cmdObj.RunAndProcessLines(func(line string) (bool, error) {
|
||||
commit := self.extractCommitFromLine(line)
|
||||
matchingCommit := commits[i]
|
||||
commit.Action = matchingCommit.Action
|
||||
commit.Status = matchingCommit.Status
|
||||
hydratedCommits = append(hydratedCommits, commit)
|
||||
i++
|
||||
fullCommits[commit.Sha] = commit
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hydratedCommits := make([]*models.Commit, 0, len(commits))
|
||||
for _, rebasingCommit := range commits {
|
||||
if rebasingCommit.Sha == "" {
|
||||
hydratedCommits = append(hydratedCommits, rebasingCommit)
|
||||
} else if commit := fullCommits[rebasingCommit.Sha]; commit != nil {
|
||||
commit.Action = rebasingCommit.Action
|
||||
commit.Status = rebasingCommit.Status
|
||||
hydratedCommits = append(hydratedCommits, commit)
|
||||
}
|
||||
}
|
||||
return hydratedCommits, nil
|
||||
}
|
||||
|
||||
@@ -316,15 +311,17 @@ func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, err
|
||||
}
|
||||
|
||||
for _, t := range todos {
|
||||
if t.Commit == "" {
|
||||
if t.Command == todo.UpdateRef {
|
||||
t.Msg = strings.TrimPrefix(t.Ref, "refs/heads/")
|
||||
} else if t.Commit == "" {
|
||||
// Command does not have a commit associated, skip
|
||||
continue
|
||||
}
|
||||
commits = slices.Prepend(commits, &models.Commit{
|
||||
Sha: t.Commit,
|
||||
Name: t.Msg,
|
||||
Status: "rebasing",
|
||||
Action: t.Command.String(),
|
||||
Status: models.StatusRebasing,
|
||||
Action: t.Command,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -343,47 +340,66 @@ func (self *CommitLoader) commitFromPatch(content string) *models.Commit {
|
||||
return &models.Commit{
|
||||
Sha: sha,
|
||||
Name: name,
|
||||
Status: "rebasing",
|
||||
Status: models.StatusRebasing,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitLoader) setCommitMergedStatuses(refName string, commits []*models.Commit) ([]*models.Commit, error) {
|
||||
ancestor, err := self.getMergeBase(refName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func (self *CommitLoader) setCommitMergedStatuses(refName string, commits []*models.Commit) []*models.Commit {
|
||||
ancestor := self.getMergeBase(refName)
|
||||
if ancestor == "" {
|
||||
return commits, nil
|
||||
return commits
|
||||
}
|
||||
passedAncestor := false
|
||||
for i, commit := range commits {
|
||||
if strings.HasPrefix(ancestor, commit.Sha) {
|
||||
passedAncestor = true
|
||||
}
|
||||
if commit.Status != "pushed" {
|
||||
if commit.Status != models.StatusPushed && commit.Status != models.StatusUnpushed {
|
||||
continue
|
||||
}
|
||||
if passedAncestor {
|
||||
commits[i].Status = "merged"
|
||||
commits[i].Status = models.StatusMerged
|
||||
}
|
||||
}
|
||||
return commits, nil
|
||||
return commits
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getMergeBase(refName string) (string, error) {
|
||||
info, err := self.getCurrentBranchInfo()
|
||||
func (self *CommitLoader) getMergeBase(refName string) string {
|
||||
if self.mainBranches == nil {
|
||||
self.mainBranches = self.getExistingMainBranches()
|
||||
}
|
||||
|
||||
if len(self.mainBranches) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// We pass all configured main branches to the merge-base call; git will
|
||||
// return the base commit for the closest one.
|
||||
|
||||
output, err := self.cmd.New(
|
||||
NewGitCmd("merge-base").Arg(refName).Arg(self.mainBranches...).
|
||||
ToArgv(),
|
||||
).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
// If there's an error, it must be because one of the main branches that
|
||||
// used to exist when we called getExistingMainBranches() was deleted
|
||||
// meanwhile. To fix this for next time, throw away our cache.
|
||||
self.mainBranches = nil
|
||||
}
|
||||
return ignoringWarnings(output)
|
||||
}
|
||||
|
||||
baseBranch := "master"
|
||||
if strings.HasPrefix(info.RefName, "feature/") {
|
||||
baseBranch = "develop"
|
||||
}
|
||||
|
||||
// swallowing error because it's not a big deal; probably because there are no commits yet
|
||||
output, _ := self.cmd.New(fmt.Sprintf("git merge-base %s %s", self.cmd.Quote(refName), self.cmd.Quote(baseBranch))).DontLog().RunWithOutput()
|
||||
return ignoringWarnings(output), nil
|
||||
func (self *CommitLoader) getExistingMainBranches() []string {
|
||||
return lo.FilterMap(self.UserConfig.Git.MainBranches,
|
||||
func(branchName string, _ int) (string, bool) {
|
||||
ref := "refs/heads/" + branchName
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("rev-parse").Arg("--verify", "--quiet", ref).ToArgv(),
|
||||
).DontLog().Run(); err != nil {
|
||||
return "", false
|
||||
}
|
||||
return ref, true
|
||||
})
|
||||
}
|
||||
|
||||
func ignoringWarnings(commandOutput string) string {
|
||||
@@ -399,10 +415,12 @@ func ignoringWarnings(commandOutput string) string {
|
||||
// getFirstPushedCommit returns the first commit SHA which has been pushed to the ref's upstream.
|
||||
// all commits above this are deemed unpushed and marked as such.
|
||||
func (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) {
|
||||
output, err := self.cmd.
|
||||
New(
|
||||
fmt.Sprintf("git merge-base %s %s@{u}", self.cmd.Quote(refName), self.cmd.Quote(refName)),
|
||||
).
|
||||
output, err := self.cmd.New(
|
||||
NewGitCmd("merge-base").
|
||||
Arg(refName).
|
||||
Arg(strings.TrimPrefix(refName, "refs/heads/") + "@{u}").
|
||||
ToArgv(),
|
||||
).
|
||||
DontLog().
|
||||
RunWithOutput()
|
||||
if err != nil {
|
||||
@@ -414,39 +432,23 @@ func (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) {
|
||||
|
||||
// getLog gets the git log.
|
||||
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
|
||||
limitFlag := ""
|
||||
if opts.Limit {
|
||||
limitFlag = " -300"
|
||||
}
|
||||
|
||||
filterFlag := ""
|
||||
if opts.FilterPath != "" {
|
||||
filterFlag = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(opts.FilterPath))
|
||||
}
|
||||
|
||||
config := self.UserConfig.Git.Log
|
||||
|
||||
orderFlag := ""
|
||||
if config.Order != "default" {
|
||||
orderFlag = " --" + config.Order
|
||||
}
|
||||
allFlag := ""
|
||||
if opts.All {
|
||||
allFlag = " --all"
|
||||
}
|
||||
cmdArgs := NewGitCmd("log").
|
||||
Arg(opts.RefName).
|
||||
ArgIf(config.Order != "default", "--"+config.Order).
|
||||
ArgIf(opts.All, "--all").
|
||||
Arg("--oneline").
|
||||
Arg(prettyFormat).
|
||||
Arg("--abbrev=40").
|
||||
ArgIf(opts.Limit, "-300").
|
||||
ArgIf(opts.FilterPath != "", "--follow").
|
||||
Arg("--no-show-signature").
|
||||
Arg("--").
|
||||
ArgIf(opts.FilterPath != "", opts.FilterPath).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(
|
||||
fmt.Sprintf(
|
||||
"git -c log.showSignature=false log %s%s%s --oneline %s%s --abbrev=%d%s",
|
||||
self.cmd.Quote(opts.RefName),
|
||||
orderFlag,
|
||||
allFlag,
|
||||
prettyFormat,
|
||||
limitFlag,
|
||||
40,
|
||||
filterFlag,
|
||||
),
|
||||
).DontLog()
|
||||
return self.cmd.New(cmdArgs).DontLog()
|
||||
}
|
||||
|
||||
const prettyFormat = `--pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s"`
|
||||
const prettyFormat = `--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s`
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -21,52 +22,68 @@ d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield|jessedduffiel
|
||||
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield|jessedduffield@gmail.com||053a66a7be3da43aacdc|WIP
|
||||
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield|jessedduffield@gmail.com||985fe482e806b172aea4|refactoring the config struct`, "|", "\x00", -1)
|
||||
|
||||
var singleCommitOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com| (HEAD -> better-tests)|b21997d6b4cbdf84b149|better typing for rebase mode`, "|", "\x00", -1)
|
||||
|
||||
func TestGetCommits(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
expectedCommits []*models.Commit
|
||||
expectedError error
|
||||
logOrder string
|
||||
rebaseMode enums.RebaseMode
|
||||
currentBranchName string
|
||||
opts GetCommitsOptions
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
expectedCommits []*models.Commit
|
||||
expectedError error
|
||||
logOrder string
|
||||
rebaseMode enums.RebaseMode
|
||||
opts GetCommitsOptions
|
||||
mainBranches []string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "should return no commits if there are none",
|
||||
logOrder: "topo-order",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
currentBranchName: "master",
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
testName: "should return no commits if there are none",
|
||||
logOrder: "topo-order",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
Expect(`git -c log.showSignature=false log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40`, "", nil),
|
||||
ExpectGitArgs([]string{"merge-base", "HEAD", "HEAD@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should return commits if they are present",
|
||||
logOrder: "topo-order",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
currentBranchName: "master",
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
testName: "should use proper upstream name for branch",
|
||||
logOrder: "topo-order",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "refs/heads/mybranch", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"merge-base", "refs/heads/mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should return commits if they are present",
|
||||
logOrder: "topo-order",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
mainBranches: []string{"master", "main"},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
// here it's seeing which commits are yet to be pushed
|
||||
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
ExpectGitArgs([]string{"merge-base", "HEAD", "HEAD@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
Expect(`git -c log.showSignature=false log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40`, commitsOutput, nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil).
|
||||
// here it's testing which of the configured main branches exist
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/heads/master"}, "", nil). // this one does
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/heads/main"}, "", errors.New("error")). // this one doesn't
|
||||
// here it's seeing where our branch diverged from the master branch so that we can mark that commit and parent commits as 'merged'
|
||||
Expect(`git merge-base "HEAD" "master"`, "26c07b1ab33860a1a7591a0638f9925ccf497ffa", nil),
|
||||
ExpectGitArgs([]string{"merge-base", "HEAD", "refs/heads/master"}, "26c07b1ab33860a1a7591a0638f9925ccf497ffa", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{
|
||||
{
|
||||
Sha: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
|
||||
Name: "better typing for rebase mode",
|
||||
Status: "unpushed",
|
||||
Action: "",
|
||||
Status: models.StatusUnpushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "(HEAD -> better-tests)",
|
||||
AuthorName: "Jesse Duffield",
|
||||
@@ -79,8 +96,8 @@ func TestGetCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164",
|
||||
Name: "fix logging",
|
||||
Status: "pushed",
|
||||
Action: "",
|
||||
Status: models.StatusPushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "(origin/better-tests)",
|
||||
AuthorName: "Jesse Duffield",
|
||||
@@ -93,8 +110,8 @@ func TestGetCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c",
|
||||
Name: "refactor",
|
||||
Status: "pushed",
|
||||
Action: "",
|
||||
Status: models.StatusPushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
@@ -107,8 +124,8 @@ func TestGetCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "d8084cd558925eb7c9c38afeed5725c21653ab90",
|
||||
Name: "WIP",
|
||||
Status: "pushed",
|
||||
Action: "",
|
||||
Status: models.StatusPushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
@@ -121,8 +138,8 @@ func TestGetCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "65f910ebd85283b5cce9bf67d03d3f1a9ea3813a",
|
||||
Name: "WIP",
|
||||
Status: "pushed",
|
||||
Action: "",
|
||||
Status: models.StatusPushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
@@ -135,8 +152,8 @@ func TestGetCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "26c07b1ab33860a1a7591a0638f9925ccf497ffa",
|
||||
Name: "WIP",
|
||||
Status: "merged",
|
||||
Action: "",
|
||||
Status: models.StatusMerged,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
@@ -149,8 +166,8 @@ func TestGetCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "3d4470a6c072208722e5ae9a54bcb9634959a1c5",
|
||||
Name: "WIP",
|
||||
Status: "merged",
|
||||
Action: "",
|
||||
Status: models.StatusMerged,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
@@ -163,8 +180,8 @@ func TestGetCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "053a66a7be3da43aacdc7aa78e1fe757b82c4dd2",
|
||||
Name: "refactoring the config struct",
|
||||
Status: "merged",
|
||||
Action: "",
|
||||
Status: models.StatusMerged,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
@@ -178,14 +195,95 @@ func TestGetCommits(t *testing.T) {
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should not specify order if `log.order` is `default`",
|
||||
logOrder: "default",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
currentBranchName: "master",
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
testName: "should not call merge-base for mainBranches if none exist",
|
||||
logOrder: "topo-order",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
mainBranches: []string{"master", "main"},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
Expect(`git -c log.showSignature=false log "HEAD" --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40`, "", nil),
|
||||
// here it's seeing which commits are yet to be pushed
|
||||
ExpectGitArgs([]string{"merge-base", "HEAD", "HEAD@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
|
||||
// here it's testing which of the configured main branches exist; neither does
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/heads/master"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/heads/main"}, "", errors.New("error")),
|
||||
|
||||
expectedCommits: []*models.Commit{
|
||||
{
|
||||
Sha: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
|
||||
Name: "better typing for rebase mode",
|
||||
Status: models.StatusUnpushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "(HEAD -> better-tests)",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640826609,
|
||||
Parents: []string{
|
||||
"b21997d6b4cbdf84b149",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should call merge-base for all main branches that exist",
|
||||
logOrder: "topo-order",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
mainBranches: []string{"master", "main", "develop", "1.0-hotfixes"},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
// here it's seeing which commits are yet to be pushed
|
||||
ExpectGitArgs([]string{"merge-base", "HEAD", "HEAD@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
|
||||
// here it's testing which of the configured main branches exist
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/heads/master"}, "", nil).
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/heads/main"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/heads/develop"}, "", nil).
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/heads/1.0-hotfixes"}, "", nil).
|
||||
// here it's seeing where our branch diverged from the master branch so that we can mark that commit and parent commits as 'merged'
|
||||
ExpectGitArgs([]string{"merge-base", "HEAD", "refs/heads/master", "refs/heads/develop", "refs/heads/1.0-hotfixes"}, "26c07b1ab33860a1a7591a0638f9925ccf497ffa", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{
|
||||
{
|
||||
Sha: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
|
||||
Name: "better typing for rebase mode",
|
||||
Status: models.StatusUnpushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "(HEAD -> better-tests)",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640826609,
|
||||
Parents: []string{
|
||||
"b21997d6b4cbdf84b149",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should not specify order if `log.order` is `default`",
|
||||
logOrder: "default",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"merge-base", "HEAD", "HEAD@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should set filter path",
|
||||
logOrder: "default",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "HEAD", FilterPath: "src"},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"merge-base", "HEAD", "HEAD@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s", "--abbrev=40", "--follow", "--no-show-signature", "--", "src"}, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
@@ -199,11 +297,8 @@ func TestGetCommits(t *testing.T) {
|
||||
common.UserConfig.Git.Log.Order = scenario.logOrder
|
||||
|
||||
builder := &CommitLoader{
|
||||
Common: common,
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
|
||||
getCurrentBranchInfo: func() (BranchInfo, error) {
|
||||
return BranchInfo{RefName: scenario.currentBranchName, DisplayName: scenario.currentBranchName, DetachedHead: false}, nil
|
||||
},
|
||||
Common: common,
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
|
||||
getRebaseMode: func() (enums.RebaseMode, error) { return scenario.rebaseMode, nil },
|
||||
dotGitDir: ".git",
|
||||
readFile: func(filename string) ([]byte, error) {
|
||||
@@ -214,6 +309,7 @@ func TestGetCommits(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
common.UserConfig.Git.MainBranches = scenario.mainBranches
|
||||
commits, err := builder.GetCommits(scenario.opts)
|
||||
|
||||
assert.Equal(t, scenario.expectedCommits, commits)
|
||||
|
||||
@@ -9,12 +9,32 @@ import (
|
||||
)
|
||||
|
||||
func TestCommitRewordCommit(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil)
|
||||
instance := buildCommitCommands(commonDeps{runner: runner})
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
input string
|
||||
}
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Single line reword",
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil),
|
||||
"test",
|
||||
},
|
||||
{
|
||||
"Multi line reword",
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test", "-m", "line 2\nline 3"}, "", nil),
|
||||
"test\nline 2\nline 3",
|
||||
},
|
||||
}
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildCommitCommands(commonDeps{runner: s.runner})
|
||||
|
||||
assert.NoError(t, instance.RewordLastCommit("test"))
|
||||
runner.CheckForMissingCalls()
|
||||
assert.NoError(t, instance.RewordLastCommit(s.input))
|
||||
s.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitResetToCommit(t *testing.T) {
|
||||
@@ -33,7 +53,7 @@ func TestCommitCommitCmdObj(t *testing.T) {
|
||||
message string
|
||||
configSignoff bool
|
||||
configSkipHookPrefix string
|
||||
expected string
|
||||
expectedArgs []string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
@@ -42,35 +62,35 @@ func TestCommitCommitCmdObj(t *testing.T) {
|
||||
message: "test",
|
||||
configSignoff: false,
|
||||
configSkipHookPrefix: "",
|
||||
expected: `git commit -m "test"`,
|
||||
expectedArgs: []string{"commit", "-m", "test"},
|
||||
},
|
||||
{
|
||||
testName: "Commit with --no-verify flag",
|
||||
message: "WIP: test",
|
||||
configSignoff: false,
|
||||
configSkipHookPrefix: "WIP",
|
||||
expected: `git commit --no-verify -m "WIP: test"`,
|
||||
expectedArgs: []string{"commit", "--no-verify", "-m", "WIP: test"},
|
||||
},
|
||||
{
|
||||
testName: "Commit with multiline message",
|
||||
message: "line1\nline2",
|
||||
configSignoff: false,
|
||||
configSkipHookPrefix: "",
|
||||
expected: `git commit -m "line1" -m "line2"`,
|
||||
expectedArgs: []string{"commit", "-m", "line1", "-m", "line2"},
|
||||
},
|
||||
{
|
||||
testName: "Commit with signoff",
|
||||
message: "test",
|
||||
configSignoff: true,
|
||||
configSkipHookPrefix: "",
|
||||
expected: `git commit --signoff -m "test"`,
|
||||
expectedArgs: []string{"commit", "--signoff", "-m", "test"},
|
||||
},
|
||||
{
|
||||
testName: "Commit with signoff and no-verify",
|
||||
message: "WIP: test",
|
||||
configSignoff: true,
|
||||
configSkipHookPrefix: "WIP",
|
||||
expected: `git commit --no-verify --signoff -m "WIP: test"`,
|
||||
expectedArgs: []string{"commit", "--no-verify", "--signoff", "-m", "WIP: test"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -81,10 +101,11 @@ func TestCommitCommitCmdObj(t *testing.T) {
|
||||
userConfig.Git.Commit.SignOff = s.configSignoff
|
||||
userConfig.Git.SkipHookPrefix = s.configSkipHookPrefix
|
||||
|
||||
instance := buildCommitCommands(commonDeps{userConfig: userConfig})
|
||||
runner := oscommands.NewFakeRunner(t).ExpectGitArgs(s.expectedArgs, "", nil)
|
||||
instance := buildCommitCommands(commonDeps{userConfig: userConfig, runner: runner})
|
||||
|
||||
cmdStr := instance.CommitCmdObj(s.message).ToString()
|
||||
assert.Equal(t, s.expected, cmdStr)
|
||||
assert.NoError(t, instance.CommitCmdObj(s.message).Run())
|
||||
runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -94,7 +115,7 @@ func TestCommitCommitEditorCmdObj(t *testing.T) {
|
||||
testName string
|
||||
configSignoff bool
|
||||
configVerbose string
|
||||
expected string
|
||||
expected []string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
@@ -102,31 +123,31 @@ func TestCommitCommitEditorCmdObj(t *testing.T) {
|
||||
testName: "Commit using editor",
|
||||
configSignoff: false,
|
||||
configVerbose: "default",
|
||||
expected: `git commit`,
|
||||
expected: []string{"commit"},
|
||||
},
|
||||
{
|
||||
testName: "Commit with --no-verbose flag",
|
||||
configSignoff: false,
|
||||
configVerbose: "never",
|
||||
expected: `git commit --no-verbose`,
|
||||
expected: []string{"commit", "--no-verbose"},
|
||||
},
|
||||
{
|
||||
testName: "Commit with --verbose flag",
|
||||
configSignoff: false,
|
||||
configVerbose: "always",
|
||||
expected: `git commit --verbose`,
|
||||
expected: []string{"commit", "--verbose"},
|
||||
},
|
||||
{
|
||||
testName: "Commit with --signoff",
|
||||
configSignoff: true,
|
||||
configVerbose: "default",
|
||||
expected: `git commit --signoff`,
|
||||
expected: []string{"commit", "--signoff"},
|
||||
},
|
||||
{
|
||||
testName: "Commit with --signoff and --no-verbose",
|
||||
configSignoff: true,
|
||||
configVerbose: "never",
|
||||
expected: `git commit --signoff --no-verbose`,
|
||||
expected: []string{"commit", "--signoff", "--no-verbose"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -137,10 +158,11 @@ func TestCommitCommitEditorCmdObj(t *testing.T) {
|
||||
userConfig.Git.Commit.SignOff = s.configSignoff
|
||||
userConfig.Git.Commit.Verbose = s.configVerbose
|
||||
|
||||
instance := buildCommitCommands(commonDeps{userConfig: userConfig})
|
||||
runner := oscommands.NewFakeRunner(t).ExpectGitArgs(s.expected, "", nil)
|
||||
instance := buildCommitCommands(commonDeps{userConfig: userConfig, runner: runner})
|
||||
|
||||
cmdStr := instance.CommitEditorCmdObj().ToString()
|
||||
assert.Equal(t, s.expected, cmdStr)
|
||||
assert.NoError(t, instance.CommitEditorCmdObj().Run())
|
||||
runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -158,7 +180,7 @@ func TestCommitCreateFixupCommit(t *testing.T) {
|
||||
testName: "valid case",
|
||||
sha: "12345",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git commit --fixup=12345`, "", nil),
|
||||
ExpectGitArgs([]string{"commit", "--fixup=12345"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -177,30 +199,41 @@ func TestCommitCreateFixupCommit(t *testing.T) {
|
||||
|
||||
func TestCommitShowCmdObj(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
filterPath string
|
||||
contextSize int
|
||||
expected string
|
||||
testName string
|
||||
filterPath string
|
||||
contextSize int
|
||||
ignoreWhitespace bool
|
||||
expected []string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Default case without filter path",
|
||||
filterPath: "",
|
||||
contextSize: 3,
|
||||
expected: "git show --submodule --color=always --unified=3 --no-renames --stat -p 1234567890 ",
|
||||
testName: "Default case without filter path",
|
||||
filterPath: "",
|
||||
contextSize: 3,
|
||||
ignoreWhitespace: false,
|
||||
expected: []string{"show", "--submodule", "--color=always", "--unified=3", "--stat", "-p", "1234567890"},
|
||||
},
|
||||
{
|
||||
testName: "Default case with filter path",
|
||||
filterPath: "file.txt",
|
||||
contextSize: 3,
|
||||
expected: `git show --submodule --color=always --unified=3 --no-renames --stat -p 1234567890 -- "file.txt"`,
|
||||
testName: "Default case with filter path",
|
||||
filterPath: "file.txt",
|
||||
contextSize: 3,
|
||||
ignoreWhitespace: false,
|
||||
expected: []string{"show", "--submodule", "--color=always", "--unified=3", "--stat", "-p", "1234567890", "--", "file.txt"},
|
||||
},
|
||||
{
|
||||
testName: "Show diff with custom context size",
|
||||
filterPath: "",
|
||||
contextSize: 77,
|
||||
expected: "git show --submodule --color=always --unified=77 --no-renames --stat -p 1234567890 ",
|
||||
testName: "Show diff with custom context size",
|
||||
filterPath: "",
|
||||
contextSize: 77,
|
||||
ignoreWhitespace: false,
|
||||
expected: []string{"show", "--submodule", "--color=always", "--unified=77", "--stat", "-p", "1234567890"},
|
||||
},
|
||||
{
|
||||
testName: "Show diff, ignoring whitespace",
|
||||
filterPath: "",
|
||||
contextSize: 77,
|
||||
ignoreWhitespace: true,
|
||||
expected: []string{"show", "--submodule", "--color=always", "--unified=77", "--stat", "-p", "1234567890", "--ignore-all-space"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -210,10 +243,11 @@ func TestCommitShowCmdObj(t *testing.T) {
|
||||
userConfig := config.GetDefaultConfig()
|
||||
userConfig.Git.DiffContextSize = s.contextSize
|
||||
|
||||
instance := buildCommitCommands(commonDeps{userConfig: userConfig})
|
||||
runner := oscommands.NewFakeRunner(t).ExpectGitArgs(s.expected, "", nil)
|
||||
instance := buildCommitCommands(commonDeps{userConfig: userConfig, runner: runner})
|
||||
|
||||
cmdStr := instance.ShowCmdObj("1234567890", s.filterPath).ToString()
|
||||
assert.Equal(t, s.expected, cmdStr)
|
||||
assert.NoError(t, instance.ShowCmdObj("1234567890", s.filterPath, s.ignoreWhitespace).Run())
|
||||
runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -252,7 +286,7 @@ Merge pull request #1750 from mark2185/fix-issue-template
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildCommitCommands(commonDeps{
|
||||
runner: oscommands.NewFakeRunner(t).Expect("git rev-list --format=%B --max-count=1 deadbeef", s.input, nil),
|
||||
runner: oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"rev-list", "--format=%B", "--max-count=1", "deadbeef"}, s.input, nil),
|
||||
})
|
||||
|
||||
output, err := instance.GetCommitMessage("deadbeef")
|
||||
@@ -263,3 +297,40 @@ Merge pull request #1750 from mark2185/fix-issue-template
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCommitMessageFromHistory(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(string, error)
|
||||
}
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Empty message",
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"log", "-1", "--skip=2", "--pretty=%H"}, "", nil).ExpectGitArgs([]string{"rev-list", "--format=%B", "--max-count=1"}, "", nil),
|
||||
func(output string, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Default case to retrieve a commit in history",
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"log", "-1", "--skip=2", "--pretty=%H"}, "sha3 \n", nil).ExpectGitArgs([]string{"rev-list", "--format=%B", "--max-count=1", "sha3"}, `commit sha3
|
||||
use generics to DRY up context code`, nil),
|
||||
func(output string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "use generics to DRY up context code", output)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildCommitCommands(commonDeps{runner: s.runner})
|
||||
|
||||
output, err := instance.GetCommitMessageFromHistory(2)
|
||||
|
||||
s.test(output, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package git_commands
|
||||
|
||||
import "github.com/mgutz/str"
|
||||
|
||||
type CustomCommands struct {
|
||||
*GitCommon
|
||||
}
|
||||
@@ -14,5 +16,5 @@ func NewCustomCommands(gitCommon *GitCommon) *CustomCommands {
|
||||
// If you want to run a new command, try finding a place for it in one of the neighbouring
|
||||
// files, or creating a new BlahCommands struct to hold it.
|
||||
func (self *CustomCommands) RunWithOutput(cmdStr string) (string, error) {
|
||||
return self.cmd.New(cmdStr).RunWithOutput()
|
||||
return self.cmd.New(str.ToArgv(cmdStr)).RunWithOutput()
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
@@ -117,6 +118,26 @@ func buildWorkingTreeCommands(deps commonDeps) *WorkingTreeCommands {
|
||||
return NewWorkingTreeCommands(gitCommon, submoduleCommands, fileLoader)
|
||||
}
|
||||
|
||||
func buildPatchCommands(deps commonDeps) *PatchCommands {
|
||||
gitCommon := buildGitCommon(deps)
|
||||
rebaseCommands := buildRebaseCommands(deps)
|
||||
commitCommands := buildCommitCommands(deps)
|
||||
statusCommands := buildStatusCommands(deps)
|
||||
stashCommands := buildStashCommands(deps)
|
||||
loadFileFn := func(from string, to string, reverse bool, filename string, plain bool) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
patchBuilder := patch.NewPatchBuilder(gitCommon.Log, loadFileFn)
|
||||
|
||||
return NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
|
||||
}
|
||||
|
||||
func buildStatusCommands(deps commonDeps) *StatusCommands {
|
||||
gitCommon := buildGitCommon(deps)
|
||||
|
||||
return NewStatusCommands(gitCommon)
|
||||
}
|
||||
|
||||
func buildStashCommands(deps commonDeps) *StashCommands {
|
||||
gitCommon := buildGitCommon(deps)
|
||||
fileLoader := buildFileLoader(gitCommon)
|
||||
@@ -150,3 +171,9 @@ func buildBranchCommands(deps commonDeps) *BranchCommands {
|
||||
|
||||
return NewBranchCommands(gitCommon)
|
||||
}
|
||||
|
||||
func buildFlowCommands(deps commonDeps) *FlowCommands {
|
||||
gitCommon := buildGitCommon(deps)
|
||||
|
||||
return NewFlowCommands(gitCommon)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package git_commands
|
||||
|
||||
import "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
|
||||
type DiffCommands struct {
|
||||
*GitCommon
|
||||
}
|
||||
|
||||
func NewDiffCommands(gitCommon *GitCommon) *DiffCommands {
|
||||
return &DiffCommands{
|
||||
GitCommon: gitCommon,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *DiffCommands) DiffCmdObj(diffArgs []string) oscommands.ICmdObj {
|
||||
return self.cmd.New(
|
||||
NewGitCmd("diff").Arg("--submodule", "--no-ext-diff", "--color").Arg(diffArgs...).ToArgv(),
|
||||
)
|
||||
}
|
||||
@@ -3,8 +3,10 @@ package git_commands
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -27,7 +29,7 @@ func (self *FileCommands) Cat(fileName string) (string, error) {
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
func (self *FileCommands) GetEditor() (string, error) {
|
||||
func (self *FileCommands) GetEditCmdStrLegacy(filename string, lineNumber int) (string, error) {
|
||||
editor := self.UserConfig.OS.EditCommand
|
||||
|
||||
if editor == "" {
|
||||
@@ -43,21 +45,13 @@ func (self *FileCommands) GetEditor() (string, error) {
|
||||
editor = self.os.Getenv("EDITOR")
|
||||
}
|
||||
if editor == "" {
|
||||
if err := self.cmd.New("which vi").DontLog().Run(); err == nil {
|
||||
if err := self.cmd.New([]string{"which", "vi"}).DontLog().Run(); err == nil {
|
||||
editor = "vi"
|
||||
}
|
||||
}
|
||||
if editor == "" {
|
||||
return "", errors.New("No editor defined in config file, $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
}
|
||||
return editor, nil
|
||||
}
|
||||
|
||||
func (self *FileCommands) GetEditCmdStr(filename string, lineNumber int) (string, error) {
|
||||
editor, err := self.GetEditor()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
templateValues := map[string]string{
|
||||
"editor": editor,
|
||||
@@ -80,3 +74,82 @@ func (self *FileCommands) GetEditCmdStr(filename string, lineNumber int) (string
|
||||
}
|
||||
return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil
|
||||
}
|
||||
|
||||
func (self *FileCommands) GetEditCmdStr(filename string) (string, bool) {
|
||||
// Legacy support for old config; to be removed at some point
|
||||
if self.UserConfig.OS.Edit == "" && self.UserConfig.OS.EditCommandTemplate != "" {
|
||||
if cmdStr, err := self.GetEditCmdStrLegacy(filename, 1); err == nil {
|
||||
return cmdStr, true
|
||||
}
|
||||
}
|
||||
|
||||
template, editInTerminal := config.GetEditTemplate(&self.UserConfig.OS, self.guessDefaultEditor)
|
||||
|
||||
templateValues := map[string]string{
|
||||
"filename": self.cmd.Quote(filename),
|
||||
}
|
||||
|
||||
cmdStr := utils.ResolvePlaceholderString(template, templateValues)
|
||||
return cmdStr, editInTerminal
|
||||
}
|
||||
|
||||
func (self *FileCommands) GetEditAtLineCmdStr(filename string, lineNumber int) (string, bool) {
|
||||
// Legacy support for old config; to be removed at some point
|
||||
if self.UserConfig.OS.EditAtLine == "" && self.UserConfig.OS.EditCommandTemplate != "" {
|
||||
if cmdStr, err := self.GetEditCmdStrLegacy(filename, lineNumber); err == nil {
|
||||
return cmdStr, true
|
||||
}
|
||||
}
|
||||
|
||||
template, editInTerminal := config.GetEditAtLineTemplate(&self.UserConfig.OS, self.guessDefaultEditor)
|
||||
|
||||
templateValues := map[string]string{
|
||||
"filename": self.cmd.Quote(filename),
|
||||
"line": strconv.Itoa(lineNumber),
|
||||
}
|
||||
|
||||
cmdStr := utils.ResolvePlaceholderString(template, templateValues)
|
||||
return cmdStr, editInTerminal
|
||||
}
|
||||
|
||||
func (self *FileCommands) GetEditAtLineAndWaitCmdStr(filename string, lineNumber int) string {
|
||||
// Legacy support for old config; to be removed at some point
|
||||
if self.UserConfig.OS.EditAtLineAndWait == "" && self.UserConfig.OS.EditCommandTemplate != "" {
|
||||
if cmdStr, err := self.GetEditCmdStrLegacy(filename, lineNumber); err == nil {
|
||||
return cmdStr
|
||||
}
|
||||
}
|
||||
|
||||
template := config.GetEditAtLineAndWaitTemplate(&self.UserConfig.OS, self.guessDefaultEditor)
|
||||
|
||||
templateValues := map[string]string{
|
||||
"filename": self.cmd.Quote(filename),
|
||||
"line": strconv.Itoa(lineNumber),
|
||||
}
|
||||
|
||||
cmdStr := utils.ResolvePlaceholderString(template, templateValues)
|
||||
return cmdStr
|
||||
}
|
||||
|
||||
func (self *FileCommands) guessDefaultEditor() string {
|
||||
// Try to query a few places where editors get configured
|
||||
editor := self.config.GetCoreEditor()
|
||||
if editor == "" {
|
||||
editor = self.os.Getenv("GIT_EDITOR")
|
||||
}
|
||||
if editor == "" {
|
||||
editor = self.os.Getenv("VISUAL")
|
||||
}
|
||||
if editor == "" {
|
||||
editor = self.os.Getenv("EDITOR")
|
||||
}
|
||||
|
||||
if editor != "" {
|
||||
// At this point, it might be more than just the name of the editor;
|
||||
// e.g. it might be "code -w" or "vim -u myvim.rc". So assume that
|
||||
// everything up to the first space is the editor name.
|
||||
editor = strings.Split(editor, " ")[0]
|
||||
}
|
||||
|
||||
return editor
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
|
||||
}
|
||||
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
|
||||
|
||||
statuses, err := self.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
|
||||
statuses, err := self.gitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
|
||||
if err != nil {
|
||||
self.Log.Error(err)
|
||||
}
|
||||
@@ -81,13 +81,15 @@ type FileStatus struct {
|
||||
PreviousName string
|
||||
}
|
||||
|
||||
func (c *FileLoader) GitStatus(opts GitStatusOptions) ([]FileStatus, error) {
|
||||
noRenamesFlag := ""
|
||||
if opts.NoRenames {
|
||||
noRenamesFlag = " --no-renames"
|
||||
}
|
||||
func (c *FileLoader) gitStatus(opts GitStatusOptions) ([]FileStatus, error) {
|
||||
cmdArgs := NewGitCmd("status").
|
||||
Arg(opts.UntrackedFilesArg).
|
||||
Arg("--porcelain").
|
||||
Arg("-z").
|
||||
ArgIf(opts.NoRenames, "--no-renames").
|
||||
ToArgv()
|
||||
|
||||
statusLines, _, err := c.cmd.New(fmt.Sprintf("git status %s --porcelain -z%s", opts.UntrackedFilesArg, noRenamesFlag)).DontLog().RunWithOutputs()
|
||||
statusLines, _, err := c.cmd.New(cmdArgs).DontLog().RunWithOutputs()
|
||||
if err != nil {
|
||||
return []FileStatus{}, err
|
||||
}
|
||||
|
||||
@@ -20,14 +20,13 @@ func TestFileGetStatusFiles(t *testing.T) {
|
||||
{
|
||||
"No files found",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git status --untracked-files=yes --porcelain -z`, "", nil),
|
||||
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z"}, "", nil),
|
||||
[]*models.File{},
|
||||
},
|
||||
{
|
||||
"Several files found",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(
|
||||
`git status --untracked-files=yes --porcelain -z`,
|
||||
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z"},
|
||||
"MM file1.txt\x00A file3.txt\x00AM file2.txt\x00?? file4.txt\x00UU file5.txt",
|
||||
nil,
|
||||
),
|
||||
@@ -102,7 +101,7 @@ func TestFileGetStatusFiles(t *testing.T) {
|
||||
{
|
||||
"File with new line char",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git status --untracked-files=yes --porcelain -z`, "MM a\nb.txt", nil),
|
||||
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z"}, "MM a\nb.txt", nil),
|
||||
[]*models.File{
|
||||
{
|
||||
Name: "a\nb.txt",
|
||||
@@ -122,8 +121,7 @@ func TestFileGetStatusFiles(t *testing.T) {
|
||||
{
|
||||
"Renamed files",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(
|
||||
`git status --untracked-files=yes --porcelain -z`,
|
||||
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z"},
|
||||
"R after1.txt\x00before1.txt\x00RM after2.txt\x00before2.txt",
|
||||
nil,
|
||||
),
|
||||
@@ -161,8 +159,7 @@ func TestFileGetStatusFiles(t *testing.T) {
|
||||
{
|
||||
"File with arrow in name",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(
|
||||
`git status --untracked-files=yes --porcelain -z`,
|
||||
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z"},
|
||||
`?? a -> b.txt`,
|
||||
nil,
|
||||
),
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEditFileCmdStr(t *testing.T) {
|
||||
func TestEditFileCmdStrLegacy(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
configEditCommand string
|
||||
@@ -27,7 +27,7 @@ func TestEditFileCmdStr(t *testing.T) {
|
||||
configEditCommand: "",
|
||||
configEditCommandTemplate: "{{editor}} {{filename}}",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`which vi`, "", errors.New("error")),
|
||||
ExpectArgs([]string{"which", "vi"}, "", errors.New("error")),
|
||||
getenv: func(env string) string {
|
||||
return ""
|
||||
},
|
||||
@@ -105,7 +105,7 @@ func TestEditFileCmdStr(t *testing.T) {
|
||||
configEditCommand: "",
|
||||
configEditCommandTemplate: "{{editor}} {{filename}}",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`which vi`, "/usr/bin/vi", nil),
|
||||
ExpectArgs([]string{"which", "vi"}, "/usr/bin/vi", nil),
|
||||
getenv: func(env string) string {
|
||||
return ""
|
||||
},
|
||||
@@ -120,7 +120,7 @@ func TestEditFileCmdStr(t *testing.T) {
|
||||
configEditCommand: "",
|
||||
configEditCommandTemplate: "{{editor}} {{filename}}",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`which vi`, "/usr/bin/vi", nil),
|
||||
ExpectArgs([]string{"which", "vi"}, "/usr/bin/vi", nil),
|
||||
getenv: func(env string) string {
|
||||
return ""
|
||||
},
|
||||
@@ -172,7 +172,206 @@ func TestEditFileCmdStr(t *testing.T) {
|
||||
getenv: s.getenv,
|
||||
})
|
||||
|
||||
s.test(instance.GetEditCmdStr(s.filename, 1))
|
||||
s.test(instance.GetEditCmdStrLegacy(s.filename, 1))
|
||||
s.runner.CheckForMissingCalls()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditFileCmd(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
osConfig config.OSConfig
|
||||
expectedCmdStr string
|
||||
expectedEditInTerminal bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
filename: "test",
|
||||
osConfig: config.OSConfig{},
|
||||
expectedCmdStr: `vim -- "test"`,
|
||||
expectedEditInTerminal: true,
|
||||
},
|
||||
{
|
||||
filename: "test",
|
||||
osConfig: config.OSConfig{
|
||||
Edit: "nano {{filename}}",
|
||||
},
|
||||
expectedCmdStr: `nano "test"`,
|
||||
expectedEditInTerminal: true,
|
||||
},
|
||||
{
|
||||
filename: "file/with space",
|
||||
osConfig: config.OSConfig{
|
||||
EditPreset: "sublime",
|
||||
},
|
||||
expectedCmdStr: `subl -- "file/with space"`,
|
||||
expectedEditInTerminal: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
userConfig := config.GetDefaultConfig()
|
||||
userConfig.OS = s.osConfig
|
||||
|
||||
instance := buildFileCommands(commonDeps{
|
||||
userConfig: userConfig,
|
||||
})
|
||||
|
||||
cmdStr, editInTerminal := instance.GetEditCmdStr(s.filename)
|
||||
assert.Equal(t, s.expectedCmdStr, cmdStr)
|
||||
assert.Equal(t, s.expectedEditInTerminal, editInTerminal)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditFileAtLineCmd(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
lineNumber int
|
||||
osConfig config.OSConfig
|
||||
expectedCmdStr string
|
||||
expectedEditInTerminal bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
filename: "test",
|
||||
lineNumber: 42,
|
||||
osConfig: config.OSConfig{},
|
||||
expectedCmdStr: `vim +42 -- "test"`,
|
||||
expectedEditInTerminal: true,
|
||||
},
|
||||
{
|
||||
filename: "test",
|
||||
lineNumber: 35,
|
||||
osConfig: config.OSConfig{
|
||||
EditAtLine: "nano +{{line}} {{filename}}",
|
||||
},
|
||||
expectedCmdStr: `nano +35 "test"`,
|
||||
expectedEditInTerminal: true,
|
||||
},
|
||||
{
|
||||
filename: "file/with space",
|
||||
lineNumber: 12,
|
||||
osConfig: config.OSConfig{
|
||||
EditPreset: "sublime",
|
||||
},
|
||||
expectedCmdStr: `subl -- "file/with space":12`,
|
||||
expectedEditInTerminal: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
userConfig := config.GetDefaultConfig()
|
||||
userConfig.OS = s.osConfig
|
||||
|
||||
instance := buildFileCommands(commonDeps{
|
||||
userConfig: userConfig,
|
||||
})
|
||||
|
||||
cmdStr, editInTerminal := instance.GetEditAtLineCmdStr(s.filename, s.lineNumber)
|
||||
assert.Equal(t, s.expectedCmdStr, cmdStr)
|
||||
assert.Equal(t, s.expectedEditInTerminal, editInTerminal)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditFileAtLineAndWaitCmd(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
lineNumber int
|
||||
osConfig config.OSConfig
|
||||
expectedCmdStr string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
filename: "test",
|
||||
lineNumber: 42,
|
||||
osConfig: config.OSConfig{},
|
||||
expectedCmdStr: `vim +42 -- "test"`,
|
||||
},
|
||||
{
|
||||
filename: "file/with space",
|
||||
lineNumber: 12,
|
||||
osConfig: config.OSConfig{
|
||||
EditPreset: "sublime",
|
||||
},
|
||||
expectedCmdStr: `subl --wait -- "file/with space":12`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
userConfig := config.GetDefaultConfig()
|
||||
userConfig.OS = s.osConfig
|
||||
|
||||
instance := buildFileCommands(commonDeps{
|
||||
userConfig: userConfig,
|
||||
})
|
||||
|
||||
cmdStr := instance.GetEditAtLineAndWaitCmdStr(s.filename, s.lineNumber)
|
||||
assert.Equal(t, s.expectedCmdStr, cmdStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGuessDefaultEditor(t *testing.T) {
|
||||
type scenario struct {
|
||||
gitConfigMockResponses map[string]string
|
||||
getenv func(string) string
|
||||
expectedResult string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
gitConfigMockResponses: nil,
|
||||
getenv: func(env string) string {
|
||||
return ""
|
||||
},
|
||||
expectedResult: "",
|
||||
},
|
||||
{
|
||||
gitConfigMockResponses: map[string]string{"core.editor": "nano"},
|
||||
getenv: func(env string) string {
|
||||
return ""
|
||||
},
|
||||
expectedResult: "nano",
|
||||
},
|
||||
{
|
||||
gitConfigMockResponses: map[string]string{"core.editor": "code -w"},
|
||||
getenv: func(env string) string {
|
||||
return ""
|
||||
},
|
||||
expectedResult: "code",
|
||||
},
|
||||
{
|
||||
gitConfigMockResponses: nil,
|
||||
getenv: func(env string) string {
|
||||
if env == "VISUAL" {
|
||||
return "emacs"
|
||||
}
|
||||
|
||||
return ""
|
||||
},
|
||||
expectedResult: "emacs",
|
||||
},
|
||||
{
|
||||
gitConfigMockResponses: nil,
|
||||
getenv: func(env string) string {
|
||||
if env == "EDITOR" {
|
||||
return "bbedit -w"
|
||||
}
|
||||
|
||||
return ""
|
||||
},
|
||||
expectedResult: "bbedit",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
instance := buildFileCommands(commonDeps{
|
||||
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
|
||||
getenv: s.getenv,
|
||||
})
|
||||
|
||||
assert.Equal(t, s.expectedResult, instance.guessDefaultEditor())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ func (self *FlowCommands) FinishCmdObj(branchName string) (oscommands.ICmdObj, e
|
||||
branchType := ""
|
||||
for _, line := range strings.Split(strings.TrimSpace(prefixes), "\n") {
|
||||
if strings.HasPrefix(line, "gitflow.prefix.") && strings.HasSuffix(line, prefix) {
|
||||
|
||||
regex := regexp.MustCompile("gitflow.prefix.([^ ]*) .*")
|
||||
matches := regex.FindAllStringSubmatch(line, 1)
|
||||
|
||||
@@ -48,9 +49,13 @@ func (self *FlowCommands) FinishCmdObj(branchName string) (oscommands.ICmdObj, e
|
||||
return nil, errors.New(self.Tr.NotAGitFlowBranch)
|
||||
}
|
||||
|
||||
return self.cmd.New("git flow " + branchType + " finish " + suffix), nil
|
||||
cmdArgs := NewGitCmd("flow").Arg(branchType, "finish", suffix).ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs), nil
|
||||
}
|
||||
|
||||
func (self *FlowCommands) StartCmdObj(branchType string, name string) oscommands.ICmdObj {
|
||||
return self.cmd.New("git flow " + branchType + " start " + name)
|
||||
cmdArgs := NewGitCmd("flow").Arg(branchType, "start", name).ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStartCmdObj(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
branchType string
|
||||
name string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
testName: "basic",
|
||||
branchType: "feature",
|
||||
name: "test",
|
||||
expected: []string{"git", "flow", "feature", "start", "test"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildFlowCommands(commonDeps{})
|
||||
|
||||
assert.Equal(t,
|
||||
instance.StartCmdObj(s.branchType, s.name).Args(),
|
||||
s.expected,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinishCmdObj(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
branchName string
|
||||
expected []string
|
||||
expectedError string
|
||||
gitConfigMockResponses map[string]string
|
||||
}{
|
||||
{
|
||||
testName: "not a git flow branch",
|
||||
branchName: "mybranch",
|
||||
expected: nil,
|
||||
expectedError: "This does not seem to be a git flow branch",
|
||||
gitConfigMockResponses: nil,
|
||||
},
|
||||
{
|
||||
testName: "feature branch without config",
|
||||
branchName: "feature/mybranch",
|
||||
expected: nil,
|
||||
expectedError: "This does not seem to be a git flow branch",
|
||||
gitConfigMockResponses: nil,
|
||||
},
|
||||
{
|
||||
testName: "feature branch with config",
|
||||
branchName: "feature/mybranch",
|
||||
expected: []string{"git", "flow", "feature", "finish", "mybranch"},
|
||||
expectedError: "",
|
||||
gitConfigMockResponses: map[string]string{
|
||||
"--local --get-regexp gitflow.prefix": "gitflow.prefix.feature feature/",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildFlowCommands(commonDeps{
|
||||
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
|
||||
})
|
||||
|
||||
cmd, err := instance.FinishCmdObj(s.branchName)
|
||||
|
||||
if s.expectedError != "" {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got nil")
|
||||
} else {
|
||||
assert.Equal(t, err.Error(), s.expectedError)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, cmd.Args(), s.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package git_commands
|
||||
|
||||
import "strings"
|
||||
|
||||
// convenience struct for building git commands. Especially useful when
|
||||
// including conditional args
|
||||
type GitCommandBuilder struct {
|
||||
// command string
|
||||
args []string
|
||||
}
|
||||
|
||||
func NewGitCmd(command string) *GitCommandBuilder {
|
||||
return &GitCommandBuilder{args: []string{command}}
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) Arg(args ...string) *GitCommandBuilder {
|
||||
self.args = append(self.args, args...)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) ArgIf(condition bool, ifTrue ...string) *GitCommandBuilder {
|
||||
if condition {
|
||||
self.Arg(ifTrue...)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) ArgIfElse(condition bool, ifTrue string, ifFalse string) *GitCommandBuilder {
|
||||
if condition {
|
||||
return self.Arg(ifTrue)
|
||||
} else {
|
||||
return self.Arg(ifFalse)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) Config(value string) *GitCommandBuilder {
|
||||
// config settings come before the command
|
||||
self.args = append([]string{"-c", value}, self.args...)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) RepoPath(value string) *GitCommandBuilder {
|
||||
// repo path comes before the command
|
||||
self.args = append([]string{"-C", value}, self.args...)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) ToArgv() []string {
|
||||
return append([]string{"git"}, self.args...)
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) ToString() string {
|
||||
return strings.Join(self.ToArgv(), " ")
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGitCommandBuilder(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
input []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
input: NewGitCmd("push").
|
||||
Arg("--force-with-lease").
|
||||
Arg("--set-upstream").
|
||||
Arg("origin").
|
||||
Arg("master").
|
||||
ToArgv(),
|
||||
expected: []string{"git", "push", "--force-with-lease", "--set-upstream", "origin", "master"},
|
||||
},
|
||||
{
|
||||
input: NewGitCmd("push").ArgIf(true, "--test").ToArgv(),
|
||||
expected: []string{"git", "push", "--test"},
|
||||
},
|
||||
{
|
||||
input: NewGitCmd("push").ArgIf(false, "--test").ToArgv(),
|
||||
expected: []string{"git", "push"},
|
||||
},
|
||||
{
|
||||
input: NewGitCmd("push").ArgIfElse(true, "-b", "-a").ToArgv(),
|
||||
expected: []string{"git", "push", "-b"},
|
||||
},
|
||||
{
|
||||
input: NewGitCmd("push").ArgIfElse(false, "-a", "-b").ToArgv(),
|
||||
expected: []string{"git", "push", "-b"},
|
||||
},
|
||||
{
|
||||
input: NewGitCmd("push").Arg("-a", "-b").ToArgv(),
|
||||
expected: []string{"git", "push", "-a", "-b"},
|
||||
},
|
||||
{
|
||||
input: NewGitCmd("push").Config("user.name=foo").Config("user.email=bar").ToArgv(),
|
||||
expected: []string{"git", "-c", "user.email=bar", "-c", "user.name=foo", "push"},
|
||||
},
|
||||
{
|
||||
input: NewGitCmd("push").RepoPath("a/b/c").ToArgv(),
|
||||
expected: []string{"git", "-C", "a/b/c", "push"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
assert.Equal(t, s.input, s.expected)
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,16 @@ package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type PatchCommands struct {
|
||||
@@ -16,7 +21,7 @@ type PatchCommands struct {
|
||||
status *StatusCommands
|
||||
stash *StashCommands
|
||||
|
||||
PatchManager *patch.PatchManager
|
||||
PatchBuilder *patch.PatchBuilder
|
||||
}
|
||||
|
||||
func NewPatchCommands(
|
||||
@@ -25,7 +30,7 @@ func NewPatchCommands(
|
||||
commit *CommitCommands,
|
||||
status *StatusCommands,
|
||||
stash *StashCommands,
|
||||
patchManager *patch.PatchManager,
|
||||
patchBuilder *patch.PatchBuilder,
|
||||
) *PatchCommands {
|
||||
return &PatchCommands{
|
||||
GitCommon: gitCommon,
|
||||
@@ -33,21 +38,66 @@ func NewPatchCommands(
|
||||
commit: commit,
|
||||
status: status,
|
||||
stash: stash,
|
||||
PatchManager: patchManager,
|
||||
PatchBuilder: patchBuilder,
|
||||
}
|
||||
}
|
||||
|
||||
type ApplyPatchOpts struct {
|
||||
ThreeWay bool
|
||||
Cached bool
|
||||
Index bool
|
||||
Reverse bool
|
||||
}
|
||||
|
||||
func (self *PatchCommands) ApplyCustomPatch(reverse bool) error {
|
||||
patch := self.PatchBuilder.PatchToApply(reverse)
|
||||
|
||||
return self.ApplyPatch(patch, ApplyPatchOpts{
|
||||
Index: true,
|
||||
ThreeWay: true,
|
||||
Reverse: reverse,
|
||||
})
|
||||
}
|
||||
|
||||
func (self *PatchCommands) ApplyPatch(patch string, opts ApplyPatchOpts) error {
|
||||
filepath, err := self.SaveTemporaryPatch(patch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return self.applyPatchFile(filepath, opts)
|
||||
}
|
||||
|
||||
func (self *PatchCommands) applyPatchFile(filepath string, opts ApplyPatchOpts) error {
|
||||
cmdArgs := NewGitCmd("apply").
|
||||
ArgIf(opts.ThreeWay, "--3way").
|
||||
ArgIf(opts.Cached, "--cached").
|
||||
ArgIf(opts.Index, "--index").
|
||||
ArgIf(opts.Reverse, "--reverse").
|
||||
Arg(filepath).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *PatchCommands) SaveTemporaryPatch(patch string) (string, error) {
|
||||
filepath := filepath.Join(self.os.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
|
||||
self.Log.Infof("saving temporary patch to %s", filepath)
|
||||
if err := self.os.CreateFileWithContent(filepath, patch); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath, nil
|
||||
}
|
||||
|
||||
// DeletePatchesFromCommit applies a patch in reverse for a commit
|
||||
func (self *PatchCommands) DeletePatchesFromCommit(commits []*models.Commit, commitIndex int) error {
|
||||
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
|
||||
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIndex, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// apply each patch in reverse
|
||||
if err := self.PatchManager.ApplyPatches(true); err != nil {
|
||||
if err := self.rebase.AbortRebase(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.ApplyCustomPatch(true); err != nil {
|
||||
_ = self.rebase.AbortRebase()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -57,7 +107,7 @@ func (self *PatchCommands) DeletePatchesFromCommit(commits []*models.Commit, com
|
||||
}
|
||||
|
||||
self.rebase.onSuccessfulContinue = func() error {
|
||||
self.PatchManager.Reset()
|
||||
self.PatchBuilder.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -67,15 +117,17 @@ func (self *PatchCommands) DeletePatchesFromCommit(commits []*models.Commit, com
|
||||
|
||||
func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, sourceCommitIdx int, destinationCommitIdx int) error {
|
||||
if sourceCommitIdx < destinationCommitIdx {
|
||||
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx); err != nil {
|
||||
// Passing true for keepCommitsThatBecomeEmpty: if the moved-from
|
||||
// commit becomes empty, we want to keep it, mainly for consistency with
|
||||
// moving the patch to a *later* commit, which behaves the same.
|
||||
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// apply each patch forward
|
||||
if err := self.PatchManager.ApplyPatches(false); err != nil {
|
||||
if err := self.rebase.AbortRebase(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.ApplyCustomPatch(false); err != nil {
|
||||
// Don't abort the rebase here; this might cause conflicts, so give
|
||||
// the user a chance to resolve them
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -85,7 +137,7 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
|
||||
}
|
||||
|
||||
self.rebase.onSuccessfulContinue = func() error {
|
||||
self.PatchManager.Reset()
|
||||
self.PatchBuilder.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -106,24 +158,24 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
|
||||
|
||||
baseIndex := sourceCommitIdx + 1
|
||||
|
||||
todoLines := self.rebase.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
|
||||
if i == sourceCommitIdx || i == destinationCommitIdx {
|
||||
return "edit"
|
||||
} else {
|
||||
return "pick"
|
||||
}
|
||||
})
|
||||
changes := []daemon.ChangeTodoAction{
|
||||
{Sha: commits[sourceCommitIdx].Sha, NewAction: todo.Edit},
|
||||
{Sha: commits[destinationCommitIdx].Sha, NewAction: todo.Edit},
|
||||
}
|
||||
self.os.LogCommand(logTodoChanges(changes), false)
|
||||
|
||||
err := self.rebase.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todoLines, true).Run()
|
||||
err := self.rebase.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: commits[baseIndex].Sha,
|
||||
overrideEditor: true,
|
||||
instruction: daemon.NewChangeTodoActionsInstruction(changes),
|
||||
}).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// apply each patch in reverse
|
||||
if err := self.PatchManager.ApplyPatches(true); err != nil {
|
||||
if err := self.rebase.AbortRebase(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.ApplyCustomPatch(true); err != nil {
|
||||
_ = self.rebase.AbortRebase()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -132,6 +184,12 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
|
||||
return err
|
||||
}
|
||||
|
||||
patch, err := self.diffHeadAgainstCommit(commits[sourceCommitIdx])
|
||||
if err != nil {
|
||||
_ = self.rebase.AbortRebase()
|
||||
return err
|
||||
}
|
||||
|
||||
if self.rebase.onSuccessfulContinue != nil {
|
||||
return errors.New("You are midway through another rebase operation. Please abort to start again")
|
||||
}
|
||||
@@ -139,10 +197,9 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
|
||||
self.rebase.onSuccessfulContinue = func() error {
|
||||
// now we should be up to the destination, so let's apply forward these patches to that.
|
||||
// ideally we would ensure we're on the right commit but I'm not sure if that check is necessary
|
||||
if err := self.PatchManager.ApplyPatches(false); err != nil {
|
||||
if err := self.rebase.AbortRebase(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.ApplyPatch(patch, ApplyPatchOpts{Index: true, ThreeWay: true}); err != nil {
|
||||
// Don't abort the rebase here; this might cause conflicts, so give
|
||||
// the user a chance to resolve them
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -152,7 +209,7 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
|
||||
}
|
||||
|
||||
self.rebase.onSuccessfulContinue = func() error {
|
||||
self.PatchManager.Reset()
|
||||
self.PatchBuilder.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -169,15 +226,13 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
|
||||
}
|
||||
}
|
||||
|
||||
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
|
||||
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIdx, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.PatchManager.ApplyPatches(true); err != nil {
|
||||
if err := self.ApplyCustomPatch(true); err != nil {
|
||||
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
|
||||
if err := self.rebase.AbortRebase(); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = self.rebase.AbortRebase()
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -187,17 +242,21 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
|
||||
return err
|
||||
}
|
||||
|
||||
patch, err := self.diffHeadAgainstCommit(commits[commitIdx])
|
||||
if err != nil {
|
||||
_ = self.rebase.AbortRebase()
|
||||
return err
|
||||
}
|
||||
|
||||
if self.rebase.onSuccessfulContinue != nil {
|
||||
return errors.New("You are midway through another rebase operation. Please abort to start again")
|
||||
}
|
||||
|
||||
self.rebase.onSuccessfulContinue = func() error {
|
||||
// add patches to index
|
||||
if err := self.PatchManager.ApplyPatches(false); err != nil {
|
||||
if err := self.ApplyPatch(patch, ApplyPatchOpts{Index: true, ThreeWay: true}); err != nil {
|
||||
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
|
||||
if err := self.rebase.AbortRebase(); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = self.rebase.AbortRebase()
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -208,7 +267,7 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
|
||||
}
|
||||
}
|
||||
|
||||
self.PatchManager.Reset()
|
||||
self.PatchBuilder.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -216,14 +275,12 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
|
||||
}
|
||||
|
||||
func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx int) error {
|
||||
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
|
||||
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIdx, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.PatchManager.ApplyPatches(true); err != nil {
|
||||
if err := self.rebase.AbortRebase(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.ApplyCustomPatch(true); err != nil {
|
||||
_ = self.rebase.AbortRebase()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -232,18 +289,20 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
|
||||
return err
|
||||
}
|
||||
|
||||
// add patches to index
|
||||
if err := self.PatchManager.ApplyPatches(false); err != nil {
|
||||
if err := self.rebase.AbortRebase(); err != nil {
|
||||
return err
|
||||
}
|
||||
patch, err := self.diffHeadAgainstCommit(commits[commitIdx])
|
||||
if err != nil {
|
||||
_ = self.rebase.AbortRebase()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.ApplyPatch(patch, ApplyPatchOpts{Index: true, ThreeWay: true}); err != nil {
|
||||
_ = self.rebase.AbortRebase()
|
||||
return err
|
||||
}
|
||||
|
||||
head_message, _ := self.commit.GetHeadCommitMessage()
|
||||
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
|
||||
err := self.commit.CommitCmdObj(new_message).Run()
|
||||
if err != nil {
|
||||
if err := self.commit.CommitCmdObj(new_message).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -251,6 +310,17 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
|
||||
return errors.New("You are midway through another rebase operation. Please abort to start again")
|
||||
}
|
||||
|
||||
self.PatchManager.Reset()
|
||||
self.PatchBuilder.Reset()
|
||||
return self.rebase.ContinueRebase()
|
||||
}
|
||||
|
||||
// We have just applied a patch in reverse to discard it from a commit; if we
|
||||
// now try to apply the patch again to move it to a later commit, or to the
|
||||
// index, then this would conflict "with itself" in case the patch contained
|
||||
// only some lines of a range of adjacent added lines. To solve this, we
|
||||
// get the diff of HEAD and the original commit and then apply that.
|
||||
func (self *PatchCommands) diffHeadAgainstCommit(commit *models.Commit) (string, error) {
|
||||
cmdArgs := NewGitCmd("diff").Arg("HEAD.." + commit.Sha).ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).RunWithOutput()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPatchApplyPatch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
opts ApplyPatchOpts
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(error)
|
||||
}
|
||||
|
||||
// expectedArgs excludes the last argument which is an indeterminate filename
|
||||
expectFn := func(expectedArgs []string, errToReturn error) func(cmdObj oscommands.ICmdObj) (string, error) {
|
||||
return func(cmdObj oscommands.ICmdObj) (string, error) {
|
||||
args := cmdObj.Args()
|
||||
|
||||
assert.Equal(t, len(args), len(expectedArgs)+1, fmt.Sprintf("unexpected command: %s", cmdObj.ToString()))
|
||||
|
||||
filename := args[len(args)-1]
|
||||
|
||||
content, err := os.ReadFile(filename)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "test", string(content))
|
||||
|
||||
return "", errToReturn
|
||||
}
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "valid case",
|
||||
opts: ApplyPatchOpts{Cached: true},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectFunc(expectFn([]string{"git", "apply", "--cached"}, nil)),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "command returns error",
|
||||
opts: ApplyPatchOpts{Cached: true},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectFunc(expectFn([]string{"git", "apply", "--cached"}, errors.New("error"))),
|
||||
test: func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildPatchCommands(commonDeps{runner: s.runner})
|
||||
s.test(instance.ApplyPatch("test", s.opts))
|
||||
s.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
+173
-158
@@ -2,15 +2,17 @@ package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type RebaseCommands struct {
|
||||
@@ -34,12 +36,12 @@ func NewRebaseCommands(
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, message string) error {
|
||||
if index == 0 {
|
||||
if models.IsHeadCommit(commits, index) {
|
||||
// we've selected the top commit so no rebase is required
|
||||
return self.commit.RewordLastCommit(message)
|
||||
}
|
||||
|
||||
err := self.BeginInteractiveRebaseForCommit(commits, index)
|
||||
err := self.BeginInteractiveRebaseForCommit(commits, index, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -54,12 +56,16 @@ func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, me
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index int) (oscommands.ICmdObj, error) {
|
||||
todo, sha, err := self.BuildSingleActionTodo(commits, index, "reword")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
changes := []daemon.ChangeTodoAction{{
|
||||
Sha: commits[index].Sha,
|
||||
NewAction: todo.Reword,
|
||||
}}
|
||||
self.os.LogCommand(logTodoChanges(changes), false)
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(sha, todo, false), nil
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: getBaseShaOrRoot(commits, index+1),
|
||||
instruction: daemon.NewChangeTodoActionsInstruction(changes),
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) ResetCommitAuthor(commits []*models.Commit, index int) error {
|
||||
@@ -75,12 +81,12 @@ func (self *RebaseCommands) SetCommitAuthor(commits []*models.Commit, index int,
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) GenericAmend(commits []*models.Commit, index int, f func() error) error {
|
||||
if index == 0 {
|
||||
if models.IsHeadCommit(commits, index) {
|
||||
// we've selected the top commit so no rebase is required
|
||||
return f()
|
||||
}
|
||||
|
||||
err := self.BeginInteractiveRebaseForCommit(commits, index)
|
||||
err := self.BeginInteractiveRebaseForCommit(commits, index, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -95,176 +101,182 @@ func (self *RebaseCommands) GenericAmend(commits []*models.Commit, index int, f
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) MoveCommitDown(commits []*models.Commit, index int) error {
|
||||
// we must ensure that we have at least two commits after the selected one
|
||||
if len(commits) <= index+2 {
|
||||
// assuming they aren't picking the bottom commit
|
||||
return errors.New(self.Tr.NoRoom)
|
||||
}
|
||||
baseShaOrRoot := getBaseShaOrRoot(commits, index+2)
|
||||
|
||||
orderedCommits := append(commits[0:index], commits[index+1], commits[index])
|
||||
sha := commits[index].Sha
|
||||
|
||||
todoLines := self.BuildTodoLinesSingleAction(orderedCommits, "pick")
|
||||
self.os.LogCommand(fmt.Sprintf("Moving TODO down: %s", utils.ShortSha(sha)), false)
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todoLines, true).Run()
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: baseShaOrRoot,
|
||||
instruction: daemon.NewMoveTodoDownInstruction(sha),
|
||||
overrideEditor: true,
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action string) error {
|
||||
todo, sha, err := self.BuildSingleActionTodo(commits, index, action)
|
||||
if err != nil {
|
||||
return err
|
||||
func (self *RebaseCommands) MoveCommitUp(commits []*models.Commit, index int) error {
|
||||
baseShaOrRoot := getBaseShaOrRoot(commits, index+1)
|
||||
|
||||
sha := commits[index].Sha
|
||||
|
||||
self.os.LogCommand(fmt.Sprintf("Moving TODO up: %s", utils.ShortSha(sha)), false)
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: baseShaOrRoot,
|
||||
instruction: daemon.NewMoveTodoUpInstruction(sha),
|
||||
overrideEditor: true,
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action todo.TodoCommand) error {
|
||||
baseIndex := index + 1
|
||||
if action == todo.Squash || action == todo.Fixup {
|
||||
baseIndex++
|
||||
}
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(sha, todo, true).Run()
|
||||
baseShaOrRoot := getBaseShaOrRoot(commits, baseIndex)
|
||||
|
||||
changes := []daemon.ChangeTodoAction{{
|
||||
Sha: commits[index].Sha,
|
||||
NewAction: action,
|
||||
}}
|
||||
self.os.LogCommand(logTodoChanges(changes), false)
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: baseShaOrRoot,
|
||||
overrideEditor: true,
|
||||
instruction: daemon.NewChangeTodoActionsInstruction(changes),
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) EditRebase(branchRef string) error {
|
||||
self.os.LogCommand(fmt.Sprintf("Beginning interactive rebase at '%s'", branchRef), false)
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: branchRef,
|
||||
instruction: daemon.NewInsertBreakInstruction(),
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func logTodoChanges(changes []daemon.ChangeTodoAction) string {
|
||||
changeTodoStr := strings.Join(slices.Map(changes, func(c daemon.ChangeTodoAction) string {
|
||||
return fmt.Sprintf("%s:%s", c.Sha, c.NewAction)
|
||||
}), "\n")
|
||||
return fmt.Sprintf("Changing TODO actions: %s", changeTodoStr)
|
||||
}
|
||||
|
||||
type PrepareInteractiveRebaseCommandOpts struct {
|
||||
baseShaOrRoot string
|
||||
instruction daemon.Instruction
|
||||
overrideEditor bool
|
||||
keepCommitsThatBecomeEmpty bool
|
||||
}
|
||||
|
||||
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
|
||||
// we tell git to run lazygit to edit the todo list, and we pass the client
|
||||
// lazygit a todo string to write to the todo file
|
||||
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todoLines []TodoLine, overrideEditor bool) oscommands.ICmdObj {
|
||||
todo := self.buildTodo(todoLines)
|
||||
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteractiveRebaseCommandOpts) oscommands.ICmdObj {
|
||||
ex := oscommands.GetLazygitPath()
|
||||
|
||||
cmdArgs := NewGitCmd("rebase").
|
||||
Arg("--interactive").
|
||||
Arg("--autostash").
|
||||
Arg("--keep-empty").
|
||||
ArgIf(opts.keepCommitsThatBecomeEmpty && !self.version.IsOlderThan(2, 26, 0), "--empty=keep").
|
||||
Arg("--no-autosquash").
|
||||
ArgIf(!self.version.IsOlderThan(2, 22, 0), "--rebase-merges").
|
||||
Arg(opts.baseShaOrRoot).
|
||||
ToArgv()
|
||||
|
||||
debug := "FALSE"
|
||||
if self.Debug {
|
||||
debug = "TRUE"
|
||||
}
|
||||
|
||||
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha)
|
||||
self.Log.WithField("command", cmdStr).Debug("RunCommand")
|
||||
self.Log.WithField("command", cmdArgs).Debug("RunCommand")
|
||||
|
||||
cmdObj := self.cmd.New(cmdStr)
|
||||
cmdObj := self.cmd.New(cmdArgs)
|
||||
|
||||
gitSequenceEditor := ex
|
||||
if todo == "" {
|
||||
gitSequenceEditor = "true"
|
||||
|
||||
if opts.instruction != nil {
|
||||
cmdObj.AddEnvVars(daemon.ToEnvVars(opts.instruction)...)
|
||||
} else {
|
||||
self.os.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false)
|
||||
gitSequenceEditor = "true"
|
||||
}
|
||||
|
||||
cmdObj.AddEnvVars(
|
||||
daemon.DaemonKindEnvKey+"="+string(daemon.InteractiveRebase),
|
||||
daemon.RebaseTODOEnvKey+"="+todo,
|
||||
"DEBUG="+debug,
|
||||
"LANG=en_US.UTF-8", // Force using EN as language
|
||||
"LC_ALL=en_US.UTF-8", // Force using EN as language
|
||||
"GIT_SEQUENCE_EDITOR="+gitSequenceEditor,
|
||||
)
|
||||
|
||||
if overrideEditor {
|
||||
if opts.overrideEditor {
|
||||
cmdObj.AddEnvVars("GIT_EDITOR=" + ex)
|
||||
}
|
||||
|
||||
return cmdObj
|
||||
}
|
||||
|
||||
// produces TodoLines where every commit is picked (or dropped for merge commits) except for the commit at the given index, which
|
||||
// will have the given action applied to it.
|
||||
func (self *RebaseCommands) BuildSingleActionTodo(commits []*models.Commit, actionIndex int, action string) ([]TodoLine, string, error) {
|
||||
baseIndex := actionIndex + 1
|
||||
|
||||
if len(commits) <= baseIndex {
|
||||
return nil, "", errors.New(self.Tr.CannotRebaseOntoFirstCommit)
|
||||
}
|
||||
|
||||
if action == "squash" || action == "fixup" {
|
||||
baseIndex++
|
||||
|
||||
if len(commits) <= baseIndex {
|
||||
return nil, "", errors.New(self.Tr.CannotSquashOntoSecondCommit)
|
||||
}
|
||||
}
|
||||
|
||||
todoLines := self.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
|
||||
if i == actionIndex {
|
||||
return action
|
||||
} else if commit.IsMerge() {
|
||||
// your typical interactive rebase will actually drop merge commits by default. Damn git CLI, you scary!
|
||||
// doing this means we don't need to worry about rebasing over merges which always causes problems.
|
||||
// you typically shouldn't be doing rebases that pass over merge commits anyway.
|
||||
return "drop"
|
||||
} else {
|
||||
return "pick"
|
||||
}
|
||||
})
|
||||
|
||||
return todoLines, commits[baseIndex].Sha, nil
|
||||
}
|
||||
|
||||
// AmendTo amends the given commit with whatever files are staged
|
||||
func (self *RebaseCommands) AmendTo(sha string) error {
|
||||
if err := self.commit.CreateFixupCommit(sha); err != nil {
|
||||
func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) error {
|
||||
commit := commits[commitIndex]
|
||||
|
||||
if err := self.commit.CreateFixupCommit(commit.Sha); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return self.SquashAllAboveFixupCommits(sha)
|
||||
}
|
||||
|
||||
// EditRebaseTodo sets the action at a given index in the git-rebase-todo file
|
||||
func (self *RebaseCommands) EditRebaseTodo(index int, action string) error {
|
||||
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
|
||||
bytes, err := os.ReadFile(fileName)
|
||||
// Get the sha of the commit we just created
|
||||
cmdArgs := NewGitCmd("rev-parse").Arg("--verify", "HEAD").ToArgv()
|
||||
fixupSha, err := self.cmd.New(cmdArgs).RunWithOutput()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content := strings.Split(string(bytes), "\n")
|
||||
commitCount := self.getTodoCommitCount(content)
|
||||
|
||||
// we have the most recent commit at the bottom whereas the todo file has
|
||||
// it at the bottom, so we need to subtract our index from the commit count
|
||||
contentIndex := commitCount - 1 - index
|
||||
splitLine := strings.Split(content[contentIndex], " ")
|
||||
content[contentIndex] = action + " " + strings.Join(splitLine[1:], " ")
|
||||
result := strings.Join(content, "\n")
|
||||
|
||||
return os.WriteFile(fileName, []byte(result), 0o644)
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: getBaseShaOrRoot(commits, commitIndex+1),
|
||||
overrideEditor: true,
|
||||
instruction: daemon.NewMoveFixupCommitDownInstruction(commit.Sha, fixupSha),
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) getTodoCommitCount(content []string) int {
|
||||
// count lines that are not blank and are not comments
|
||||
commitCount := 0
|
||||
for _, line := range content {
|
||||
if line != "" && !strings.HasPrefix(line, "#") {
|
||||
commitCount++
|
||||
}
|
||||
}
|
||||
return commitCount
|
||||
// EditRebaseTodo sets the action for a given rebase commit in the git-rebase-todo file
|
||||
func (self *RebaseCommands) EditRebaseTodo(commit *models.Commit, action todo.TodoCommand) error {
|
||||
return utils.EditRebaseTodo(
|
||||
filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo"), commit.Sha, commit.Action, action)
|
||||
}
|
||||
|
||||
// MoveTodoDown moves a rebase todo item down by one position
|
||||
func (self *RebaseCommands) MoveTodoDown(index int) error {
|
||||
func (self *RebaseCommands) MoveTodoDown(commit *models.Commit) error {
|
||||
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
|
||||
bytes, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return utils.MoveTodoDown(fileName, commit.Sha, commit.Action)
|
||||
}
|
||||
|
||||
content := strings.Split(string(bytes), "\n")
|
||||
commitCount := self.getTodoCommitCount(content)
|
||||
contentIndex := commitCount - 1 - index
|
||||
|
||||
rearrangedContent := append(content[0:contentIndex-1], content[contentIndex], content[contentIndex-1])
|
||||
rearrangedContent = append(rearrangedContent, content[contentIndex+1:]...)
|
||||
result := strings.Join(rearrangedContent, "\n")
|
||||
|
||||
return os.WriteFile(fileName, []byte(result), 0o644)
|
||||
// MoveTodoDown moves a rebase todo item down by one position
|
||||
func (self *RebaseCommands) MoveTodoUp(commit *models.Commit) error {
|
||||
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
|
||||
return utils.MoveTodoUp(fileName, commit.Sha, commit.Action)
|
||||
}
|
||||
|
||||
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
|
||||
func (self *RebaseCommands) SquashAllAboveFixupCommits(sha string) error {
|
||||
return self.runSkipEditorCommand(
|
||||
self.cmd.New(
|
||||
fmt.Sprintf(
|
||||
"git rebase --interactive --rebase-merges --autostash --autosquash %s^",
|
||||
sha,
|
||||
),
|
||||
),
|
||||
)
|
||||
func (self *RebaseCommands) SquashAllAboveFixupCommits(commit *models.Commit) error {
|
||||
shaOrRoot := commit.Sha + "^"
|
||||
if commit.IsFirstCommit() {
|
||||
shaOrRoot = "--root"
|
||||
}
|
||||
|
||||
cmdArgs := NewGitCmd("rebase").
|
||||
Arg("--interactive", "--rebase-merges", "--autostash", "--autosquash", shaOrRoot).
|
||||
ToArgv()
|
||||
|
||||
return self.runSkipEditorCommand(self.cmd.New(cmdArgs))
|
||||
}
|
||||
|
||||
// BeginInteractiveRebaseForCommit starts an interactive rebase to edit the current
|
||||
// commit and pick all others. After this you'll want to call `self.ContinueRebase()
|
||||
func (self *RebaseCommands) BeginInteractiveRebaseForCommit(commits []*models.Commit, commitIndex int) error {
|
||||
func (self *RebaseCommands) BeginInteractiveRebaseForCommit(
|
||||
commits []*models.Commit, commitIndex int, keepCommitsThatBecomeEmpty bool,
|
||||
) error {
|
||||
if len(commits)-1 < commitIndex {
|
||||
return errors.New("index outside of range of commits")
|
||||
}
|
||||
@@ -276,21 +288,29 @@ func (self *RebaseCommands) BeginInteractiveRebaseForCommit(commits []*models.Co
|
||||
return errors.New(self.Tr.DisabledForGPG)
|
||||
}
|
||||
|
||||
todo, sha, err := self.BuildSingleActionTodo(commits, commitIndex, "edit")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
changes := []daemon.ChangeTodoAction{{
|
||||
Sha: commits[commitIndex].Sha,
|
||||
NewAction: todo.Edit,
|
||||
}}
|
||||
self.os.LogCommand(logTodoChanges(changes), false)
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(sha, todo, true).Run()
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: getBaseShaOrRoot(commits, commitIndex+1),
|
||||
overrideEditor: true,
|
||||
keepCommitsThatBecomeEmpty: keepCommitsThatBecomeEmpty,
|
||||
instruction: daemon.NewChangeTodoActionsInstruction(changes),
|
||||
}).Run()
|
||||
}
|
||||
|
||||
// RebaseBranch interactive rebases onto a branch
|
||||
func (self *RebaseCommands) RebaseBranch(branchName string) error {
|
||||
return self.PrepareInteractiveRebaseCommand(branchName, nil, false).Run()
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{baseShaOrRoot: branchName}).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) oscommands.ICmdObj {
|
||||
return self.cmd.New("git " + commandType + " --" + command)
|
||||
cmdArgs := NewGitCmd(commandType).Arg("--" + command).ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) ContinueRebase() error {
|
||||
@@ -327,25 +347,29 @@ func (self *RebaseCommands) GenericMergeOrRebaseAction(commandType string, comma
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) runSkipEditorCommand(cmdObj oscommands.ICmdObj) error {
|
||||
instruction := daemon.NewExitImmediatelyInstruction()
|
||||
lazyGitPath := oscommands.GetLazygitPath()
|
||||
return cmdObj.
|
||||
AddEnvVars(
|
||||
daemon.DaemonKindEnvKey+"="+string(daemon.ExitImmediately),
|
||||
"GIT_EDITOR="+lazyGitPath,
|
||||
"GIT_SEQUENCE_EDITOR="+lazyGitPath,
|
||||
"EDITOR="+lazyGitPath,
|
||||
"VISUAL="+lazyGitPath,
|
||||
).
|
||||
AddEnvVars(daemon.ToEnvVars(instruction)...).
|
||||
Run()
|
||||
}
|
||||
|
||||
// DiscardOldFileChanges discards changes to a file from an old commit
|
||||
func (self *RebaseCommands) DiscardOldFileChanges(commits []*models.Commit, commitIndex int, fileName string) error {
|
||||
if err := self.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
|
||||
if err := self.BeginInteractiveRebaseForCommit(commits, commitIndex, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
|
||||
if err := self.cmd.New("git cat-file -e HEAD^:" + self.cmd.Quote(fileName)).Run(); err != nil {
|
||||
cmdArgs := NewGitCmd("cat-file").Arg("-e", "HEAD^:"+fileName).ToArgv()
|
||||
|
||||
if err := self.cmd.New(cmdArgs).Run(); err != nil {
|
||||
if err := self.os.Remove(fileName); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -368,36 +392,27 @@ func (self *RebaseCommands) DiscardOldFileChanges(commits []*models.Commit, comm
|
||||
|
||||
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
|
||||
func (self *RebaseCommands) CherryPickCommits(commits []*models.Commit) error {
|
||||
todoLines := self.BuildTodoLinesSingleAction(commits, "pick")
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand("HEAD", todoLines, false).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) buildTodo(todoLines []TodoLine) string {
|
||||
lines := slices.Map(todoLines, func(todoLine TodoLine) string {
|
||||
return todoLine.ToString()
|
||||
commitLines := lo.Map(commits, func(commit *models.Commit, _ int) string {
|
||||
return fmt.Sprintf("%s %s", utils.ShortSha(commit.Sha), commit.Name)
|
||||
})
|
||||
self.os.LogCommand(fmt.Sprintf("Cherry-picking commits:\n%s", strings.Join(commitLines, "\n")), false)
|
||||
|
||||
return strings.Join(slices.Reverse(lines), "")
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: "HEAD",
|
||||
instruction: daemon.NewCherryPickCommitsInstruction(commits),
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) BuildTodoLines(commits []*models.Commit, f func(*models.Commit, int) string) []TodoLine {
|
||||
return slices.MapWithIndex(commits, func(commit *models.Commit, i int) TodoLine {
|
||||
return TodoLine{Action: f(commit, i), Commit: commit}
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) BuildTodoLinesSingleAction(commits []*models.Commit, action string) []TodoLine {
|
||||
return self.BuildTodoLines(commits, func(commit *models.Commit, i int) string {
|
||||
return action
|
||||
})
|
||||
}
|
||||
|
||||
type TodoLine struct {
|
||||
Action string
|
||||
Commit *models.Commit
|
||||
}
|
||||
|
||||
func (self *TodoLine) ToString() string {
|
||||
return self.Action + " " + self.Commit.Sha + " " + self.Commit.Name + "\n"
|
||||
// we can't start an interactive rebase from the first commit without passing the
|
||||
// '--root' arg
|
||||
func getBaseShaOrRoot(commits []*models.Commit, index int) string {
|
||||
// We assume that the commits slice contains the initial commit of the repo.
|
||||
// Technically this assumption could prove false, but it's unlikely you'll
|
||||
// be starting a rebase from 300 commits ago (which is the original commit limit
|
||||
// at time of writing)
|
||||
if index < len(commits) {
|
||||
return commits[index].Sha
|
||||
} else {
|
||||
return "--root"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package git_commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
@@ -15,37 +16,60 @@ import (
|
||||
|
||||
func TestRebaseRebaseBranch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
arg string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(error)
|
||||
testName string
|
||||
arg string
|
||||
gitVersion *GitVersion
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "successful rebase",
|
||||
arg: "master",
|
||||
testName: "successful rebase",
|
||||
arg: "master",
|
||||
gitVersion: &GitVersion{2, 26, 0, ""},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git rebase --interactive --autostash --keep-empty master`, "", nil),
|
||||
ExpectGitArgs([]string{"rebase", "--interactive", "--autostash", "--keep-empty", "--no-autosquash", "--rebase-merges", "master"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "unsuccessful rebase",
|
||||
arg: "master",
|
||||
testName: "unsuccessful rebase",
|
||||
arg: "master",
|
||||
gitVersion: &GitVersion{2, 26, 0, ""},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git rebase --interactive --autostash --keep-empty master`, "", errors.New("error")),
|
||||
ExpectGitArgs([]string{"rebase", "--interactive", "--autostash", "--keep-empty", "--no-autosquash", "--rebase-merges", "master"}, "", errors.New("error")),
|
||||
test: func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "successful rebase (< 2.26.0)",
|
||||
arg: "master",
|
||||
gitVersion: &GitVersion{2, 25, 5, ""},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"rebase", "--interactive", "--autostash", "--keep-empty", "--no-autosquash", "--rebase-merges", "master"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "successful rebase (< 2.22.0)",
|
||||
arg: "master",
|
||||
gitVersion: &GitVersion{2, 21, 9, ""},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"rebase", "--interactive", "--autostash", "--keep-empty", "--no-autosquash", "master"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildRebaseCommands(commonDeps{runner: s.runner})
|
||||
instance := buildRebaseCommands(commonDeps{runner: s.runner, gitVersion: s.gitVersion})
|
||||
s.test(instance.RebaseBranch(s.arg))
|
||||
})
|
||||
}
|
||||
@@ -54,15 +78,16 @@ func TestRebaseRebaseBranch(t *testing.T) {
|
||||
// TestRebaseSkipEditorCommand confirms that SkipEditorCommand injects
|
||||
// environment variables that suppress an interactive editor
|
||||
func TestRebaseSkipEditorCommand(t *testing.T) {
|
||||
commandStr := "git blah"
|
||||
cmdArgs := []string{"git", "blah"}
|
||||
runner := oscommands.NewFakeRunner(t).ExpectFunc(func(cmdObj oscommands.ICmdObj) (string, error) {
|
||||
assert.Equal(t, commandStr, cmdObj.ToString())
|
||||
assert.EqualValues(t, cmdArgs, cmdObj.Args())
|
||||
envVars := cmdObj.GetEnvVars()
|
||||
for _, regexStr := range []string{
|
||||
`^VISUAL=.*$`,
|
||||
`^EDITOR=.*$`,
|
||||
`^GIT_EDITOR=.*$`,
|
||||
"^" + daemon.DaemonKindEnvKey + "=" + string(daemon.ExitImmediately) + "$",
|
||||
`^GIT_SEQUENCE_EDITOR=.*$`,
|
||||
"^" + daemon.DaemonKindEnvKey + "=" + strconv.Itoa(int(daemon.DaemonKindExitImmediately)) + "$",
|
||||
} {
|
||||
regexStr := regexStr
|
||||
foundMatch := lo.ContainsBy(envVars, func(envVar string) bool {
|
||||
@@ -75,7 +100,7 @@ func TestRebaseSkipEditorCommand(t *testing.T) {
|
||||
return "", nil
|
||||
})
|
||||
instance := buildRebaseCommands(commonDeps{runner: runner})
|
||||
err := instance.runSkipEditorCommand(instance.cmd.New(commandStr))
|
||||
err := instance.runSkipEditorCommand(instance.cmd.New(cmdArgs))
|
||||
assert.NoError(t, err)
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
@@ -124,11 +149,11 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
|
||||
commitIndex: 0,
|
||||
fileName: "test999.txt",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git rebase --interactive --autostash --keep-empty abcdef`, "", nil).
|
||||
Expect(`git cat-file -e HEAD^:"test999.txt"`, "", nil).
|
||||
Expect(`git checkout HEAD^ -- "test999.txt"`, "", nil).
|
||||
Expect(`git commit --amend --no-edit --allow-empty`, "", nil).
|
||||
Expect(`git rebase --continue`, "", nil),
|
||||
ExpectGitArgs([]string{"rebase", "--interactive", "--autostash", "--keep-empty", "--no-autosquash", "--rebase-merges", "abcdef"}, "", nil).
|
||||
ExpectGitArgs([]string{"cat-file", "-e", "HEAD^:test999.txt"}, "", nil).
|
||||
ExpectGitArgs([]string{"checkout", "HEAD^", "--", "test999.txt"}, "", nil).
|
||||
ExpectGitArgs([]string{"commit", "--amend", "--no-edit", "--allow-empty"}, "", nil).
|
||||
ExpectGitArgs([]string{"rebase", "--continue"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -141,8 +166,9 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildRebaseCommands(commonDeps{
|
||||
runner: s.runner,
|
||||
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
|
||||
runner: s.runner,
|
||||
gitVersion: &GitVersion{2, 26, 0, ""},
|
||||
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
|
||||
})
|
||||
|
||||
s.test(instance.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -27,12 +26,15 @@ func NewReflogCommitLoader(common *common.Common, cmd oscommands.ICmdObjBuilder)
|
||||
func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string) ([]*models.Commit, bool, error) {
|
||||
commits := make([]*models.Commit, 0)
|
||||
|
||||
filterPathArg := ""
|
||||
if filterPath != "" {
|
||||
filterPathArg = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(filterPath))
|
||||
}
|
||||
cmdArgs := NewGitCmd("log").
|
||||
Config("log.showSignature=false").
|
||||
Arg("-g").
|
||||
Arg("--abbrev=40").
|
||||
Arg("--format=%h%x00%ct%x00%gs%x00%p").
|
||||
ArgIf(filterPath != "", "--follow", "--", filterPath).
|
||||
ToArgv()
|
||||
|
||||
cmdObj := self.cmd.New(fmt.Sprintf(`git -c log.showSignature=false log -g --abbrev=40 --format="%s"%s`, "%h%x00%ct%x00%gs%x00%p", filterPathArg)).DontLog()
|
||||
cmdObj := self.cmd.New(cmdArgs).DontLog()
|
||||
onlyObtainedNewReflogCommits := false
|
||||
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
|
||||
fields := strings.SplitN(line, "\x00", 4)
|
||||
@@ -52,7 +54,7 @@ func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit
|
||||
Sha: fields[0],
|
||||
Name: fields[2],
|
||||
UnixTimestamp: int64(unixTimestamp),
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
Parents: parents,
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
{
|
||||
testName: "no reflog entries",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, "", nil),
|
||||
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%p"}, "", nil),
|
||||
|
||||
lastReflogCommit: nil,
|
||||
expectedCommits: []*models.Commit{},
|
||||
@@ -44,42 +44,42 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
{
|
||||
testName: "some reflog entries",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, reflogOutput, nil),
|
||||
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%p"}, reflogOutput, nil),
|
||||
|
||||
lastReflogCommit: nil,
|
||||
expectedCommits: []*models.Commit{
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from B to A",
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from master to A",
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "f4ddf2f0d4be4ccc7efa",
|
||||
Name: "checkout: moving from A to master",
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643149435,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
@@ -90,12 +90,12 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
{
|
||||
testName: "some reflog entries where last commit is given",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, reflogOutput, nil),
|
||||
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%p"}, reflogOutput, nil),
|
||||
|
||||
lastReflogCommit: &models.Commit{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from B to A",
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
@@ -103,7 +103,7 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
@@ -114,12 +114,12 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
{
|
||||
testName: "when passing filterPath",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p" --follow -- "path"`, reflogOutput, nil),
|
||||
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%p", "--follow", "--", "path"}, reflogOutput, nil),
|
||||
|
||||
lastReflogCommit: &models.Commit{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from B to A",
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
@@ -128,7 +128,7 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
@@ -139,7 +139,7 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
{
|
||||
testName: "when command returns error",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, "", errors.New("haha")),
|
||||
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%p"}, "", errors.New("haha")),
|
||||
|
||||
lastReflogCommit: nil,
|
||||
filterPath: "",
|
||||
|
||||
@@ -15,44 +15,52 @@ func NewRemoteCommands(gitCommon *GitCommon) *RemoteCommands {
|
||||
}
|
||||
|
||||
func (self *RemoteCommands) AddRemote(name string, url string) error {
|
||||
return self.cmd.
|
||||
New(fmt.Sprintf("git remote add %s %s", self.cmd.Quote(name), self.cmd.Quote(url))).
|
||||
Run()
|
||||
cmdArgs := NewGitCmd("remote").
|
||||
Arg("add", name, url).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *RemoteCommands) RemoveRemote(name string) error {
|
||||
return self.cmd.
|
||||
New(fmt.Sprintf("git remote remove %s", self.cmd.Quote(name))).
|
||||
Run()
|
||||
cmdArgs := NewGitCmd("remote").
|
||||
Arg("remove", name).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *RemoteCommands) RenameRemote(oldRemoteName string, newRemoteName string) error {
|
||||
return self.cmd.
|
||||
New(fmt.Sprintf("git remote rename %s %s", self.cmd.Quote(oldRemoteName), self.cmd.Quote(newRemoteName))).
|
||||
Run()
|
||||
cmdArgs := NewGitCmd("remote").
|
||||
Arg("rename", oldRemoteName, newRemoteName).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *RemoteCommands) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
|
||||
return self.cmd.
|
||||
New(fmt.Sprintf("git remote set-url %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(updatedUrl))).
|
||||
Run()
|
||||
cmdArgs := NewGitCmd("remote").
|
||||
Arg("set-url", remoteName, updatedUrl).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *RemoteCommands) DeleteRemoteBranch(remoteName string, branchName string) error {
|
||||
command := fmt.Sprintf("git push %s --delete %s", self.cmd.Quote(remoteName), self.cmd.Quote(branchName))
|
||||
return self.cmd.New(command).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
cmdArgs := NewGitCmd("push").
|
||||
Arg(remoteName, "--delete", branchName).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
}
|
||||
|
||||
// CheckRemoteBranchExists Returns remote branch
|
||||
func (self *RemoteCommands) CheckRemoteBranchExists(branchName string) bool {
|
||||
_, err := self.cmd.
|
||||
New(
|
||||
fmt.Sprintf("git show-ref --verify -- refs/remotes/origin/%s",
|
||||
self.cmd.Quote(branchName),
|
||||
),
|
||||
).
|
||||
DontLog().
|
||||
RunWithOutput()
|
||||
cmdArgs := NewGitCmd("show-ref").
|
||||
Arg("--verify", "--", fmt.Sprintf("refs/remotes/origin/%s", branchName)).
|
||||
ToArgv()
|
||||
|
||||
_, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ func NewRemoteLoader(
|
||||
}
|
||||
|
||||
func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
|
||||
remoteBranchesStr, err := self.cmd.New("git branch -r").DontLog().RunWithOutput()
|
||||
cmdArgs := NewGitCmd("branch").Arg("-r").ToArgv()
|
||||
remoteBranchesStr, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -26,57 +26,94 @@ func NewStashCommands(
|
||||
}
|
||||
|
||||
func (self *StashCommands) DropNewest() error {
|
||||
return self.cmd.New("git stash drop").Run()
|
||||
cmdArgs := NewGitCmd("stash").Arg("drop").ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *StashCommands) Drop(index int) error {
|
||||
return self.cmd.New(fmt.Sprintf("git stash drop stash@{%d}", index)).Run()
|
||||
cmdArgs := NewGitCmd("stash").Arg("drop", fmt.Sprintf("stash@{%d}", index)).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *StashCommands) Pop(index int) error {
|
||||
return self.cmd.New(fmt.Sprintf("git stash pop stash@{%d}", index)).Run()
|
||||
cmdArgs := NewGitCmd("stash").Arg("pop", fmt.Sprintf("stash@{%d}", index)).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *StashCommands) Apply(index int) error {
|
||||
return self.cmd.New(fmt.Sprintf("git stash apply stash@{%d}", index)).Run()
|
||||
cmdArgs := NewGitCmd("stash").Arg("apply", fmt.Sprintf("stash@{%d}", index)).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// Save save stash
|
||||
func (self *StashCommands) Save(message string) error {
|
||||
return self.cmd.New("git stash save " + self.cmd.Quote(message)).Run()
|
||||
cmdArgs := NewGitCmd("stash").Arg("save", message).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *StashCommands) Store(sha string, message string) error {
|
||||
trimmedMessage := strings.Trim(message, " \t")
|
||||
if len(trimmedMessage) > 0 {
|
||||
return self.cmd.New(fmt.Sprintf("git stash store %s -m %s", self.cmd.Quote(sha), self.cmd.Quote(trimmedMessage))).Run()
|
||||
}
|
||||
return self.cmd.New(fmt.Sprintf("git stash store %s", self.cmd.Quote(sha))).Run()
|
||||
|
||||
cmdArgs := NewGitCmd("stash").Arg("store", sha).
|
||||
ArgIf(trimmedMessage != "", "-m", trimmedMessage).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *StashCommands) Sha(index int) (string, error) {
|
||||
sha, _, err := self.cmd.New(fmt.Sprintf("git rev-parse refs/stash@{%d}", index)).DontLog().RunWithOutputs()
|
||||
cmdArgs := NewGitCmd("rev-parse").
|
||||
Arg(fmt.Sprintf("refs/stash@{%d}", index)).
|
||||
ToArgv()
|
||||
|
||||
sha, _, err := self.cmd.New(cmdArgs).DontLog().RunWithOutputs()
|
||||
return strings.Trim(sha, "\r\n"), err
|
||||
}
|
||||
|
||||
func (self *StashCommands) ShowStashEntryCmdObj(index int) oscommands.ICmdObj {
|
||||
cmdStr := fmt.Sprintf("git stash show -p --stat --color=%s --unified=%d stash@{%d}", self.UserConfig.Git.Paging.ColorArg, self.UserConfig.Git.DiffContextSize, index)
|
||||
func (self *StashCommands) ShowStashEntryCmdObj(index int, ignoreWhitespace bool) oscommands.ICmdObj {
|
||||
cmdArgs := NewGitCmd("stash").Arg("show").
|
||||
Arg("-p").
|
||||
Arg("--stat").
|
||||
Arg(fmt.Sprintf("--color=%s", self.UserConfig.Git.Paging.ColorArg)).
|
||||
Arg(fmt.Sprintf("--unified=%d", self.UserConfig.Git.DiffContextSize)).
|
||||
ArgIf(ignoreWhitespace, "--ignore-all-space").
|
||||
Arg(fmt.Sprintf("stash@{%d}", index)).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdStr).DontLog()
|
||||
return self.cmd.New(cmdArgs).DontLog()
|
||||
}
|
||||
|
||||
func (self *StashCommands) StashAndKeepIndex(message string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git stash save %s --keep-index", self.cmd.Quote(message))).Run()
|
||||
cmdArgs := NewGitCmd("stash").Arg("save", message, "--keep-index").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *StashCommands) StashUnstagedChanges(message string) error {
|
||||
if err := self.cmd.New("git commit --no-verify -m \"[lazygit] stashing unstaged changes\"").Run(); err != nil {
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("commit").
|
||||
Arg("--no-verify", "-m", "[lazygit] stashing unstaged changes").
|
||||
ToArgv(),
|
||||
).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.Save(message); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.cmd.New("git reset --soft HEAD^").Run(); err != nil {
|
||||
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("reset").Arg("--soft", "HEAD^").ToArgv(),
|
||||
).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -86,7 +123,9 @@ func (self *StashCommands) StashUnstagedChanges(message string) error {
|
||||
// shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
|
||||
func (self *StashCommands) SaveStagedChanges(message string) error {
|
||||
// wrap in 'writing', which uses a mutex
|
||||
if err := self.cmd.New("git stash --keep-index").Run(); err != nil {
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("stash").Arg("--keep-index").ToArgv(),
|
||||
).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -94,15 +133,22 @@ func (self *StashCommands) SaveStagedChanges(message string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.cmd.New("git stash apply stash@{1}").Run(); err != nil {
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("stash").Arg("apply", "stash@{1}").ToArgv(),
|
||||
).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.os.PipeCommands("git stash show -p", "git apply -R"); err != nil {
|
||||
if err := self.os.PipeCommands(
|
||||
self.cmd.New(NewGitCmd("stash").Arg("show", "-p").ToArgv()),
|
||||
self.cmd.New(NewGitCmd("apply").Arg("-R").ToArgv()),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.cmd.New("git stash drop stash@{1}").Run(); err != nil {
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("stash").Arg("drop", "stash@{1}").ToArgv(),
|
||||
).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -124,7 +170,10 @@ func (self *StashCommands) SaveStagedChanges(message string) error {
|
||||
}
|
||||
|
||||
func (self *StashCommands) StashIncludeUntrackedChanges(message string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git stash save %s --include-untracked", self.cmd.Quote(message))).Run()
|
||||
return self.cmd.New(
|
||||
NewGitCmd("stash").Arg("save", message, "--include-untracked").
|
||||
ToArgv(),
|
||||
).Run()
|
||||
}
|
||||
|
||||
func (self *StashCommands) Rename(index int, message string) error {
|
||||
|
||||
@@ -32,7 +32,8 @@ func (self *StashLoader) GetStashEntries(filterPath string) []*models.StashEntry
|
||||
return self.getUnfilteredStashEntries()
|
||||
}
|
||||
|
||||
rawString, err := self.cmd.New("git stash list --name-only").DontLog().RunWithOutput()
|
||||
cmdArgs := NewGitCmd("stash").Arg("list", "--name-only").ToArgv()
|
||||
rawString, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return self.getUnfilteredStashEntries()
|
||||
}
|
||||
@@ -65,7 +66,9 @@ outer:
|
||||
}
|
||||
|
||||
func (self *StashLoader) getUnfilteredStashEntries() []*models.StashEntry {
|
||||
rawString, _ := self.cmd.New("git stash list -z --pretty='%gs'").DontLog().RunWithOutput()
|
||||
cmdArgs := NewGitCmd("stash").Arg("list", "-z", "--pretty=%gs").ToArgv()
|
||||
|
||||
rawString, _ := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
return slices.MapWithIndex(utils.SplitNul(rawString), func(line string, index int) *models.StashEntry {
|
||||
return self.stashEntryFromLine(line, index)
|
||||
})
|
||||
|
||||
@@ -22,15 +22,14 @@ func TestGetStashEntries(t *testing.T) {
|
||||
"No stash entries found",
|
||||
"",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git stash list -z --pretty='%gs'`, "", nil),
|
||||
ExpectGitArgs([]string{"stash", "list", "-z", "--pretty=%gs"}, "", nil),
|
||||
[]*models.StashEntry{},
|
||||
},
|
||||
{
|
||||
"Several stash entries found",
|
||||
"",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(
|
||||
`git stash list -z --pretty='%gs'`,
|
||||
ExpectGitArgs([]string{"stash", "list", "-z", "--pretty=%gs"},
|
||||
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build\x00WIP on master: bb86a3f update github template\x00",
|
||||
nil,
|
||||
),
|
||||
|
||||
@@ -99,24 +99,34 @@ func TestStashSha(t *testing.T) {
|
||||
|
||||
func TestStashStashEntryCmdObj(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
index int
|
||||
contextSize int
|
||||
expected string
|
||||
testName string
|
||||
index int
|
||||
contextSize int
|
||||
ignoreWhitespace bool
|
||||
expected []string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Default case",
|
||||
index: 5,
|
||||
contextSize: 3,
|
||||
expected: "git stash show -p --stat --color=always --unified=3 stash@{5}",
|
||||
testName: "Default case",
|
||||
index: 5,
|
||||
contextSize: 3,
|
||||
ignoreWhitespace: false,
|
||||
expected: []string{"git", "stash", "show", "-p", "--stat", "--color=always", "--unified=3", "stash@{5}"},
|
||||
},
|
||||
{
|
||||
testName: "Show diff with custom context size",
|
||||
index: 5,
|
||||
contextSize: 77,
|
||||
expected: "git stash show -p --stat --color=always --unified=77 stash@{5}",
|
||||
testName: "Show diff with custom context size",
|
||||
index: 5,
|
||||
contextSize: 77,
|
||||
ignoreWhitespace: false,
|
||||
expected: []string{"git", "stash", "show", "-p", "--stat", "--color=always", "--unified=77", "stash@{5}"},
|
||||
},
|
||||
{
|
||||
testName: "Default case",
|
||||
index: 5,
|
||||
contextSize: 3,
|
||||
ignoreWhitespace: true,
|
||||
expected: []string{"git", "stash", "show", "-p", "--stat", "--color=always", "--unified=3", "--ignore-all-space", "stash@{5}"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -127,7 +137,7 @@ func TestStashStashEntryCmdObj(t *testing.T) {
|
||||
userConfig.Git.DiffContextSize = s.contextSize
|
||||
instance := buildStashCommands(commonDeps{userConfig: userConfig})
|
||||
|
||||
cmdStr := instance.ShowStashEntryCmdObj(s.index).ToString()
|
||||
cmdStr := instance.ShowStashEntryCmdObj(s.index, s.ignoreWhitespace).Args()
|
||||
assert.Equal(t, s.expected, cmdStr)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -56,7 +56,9 @@ func (self *StatusCommands) IsBareRepo() (bool, error) {
|
||||
}
|
||||
|
||||
func IsBareRepo(osCommand *oscommands.OSCommand) (bool, error) {
|
||||
res, err := osCommand.Cmd.New("git rev-parse --is-bare-repository").DontLog().RunWithOutput()
|
||||
res, err := osCommand.Cmd.New(
|
||||
NewGitCmd("rev-parse").Arg("--is-bare-repository").ToArgv(),
|
||||
).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package git_commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -82,38 +81,60 @@ func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.cmd.New("git -C " + self.cmd.Quote(submodule.Path) + " stash --include-untracked").Run()
|
||||
cmdArgs := NewGitCmd("stash").
|
||||
RepoPath(submodule.Path).
|
||||
Arg("--include-untracked").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) Reset(submodule *models.SubmoduleConfig) error {
|
||||
return self.cmd.New("git submodule update --init --force -- " + self.cmd.Quote(submodule.Path)).Run()
|
||||
cmdArgs := NewGitCmd("submodule").
|
||||
Arg("update", "--init", "--force", "--", submodule.Path).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) UpdateAll() error {
|
||||
// not doing an --init here because the user probably doesn't want that
|
||||
return self.cmd.New("git submodule update --force").Run()
|
||||
cmdArgs := NewGitCmd("submodule").Arg("update", "--force").ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error {
|
||||
// based on https://gist.github.com/myusuf3/7f645819ded92bda6677
|
||||
|
||||
if err := self.cmd.New("git submodule deinit --force -- " + self.cmd.Quote(submodule.Path)).Run(); err != nil {
|
||||
if strings.Contains(err.Error(), "did not match any file(s) known to git") {
|
||||
if err := self.cmd.New("git config --file .gitmodules --remove-section submodule." + self.cmd.Quote(submodule.Name)).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("submodule").
|
||||
Arg("deinit", "--force", "--", submodule.Path).ToArgv(),
|
||||
).Run(); err != nil {
|
||||
if !strings.Contains(err.Error(), "did not match any file(s) known to git") {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.cmd.New("git config --remove-section submodule." + self.cmd.Quote(submodule.Name)).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("config").
|
||||
Arg("--file", ".gitmodules", "--remove-section", "submodule."+submodule.Path).
|
||||
ToArgv(),
|
||||
).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if there's an error here about it not existing then we'll just continue to do `git rm`
|
||||
} else {
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("config").
|
||||
Arg("--remove-section", "submodule."+submodule.Path).
|
||||
ToArgv(),
|
||||
).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := self.cmd.New("git rm --force -r " + submodule.Path).Run(); err != nil {
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("rm").Arg("--force", "-r", submodule.Path).ToArgv(),
|
||||
).Run(); err != nil {
|
||||
// if the directory isn't there then that's fine
|
||||
self.Log.Error(err)
|
||||
}
|
||||
@@ -122,24 +143,35 @@ func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error {
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) Add(name string, path string, url string) error {
|
||||
return self.cmd.
|
||||
New(
|
||||
fmt.Sprintf(
|
||||
"git submodule add --force --name %s -- %s %s ",
|
||||
self.cmd.Quote(name),
|
||||
self.cmd.Quote(url),
|
||||
self.cmd.Quote(path),
|
||||
)).
|
||||
Run()
|
||||
cmdArgs := NewGitCmd("submodule").
|
||||
Arg("add").
|
||||
Arg("--force").
|
||||
Arg("--name").
|
||||
Arg(name).
|
||||
Arg("--").
|
||||
Arg(url).
|
||||
Arg(path).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) UpdateUrl(name string, path string, newUrl string) error {
|
||||
setUrlCmdStr := NewGitCmd("config").
|
||||
Arg(
|
||||
"--file", ".gitmodules", "submodule."+name+".url", newUrl,
|
||||
).
|
||||
ToArgv()
|
||||
|
||||
// the set-url command is only for later git versions so we're doing it manually here
|
||||
if err := self.cmd.New("git config --file .gitmodules submodule." + self.cmd.Quote(name) + ".url " + self.cmd.Quote(newUrl)).Run(); err != nil {
|
||||
if err := self.cmd.New(setUrlCmdStr).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.cmd.New("git submodule sync -- " + self.cmd.Quote(path)).Run(); err != nil {
|
||||
syncCmdStr := NewGitCmd("submodule").Arg("sync", "--", path).
|
||||
ToArgv()
|
||||
|
||||
if err := self.cmd.New(syncCmdStr).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -147,27 +179,45 @@ func (self *SubmoduleCommands) UpdateUrl(name string, path string, newUrl string
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) Init(path string) error {
|
||||
return self.cmd.New("git submodule init -- " + self.cmd.Quote(path)).Run()
|
||||
cmdArgs := NewGitCmd("submodule").Arg("init", "--", path).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) Update(path string) error {
|
||||
return self.cmd.New("git submodule update --init -- " + self.cmd.Quote(path)).Run()
|
||||
cmdArgs := NewGitCmd("submodule").Arg("update", "--init", "--", path).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) BulkInitCmdObj() oscommands.ICmdObj {
|
||||
return self.cmd.New("git submodule init")
|
||||
cmdArgs := NewGitCmd("submodule").Arg("init").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) BulkUpdateCmdObj() oscommands.ICmdObj {
|
||||
return self.cmd.New("git submodule update")
|
||||
cmdArgs := NewGitCmd("submodule").Arg("update").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) ForceBulkUpdateCmdObj() oscommands.ICmdObj {
|
||||
return self.cmd.New("git submodule update --force")
|
||||
cmdArgs := NewGitCmd("submodule").Arg("update", "--force").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) BulkDeinitCmdObj() oscommands.ICmdObj {
|
||||
return self.cmd.New("git submodule deinit --all --force")
|
||||
cmdArgs := NewGitCmd("submodule").Arg("deinit", "--all", "--force").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) ResetSubmodules(submodules []*models.SubmoduleConfig) error {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
@@ -26,32 +24,18 @@ type PushOpts struct {
|
||||
}
|
||||
|
||||
func (self *SyncCommands) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error) {
|
||||
cmdStr := "git push"
|
||||
|
||||
if opts.Force {
|
||||
if self.version.IsOlderThan(2, 30, 0) {
|
||||
cmdStr += " --force-with-lease"
|
||||
} else {
|
||||
cmdStr += " --force-with-lease --force-if-includes"
|
||||
}
|
||||
if opts.UpstreamBranch != "" && opts.UpstreamRemote == "" {
|
||||
return nil, errors.New(self.Tr.MustSpecifyOriginError)
|
||||
}
|
||||
|
||||
if opts.SetUpstream {
|
||||
cmdStr += " --set-upstream"
|
||||
}
|
||||
cmdArgs := NewGitCmd("push").
|
||||
ArgIf(opts.Force, "--force-with-lease").
|
||||
ArgIf(opts.SetUpstream, "--set-upstream").
|
||||
ArgIf(opts.UpstreamRemote != "", opts.UpstreamRemote).
|
||||
ArgIf(opts.UpstreamBranch != "", opts.UpstreamBranch).
|
||||
ToArgv()
|
||||
|
||||
if opts.UpstreamRemote != "" {
|
||||
cmdStr += " " + self.cmd.Quote(opts.UpstreamRemote)
|
||||
}
|
||||
|
||||
if opts.UpstreamBranch != "" {
|
||||
if opts.UpstreamRemote == "" {
|
||||
return nil, errors.New(self.Tr.MustSpecifyOriginError)
|
||||
}
|
||||
cmdStr += " " + self.cmd.Quote(opts.UpstreamBranch)
|
||||
}
|
||||
|
||||
cmdObj := self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex)
|
||||
cmdObj := self.cmd.New(cmdArgs).PromptOnCredentialRequest().WithMutex(self.syncMutex)
|
||||
return cmdObj, nil
|
||||
}
|
||||
|
||||
@@ -72,16 +56,12 @@ type FetchOptions struct {
|
||||
|
||||
// Fetch fetch git repo
|
||||
func (self *SyncCommands) Fetch(opts FetchOptions) error {
|
||||
cmdStr := "git fetch"
|
||||
cmdArgs := NewGitCmd("fetch").
|
||||
ArgIf(opts.RemoteName != "", opts.RemoteName).
|
||||
ArgIf(opts.BranchName != "", opts.BranchName).
|
||||
ToArgv()
|
||||
|
||||
if opts.RemoteName != "" {
|
||||
cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.RemoteName))
|
||||
}
|
||||
if opts.BranchName != "" {
|
||||
cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.BranchName))
|
||||
}
|
||||
|
||||
cmdObj := self.cmd.New(cmdStr)
|
||||
cmdObj := self.cmd.New(cmdArgs)
|
||||
if opts.Background {
|
||||
cmdObj.DontLog().FailOnCredentialRequest()
|
||||
} else {
|
||||
@@ -97,30 +77,31 @@ type PullOptions struct {
|
||||
}
|
||||
|
||||
func (self *SyncCommands) Pull(opts PullOptions) error {
|
||||
cmdStr := "git pull --no-edit"
|
||||
|
||||
if opts.FastForwardOnly {
|
||||
cmdStr += " --ff-only"
|
||||
}
|
||||
|
||||
if opts.RemoteName != "" {
|
||||
cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.RemoteName))
|
||||
}
|
||||
if opts.BranchName != "" {
|
||||
cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.BranchName))
|
||||
}
|
||||
cmdArgs := NewGitCmd("pull").
|
||||
Arg("--no-edit").
|
||||
ArgIf(opts.FastForwardOnly, "--ff-only").
|
||||
ArgIf(opts.RemoteName != "", opts.RemoteName).
|
||||
ArgIf(opts.BranchName != "", opts.BranchName).
|
||||
ToArgv()
|
||||
|
||||
// setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
|
||||
// has 'pull.rebase = interactive' configured.
|
||||
return self.cmd.New(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
return self.cmd.New(cmdArgs).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
}
|
||||
|
||||
func (self *SyncCommands) FastForward(branchName string, remoteName string, remoteBranchName string) error {
|
||||
cmdStr := fmt.Sprintf("git fetch %s %s:%s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName), self.cmd.Quote(branchName))
|
||||
return self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
cmdArgs := NewGitCmd("fetch").
|
||||
Arg(remoteName).
|
||||
Arg(remoteBranchName + ":" + branchName).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
}
|
||||
|
||||
func (self *SyncCommands) FetchRemote(remoteName string) error {
|
||||
cmdStr := fmt.Sprintf("git fetch %s", self.cmd.Quote(remoteName))
|
||||
return self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
cmdArgs := NewGitCmd("fetch").
|
||||
Arg(remoteName).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
func TestSyncPush(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
version *GitVersion
|
||||
opts PushOpts
|
||||
test func(oscommands.ICmdObj, error)
|
||||
}
|
||||
@@ -18,47 +17,34 @@ func TestSyncPush(t *testing.T) {
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Push with force disabled",
|
||||
version: &GitVersion{2, 29, 3, ""},
|
||||
opts: PushOpts{Force: false},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.ToString(), "git push")
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with force enabled",
|
||||
version: &GitVersion{2, 29, 3, ""},
|
||||
opts: PushOpts{Force: true},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.ToString(), "git push --force-with-lease")
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with force enabled (>= 2.30.0)",
|
||||
version: &GitVersion{2, 30, 0, ""},
|
||||
opts: PushOpts{Force: true},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.ToString(), "git push --force-with-lease --force-if-includes")
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force-with-lease"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with force disabled, upstream supplied",
|
||||
version: &GitVersion{2, 29, 3, ""},
|
||||
opts: PushOpts{
|
||||
Force: false,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.ToString(), `git push "origin" "master"`)
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "origin", "master"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with force disabled, setting upstream",
|
||||
version: &GitVersion{2, 29, 3, ""},
|
||||
opts: PushOpts{
|
||||
Force: false,
|
||||
UpstreamRemote: "origin",
|
||||
@@ -66,13 +52,12 @@ func TestSyncPush(t *testing.T) {
|
||||
SetUpstream: true,
|
||||
},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.ToString(), `git push --set-upstream "origin" "master"`)
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--set-upstream", "origin", "master"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with force enabled, setting upstream",
|
||||
version: &GitVersion{2, 29, 3, ""},
|
||||
opts: PushOpts{
|
||||
Force: true,
|
||||
UpstreamRemote: "origin",
|
||||
@@ -80,27 +65,12 @@ func TestSyncPush(t *testing.T) {
|
||||
SetUpstream: true,
|
||||
},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.ToString(), `git push --force-with-lease --set-upstream "origin" "master"`)
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with force enabled, setting upstream (>= 2.30.0)",
|
||||
version: &GitVersion{2, 30, 0, ""},
|
||||
opts: PushOpts{
|
||||
Force: true,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
SetUpstream: true,
|
||||
},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.ToString(), `git push --force-with-lease --force-if-includes --set-upstream "origin" "master"`)
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force-with-lease", "--set-upstream", "origin", "master"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with remote branch but no origin",
|
||||
version: &GitVersion{2, 29, 3, ""},
|
||||
opts: PushOpts{
|
||||
Force: true,
|
||||
UpstreamRemote: "",
|
||||
@@ -117,7 +87,7 @@ func TestSyncPush(t *testing.T) {
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildSyncCommands(commonDeps{gitVersion: s.version})
|
||||
instance := buildSyncCommands(commonDeps{})
|
||||
s.test(instance.PushCmdObj(s.opts))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type TagCommands struct {
|
||||
*GitCommon
|
||||
}
|
||||
@@ -14,18 +10,33 @@ func NewTagCommands(gitCommon *GitCommon) *TagCommands {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TagCommands) CreateLightweight(tagName string, commitSha string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git tag -- %s %s", self.cmd.Quote(tagName), commitSha)).Run()
|
||||
func (self *TagCommands) CreateLightweight(tagName string, ref string) error {
|
||||
cmdArgs := NewGitCmd("tag").Arg("--", tagName).
|
||||
ArgIf(len(ref) > 0, ref).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *TagCommands) CreateAnnotated(tagName, commitSha, msg string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git tag %s %s -m %s", tagName, commitSha, self.cmd.Quote(msg))).Run()
|
||||
func (self *TagCommands) CreateAnnotated(tagName, ref, msg string) error {
|
||||
cmdArgs := NewGitCmd("tag").Arg(tagName).
|
||||
ArgIf(len(ref) > 0, ref).
|
||||
Arg("-m", msg).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *TagCommands) Delete(tagName string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git tag -d %s", self.cmd.Quote(tagName))).Run()
|
||||
cmdArgs := NewGitCmd("tag").Arg("-d", tagName).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *TagCommands) Push(remoteName string, tagName string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git push %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
cmdArgs := NewGitCmd("push").Arg(remoteName, "tag", tagName).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
@@ -12,40 +12,43 @@ import (
|
||||
|
||||
type TagLoader struct {
|
||||
*common.Common
|
||||
version *GitVersion
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
}
|
||||
|
||||
func NewTagLoader(
|
||||
common *common.Common,
|
||||
version *GitVersion,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
) *TagLoader {
|
||||
return &TagLoader{
|
||||
Common: common,
|
||||
version: version,
|
||||
cmd: cmd,
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TagLoader) GetTags() ([]*models.Tag, error) {
|
||||
// get remote branches, sorted by creation date (descending)
|
||||
// see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt
|
||||
sortKey := "-creatordate"
|
||||
if self.version.IsOlderThan(2, 7, 0) {
|
||||
sortKey = "-v:refname"
|
||||
}
|
||||
|
||||
tagsOutput, err := self.cmd.New(fmt.Sprintf(`git tag --list --sort=%s`, sortKey)).DontLog().RunWithOutput()
|
||||
cmdArgs := NewGitCmd("tag").Arg("--list", "-n", "--sort=-creatordate").ToArgv()
|
||||
tagsOutput, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
split := utils.SplitLines(tagsOutput)
|
||||
|
||||
tags := slices.Map(split, func(tagName string) *models.Tag {
|
||||
lineRegex := regexp.MustCompile(`^([^\s]+)(\s+)?(.*)$`)
|
||||
|
||||
tags := slices.Map(split, func(line string) *models.Tag {
|
||||
matches := lineRegex.FindStringSubmatch(line)
|
||||
tagName := matches[1]
|
||||
message := ""
|
||||
if len(matches) > 3 {
|
||||
message = matches[3]
|
||||
}
|
||||
|
||||
return &models.Tag{
|
||||
Name: tagName,
|
||||
Name: tagName,
|
||||
Message: message,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -9,18 +9,14 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const tagsOutput = `v0.34
|
||||
v0.33
|
||||
v0.32.2
|
||||
v0.32.1
|
||||
v0.32
|
||||
testtag
|
||||
const tagsOutput = `tag1 this is my message
|
||||
tag2
|
||||
tag3 this is my other message
|
||||
`
|
||||
|
||||
func TestGetTags(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
gitVersion *GitVersion
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
expectedTags []*models.Tag
|
||||
expectedError error
|
||||
@@ -28,48 +24,20 @@ func TestGetTags(t *testing.T) {
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "should return no tags if there are none",
|
||||
gitVersion: &GitVersion{2, 7, 0, ""},
|
||||
testName: "should return no tags if there are none",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git tag --list --sort=-creatordate`, "", nil),
|
||||
ExpectGitArgs([]string{"tag", "--list", "-n", "--sort=-creatordate"}, "", nil),
|
||||
expectedTags: []*models.Tag{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should return no tags if there are none (< 2.7.0)",
|
||||
gitVersion: &GitVersion{2, 6, 7, ""},
|
||||
testName: "should return tags if present",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git tag --list --sort=-v:refname`, "", nil),
|
||||
expectedTags: []*models.Tag{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should return tags if present",
|
||||
gitVersion: &GitVersion{2, 7, 0, ""},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git tag --list --sort=-creatordate`, tagsOutput, nil),
|
||||
ExpectGitArgs([]string{"tag", "--list", "-n", "--sort=-creatordate"}, tagsOutput, nil),
|
||||
expectedTags: []*models.Tag{
|
||||
{Name: "v0.34"},
|
||||
{Name: "v0.33"},
|
||||
{Name: "v0.32.2"},
|
||||
{Name: "v0.32.1"},
|
||||
{Name: "v0.32"},
|
||||
{Name: "testtag"},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should return tags if present (< 2.7.0)",
|
||||
gitVersion: &GitVersion{2, 6, 7, ""},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git tag --list --sort=-v:refname`, tagsOutput, nil),
|
||||
expectedTags: []*models.Tag{
|
||||
{Name: "v0.34"},
|
||||
{Name: "v0.33"},
|
||||
{Name: "v0.32.2"},
|
||||
{Name: "v0.32.1"},
|
||||
{Name: "v0.32"},
|
||||
{Name: "testtag"},
|
||||
{Name: "tag1", Message: "this is my message"},
|
||||
{Name: "tag2", Message: ""},
|
||||
{Name: "tag3", Message: "this is my other message"},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
@@ -78,11 +46,10 @@ func TestGetTags(t *testing.T) {
|
||||
for _, scenario := range scenarios {
|
||||
scenario := scenario
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
loader := NewTagLoader(
|
||||
utils.NewDummyCommon(),
|
||||
scenario.gitVersion,
|
||||
oscommands.NewDummyCmdObjBuilder(scenario.runner),
|
||||
)
|
||||
loader := &TagLoader{
|
||||
Common: utils.NewDummyCommon(),
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
|
||||
}
|
||||
|
||||
tags, err := loader.GetTags()
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ type GitVersion struct {
|
||||
}
|
||||
|
||||
func GetGitVersion(osCommand *oscommands.OSCommand) (*GitVersion, error) {
|
||||
versionStr, _, err := osCommand.Cmd.New("git --version").RunWithOutputs()
|
||||
versionStr, _, err := osCommand.Cmd.New(NewGitCmd("--version").ToArgv()).RunWithOutputs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func ParseGitVersion(versionStr string) (*GitVersion, error) {
|
||||
// versionStr should be something like:
|
||||
// git version 2.39.0
|
||||
// git version 2.37.1 (Apple Git-137.1)
|
||||
re := regexp.MustCompile(`[^\d]+(\d+)(\.\d+)?(\.\d+)?(.*)`)
|
||||
re := regexp.MustCompile(`[^\d]*(\d+)(\.\d+)?(\.\d+)?(.*)`)
|
||||
matches := re.FindStringSubmatch(versionStr)
|
||||
|
||||
if len(matches) < 5 {
|
||||
@@ -65,3 +65,7 @@ func (v *GitVersion) IsOlderThan(major, minor, patch int) bool {
|
||||
required := major*1000*1000 + minor*1000 + patch
|
||||
return actual < required
|
||||
}
|
||||
|
||||
func (v *GitVersion) IsOlderThanVersion(version *GitVersion) bool {
|
||||
return v.IsOlderThan(version.Major, version.Minor, version.Patch)
|
||||
}
|
||||
|
||||
@@ -3,15 +3,10 @@ package git_commands
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type WorkingTreeCommands struct {
|
||||
@@ -33,7 +28,7 @@ func NewWorkingTreeCommands(
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) OpenMergeToolCmdObj() oscommands.ICmdObj {
|
||||
return self.cmd.New("git mergetool")
|
||||
return self.cmd.New(NewGitCmd("mergetool").ToArgv())
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) OpenMergeTool() error {
|
||||
@@ -46,33 +41,36 @@ func (self *WorkingTreeCommands) StageFile(path string) error {
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) StageFiles(paths []string) error {
|
||||
quotedPaths := slices.Map(paths, func(path string) string {
|
||||
return self.cmd.Quote(path)
|
||||
})
|
||||
return self.cmd.New(fmt.Sprintf("git add -- %s", strings.Join(quotedPaths, " "))).Run()
|
||||
cmdArgs := NewGitCmd("add").Arg("--").Arg(paths...).ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// StageAll stages all files
|
||||
func (self *WorkingTreeCommands) StageAll() error {
|
||||
return self.cmd.New("git add -A").Run()
|
||||
cmdArgs := NewGitCmd("add").Arg("-A").ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// UnstageAll unstages all files
|
||||
func (self *WorkingTreeCommands) UnstageAll() error {
|
||||
return self.cmd.New("git reset").Run()
|
||||
return self.cmd.New(NewGitCmd("reset").ToArgv()).Run()
|
||||
}
|
||||
|
||||
// UnStageFile unstages a file
|
||||
// we accept an array of filenames for the cases where a file has been renamed i.e.
|
||||
// we accept the current name and the previous name
|
||||
func (self *WorkingTreeCommands) UnStageFile(fileNames []string, reset bool) error {
|
||||
command := "git rm --cached --force -- %s"
|
||||
if reset {
|
||||
command = "git reset HEAD -- %s"
|
||||
}
|
||||
|
||||
for _, name := range fileNames {
|
||||
err := self.cmd.New(fmt.Sprintf(command, self.cmd.Quote(name))).Run()
|
||||
var cmdArgs []string
|
||||
if reset {
|
||||
cmdArgs = NewGitCmd("reset").Arg("HEAD", "--", name).ToArgv()
|
||||
} else {
|
||||
cmdArgs = NewGitCmd("rm").Arg("--cached", "--force", "--", name).ToArgv()
|
||||
}
|
||||
|
||||
err := self.cmd.New(cmdArgs).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -134,25 +132,32 @@ func (self *WorkingTreeCommands) DiscardAllFileChanges(file *models.File) error
|
||||
return nil
|
||||
}
|
||||
|
||||
quotedFileName := self.cmd.Quote(file.Name)
|
||||
|
||||
if file.ShortStatus == "AA" {
|
||||
if err := self.cmd.New("git checkout --ours -- " + quotedFileName).Run(); err != nil {
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("checkout").Arg("--ours", "--", file.Name).ToArgv(),
|
||||
).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.cmd.New("git add -- " + quotedFileName).Run(); err != nil {
|
||||
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("add").Arg("--", file.Name).ToArgv(),
|
||||
).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if file.ShortStatus == "DU" {
|
||||
return self.cmd.New("git rm -- " + quotedFileName).Run()
|
||||
return self.cmd.New(
|
||||
NewGitCmd("rm").Arg("--", file.Name).ToArgv(),
|
||||
).Run()
|
||||
}
|
||||
|
||||
// if the file isn't tracked, we assume you want to delete it
|
||||
if file.HasStagedChanges || file.HasMergeConflicts {
|
||||
if err := self.cmd.New("git reset -- " + quotedFileName).Run(); err != nil {
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("reset").Arg("--", file.Name).ToArgv(),
|
||||
).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -183,8 +188,8 @@ func (self *WorkingTreeCommands) DiscardUnstagedDirChanges(node IFileNode) error
|
||||
return err
|
||||
}
|
||||
|
||||
quotedPath := self.cmd.Quote(node.GetPath())
|
||||
if err := self.cmd.New("git checkout -- " + quotedPath).Run(); err != nil {
|
||||
cmdArgs := NewGitCmd("checkout").Arg("--", node.GetPath()).ToArgv()
|
||||
if err := self.cmd.New(cmdArgs).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -208,8 +213,8 @@ func (self *WorkingTreeCommands) RemoveUntrackedDirFiles(node IFileNode) error {
|
||||
|
||||
// DiscardUnstagedFileChanges directly
|
||||
func (self *WorkingTreeCommands) DiscardUnstagedFileChanges(file *models.File) error {
|
||||
quotedFileName := self.cmd.Quote(file.Name)
|
||||
return self.cmd.New("git checkout -- " + quotedFileName).Run()
|
||||
cmdArgs := NewGitCmd("checkout").Arg("--", file.Name).ToArgv()
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// Ignore adds a file to the gitignore for the repo
|
||||
@@ -230,106 +235,96 @@ func (self *WorkingTreeCommands) WorktreeFileDiff(file *models.File, plain bool,
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) oscommands.ICmdObj {
|
||||
cachedArg := ""
|
||||
trackedArg := "--"
|
||||
colorArg := self.UserConfig.Git.Paging.ColorArg
|
||||
quotedPath := self.cmd.Quote(node.GetPath())
|
||||
quotedPrevPath := ""
|
||||
ignoreWhitespaceArg := ""
|
||||
contextSize := self.UserConfig.Git.DiffContextSize
|
||||
if cached {
|
||||
cachedArg = " --cached"
|
||||
}
|
||||
if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached && node.GetIsFile() {
|
||||
trackedArg = "--no-index -- /dev/null"
|
||||
}
|
||||
if plain {
|
||||
colorArg = "never"
|
||||
}
|
||||
if ignoreWhitespace {
|
||||
ignoreWhitespaceArg = " --ignore-all-space"
|
||||
}
|
||||
if prevPath := node.GetPreviousPath(); prevPath != "" {
|
||||
quotedPrevPath = " " + self.cmd.Quote(prevPath)
|
||||
}
|
||||
|
||||
cmdStr := fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --color=%s%s%s %s %s%s", contextSize, colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath, quotedPrevPath)
|
||||
contextSize := self.UserConfig.Git.DiffContextSize
|
||||
prevPath := node.GetPreviousPath()
|
||||
noIndex := !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached && node.GetIsFile()
|
||||
|
||||
return self.cmd.New(cmdStr).DontLog()
|
||||
}
|
||||
cmdArgs := NewGitCmd("diff").
|
||||
Arg("--submodule").
|
||||
Arg("--no-ext-diff").
|
||||
Arg(fmt.Sprintf("--unified=%d", contextSize)).
|
||||
Arg(fmt.Sprintf("--color=%s", colorArg)).
|
||||
ArgIf(ignoreWhitespace, "--ignore-all-space").
|
||||
ArgIf(cached, "--cached").
|
||||
ArgIf(noIndex, "--no-index").
|
||||
Arg("--").
|
||||
ArgIf(noIndex, "/dev/null").
|
||||
Arg(node.GetPath()).
|
||||
ArgIf(prevPath != "", prevPath).
|
||||
ToArgv()
|
||||
|
||||
func (self *WorkingTreeCommands) ApplyPatch(patch string, flags ...string) error {
|
||||
filepath, err := self.SaveTemporaryPatch(patch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return self.ApplyPatchFile(filepath, flags...)
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) ApplyPatchFile(filepath string, flags ...string) error {
|
||||
flagStr := ""
|
||||
for _, flag := range flags {
|
||||
flagStr += " --" + flag
|
||||
}
|
||||
|
||||
return self.cmd.New(fmt.Sprintf("git apply%s %s", flagStr, self.cmd.Quote(filepath))).Run()
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) SaveTemporaryPatch(patch string) (string, error) {
|
||||
filepath := filepath.Join(self.os.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
|
||||
self.Log.Infof("saving temporary patch to %s", filepath)
|
||||
if err := self.os.CreateFileWithContent(filepath, patch); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath, nil
|
||||
return self.cmd.New(cmdArgs).DontLog()
|
||||
}
|
||||
|
||||
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
|
||||
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
|
||||
func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
|
||||
return self.ShowFileDiffCmdObj(from, to, reverse, fileName, plain).RunWithOutput()
|
||||
func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool,
|
||||
ignoreWhitespace bool,
|
||||
) (string, error) {
|
||||
return self.ShowFileDiffCmdObj(from, to, reverse, fileName, plain, ignoreWhitespace).RunWithOutput()
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) oscommands.ICmdObj {
|
||||
colorArg := self.UserConfig.Git.Paging.ColorArg
|
||||
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool,
|
||||
ignoreWhitespace bool,
|
||||
) oscommands.ICmdObj {
|
||||
contextSize := self.UserConfig.Git.DiffContextSize
|
||||
|
||||
colorArg := self.UserConfig.Git.Paging.ColorArg
|
||||
if plain {
|
||||
colorArg = "never"
|
||||
}
|
||||
|
||||
reverseFlag := ""
|
||||
if reverse {
|
||||
reverseFlag = " -R"
|
||||
}
|
||||
cmdArgs := NewGitCmd("diff").
|
||||
Arg("--submodule").
|
||||
Arg("--no-ext-diff").
|
||||
Arg(fmt.Sprintf("--unified=%d", contextSize)).
|
||||
Arg("--no-renames").
|
||||
Arg(fmt.Sprintf("--color=%s", colorArg)).
|
||||
Arg(from).
|
||||
Arg(to).
|
||||
ArgIf(reverse, "-R").
|
||||
ArgIf(ignoreWhitespace, "--ignore-all-space").
|
||||
Arg("--").
|
||||
Arg(fileName).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.
|
||||
New(
|
||||
fmt.Sprintf(
|
||||
"git diff --submodule --no-ext-diff --unified=%d --no-renames --color=%s%s%s%s -- %s",
|
||||
contextSize, colorArg, pad(from), pad(to), reverseFlag, self.cmd.Quote(fileName)),
|
||||
).
|
||||
DontLog()
|
||||
return self.cmd.New(cmdArgs).DontLog()
|
||||
}
|
||||
|
||||
// CheckoutFile checks out the file for the given commit
|
||||
func (self *WorkingTreeCommands) CheckoutFile(commitSha, fileName string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git checkout %s -- %s", commitSha, self.cmd.Quote(fileName))).Run()
|
||||
cmdArgs := NewGitCmd("checkout").Arg(commitSha, "--", fileName).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
|
||||
// DiscardAnyUnstagedFileChanges discards any unstaged file changes via `git checkout -- .`
|
||||
func (self *WorkingTreeCommands) DiscardAnyUnstagedFileChanges() error {
|
||||
return self.cmd.New("git checkout -- .").Run()
|
||||
cmdArgs := NewGitCmd("checkout").Arg("--", ".").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
|
||||
func (self *WorkingTreeCommands) RemoveTrackedFiles(name string) error {
|
||||
return self.cmd.New("git rm -r --cached -- " + self.cmd.Quote(name)).Run()
|
||||
cmdArgs := NewGitCmd("rm").Arg("-r", "--cached", "--", name).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// RemoveUntrackedFiles runs `git clean -fd`
|
||||
func (self *WorkingTreeCommands) RemoveUntrackedFiles() error {
|
||||
return self.cmd.New("git clean -fd").Run()
|
||||
cmdArgs := NewGitCmd("clean").Arg("-fd").ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// ResetAndClean removes all unstaged changes and removes all untracked files
|
||||
@@ -354,23 +349,23 @@ func (self *WorkingTreeCommands) ResetAndClean() error {
|
||||
|
||||
// ResetHardHead runs `git reset --hard`
|
||||
func (self *WorkingTreeCommands) ResetHard(ref string) error {
|
||||
return self.cmd.New("git reset --hard " + self.cmd.Quote(ref)).Run()
|
||||
cmdArgs := NewGitCmd("reset").Arg("--hard", ref).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// ResetSoft runs `git reset --soft HEAD`
|
||||
func (self *WorkingTreeCommands) ResetSoft(ref string) error {
|
||||
return self.cmd.New("git reset --soft " + self.cmd.Quote(ref)).Run()
|
||||
cmdArgs := NewGitCmd("reset").Arg("--soft", ref).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) ResetMixed(ref string) error {
|
||||
return self.cmd.New("git reset --mixed " + self.cmd.Quote(ref)).Run()
|
||||
}
|
||||
cmdArgs := NewGitCmd("reset").Arg("--mixed", ref).
|
||||
ToArgv()
|
||||
|
||||
// so that we don't have unnecessary space in our commands we use this helper function to prepend spaces to args so that in the format string we can go '%s%s%s' and if any args are missing we won't have gaps.
|
||||
func pad(str string) string {
|
||||
if str == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return " " + str
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
@@ -15,7 +13,7 @@ import (
|
||||
|
||||
func TestWorkingTreeStageFile(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
Expect(`git add -- "test.txt"`, "", nil)
|
||||
ExpectGitArgs([]string{"add", "--", "test.txt"}, "", nil)
|
||||
|
||||
instance := buildWorkingTreeCommands(commonDeps{runner: runner})
|
||||
|
||||
@@ -25,7 +23,7 @@ func TestWorkingTreeStageFile(t *testing.T) {
|
||||
|
||||
func TestWorkingTreeStageFiles(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
Expect(`git add -- "test.txt" "test2.txt"`, "", nil)
|
||||
ExpectGitArgs([]string{"add", "--", "test.txt", "test2.txt"}, "", nil)
|
||||
|
||||
instance := buildWorkingTreeCommands(commonDeps{runner: runner})
|
||||
|
||||
@@ -46,7 +44,7 @@ func TestWorkingTreeUnstageFile(t *testing.T) {
|
||||
testName: "Remove an untracked file from staging",
|
||||
reset: false,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git rm --cached --force -- "test.txt"`, "", nil),
|
||||
ExpectGitArgs([]string{"rm", "--cached", "--force", "--", "test.txt"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -55,7 +53,7 @@ func TestWorkingTreeUnstageFile(t *testing.T) {
|
||||
testName: "Remove a tracked file from staging",
|
||||
reset: true,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git reset HEAD -- "test.txt"`, "", nil),
|
||||
ExpectGitArgs([]string{"reset", "HEAD", "--", "test.txt"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -92,7 +90,7 @@ func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
|
||||
},
|
||||
removeFile: func(string) error { return nil },
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git reset -- "test"`, "", errors.New("error")),
|
||||
ExpectGitArgs([]string{"reset", "--", "test"}, "", errors.New("error")),
|
||||
expectedError: "error",
|
||||
},
|
||||
{
|
||||
@@ -117,7 +115,7 @@ func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
|
||||
},
|
||||
removeFile: func(string) error { return nil },
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git checkout -- "test"`, "", errors.New("error")),
|
||||
ExpectGitArgs([]string{"checkout", "--", "test"}, "", errors.New("error")),
|
||||
expectedError: "error",
|
||||
},
|
||||
{
|
||||
@@ -129,7 +127,7 @@ func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
|
||||
},
|
||||
removeFile: func(string) error { return nil },
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git checkout -- "test"`, "", nil),
|
||||
ExpectGitArgs([]string{"checkout", "--", "test"}, "", nil),
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
@@ -141,8 +139,8 @@ func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
|
||||
},
|
||||
removeFile: func(string) error { return nil },
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git reset -- "test"`, "", nil).
|
||||
Expect(`git checkout -- "test"`, "", nil),
|
||||
ExpectGitArgs([]string{"reset", "--", "test"}, "", nil).
|
||||
ExpectGitArgs([]string{"checkout", "--", "test"}, "", nil),
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
@@ -154,8 +152,8 @@ func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
|
||||
},
|
||||
removeFile: func(string) error { return nil },
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git reset -- "test"`, "", nil).
|
||||
Expect(`git checkout -- "test"`, "", nil),
|
||||
ExpectGitArgs([]string{"reset", "--", "test"}, "", nil).
|
||||
ExpectGitArgs([]string{"checkout", "--", "test"}, "", nil),
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
@@ -171,7 +169,7 @@ func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
|
||||
return nil
|
||||
},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git reset -- "test"`, "", nil),
|
||||
ExpectGitArgs([]string{"reset", "--", "test"}, "", nil),
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
@@ -233,7 +231,7 @@ func TestWorkingTreeDiff(t *testing.T) {
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 3,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git diff --submodule --no-ext-diff --unified=3 --color=always -- "test.txt"`, expectedResult, nil),
|
||||
ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--", "test.txt"}, expectedResult, nil),
|
||||
},
|
||||
{
|
||||
testName: "cached",
|
||||
@@ -247,7 +245,7 @@ func TestWorkingTreeDiff(t *testing.T) {
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 3,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git diff --submodule --no-ext-diff --unified=3 --color=always --cached -- "test.txt"`, expectedResult, nil),
|
||||
ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--cached", "--", "test.txt"}, expectedResult, nil),
|
||||
},
|
||||
{
|
||||
testName: "plain",
|
||||
@@ -261,7 +259,7 @@ func TestWorkingTreeDiff(t *testing.T) {
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 3,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git diff --submodule --no-ext-diff --unified=3 --color=never -- "test.txt"`, expectedResult, nil),
|
||||
ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=never", "--", "test.txt"}, expectedResult, nil),
|
||||
},
|
||||
{
|
||||
testName: "File not tracked and file has no staged changes",
|
||||
@@ -275,7 +273,7 @@ func TestWorkingTreeDiff(t *testing.T) {
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 3,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git diff --submodule --no-ext-diff --unified=3 --color=always --no-index -- /dev/null "test.txt"`, expectedResult, nil),
|
||||
ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--no-index", "--", "/dev/null", "test.txt"}, expectedResult, nil),
|
||||
},
|
||||
{
|
||||
testName: "Default case (ignore whitespace)",
|
||||
@@ -289,7 +287,7 @@ func TestWorkingTreeDiff(t *testing.T) {
|
||||
ignoreWhitespace: true,
|
||||
contextSize: 3,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git diff --submodule --no-ext-diff --unified=3 --color=always --ignore-all-space -- "test.txt"`, expectedResult, nil),
|
||||
ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--color=always", "--ignore-all-space", "--", "test.txt"}, expectedResult, nil),
|
||||
},
|
||||
{
|
||||
testName: "Show diff with custom context size",
|
||||
@@ -303,7 +301,7 @@ func TestWorkingTreeDiff(t *testing.T) {
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 17,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git diff --submodule --no-ext-diff --unified=17 --color=always -- "test.txt"`, expectedResult, nil),
|
||||
ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=17", "--color=always", "--", "test.txt"}, expectedResult, nil),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -323,37 +321,51 @@ func TestWorkingTreeDiff(t *testing.T) {
|
||||
|
||||
func TestWorkingTreeShowFileDiff(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
from string
|
||||
to string
|
||||
reverse bool
|
||||
plain bool
|
||||
contextSize int
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
testName string
|
||||
from string
|
||||
to string
|
||||
reverse bool
|
||||
plain bool
|
||||
ignoreWhitespace bool
|
||||
contextSize int
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
}
|
||||
|
||||
const expectedResult = "pretend this is an actual git diff"
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Default case",
|
||||
from: "1234567890",
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
plain: false,
|
||||
contextSize: 3,
|
||||
testName: "Default case",
|
||||
from: "1234567890",
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
plain: false,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 3,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git diff --submodule --no-ext-diff --unified=3 --no-renames --color=always 1234567890 0987654321 -- "test.txt"`, expectedResult, nil),
|
||||
ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--no-renames", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
|
||||
},
|
||||
{
|
||||
testName: "Show diff with custom context size",
|
||||
from: "1234567890",
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
plain: false,
|
||||
contextSize: 123,
|
||||
testName: "Show diff with custom context size",
|
||||
from: "1234567890",
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
plain: false,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 123,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git diff --submodule --no-ext-diff --unified=123 --no-renames --color=always 1234567890 0987654321 -- "test.txt"`, expectedResult, nil),
|
||||
ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=123", "--no-renames", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
|
||||
},
|
||||
{
|
||||
testName: "Default case (ignore whitespace)",
|
||||
from: "1234567890",
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
plain: false,
|
||||
ignoreWhitespace: true,
|
||||
contextSize: 3,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"diff", "--submodule", "--no-ext-diff", "--unified=3", "--no-renames", "--color=always", "1234567890", "0987654321", "--ignore-all-space", "--", "test.txt"}, expectedResult, nil),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -365,7 +377,7 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
|
||||
|
||||
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner, userConfig: userConfig})
|
||||
|
||||
result, err := instance.ShowFileDiff(s.from, s.to, s.reverse, "test.txt", s.plain)
|
||||
result, err := instance.ShowFileDiff(s.from, s.to, s.reverse, "test.txt", s.plain, s.ignoreWhitespace)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedResult, result)
|
||||
s.runner.CheckForMissingCalls()
|
||||
@@ -388,7 +400,7 @@ func TestWorkingTreeCheckoutFile(t *testing.T) {
|
||||
commitSha: "11af912",
|
||||
fileName: "test999.txt",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git checkout 11af912 -- "test999.txt"`, "", nil),
|
||||
ExpectGitArgs([]string{"checkout", "11af912", "--", "test999.txt"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -398,7 +410,7 @@ func TestWorkingTreeCheckoutFile(t *testing.T) {
|
||||
commitSha: "11af912",
|
||||
fileName: "test999.txt",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git checkout 11af912 -- "test999.txt"`, "", errors.New("error")),
|
||||
ExpectGitArgs([]string{"checkout", "11af912", "--", "test999.txt"}, "", errors.New("error")),
|
||||
test: func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
@@ -416,60 +428,6 @@ func TestWorkingTreeCheckoutFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkingTreeApplyPatch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(error)
|
||||
}
|
||||
|
||||
expectFn := func(regexStr string, errToReturn error) func(cmdObj oscommands.ICmdObj) (string, error) {
|
||||
return func(cmdObj oscommands.ICmdObj) (string, error) {
|
||||
re := regexp.MustCompile(regexStr)
|
||||
cmdStr := cmdObj.ToString()
|
||||
matches := re.FindStringSubmatch(cmdStr)
|
||||
assert.Equal(t, 2, len(matches), fmt.Sprintf("unexpected command: %s", cmdStr))
|
||||
|
||||
filename := matches[1]
|
||||
|
||||
content, err := os.ReadFile(filename)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "test", string(content))
|
||||
|
||||
return "", errToReturn
|
||||
}
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "valid case",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectFunc(expectFn(`git apply --cached "(.*)"`, nil)),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "command returns error",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectFunc(expectFn(`git apply --cached "(.*)"`, errors.New("error"))),
|
||||
test: func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
|
||||
s.test(instance.ApplyPatch("test", "cached"))
|
||||
s.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkingTreeDiscardUnstagedFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
@@ -483,7 +441,7 @@ func TestWorkingTreeDiscardUnstagedFileChanges(t *testing.T) {
|
||||
testName: "valid case",
|
||||
file: &models.File{Name: "test.txt"},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git checkout -- "test.txt"`, "", nil),
|
||||
ExpectGitArgs([]string{"checkout", "--", "test.txt"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -511,7 +469,7 @@ func TestWorkingTreeDiscardAnyUnstagedFileChanges(t *testing.T) {
|
||||
{
|
||||
testName: "valid case",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git checkout -- .`, "", nil),
|
||||
ExpectGitArgs([]string{"checkout", "--", "."}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -539,7 +497,7 @@ func TestWorkingTreeRemoveUntrackedFiles(t *testing.T) {
|
||||
{
|
||||
testName: "valid case",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git clean -fd`, "", nil),
|
||||
ExpectGitArgs([]string{"clean", "-fd"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -569,7 +527,7 @@ func TestWorkingTreeResetHard(t *testing.T) {
|
||||
"valid case",
|
||||
"HEAD",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git reset --hard "HEAD"`, "", nil),
|
||||
ExpectGitArgs([]string{"reset", "--hard", "HEAD"}, "", nil),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ package hosting_service
|
||||
// at regoio.herokuapp.com
|
||||
var defaultUrlRegexStrings = []string{
|
||||
`^(?:https?|ssh)://[^/]+/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
`^git@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
`^.*?@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
}
|
||||
var defaultRepoURLTemplate = "https://{{.webDomain}}/{{.owner}}/{{.repo}}"
|
||||
|
||||
@@ -33,9 +33,9 @@ var bitbucketServiceDef = ServiceDefinition{
|
||||
|
||||
var gitLabServiceDef = ServiceDefinition{
|
||||
provider: "gitlab",
|
||||
pullRequestURLIntoDefaultBranch: "/merge_requests/new?merge_request[source_branch]={{.From}}",
|
||||
pullRequestURLIntoTargetBranch: "/merge_requests/new?merge_request[source_branch]={{.From}}&merge_request[target_branch]={{.To}}",
|
||||
commitURL: "/commit/{{.CommitSha}}",
|
||||
pullRequestURLIntoDefaultBranch: "/-/merge_requests/new?merge_request[source_branch]={{.From}}",
|
||||
pullRequestURLIntoTargetBranch: "/-/merge_requests/new?merge_request[source_branch]={{.From}}&merge_request[target_branch]={{.To}}",
|
||||
commitURL: "/-/commit/{{.CommitSha}}",
|
||||
regexStrings: defaultUrlRegexStrings,
|
||||
repoURLTemplate: defaultRepoURLTemplate,
|
||||
}
|
||||
@@ -64,12 +64,22 @@ var bitbucketServerServiceDef = ServiceDefinition{
|
||||
repoURLTemplate: "https://{{.webDomain}}/projects/{{.project}}/repos/{{.repo}}",
|
||||
}
|
||||
|
||||
var giteaServiceDef = ServiceDefinition{
|
||||
provider: "gitea",
|
||||
pullRequestURLIntoDefaultBranch: "/compare/{{.From}}",
|
||||
pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}",
|
||||
commitURL: "/commit/{{.CommitSha}}",
|
||||
regexStrings: defaultUrlRegexStrings,
|
||||
repoURLTemplate: defaultRepoURLTemplate,
|
||||
}
|
||||
|
||||
var serviceDefinitions = []ServiceDefinition{
|
||||
githubServiceDef,
|
||||
bitbucketServiceDef,
|
||||
gitLabServiceDef,
|
||||
azdoServiceDef,
|
||||
bitbucketServerServiceDef,
|
||||
giteaServiceDef,
|
||||
}
|
||||
|
||||
var defaultServiceDomains = []ServiceDomain{
|
||||
@@ -93,4 +103,9 @@ var defaultServiceDomains = []ServiceDomain{
|
||||
gitDomain: "dev.azure.com",
|
||||
webDomain: "dev.azure.com",
|
||||
},
|
||||
{
|
||||
serviceDefinition: giteaServiceDef,
|
||||
gitDomain: "try.gitea.io",
|
||||
webDomain: "try.gitea.io",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
)
|
||||
|
||||
// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, etc.
|
||||
// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, gitea, etc.
|
||||
// Different git hosting services have different URL formats for when you want to open a PR or view a commit,
|
||||
// and this package's responsibility is to determine which service you're using based on the remote URL,
|
||||
// and then which URL you need for whatever use case you have.
|
||||
|
||||
@@ -3,8 +3,8 @@ package hosting_service
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/fakes"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -86,6 +86,16 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature%2Foperations...feature%2Fsum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github with specific target branch (different git username)",
|
||||
from: "feature/sum-operation",
|
||||
to: "feature/operations",
|
||||
remoteUrl: "ssh://org-12345@github.com:peter/calculator.git",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature%2Foperations...feature%2Fsum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github with https remote url with specific target branch",
|
||||
from: "feature/sum-operation",
|
||||
@@ -102,7 +112,7 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature%2Fui", url)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/-/merge_requests/new?merge_request[source_branch]=feature%2Fui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -111,7 +121,7 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature%2Fui", url)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/-/merge_requests/new?merge_request[source_branch]=feature%2Fui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -120,7 +130,7 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
remoteUrl: "https://gitlab.com/peter/public/calculator.git",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature%2Fui", url)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/-/merge_requests/new?merge_request[source_branch]=feature%2Fui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -130,7 +140,7 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature%2Fcommit-ui&merge_request[target_branch]=epic%2Fui", url)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/-/merge_requests/new?merge_request[source_branch]=feature%2Fcommit-ui&merge_request[target_branch]=epic%2Fui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -140,7 +150,7 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature%2Fcommit-ui&merge_request[target_branch]=epic%2Fui", url)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/-/merge_requests/new?merge_request[source_branch]=feature%2Fcommit-ui&merge_request[target_branch]=epic%2Fui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -150,7 +160,7 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
remoteUrl: "https://gitlab.com/peter/public/calculator.git",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature%2Fcommit-ui&merge_request[target_branch]=epic%2Fui", url)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/-/merge_requests/new?merge_request[source_branch]=feature%2Fcommit-ui&merge_request[target_branch]=epic%2Fui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -254,6 +264,60 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
assert.Equal(t, "https://mycompany.bitbucket.com/projects/myproject/repos/myrepo/pull-requests?create&targetBranch=dev&sourceBranch=feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Gitea Server (SSH)",
|
||||
from: "feature/new",
|
||||
remoteUrl: "ssh://git@mycompany.gitea.io/myproject/myrepo.git",
|
||||
configServiceDomains: map[string]string{
|
||||
// valid configuration for a gitea server URL
|
||||
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Gitea Server (SSH) with specific target",
|
||||
from: "feature/new",
|
||||
to: "dev",
|
||||
remoteUrl: "ssh://git@mycompany.gitea.io/myproject/myrepo.git",
|
||||
configServiceDomains: map[string]string{
|
||||
// valid configuration for a gitea server URL
|
||||
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/dev...feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Gitea Server (HTTP)",
|
||||
from: "feature/new",
|
||||
remoteUrl: "https://mycompany.gitea.io/myproject/myrepo.git",
|
||||
configServiceDomains: map[string]string{
|
||||
// valid configuration for a gitea server URL
|
||||
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Gitea Server (HTTP) with specific target",
|
||||
from: "feature/new",
|
||||
to: "dev",
|
||||
remoteUrl: "https://mycompany.gitea.io/myproject/myrepo.git",
|
||||
configServiceDomains: map[string]string{
|
||||
// valid configuration for a gitea server URL
|
||||
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/dev...feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Throws an error if git service is unsupported",
|
||||
from: "feature/divide-operation",
|
||||
@@ -300,7 +364,7 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature%2Fprofile-page&t=1", url)
|
||||
},
|
||||
expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab, azuredevops, bitbucketServer"},
|
||||
expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab, azuredevops, bitbucketServer, gitea"},
|
||||
},
|
||||
{
|
||||
testName: "Escapes reserved URL characters in from branch name",
|
||||
@@ -309,7 +373,7 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
remoteUrl: "git@gitlab.com:me/public/repo-with-issues.git",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/me/public/repo-with-issues/merge_requests/new?merge_request[source_branch]=feature%2FsomeIssue%23123&merge_request[target_branch]=master", url)
|
||||
assert.Equal(t, "https://gitlab.com/me/public/repo-with-issues/-/merge_requests/new?merge_request[source_branch]=feature%2FsomeIssue%23123&merge_request[target_branch]=master", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -319,7 +383,7 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
remoteUrl: "git@gitlab.com:me/public/repo-with-issues.git",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/me/public/repo-with-issues/merge_requests/new?merge_request[source_branch]=yolo&merge_request[target_branch]=archive%2Fnever-ending-feature%23666", url)
|
||||
assert.Equal(t, "https://gitlab.com/me/public/repo-with-issues/-/merge_requests/new?merge_request[source_branch]=yolo&merge_request[target_branch]=archive%2Fnever-ending-feature%23666", url)
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -328,7 +392,7 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
tr := i18n.EnglishTranslationSet()
|
||||
log := &test.FakeFieldLogger{}
|
||||
log := &fakes.FakeFieldLogger{}
|
||||
hostingServiceMgr := NewHostingServiceMgr(log, &tr, s.remoteUrl, s.configServiceDomains)
|
||||
s.test(hostingServiceMgr.GetPullRequestURL(s.from, s.to))
|
||||
log.AssertErrors(t, s.expectedLoggedErrors)
|
||||
|
||||
@@ -3,18 +3,37 @@ package models
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// Special commit hash for empty tree object
|
||||
const EmptyTreeCommitHash = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
||||
|
||||
type CommitStatus int
|
||||
|
||||
const (
|
||||
StatusNone CommitStatus = iota
|
||||
StatusUnpushed
|
||||
StatusPushed
|
||||
StatusMerged
|
||||
StatusRebasing
|
||||
StatusSelected
|
||||
StatusReflog
|
||||
)
|
||||
|
||||
const (
|
||||
// Conveniently for us, the todo package starts the enum at 1, and given
|
||||
// that it doesn't have a "none" value, we're setting ours to 0
|
||||
ActionNone todo.TodoCommand = 0
|
||||
)
|
||||
|
||||
// Commit : A git commit
|
||||
type Commit struct {
|
||||
Sha string
|
||||
Name string
|
||||
Status string // one of "unpushed", "pushed", "merged", "rebasing" or "selected"
|
||||
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
|
||||
Status CommitStatus
|
||||
Action todo.TodoCommand
|
||||
Tags []string
|
||||
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
|
||||
AuthorName string // something like 'Jesse Duffield'
|
||||
@@ -63,5 +82,9 @@ func (c *Commit) IsMerge() bool {
|
||||
// returns true if this commit is not actually in the git log but instead
|
||||
// is from a TODO file for an interactive rebase.
|
||||
func (c *Commit) IsTODO() bool {
|
||||
return c.Action != ""
|
||||
return c.Action != ActionNone
|
||||
}
|
||||
|
||||
func IsHeadCommit(commits []*Commit, index int) bool {
|
||||
return !commits[index].IsTODO() && (index == 0 || commits[index-1].IsTODO())
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package models
|
||||
// Tag : A git tag
|
||||
type Tag struct {
|
||||
Name string
|
||||
// this is either the first line of the message of an annotated tag, or the
|
||||
// first line of a commit message for a lightweight tag
|
||||
Message string
|
||||
}
|
||||
|
||||
func (t *Tag) FullRefName() string {
|
||||
@@ -22,5 +25,5 @@ func (t *Tag) ID() string {
|
||||
}
|
||||
|
||||
func (t *Tag) Description() string {
|
||||
return "tag " + t.Name
|
||||
return t.Message
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package oscommands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
)
|
||||
|
||||
@@ -15,6 +17,9 @@ type ICmdObj interface {
|
||||
// into a terminal e.g. 'sh -c git commit' as opposed to 'sh -c "git commit"'
|
||||
ToString() string
|
||||
|
||||
// outputs args vector e.g. ["git", "commit", "-m", "my message"]
|
||||
Args() []string
|
||||
|
||||
AddEnvVars(...string) ICmdObj
|
||||
GetEnvVars() []string
|
||||
|
||||
@@ -61,8 +66,11 @@ type ICmdObj interface {
|
||||
}
|
||||
|
||||
type CmdObj struct {
|
||||
cmdStr string
|
||||
cmd *exec.Cmd
|
||||
// the secureexec package will swap out the first arg with the full path to the binary,
|
||||
// so we store these args separately so that ToString() will output the original
|
||||
args []string
|
||||
|
||||
cmd *exec.Cmd
|
||||
|
||||
runner ICmdObjRunner
|
||||
|
||||
@@ -104,7 +112,19 @@ func (self *CmdObj) GetCmd() *exec.Cmd {
|
||||
}
|
||||
|
||||
func (self *CmdObj) ToString() string {
|
||||
return self.cmdStr
|
||||
// if a given arg contains a space, we need to wrap it in quotes
|
||||
quotedArgs := lo.Map(self.args, func(arg string, _ int) string {
|
||||
if strings.Contains(arg, " ") {
|
||||
return `"` + arg + `"`
|
||||
}
|
||||
return arg
|
||||
})
|
||||
|
||||
return strings.Join(quotedArgs, " ")
|
||||
}
|
||||
|
||||
func (self *CmdObj) Args() []string {
|
||||
return self.args
|
||||
}
|
||||
|
||||
func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj {
|
||||
|
||||
@@ -10,12 +10,10 @@ import (
|
||||
)
|
||||
|
||||
type ICmdObjBuilder interface {
|
||||
// New returns a new command object based on the string provided
|
||||
New(cmdStr string) ICmdObj
|
||||
// NewFromArgs takes a slice of strings like []string{"git", "commit"} and returns a new command object.
|
||||
New(args []string) ICmdObj
|
||||
// NewShell takes a string like `git commit` and returns an executable shell command for it e.g. `sh -c 'git commit'`
|
||||
NewShell(commandStr string) ICmdObj
|
||||
// NewFromArgs takes a slice of strings like []string{"git", "commit"} and returns a new command object. This can be useful when you don't want to worry about whitespace and quoting and stuff.
|
||||
NewFromArgs(args []string) ICmdObj
|
||||
// Quote wraps a string in quotes with any necessary escaping applied. The reason for bundling this up with the other methods in this interface is that we basically always need to make use of this when creating new command objects.
|
||||
Quote(str string) string
|
||||
}
|
||||
@@ -28,31 +26,19 @@ type CmdObjBuilder struct {
|
||||
// poor man's version of explicitly saying that struct X implements interface Y
|
||||
var _ ICmdObjBuilder = &CmdObjBuilder{}
|
||||
|
||||
func (self *CmdObjBuilder) New(cmdStr string) ICmdObj {
|
||||
args := str.ToArgv(cmdStr)
|
||||
func (self *CmdObjBuilder) New(args []string) ICmdObj {
|
||||
cmd := secureexec.Command(args[0], args[1:]...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
return &CmdObj{
|
||||
cmdStr: cmdStr,
|
||||
cmd: cmd,
|
||||
runner: self.runner,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CmdObjBuilder) NewFromArgs(args []string) ICmdObj {
|
||||
cmd := secureexec.Command(args[0], args[1:]...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
return &CmdObj{
|
||||
cmdStr: strings.Join(args, " "),
|
||||
args: args,
|
||||
cmd: cmd,
|
||||
runner: self.runner,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CmdObjBuilder) NewShell(commandStr string) ICmdObj {
|
||||
quotedCommand := ""
|
||||
var quotedCommand string
|
||||
// Windows does not seem to like quotes around the command
|
||||
if self.platform.OS == "windows" {
|
||||
quotedCommand = strings.NewReplacer(
|
||||
@@ -67,8 +53,9 @@ func (self *CmdObjBuilder) NewShell(commandStr string) ICmdObj {
|
||||
quotedCommand = self.Quote(commandStr)
|
||||
}
|
||||
|
||||
shellCommand := fmt.Sprintf("%s %s %s", self.platform.Shell, self.platform.ShellArg, quotedCommand)
|
||||
return self.New(shellCommand)
|
||||
cmdArgs := str.ToArgv(fmt.Sprintf("%s %s %s", self.platform.Shell, self.platform.ShellArg, quotedCommand))
|
||||
|
||||
return self.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *CmdObjBuilder) CloneWithNewRunner(decorate func(ICmdObjRunner) ICmdObjRunner) *CmdObjBuilder {
|
||||
@@ -80,6 +67,9 @@ func (self *CmdObjBuilder) CloneWithNewRunner(decorate func(ICmdObjRunner) ICmdO
|
||||
}
|
||||
}
|
||||
|
||||
const CHARS_REQUIRING_QUOTES = "\"\\$` "
|
||||
|
||||
// If you update this method, be sure to update CHARS_REQUIRING_QUOTES
|
||||
func (self *CmdObjBuilder) Quote(message string) string {
|
||||
var quote string
|
||||
if self.platform.OS == "windows" {
|
||||
|
||||
@@ -323,6 +323,23 @@ func (self *cmdObjRunner) processOutput(reader io.Reader, writer io.Writer, prom
|
||||
// having a function that returns a function because we need to maintain some state inbetween calls hence the closure
|
||||
func (self *cmdObjRunner) getCheckForCredentialRequestFunc() func([]byte) (CredentialType, bool) {
|
||||
var ttyText strings.Builder
|
||||
prompts := map[string]CredentialType{
|
||||
`Password:`: Password,
|
||||
`.+'s password:`: Password,
|
||||
`Password\s*for\s*'.+':`: Password,
|
||||
`Username\s*for\s*'.+':`: Username,
|
||||
`Enter\s*passphrase\s*for\s*key\s*'.+':`: Passphrase,
|
||||
`Enter\s*PIN\s*for\s*.+\s*key\s*.+:`: PIN,
|
||||
}
|
||||
|
||||
compiledPrompts := map[*regexp.Regexp]CredentialType{}
|
||||
for pattern, askFor := range prompts {
|
||||
compiledPattern := regexp.MustCompile(pattern)
|
||||
compiledPrompts[compiledPattern] = askFor
|
||||
}
|
||||
|
||||
newlineRegex := regexp.MustCompile("\n")
|
||||
|
||||
// this function takes each word of output from the command and builds up a string to see if we're being asked for a password
|
||||
return func(newBytes []byte) (CredentialType, bool) {
|
||||
_, err := ttyText.Write(newBytes)
|
||||
@@ -330,22 +347,18 @@ func (self *cmdObjRunner) getCheckForCredentialRequestFunc() func([]byte) (Crede
|
||||
self.log.Error(err)
|
||||
}
|
||||
|
||||
prompts := map[string]CredentialType{
|
||||
`Password:`: Password,
|
||||
`.+'s password:`: Password,
|
||||
`Password\s*for\s*'.+':`: Password,
|
||||
`Username\s*for\s*'.+':`: Username,
|
||||
`Enter\s*passphrase\s*for\s*key\s*'.+':`: Passphrase,
|
||||
`Enter\s*PIN\s*for\s*.+\s*key\s*.+:`: PIN,
|
||||
}
|
||||
|
||||
for pattern, askFor := range prompts {
|
||||
if match, _ := regexp.MatchString(pattern, ttyText.String()); match {
|
||||
for pattern, askFor := range compiledPrompts {
|
||||
if match := pattern.Match([]byte(ttyText.String())); match {
|
||||
ttyText.Reset()
|
||||
return askFor, true
|
||||
}
|
||||
}
|
||||
|
||||
if indices := newlineRegex.FindIndex([]byte(ttyText.String())); indices != nil {
|
||||
newText := []byte(ttyText.String()[indices[1]:])
|
||||
ttyText.Reset()
|
||||
ttyText.Write(newText)
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func getRunner() *cmdObjRunner {
|
||||
log := utils.NewDummyLog()
|
||||
return &cmdObjRunner{
|
||||
log: log,
|
||||
guiIO: NewNullGuiIO(log),
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessOutput(t *testing.T) {
|
||||
defaultPromptUserForCredential := func(ct CredentialType) string {
|
||||
switch ct {
|
||||
case Password:
|
||||
return "password"
|
||||
case Username:
|
||||
return "username"
|
||||
case Passphrase:
|
||||
return "passphrase"
|
||||
case PIN:
|
||||
return "pin"
|
||||
default:
|
||||
panic("unexpected credential type")
|
||||
}
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
promptUserForCredential func(CredentialType) string
|
||||
output string
|
||||
expectedToWrite string
|
||||
}{
|
||||
{
|
||||
name: "no output",
|
||||
promptUserForCredential: defaultPromptUserForCredential,
|
||||
output: "",
|
||||
expectedToWrite: "",
|
||||
},
|
||||
{
|
||||
name: "password prompt",
|
||||
promptUserForCredential: defaultPromptUserForCredential,
|
||||
output: "Password:",
|
||||
expectedToWrite: "password",
|
||||
},
|
||||
{
|
||||
name: "password prompt 2",
|
||||
promptUserForCredential: defaultPromptUserForCredential,
|
||||
output: "Bill's password:",
|
||||
expectedToWrite: "password",
|
||||
},
|
||||
{
|
||||
name: "password prompt 3",
|
||||
promptUserForCredential: defaultPromptUserForCredential,
|
||||
output: "Password for 'Bill':",
|
||||
expectedToWrite: "password",
|
||||
},
|
||||
{
|
||||
name: "username prompt",
|
||||
promptUserForCredential: defaultPromptUserForCredential,
|
||||
output: "Username for 'Bill':",
|
||||
expectedToWrite: "username",
|
||||
},
|
||||
{
|
||||
name: "passphrase prompt",
|
||||
promptUserForCredential: defaultPromptUserForCredential,
|
||||
output: "Enter passphrase for key '123':",
|
||||
expectedToWrite: "passphrase",
|
||||
},
|
||||
{
|
||||
name: "pin prompt",
|
||||
promptUserForCredential: defaultPromptUserForCredential,
|
||||
output: "Enter PIN for key '123':",
|
||||
expectedToWrite: "pin",
|
||||
},
|
||||
{
|
||||
name: "username and password prompt",
|
||||
promptUserForCredential: defaultPromptUserForCredential,
|
||||
output: "Password:\nUsername for 'Alice':\n",
|
||||
expectedToWrite: "passwordusername",
|
||||
},
|
||||
{
|
||||
name: "user submits empty credential",
|
||||
promptUserForCredential: func(ct CredentialType) string { return "" },
|
||||
output: "Password:\n",
|
||||
expectedToWrite: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
runner := getRunner()
|
||||
reader := strings.NewReader(scenario.output)
|
||||
writer := &strings.Builder{}
|
||||
|
||||
runner.processOutput(reader, writer, scenario.promptUserForCredential)
|
||||
|
||||
if writer.String() != scenario.expectedToWrite {
|
||||
t.Errorf("expected to write '%s' but got '%s'", scenario.expectedToWrite, writer.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,48 +4,20 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os/exec"
|
||||
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
"github.com/creack/pty"
|
||||
)
|
||||
|
||||
type Buffer struct {
|
||||
b bytes.Buffer
|
||||
m deadlock.Mutex
|
||||
}
|
||||
|
||||
func (b *Buffer) Read(p []byte) (n int, err error) {
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
return b.b.Read(p)
|
||||
}
|
||||
|
||||
func (b *Buffer) Write(p []byte) (n int, err error) {
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
return b.b.Write(p)
|
||||
}
|
||||
|
||||
// TODO: Remove this hack and replace it with a proper way to run commands live on windows. We still have an issue where if a password is requested, the request for a password is written straight to stdout because we can't control the stdout of a subprocess of a subprocess. Keep an eye on https://github.com/creack/pty/pull/109
|
||||
func (self *cmdObjRunner) getCmdHandler(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
cmd.Stdout = stdoutWriter
|
||||
|
||||
buf := &Buffer{}
|
||||
cmd.Stdin = buf
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
ptmx, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// because we don't yet have windows support for a pty, we instead just
|
||||
// pass our standard stream handlers and because there's no pty to close
|
||||
// we pass a no-op function for that.
|
||||
return &cmdHandler{
|
||||
stdoutPipe: stdoutReader,
|
||||
stdinPipe: buf,
|
||||
close: func() error { return nil },
|
||||
stdoutPipe: ptmx,
|
||||
stdinPipe: ptmx,
|
||||
close: ptmx.Close,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCmdObjToString(t *testing.T) {
|
||||
quote := func(s string) string {
|
||||
return "\"" + s + "\""
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
cmdArgs []string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
cmdArgs: []string{"git", "push", "myfile.txt"},
|
||||
expected: "git push myfile.txt",
|
||||
},
|
||||
{
|
||||
cmdArgs: []string{"git", "push", "my file.txt"},
|
||||
expected: "git push \"my file.txt\"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
cmdObj := &CmdObj{args: scenario.cmdArgs}
|
||||
actual := cmdObj.ToString()
|
||||
if actual != scenario.expected {
|
||||
t.Errorf("Expected %s, got %s", quote(scenario.expected), quote(actual))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -91,7 +92,18 @@ func (self *FakeCmdObjRunner) Expect(expectedCmdStr string, output string, err e
|
||||
func (self *FakeCmdObjRunner) ExpectArgs(expectedArgs []string, output string, err error) *FakeCmdObjRunner {
|
||||
self.ExpectFunc(func(cmdObj ICmdObj) (string, error) {
|
||||
args := cmdObj.GetCmd().Args
|
||||
assert.EqualValues(self.t, expectedArgs, args, fmt.Sprintf("command %d did not match expectation", self.expectedCmdIndex+1))
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// thanks to the secureexec package, the first arg is something like
|
||||
// '"C:\\Program Files\\Git\\mingw64\\bin\\<command>.exe"
|
||||
// on windows so we'll just ensure it contains our program
|
||||
assert.Contains(self.t, args[0], expectedArgs[0])
|
||||
} else {
|
||||
// first arg is the program name
|
||||
assert.Equal(self.t, expectedArgs[0], args[0])
|
||||
}
|
||||
|
||||
assert.EqualValues(self.t, expectedArgs[1:], args[1:], fmt.Sprintf("command %d did not match expectation", self.expectedCmdIndex+1))
|
||||
|
||||
return output, err
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
@@ -78,21 +79,30 @@ func FileType(path string) string {
|
||||
}
|
||||
|
||||
func (c *OSCommand) OpenFile(filename string) error {
|
||||
return c.OpenFileAtLine(filename, 1)
|
||||
}
|
||||
|
||||
func (c *OSCommand) OpenFileAtLine(filename string, lineNumber int) error {
|
||||
commandTemplate := c.UserConfig.OS.OpenCommand
|
||||
commandTemplate := c.UserConfig.OS.Open
|
||||
if commandTemplate == "" {
|
||||
// Legacy support
|
||||
commandTemplate = c.UserConfig.OS.OpenCommand
|
||||
}
|
||||
if commandTemplate == "" {
|
||||
commandTemplate = config.GetPlatformDefaultConfig().Open
|
||||
}
|
||||
templateValues := map[string]string{
|
||||
"filename": c.Quote(filename),
|
||||
"line": fmt.Sprintf("%d", lineNumber),
|
||||
}
|
||||
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||
return c.Cmd.NewShell(command).Run()
|
||||
}
|
||||
|
||||
func (c *OSCommand) OpenLink(link string) error {
|
||||
commandTemplate := c.UserConfig.OS.OpenLinkCommand
|
||||
commandTemplate := c.UserConfig.OS.OpenLink
|
||||
if commandTemplate == "" {
|
||||
// Legacy support
|
||||
commandTemplate = c.UserConfig.OS.OpenLinkCommand
|
||||
}
|
||||
if commandTemplate == "" {
|
||||
commandTemplate = config.GetPlatformDefaultConfig().OpenLink
|
||||
}
|
||||
templateValues := map[string]string{
|
||||
"link": c.Quote(link),
|
||||
}
|
||||
@@ -178,12 +188,18 @@ func (c *OSCommand) FileExists(path string) (bool, error) {
|
||||
}
|
||||
|
||||
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
|
||||
func (c *OSCommand) PipeCommands(commandStrings ...string) error {
|
||||
cmds := slices.Map(commandStrings, func(cmdString string) *exec.Cmd {
|
||||
return c.Cmd.New(cmdString).GetCmd()
|
||||
func (c *OSCommand) PipeCommands(cmdObjs ...ICmdObj) error {
|
||||
cmds := slices.Map(cmdObjs, func(cmdObj ICmdObj) *exec.Cmd {
|
||||
return cmdObj.GetCmd()
|
||||
})
|
||||
|
||||
logCmdStr := strings.Join(commandStrings, " | ")
|
||||
logCmdStr := strings.Join(
|
||||
lo.Map(cmdObjs, func(cmdObj ICmdObj, _ int) string {
|
||||
return cmdObj.ToString()
|
||||
}),
|
||||
" | ",
|
||||
)
|
||||
|
||||
c.LogCommand(logCmdStr, true)
|
||||
|
||||
for i := 0; i < len(cmds)-1; i++ {
|
||||
|
||||
+7
-7
@@ -12,20 +12,20 @@ import (
|
||||
|
||||
func TestOSCommandRunWithOutput(t *testing.T) {
|
||||
type scenario struct {
|
||||
command string
|
||||
test func(string, error)
|
||||
args []string
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"echo -n '123'",
|
||||
[]string{"echo", "-n", "123"},
|
||||
func(output string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "123", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"rmdir unexisting-folder",
|
||||
[]string{"rmdir", "unexisting-folder"},
|
||||
func(output string, err error) {
|
||||
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
|
||||
},
|
||||
@@ -34,7 +34,7 @@ func TestOSCommandRunWithOutput(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
c := NewDummyOSCommand()
|
||||
s.test(c.Cmd.New(s.command).RunWithOutput())
|
||||
s.test(c.Cmd.New(s.args).RunWithOutput())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func TestOSCommandOpenFileDarwin(t *testing.T) {
|
||||
for _, s := range scenarios {
|
||||
oSCmd := NewDummyOSCommandWithRunner(s.runner)
|
||||
oSCmd.Platform.OS = "darwin"
|
||||
oSCmd.UserConfig.OS.OpenCommand = "open {{filename}}"
|
||||
oSCmd.UserConfig.OS.Open = "open {{filename}}"
|
||||
|
||||
s.test(oSCmd.OpenFile(s.filename))
|
||||
}
|
||||
@@ -135,7 +135,7 @@ func TestOSCommandOpenFileLinux(t *testing.T) {
|
||||
for _, s := range scenarios {
|
||||
oSCmd := NewDummyOSCommandWithRunner(s.runner)
|
||||
oSCmd.Platform.OS = "linux"
|
||||
oSCmd.UserConfig.OS.OpenCommand = `xdg-open {{filename}} > /dev/null`
|
||||
oSCmd.UserConfig.OS.Open = `xdg-open {{filename}} > /dev/null`
|
||||
|
||||
s.test(oSCmd.OpenFile(s.filename))
|
||||
}
|
||||
@@ -10,13 +10,13 @@ import (
|
||||
|
||||
func TestOSCommandRun(t *testing.T) {
|
||||
type scenario struct {
|
||||
command string
|
||||
test func(error)
|
||||
args []string
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"rmdir unexisting-folder",
|
||||
[]string{"rmdir", "unexisting-folder"},
|
||||
func(err error) {
|
||||
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
|
||||
},
|
||||
@@ -25,7 +25,7 @@ func TestOSCommandRun(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
c := NewDummyOSCommand()
|
||||
s.test(c.Cmd.New(s.command).Run())
|
||||
s.test(c.Cmd.New(s.args).Run())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-5
@@ -6,6 +6,7 @@ package oscommands
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cli/safeexec"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -19,11 +20,13 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
|
||||
test func(error)
|
||||
}
|
||||
|
||||
fullCmdPath, _ := safeexec.LookPath("cmd")
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
filename: "test",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", errors.New("error")),
|
||||
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "test"}, "", errors.New("error")),
|
||||
test: func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
@@ -31,7 +34,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
|
||||
{
|
||||
filename: "test",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", nil),
|
||||
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "test"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -39,7 +42,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
|
||||
{
|
||||
filename: "filename with spaces",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"cmd", "/c", "start", "", "filename with spaces"}, "", nil),
|
||||
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "filename with spaces"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -47,7 +50,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
|
||||
{
|
||||
filename: "let's_test_with_single_quote",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"cmd", "/c", "start", "", "let's_test_with_single_quote"}, "", nil),
|
||||
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "let's_test_with_single_quote"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -55,7 +58,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
|
||||
{
|
||||
filename: "$USER.txt",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"cmd", "/c", "start", "", "$USER.txt"}, "", nil),
|
||||
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "$USER.txt"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -0,0 +1,169 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type patchPresenter struct {
|
||||
patch *Patch
|
||||
// if true, all following fields are ignored
|
||||
plain bool
|
||||
|
||||
isFocused bool
|
||||
// first line index for selected cursor range
|
||||
firstLineIndex int
|
||||
// last line index for selected cursor range
|
||||
lastLineIndex int
|
||||
// line indices for tagged lines (e.g. lines added to a custom patch)
|
||||
incLineIndices *set.Set[int]
|
||||
}
|
||||
|
||||
// formats the patch as a plain string
|
||||
func formatPlain(patch *Patch) string {
|
||||
presenter := &patchPresenter{
|
||||
patch: patch,
|
||||
plain: true,
|
||||
incLineIndices: set.New[int](),
|
||||
}
|
||||
return presenter.format()
|
||||
}
|
||||
|
||||
func formatRangePlain(patch *Patch, startIdx int, endIdx int) string {
|
||||
lines := patch.Lines()[startIdx : endIdx+1]
|
||||
return strings.Join(
|
||||
lo.Map(lines, func(line *PatchLine, _ int) string {
|
||||
return line.Content + "\n"
|
||||
}),
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
type FormatViewOpts struct {
|
||||
IsFocused bool
|
||||
// first line index for selected cursor range
|
||||
FirstLineIndex int
|
||||
// last line index for selected cursor range
|
||||
LastLineIndex int
|
||||
// line indices for tagged lines (e.g. lines added to a custom patch)
|
||||
IncLineIndices *set.Set[int]
|
||||
}
|
||||
|
||||
// formats the patch for rendering within a view, meaning it's coloured and
|
||||
// highlights selected items
|
||||
func formatView(patch *Patch, opts FormatViewOpts) string {
|
||||
includedLineIndices := opts.IncLineIndices
|
||||
if includedLineIndices == nil {
|
||||
includedLineIndices = set.New[int]()
|
||||
}
|
||||
presenter := &patchPresenter{
|
||||
patch: patch,
|
||||
plain: false,
|
||||
isFocused: opts.IsFocused,
|
||||
firstLineIndex: opts.FirstLineIndex,
|
||||
lastLineIndex: opts.LastLineIndex,
|
||||
incLineIndices: includedLineIndices,
|
||||
}
|
||||
return presenter.format()
|
||||
}
|
||||
|
||||
func (self *patchPresenter) format() string {
|
||||
// if we have no changes in our patch (i.e. no additions or deletions) then
|
||||
// the patch is effectively empty and we can return an empty string
|
||||
if !self.patch.ContainsChanges() {
|
||||
return ""
|
||||
}
|
||||
|
||||
stringBuilder := &strings.Builder{}
|
||||
lineIdx := 0
|
||||
appendLine := func(line string) {
|
||||
_, _ = stringBuilder.WriteString(line + "\n")
|
||||
|
||||
lineIdx++
|
||||
}
|
||||
appendFormattedLine := func(line string, style style.TextStyle) {
|
||||
formattedLine := self.formatLine(
|
||||
line,
|
||||
style,
|
||||
lineIdx,
|
||||
)
|
||||
|
||||
appendLine(formattedLine)
|
||||
}
|
||||
|
||||
for _, line := range self.patch.header {
|
||||
appendFormattedLine(line, theme.DefaultTextColor.SetBold())
|
||||
}
|
||||
|
||||
for _, hunk := range self.patch.hunks {
|
||||
appendLine(
|
||||
self.formatLine(
|
||||
hunk.formatHeaderStart(),
|
||||
style.FgCyan,
|
||||
lineIdx,
|
||||
) +
|
||||
// we're splitting the line into two parts: the diff header and the context
|
||||
// We explicitly pass 'included' as false here so that we're only tagging the
|
||||
// first half of the line as included if the line is indeed included.
|
||||
self.formatLineAux(
|
||||
hunk.headerContext,
|
||||
theme.DefaultTextColor,
|
||||
lineIdx,
|
||||
false,
|
||||
),
|
||||
)
|
||||
|
||||
for _, line := range hunk.bodyLines {
|
||||
appendFormattedLine(line.Content, self.patchLineStyle(line))
|
||||
}
|
||||
}
|
||||
|
||||
return stringBuilder.String()
|
||||
}
|
||||
|
||||
func (self *patchPresenter) patchLineStyle(patchLine *PatchLine) style.TextStyle {
|
||||
switch patchLine.Kind {
|
||||
case ADDITION:
|
||||
return style.FgGreen
|
||||
case DELETION:
|
||||
return style.FgRed
|
||||
default:
|
||||
return theme.DefaultTextColor
|
||||
}
|
||||
}
|
||||
|
||||
func (self *patchPresenter) formatLine(str string, textStyle style.TextStyle, index int) string {
|
||||
included := self.incLineIndices.Includes(index)
|
||||
|
||||
return self.formatLineAux(str, textStyle, index, included)
|
||||
}
|
||||
|
||||
// 'selected' means you've got it highlighted with your cursor
|
||||
// 'included' means the line has been included in the patch (only applicable when
|
||||
// building a patch)
|
||||
func (self *patchPresenter) formatLineAux(str string, textStyle style.TextStyle, index int, included bool) string {
|
||||
if self.plain {
|
||||
return str
|
||||
}
|
||||
|
||||
selected := self.isFocused && index >= self.firstLineIndex && index <= self.lastLineIndex
|
||||
|
||||
if selected {
|
||||
textStyle = textStyle.MergeStyle(theme.SelectedRangeBgColor)
|
||||
}
|
||||
|
||||
firstCharStyle := textStyle
|
||||
if included {
|
||||
firstCharStyle = firstCharStyle.MergeStyle(style.BgGreen)
|
||||
}
|
||||
|
||||
if len(str) < 2 {
|
||||
return firstCharStyle.Sprint(str)
|
||||
}
|
||||
|
||||
return firstCharStyle.Sprint(str[:1]) + textStyle.Sprint(str[1:])
|
||||
}
|
||||
+49
-125
@@ -1,143 +1,67 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
import "fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
// Example hunk:
|
||||
// @@ -16,2 +14,3 @@ func (f *CommitFile) Description() string {
|
||||
// return f.Name
|
||||
// -}
|
||||
// +
|
||||
// +// test
|
||||
|
||||
type PatchHunk struct {
|
||||
FirstLineIdx int
|
||||
oldStart int
|
||||
newStart int
|
||||
heading string
|
||||
bodyLines []string
|
||||
type Hunk struct {
|
||||
// the line number of the first line in the old file ('16' in the above example)
|
||||
oldStart int
|
||||
// the line number of the first line in the new file ('14' in the above example)
|
||||
newStart int
|
||||
// the context at the end of the header line (' func (f *CommitFile) Description() string {' in the above example)
|
||||
headerContext string
|
||||
// the body of the hunk, excluding the header line
|
||||
bodyLines []*PatchLine
|
||||
}
|
||||
|
||||
func (hunk *PatchHunk) LastLineIdx() int {
|
||||
return hunk.FirstLineIdx + len(hunk.bodyLines)
|
||||
// Returns the number of lines in the hunk in the original file ('2' in the above example)
|
||||
func (self *Hunk) oldLength() int {
|
||||
return nLinesWithKind(self.bodyLines, []PatchLineKind{CONTEXT, DELETION})
|
||||
}
|
||||
|
||||
func newHunk(lines []string, firstLineIdx int) *PatchHunk {
|
||||
header := lines[0]
|
||||
bodyLines := lines[1:]
|
||||
|
||||
oldStart, newStart, heading := headerInfo(header)
|
||||
|
||||
return &PatchHunk{
|
||||
oldStart: oldStart,
|
||||
newStart: newStart,
|
||||
heading: heading,
|
||||
FirstLineIdx: firstLineIdx,
|
||||
bodyLines: bodyLines,
|
||||
}
|
||||
// Returns the number of lines in the hunk in the new file ('3' in the above example)
|
||||
func (self *Hunk) newLength() int {
|
||||
return nLinesWithKind(self.bodyLines, []PatchLineKind{CONTEXT, ADDITION})
|
||||
}
|
||||
|
||||
func headerInfo(header string) (int, int, string) {
|
||||
match := hunkHeaderRegexp.FindStringSubmatch(header)
|
||||
|
||||
oldStart := utils.MustConvertToInt(match[1])
|
||||
newStart := utils.MustConvertToInt(match[2])
|
||||
heading := match[3]
|
||||
|
||||
return oldStart, newStart, heading
|
||||
// Returns true if the hunk contains any changes (i.e. if it's not just a context hunk).
|
||||
// We'll end up with a context hunk if we're transforming a patch and one of the hunks
|
||||
// has no selected lines.
|
||||
func (self *Hunk) containsChanges() bool {
|
||||
return nLinesWithKind(self.bodyLines, []PatchLineKind{ADDITION, DELETION}) > 0
|
||||
}
|
||||
|
||||
func (hunk *PatchHunk) updatedLines(lineIndices []int, reverse bool) []string {
|
||||
skippedNewlineMessageIndex := -1
|
||||
newLines := []string{}
|
||||
// Returns the number of lines in the hunk, including the header line
|
||||
func (self *Hunk) lineCount() int {
|
||||
return len(self.bodyLines) + 1
|
||||
}
|
||||
|
||||
lineIdx := hunk.FirstLineIdx
|
||||
for _, line := range hunk.bodyLines {
|
||||
lineIdx++ // incrementing at the start to skip the header line
|
||||
if line == "" {
|
||||
break
|
||||
}
|
||||
isLineSelected := lo.Contains(lineIndices, lineIdx)
|
||||
// Returns all lines in the hunk, including the header line
|
||||
func (self *Hunk) allLines() []*PatchLine {
|
||||
lines := []*PatchLine{{Content: self.formatHeaderLine(), Kind: HUNK_HEADER}}
|
||||
lines = append(lines, self.bodyLines...)
|
||||
return lines
|
||||
}
|
||||
|
||||
firstChar, content := line[:1], line[1:]
|
||||
transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineSelected)
|
||||
// Returns the header line, including the unified diff header and the context
|
||||
func (self *Hunk) formatHeaderLine() string {
|
||||
return fmt.Sprintf("%s%s", self.formatHeaderStart(), self.headerContext)
|
||||
}
|
||||
|
||||
if isLineSelected || (transformedFirstChar == "\\" && skippedNewlineMessageIndex != lineIdx) || transformedFirstChar == " " {
|
||||
newLines = append(newLines, transformedFirstChar+content)
|
||||
continue
|
||||
}
|
||||
|
||||
if transformedFirstChar == "+" {
|
||||
// we don't want to include the 'newline at end of file' line if it involves an addition we're not including
|
||||
skippedNewlineMessageIndex = lineIdx + 1
|
||||
}
|
||||
// Returns the first part of the header line i.e. the unified diff part (excluding any context)
|
||||
func (self *Hunk) formatHeaderStart() string {
|
||||
newLengthDisplay := ""
|
||||
newLength := self.newLength()
|
||||
// if the new length is 1, it's omitted
|
||||
if newLength != 1 {
|
||||
newLengthDisplay = fmt.Sprintf(",%d", newLength)
|
||||
}
|
||||
|
||||
return newLines
|
||||
}
|
||||
|
||||
func transformedFirstChar(firstChar string, reverse bool, isLineSelected bool) string {
|
||||
if reverse {
|
||||
if !isLineSelected && firstChar == "+" {
|
||||
return " "
|
||||
} else if firstChar == "-" {
|
||||
return "+"
|
||||
} else if firstChar == "+" {
|
||||
return "-"
|
||||
} else {
|
||||
return firstChar
|
||||
}
|
||||
}
|
||||
|
||||
if !isLineSelected && firstChar == "-" {
|
||||
return " "
|
||||
}
|
||||
|
||||
return firstChar
|
||||
}
|
||||
|
||||
func (hunk *PatchHunk) formatHeader(oldStart int, oldLength int, newStart int, newLength int, heading string) string {
|
||||
return fmt.Sprintf("@@ -%d,%d +%d,%d @@%s\n", oldStart, oldLength, newStart, newLength, heading)
|
||||
}
|
||||
|
||||
func (hunk *PatchHunk) formatWithChanges(lineIndices []int, reverse bool, startOffset int) (int, string) {
|
||||
bodyLines := hunk.updatedLines(lineIndices, reverse)
|
||||
startOffset, header, ok := hunk.updatedHeader(bodyLines, startOffset, reverse)
|
||||
if !ok {
|
||||
return startOffset, ""
|
||||
}
|
||||
return startOffset, header + strings.Join(bodyLines, "")
|
||||
}
|
||||
|
||||
func (hunk *PatchHunk) updatedHeader(newBodyLines []string, startOffset int, reverse bool) (int, string, bool) {
|
||||
changeCount := nLinesWithPrefix(newBodyLines, []string{"+", "-"})
|
||||
oldLength := nLinesWithPrefix(newBodyLines, []string{" ", "-"})
|
||||
newLength := nLinesWithPrefix(newBodyLines, []string{"+", " "})
|
||||
|
||||
if changeCount == 0 {
|
||||
// if nothing has changed we just return nothing
|
||||
return startOffset, "", false
|
||||
}
|
||||
|
||||
var oldStart int
|
||||
if reverse {
|
||||
oldStart = hunk.newStart
|
||||
} else {
|
||||
oldStart = hunk.oldStart
|
||||
}
|
||||
|
||||
var newStartOffset int
|
||||
// if the hunk went from zero to positive length, we need to increment the starting point by one
|
||||
// if the hunk went from positive to zero length, we need to decrement the starting point by one
|
||||
if oldLength == 0 {
|
||||
newStartOffset = 1
|
||||
} else if newLength == 0 {
|
||||
newStartOffset = -1
|
||||
} else {
|
||||
newStartOffset = 0
|
||||
}
|
||||
|
||||
newStart := oldStart + startOffset + newStartOffset
|
||||
|
||||
newStartOffset = startOffset + newLength - oldLength
|
||||
formattedHeader := hunk.formatHeader(oldStart, oldLength, newStart, newLength, hunk.heading)
|
||||
return newStartOffset, formattedHeader, true
|
||||
return fmt.Sprintf("@@ -%d,%d +%d%s @@", self.oldStart, self.oldLength(), self.newStart, newLengthDisplay)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
|
||||
|
||||
func Parse(patchStr string) *Patch {
|
||||
// ignore trailing newline.
|
||||
lines := strings.Split(strings.TrimSuffix(patchStr, "\n"), "\n")
|
||||
|
||||
hunks := []*Hunk{}
|
||||
patchHeader := []string{}
|
||||
|
||||
var currentHunk *Hunk
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "@@") {
|
||||
oldStart, newStart, headerContext := headerInfo(line)
|
||||
|
||||
currentHunk = &Hunk{
|
||||
oldStart: oldStart,
|
||||
newStart: newStart,
|
||||
headerContext: headerContext,
|
||||
bodyLines: []*PatchLine{},
|
||||
}
|
||||
hunks = append(hunks, currentHunk)
|
||||
} else if currentHunk != nil {
|
||||
currentHunk.bodyLines = append(currentHunk.bodyLines, newHunkLine(line))
|
||||
} else {
|
||||
patchHeader = append(patchHeader, line)
|
||||
}
|
||||
}
|
||||
|
||||
return &Patch{
|
||||
hunks: hunks,
|
||||
header: patchHeader,
|
||||
}
|
||||
}
|
||||
|
||||
func headerInfo(header string) (int, int, string) {
|
||||
match := hunkHeaderRegexp.FindStringSubmatch(header)
|
||||
|
||||
oldStart := utils.MustConvertToInt(match[1])
|
||||
newStart := utils.MustConvertToInt(match[2])
|
||||
headerContext := match[3]
|
||||
|
||||
return oldStart, newStart, headerContext
|
||||
}
|
||||
|
||||
func newHunkLine(line string) *PatchLine {
|
||||
if line == "" {
|
||||
return &PatchLine{
|
||||
Kind: CONTEXT,
|
||||
Content: "",
|
||||
}
|
||||
}
|
||||
|
||||
firstChar := line[:1]
|
||||
|
||||
kind := parseFirstChar(firstChar)
|
||||
|
||||
return &PatchLine{
|
||||
Kind: kind,
|
||||
Content: line,
|
||||
}
|
||||
}
|
||||
|
||||
func parseFirstChar(firstChar string) PatchLineKind {
|
||||
switch firstChar {
|
||||
case " ":
|
||||
return CONTEXT
|
||||
case "+":
|
||||
return ADDITION
|
||||
case "-":
|
||||
return DELETION
|
||||
case "\\":
|
||||
return NEWLINE_MESSAGE
|
||||
}
|
||||
|
||||
return CONTEXT
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type Patch struct {
|
||||
// header of the patch (split on newlines) e.g.
|
||||
// diff --git a/filename b/filename
|
||||
// index dcd3485..1ba5540 100644
|
||||
// --- a/filename
|
||||
// +++ b/filename
|
||||
header []string
|
||||
// hunks of the patch
|
||||
hunks []*Hunk
|
||||
}
|
||||
|
||||
// Returns a new patch with the specified transformation applied (e.g.
|
||||
// only selecting a subset of changes).
|
||||
// Leaves the original patch unchanged.
|
||||
func (self *Patch) Transform(opts TransformOpts) *Patch {
|
||||
return transform(self, opts)
|
||||
}
|
||||
|
||||
// Returns the patch as a plain string
|
||||
func (self *Patch) FormatPlain() string {
|
||||
return formatPlain(self)
|
||||
}
|
||||
|
||||
// Returns a range of lines from the patch as a plain string (range is inclusive)
|
||||
func (self *Patch) FormatRangePlain(startIdx int, endIdx int) string {
|
||||
return formatRangePlain(self, startIdx, endIdx)
|
||||
}
|
||||
|
||||
// Returns the patch as a string with ANSI color codes for displaying in a view
|
||||
func (self *Patch) FormatView(opts FormatViewOpts) string {
|
||||
return formatView(self, opts)
|
||||
}
|
||||
|
||||
// Returns the lines of the patch
|
||||
func (self *Patch) Lines() []*PatchLine {
|
||||
lines := []*PatchLine{}
|
||||
for _, line := range self.header {
|
||||
lines = append(lines, &PatchLine{Content: line, Kind: PATCH_HEADER})
|
||||
}
|
||||
|
||||
for _, hunk := range self.hunks {
|
||||
lines = append(lines, hunk.allLines()...)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// Returns the patch line index of the first line in the given hunk
|
||||
func (self *Patch) HunkStartIdx(hunkIndex int) int {
|
||||
hunkIndex = utils.Clamp(hunkIndex, 0, len(self.hunks)-1)
|
||||
|
||||
result := len(self.header)
|
||||
for i := 0; i < hunkIndex; i++ {
|
||||
result += self.hunks[i].lineCount()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Returns the patch line index of the last line in the given hunk
|
||||
func (self *Patch) HunkEndIdx(hunkIndex int) int {
|
||||
hunkIndex = utils.Clamp(hunkIndex, 0, len(self.hunks)-1)
|
||||
|
||||
return self.HunkStartIdx(hunkIndex) + self.hunks[hunkIndex].lineCount() - 1
|
||||
}
|
||||
|
||||
func (self *Patch) ContainsChanges() bool {
|
||||
return lo.SomeBy(self.hunks, func(hunk *Hunk) bool {
|
||||
return hunk.containsChanges()
|
||||
})
|
||||
}
|
||||
|
||||
// Takes a line index in the patch and returns the line number in the new file.
|
||||
// If the line is a header line, returns 1.
|
||||
// If the line is a hunk header line, returns the first file line number in that hunk.
|
||||
// If the line is out of range below, returns the last file line number in the last hunk.
|
||||
func (self *Patch) LineNumberOfLine(idx int) int {
|
||||
if idx < len(self.header) || len(self.hunks) == 0 {
|
||||
return 1
|
||||
}
|
||||
|
||||
hunkIdx := self.HunkContainingLine(idx)
|
||||
// cursor out of range, return last file line number
|
||||
if hunkIdx == -1 {
|
||||
lastHunk := self.hunks[len(self.hunks)-1]
|
||||
return lastHunk.newStart + lastHunk.newLength() - 1
|
||||
}
|
||||
|
||||
hunk := self.hunks[hunkIdx]
|
||||
hunkStartIdx := self.HunkStartIdx(hunkIdx)
|
||||
idxInHunk := idx - hunkStartIdx
|
||||
|
||||
if idxInHunk == 0 {
|
||||
return hunk.oldStart
|
||||
}
|
||||
|
||||
lines := hunk.bodyLines[:idxInHunk-1]
|
||||
offset := nLinesWithKind(lines, []PatchLineKind{ADDITION, CONTEXT})
|
||||
return hunk.oldStart + offset
|
||||
}
|
||||
|
||||
// Returns hunk index containing the line at the given patch line index
|
||||
func (self *Patch) HunkContainingLine(idx int) int {
|
||||
for hunkIdx, hunk := range self.hunks {
|
||||
hunkStartIdx := self.HunkStartIdx(hunkIdx)
|
||||
if idx >= hunkStartIdx && idx < hunkStartIdx+hunk.lineCount() {
|
||||
return hunkIdx
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Returns the patch line index of the next change (i.e. addition or deletion).
|
||||
func (self *Patch) GetNextChangeIdx(idx int) int {
|
||||
idx = utils.Clamp(idx, 0, self.LineCount()-1)
|
||||
|
||||
lines := self.Lines()
|
||||
|
||||
for i, line := range lines[idx:] {
|
||||
if line.isChange() {
|
||||
return i + idx
|
||||
}
|
||||
}
|
||||
|
||||
// there are no changes from the cursor onwards so we'll instead
|
||||
// return the index of the last change
|
||||
for i := len(lines) - 1; i >= 0; i-- {
|
||||
line := lines[i]
|
||||
if line.isChange() {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
// should not be possible
|
||||
return 0
|
||||
}
|
||||
|
||||
// Returns the length of the patch in lines
|
||||
func (self *Patch) LineCount() int {
|
||||
count := len(self.header)
|
||||
for _, hunk := range self.hunks {
|
||||
count += hunk.lineCount()
|
||||
}
|
||||
return count
|
||||
}
|
||||
@@ -29,12 +29,11 @@ type fileInfo struct {
|
||||
}
|
||||
|
||||
type (
|
||||
applyPatchFunc func(patch string, flags ...string) error
|
||||
loadFileDiffFunc func(from string, to string, reverse bool, filename string, plain bool) (string, error)
|
||||
)
|
||||
|
||||
// PatchManager manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit). We also support building patches from things like stashes, for which there is less flexibility
|
||||
type PatchManager struct {
|
||||
// PatchBuilder manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit). We also support building patches from things like stashes, for which there is less flexibility
|
||||
type PatchBuilder struct {
|
||||
// To is the commit sha if we're dealing with files of a commit, or a stash ref for a stash
|
||||
To string
|
||||
From string
|
||||
@@ -47,23 +46,19 @@ type PatchManager struct {
|
||||
// fileInfoMap starts empty but you add files to it as you go along
|
||||
fileInfoMap map[string]*fileInfo
|
||||
Log *logrus.Entry
|
||||
applyPatch applyPatchFunc
|
||||
|
||||
// loadFileDiff loads the diff of a file, for a given to (typically a commit SHA)
|
||||
loadFileDiff loadFileDiffFunc
|
||||
}
|
||||
|
||||
// NewPatchManager returns a new PatchManager
|
||||
func NewPatchManager(log *logrus.Entry, applyPatch applyPatchFunc, loadFileDiff loadFileDiffFunc) *PatchManager {
|
||||
return &PatchManager{
|
||||
func NewPatchBuilder(log *logrus.Entry, loadFileDiff loadFileDiffFunc) *PatchBuilder {
|
||||
return &PatchBuilder{
|
||||
Log: log,
|
||||
applyPatch: applyPatch,
|
||||
loadFileDiff: loadFileDiff,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPatchManager returns a new PatchManager
|
||||
func (p *PatchManager) Start(from, to string, reverse bool, canRebase bool) {
|
||||
func (p *PatchBuilder) Start(from, to string, reverse bool, canRebase bool) {
|
||||
p.To = to
|
||||
p.From = from
|
||||
p.reverse = reverse
|
||||
@@ -71,7 +66,21 @@ func (p *PatchManager) Start(from, to string, reverse bool, canRebase bool) {
|
||||
p.fileInfoMap = map[string]*fileInfo{}
|
||||
}
|
||||
|
||||
func (p *PatchManager) addFileWhole(info *fileInfo) {
|
||||
func (p *PatchBuilder) PatchToApply(reverse bool) string {
|
||||
patch := ""
|
||||
|
||||
for filename, info := range p.fileInfoMap {
|
||||
if info.mode == UNSELECTED {
|
||||
continue
|
||||
}
|
||||
|
||||
patch += p.RenderPatchForFile(filename, true, reverse)
|
||||
}
|
||||
|
||||
return patch
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) addFileWhole(info *fileInfo) {
|
||||
info.mode = WHOLE
|
||||
lineCount := len(strings.Split(info.diff, "\n"))
|
||||
// add every line index
|
||||
@@ -82,12 +91,12 @@ func (p *PatchManager) addFileWhole(info *fileInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PatchManager) removeFile(info *fileInfo) {
|
||||
func (p *PatchBuilder) removeFile(info *fileInfo) {
|
||||
info.mode = UNSELECTED
|
||||
info.includedLineIndices = nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) AddFileWhole(filename string) error {
|
||||
func (p *PatchBuilder) AddFileWhole(filename string) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -98,7 +107,7 @@ func (p *PatchManager) AddFileWhole(filename string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) RemoveFile(filename string) error {
|
||||
func (p *PatchBuilder) RemoveFile(filename string) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -117,7 +126,7 @@ func getIndicesForRange(first, last int) []int {
|
||||
return indices
|
||||
}
|
||||
|
||||
func (p *PatchManager) getFileInfo(filename string) (*fileInfo, error) {
|
||||
func (p *PatchBuilder) getFileInfo(filename string) (*fileInfo, error) {
|
||||
info, ok := p.fileInfoMap[filename]
|
||||
if ok {
|
||||
return info, nil
|
||||
@@ -137,7 +146,7 @@ func (p *PatchManager) getFileInfo(filename string) (*fileInfo, error) {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
|
||||
func (p *PatchBuilder) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -148,7 +157,7 @@ func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineI
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
|
||||
func (p *PatchBuilder) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -162,44 +171,46 @@ func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLi
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) renderPlainPatchForFile(filename string, reverse bool, keepOriginalHeader bool) string {
|
||||
func (p *PatchBuilder) RenderPatchForFile(filename string, plain bool, reverse bool) string {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
p.Log.Error(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
switch info.mode {
|
||||
case WHOLE:
|
||||
// use the whole diff
|
||||
// the reverse flag is only for part patches so we're ignoring it here
|
||||
return info.diff
|
||||
case PART:
|
||||
// generate a new diff with just the selected lines
|
||||
return ModifiedPatchForLines(p.Log, filename, info.diff, info.includedLineIndices, reverse, keepOriginalHeader)
|
||||
default:
|
||||
if info.mode == UNSELECTED {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse bool, keepOriginalHeader bool) string {
|
||||
patch := p.renderPlainPatchForFile(filename, reverse, keepOriginalHeader)
|
||||
if plain {
|
||||
return patch
|
||||
if info.mode == WHOLE && plain {
|
||||
// Use the whole diff (spares us parsing it and then formatting it).
|
||||
// TODO: see if this is actually noticeably faster.
|
||||
// The reverse flag is only for part patches so we're ignoring it here.
|
||||
return info.diff
|
||||
}
|
||||
parser := NewPatchParser(p.Log, patch)
|
||||
|
||||
// not passing included lines because we don't want to see them in the secondary panel
|
||||
return parser.Render(false, -1, -1, nil)
|
||||
patch := Parse(info.diff).
|
||||
Transform(TransformOpts{
|
||||
Reverse: reverse,
|
||||
IncludedLineIndices: info.includedLineIndices,
|
||||
})
|
||||
|
||||
if plain {
|
||||
return patch.FormatPlain()
|
||||
} else {
|
||||
return patch.FormatView(FormatViewOpts{
|
||||
IsFocused: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PatchManager) renderEachFilePatch(plain bool) []string {
|
||||
func (p *PatchBuilder) renderEachFilePatch(plain bool) []string {
|
||||
// sort files by name then iterate through and render each patch
|
||||
filenames := maps.Keys(p.fileInfoMap)
|
||||
|
||||
sort.Strings(filenames)
|
||||
patches := slices.Map(filenames, func(filename string) string {
|
||||
return p.RenderPatchForFile(filename, plain, false, true)
|
||||
return p.RenderPatchForFile(filename, plain, false)
|
||||
})
|
||||
output := slices.Filter(patches, func(patch string) bool {
|
||||
return patch != ""
|
||||
@@ -208,17 +219,11 @@ func (p *PatchManager) renderEachFilePatch(plain bool) []string {
|
||||
return output
|
||||
}
|
||||
|
||||
func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
|
||||
result := ""
|
||||
for _, patch := range p.renderEachFilePatch(plain) {
|
||||
if patch != "" {
|
||||
result += patch + "\n"
|
||||
}
|
||||
}
|
||||
return result
|
||||
func (p *PatchBuilder) RenderAggregatedPatch(plain bool) string {
|
||||
return strings.Join(p.renderEachFilePatch(plain), "")
|
||||
}
|
||||
|
||||
func (p *PatchManager) GetFileStatus(filename string, parent string) PatchStatus {
|
||||
func (p *PatchBuilder) GetFileStatus(filename string, parent string) PatchStatus {
|
||||
if parent != p.To {
|
||||
return UNSELECTED
|
||||
}
|
||||
@@ -231,7 +236,7 @@ func (p *PatchManager) GetFileStatus(filename string, parent string) PatchStatus
|
||||
return info.mode
|
||||
}
|
||||
|
||||
func (p *PatchManager) GetFileIncLineIndices(filename string) ([]int, error) {
|
||||
func (p *PatchBuilder) GetFileIncLineIndices(filename string) ([]int, error) {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -239,56 +244,17 @@ func (p *PatchManager) GetFileIncLineIndices(filename string) ([]int, error) {
|
||||
return info.includedLineIndices, nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) ApplyPatches(reverse bool) error {
|
||||
// for whole patches we'll apply the patch in reverse
|
||||
// but for part patches we'll apply a reverse patch forwards
|
||||
for filename, info := range p.fileInfoMap {
|
||||
if info.mode == UNSELECTED {
|
||||
continue
|
||||
}
|
||||
|
||||
applyFlags := []string{"index", "3way"}
|
||||
reverseOnGenerate := false
|
||||
if reverse {
|
||||
if info.mode == WHOLE {
|
||||
applyFlags = append(applyFlags, "reverse")
|
||||
} else {
|
||||
reverseOnGenerate = true
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
// first run we try with the original header, then without
|
||||
for _, keepOriginalHeader := range []bool{true, false} {
|
||||
patch := p.RenderPatchForFile(filename, true, reverseOnGenerate, keepOriginalHeader)
|
||||
if patch == "" {
|
||||
continue
|
||||
}
|
||||
if err = p.applyPatch(patch, applyFlags...); err != nil {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clears the patch
|
||||
func (p *PatchManager) Reset() {
|
||||
func (p *PatchBuilder) Reset() {
|
||||
p.To = ""
|
||||
p.fileInfoMap = map[string]*fileInfo{}
|
||||
}
|
||||
|
||||
func (p *PatchManager) Active() bool {
|
||||
func (p *PatchBuilder) Active() bool {
|
||||
return p.To != ""
|
||||
}
|
||||
|
||||
func (p *PatchManager) IsEmpty() bool {
|
||||
func (p *PatchBuilder) IsEmpty() bool {
|
||||
for _, fileInfo := range p.fileInfoMap {
|
||||
if fileInfo.mode == WHOLE || (fileInfo.mode == PART && len(fileInfo.includedLineIndices) > 0) {
|
||||
return false
|
||||
@@ -299,6 +265,16 @@ func (p *PatchManager) IsEmpty() bool {
|
||||
}
|
||||
|
||||
// if any of these things change we'll need to reset and start a new patch
|
||||
func (p *PatchManager) NewPatchRequired(from string, to string, reverse bool) bool {
|
||||
func (p *PatchBuilder) NewPatchRequired(from string, to string, reverse bool) bool {
|
||||
return from != p.From || to != p.To || reverse != p.reverse
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) AllFilesInPatch() []string {
|
||||
files := make([]string, 0, len(p.fileInfoMap))
|
||||
|
||||
for filename := range p.fileInfoMap {
|
||||
files = append(files, filename)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package patch
|
||||
|
||||
import "github.com/samber/lo"
|
||||
|
||||
type PatchLineKind int
|
||||
|
||||
const (
|
||||
PATCH_HEADER PatchLineKind = iota
|
||||
HUNK_HEADER
|
||||
ADDITION
|
||||
DELETION
|
||||
CONTEXT
|
||||
NEWLINE_MESSAGE
|
||||
)
|
||||
|
||||
type PatchLine struct {
|
||||
Kind PatchLineKind
|
||||
Content string // something like '+ hello' (note the first character is not removed)
|
||||
}
|
||||
|
||||
func (self *PatchLine) isChange() bool {
|
||||
return self.Kind == ADDITION || self.Kind == DELETION
|
||||
}
|
||||
|
||||
// Returns the number of lines in the given slice that have one of the given kinds
|
||||
func nLinesWithKind(lines []*PatchLine, kinds []PatchLineKind) int {
|
||||
return lo.CountBy(lines, func(line *PatchLine) bool {
|
||||
return lo.Contains(kinds, line.Kind)
|
||||
})
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
|
||||
patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
|
||||
)
|
||||
|
||||
func GetHeaderFromDiff(diff string) string {
|
||||
match := patchHeaderRegexp.FindStringSubmatch(diff)
|
||||
if len(match) <= 1 {
|
||||
return ""
|
||||
}
|
||||
return match[1]
|
||||
}
|
||||
|
||||
func GetHunksFromDiff(diff string) []*PatchHunk {
|
||||
hunks := []*PatchHunk{}
|
||||
firstLineIdx := -1
|
||||
var hunkLines []string //nolint:prealloc
|
||||
pastDiffHeader := false
|
||||
|
||||
for lineIdx, line := range strings.SplitAfter(diff, "\n") {
|
||||
isHunkHeader := strings.HasPrefix(line, "@@ -")
|
||||
|
||||
if isHunkHeader {
|
||||
if pastDiffHeader { // we need to persist the current hunk
|
||||
hunks = append(hunks, newHunk(hunkLines, firstLineIdx))
|
||||
}
|
||||
pastDiffHeader = true
|
||||
firstLineIdx = lineIdx
|
||||
hunkLines = []string{line}
|
||||
continue
|
||||
}
|
||||
|
||||
if !pastDiffHeader { // skip through the stuff that precedes the first hunk
|
||||
continue
|
||||
}
|
||||
|
||||
hunkLines = append(hunkLines, line)
|
||||
}
|
||||
|
||||
if pastDiffHeader {
|
||||
hunks = append(hunks, newHunk(hunkLines, firstLineIdx))
|
||||
}
|
||||
|
||||
return hunks
|
||||
}
|
||||
|
||||
type PatchModifier struct {
|
||||
Log *logrus.Entry
|
||||
filename string
|
||||
hunks []*PatchHunk
|
||||
header string
|
||||
}
|
||||
|
||||
func NewPatchModifier(log *logrus.Entry, filename string, diffText string) *PatchModifier {
|
||||
return &PatchModifier{
|
||||
Log: log,
|
||||
filename: filename,
|
||||
hunks: GetHunksFromDiff(diffText),
|
||||
header: GetHeaderFromDiff(diffText),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *PatchModifier) ModifiedPatchForLines(lineIndices []int, reverse bool, keepOriginalHeader bool) string {
|
||||
// step one is getting only those hunks which we care about
|
||||
hunksInRange := []*PatchHunk{}
|
||||
outer:
|
||||
for _, hunk := range d.hunks {
|
||||
// if there is any line in our lineIndices array that the hunk contains, we append it
|
||||
for _, lineIdx := range lineIndices {
|
||||
if lineIdx >= hunk.FirstLineIdx && lineIdx <= hunk.LastLineIdx() {
|
||||
hunksInRange = append(hunksInRange, hunk)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// step 2 is collecting all the hunks with new headers
|
||||
startOffset := 0
|
||||
formattedHunks := ""
|
||||
var formattedHunk string
|
||||
for _, hunk := range hunksInRange {
|
||||
startOffset, formattedHunk = hunk.formatWithChanges(lineIndices, reverse, startOffset)
|
||||
formattedHunks += formattedHunk
|
||||
}
|
||||
|
||||
if formattedHunks == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var fileHeader string
|
||||
// for staging/unstaging lines we don't want the original header because
|
||||
// it makes git confused e.g. when dealing with deleted/added files
|
||||
// but with building and applying patches the original header gives git
|
||||
// information it needs to cleanly apply patches
|
||||
if keepOriginalHeader {
|
||||
fileHeader = d.header
|
||||
} else {
|
||||
fileHeader = fmt.Sprintf("--- a/%s\n+++ b/%s\n", d.filename, d.filename)
|
||||
}
|
||||
|
||||
return fileHeader + formattedHunks
|
||||
}
|
||||
|
||||
func (d *PatchModifier) ModifiedPatchForRange(firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
|
||||
// generate array of consecutive line indices from our range
|
||||
selectedLines := []int{}
|
||||
for i := firstLineIdx; i <= lastLineIdx; i++ {
|
||||
selectedLines = append(selectedLines, i)
|
||||
}
|
||||
return d.ModifiedPatchForLines(selectedLines, reverse, keepOriginalHeader)
|
||||
}
|
||||
|
||||
func (d *PatchModifier) OriginalPatchLength() int {
|
||||
if len(d.hunks) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return d.hunks[len(d.hunks)-1].LastLineIdx()
|
||||
}
|
||||
|
||||
func ModifiedPatchForRange(log *logrus.Entry, filename string, diffText string, firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
|
||||
p := NewPatchModifier(log, filename, diffText)
|
||||
return p.ModifiedPatchForRange(firstLineIdx, lastLineIdx, reverse, keepOriginalHeader)
|
||||
}
|
||||
|
||||
func ModifiedPatchForLines(log *logrus.Entry, filename string, diffText string, includedLineIndices []int, reverse bool, keepOriginalHeader bool) string {
|
||||
p := NewPatchModifier(log, filename, diffText)
|
||||
return p.ModifiedPatchForLines(includedLineIndices, reverse, keepOriginalHeader)
|
||||
}
|
||||
|
||||
// I want to know, given a hunk, what line a given index is on
|
||||
func (hunk *PatchHunk) LineNumberOfLine(idx int) int {
|
||||
n := idx - hunk.FirstLineIdx - 1
|
||||
if n < 0 {
|
||||
n = 0
|
||||
} else if n >= len(hunk.bodyLines) {
|
||||
n = len(hunk.bodyLines) - 1
|
||||
}
|
||||
|
||||
lines := hunk.bodyLines[0:n]
|
||||
|
||||
offset := nLinesWithPrefix(lines, []string{"+", " "})
|
||||
|
||||
return hunk.newStart + offset
|
||||
}
|
||||
|
||||
func nLinesWithPrefix(lines []string, chars []string) int {
|
||||
result := 0
|
||||
for _, line := range lines {
|
||||
for _, char := range chars {
|
||||
if line[:1] == char {
|
||||
result++
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type PatchLineKind int
|
||||
|
||||
const (
|
||||
PATCH_HEADER PatchLineKind = iota
|
||||
COMMIT_SHA
|
||||
COMMIT_DESCRIPTION
|
||||
HUNK_HEADER
|
||||
ADDITION
|
||||
DELETION
|
||||
CONTEXT
|
||||
NEWLINE_MESSAGE
|
||||
)
|
||||
|
||||
// the job of this file is to parse a diff, find out where the hunks begin and end, which lines are stageable, and how to find the next hunk from the current position or the next stageable line from the current position.
|
||||
|
||||
type PatchLine struct {
|
||||
Kind PatchLineKind
|
||||
Content string // something like '+ hello' (note the first character is not removed)
|
||||
}
|
||||
|
||||
type PatchParser struct {
|
||||
Log *logrus.Entry
|
||||
PatchLines []*PatchLine
|
||||
PatchHunks []*PatchHunk
|
||||
HunkStarts []int
|
||||
StageableLines []int // rename to mention we're talking about indexes
|
||||
}
|
||||
|
||||
// NewPatchParser builds a new branch list builder
|
||||
func NewPatchParser(log *logrus.Entry, patch string) *PatchParser {
|
||||
hunkStarts, stageableLines, patchLines := parsePatch(patch)
|
||||
|
||||
patchHunks := GetHunksFromDiff(patch)
|
||||
|
||||
return &PatchParser{
|
||||
Log: log,
|
||||
HunkStarts: hunkStarts, // deprecated
|
||||
StageableLines: stageableLines,
|
||||
PatchLines: patchLines,
|
||||
PatchHunks: patchHunks,
|
||||
}
|
||||
}
|
||||
|
||||
// GetHunkContainingLine takes a line index and an offset and finds the hunk
|
||||
// which contains the line index, then returns the hunk considering the offset.
|
||||
// e.g. if the offset is 1 it will return the next hunk.
|
||||
func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHunk {
|
||||
if len(p.PatchHunks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for index, hunk := range p.PatchHunks {
|
||||
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx() {
|
||||
resultIndex := index + offset
|
||||
if resultIndex < 0 {
|
||||
resultIndex = 0
|
||||
} else if resultIndex > len(p.PatchHunks)-1 {
|
||||
resultIndex = len(p.PatchHunks) - 1
|
||||
}
|
||||
return p.PatchHunks[resultIndex]
|
||||
}
|
||||
}
|
||||
|
||||
// if your cursor is past the last hunk, select the last hunk
|
||||
if lineIndex > p.PatchHunks[len(p.PatchHunks)-1].LastLineIdx() {
|
||||
return p.PatchHunks[len(p.PatchHunks)-1]
|
||||
}
|
||||
|
||||
// otherwise select the first
|
||||
return p.PatchHunks[0]
|
||||
}
|
||||
|
||||
// selected means you've got it highlighted with your cursor
|
||||
// included means the line has been included in the patch (only applicable when
|
||||
// building a patch)
|
||||
func (l *PatchLine) render(selected bool, included bool) string {
|
||||
content := l.Content
|
||||
if len(content) == 0 {
|
||||
content = " " // using the space so that we can still highlight if necessary
|
||||
}
|
||||
|
||||
// for hunk headers we need to start off cyan and then use white for the message
|
||||
if l.Kind == HUNK_HEADER {
|
||||
re := regexp.MustCompile("(@@.*?@@)(.*)")
|
||||
match := re.FindStringSubmatch(content)
|
||||
return coloredString(style.FgCyan, match[1], selected, included) + coloredString(theme.DefaultTextColor, match[2], selected, false)
|
||||
}
|
||||
|
||||
var textStyle style.TextStyle
|
||||
switch l.Kind {
|
||||
case PATCH_HEADER:
|
||||
textStyle = textStyle.SetBold()
|
||||
case ADDITION:
|
||||
textStyle = style.FgGreen
|
||||
case DELETION:
|
||||
textStyle = style.FgRed
|
||||
case COMMIT_SHA:
|
||||
textStyle = style.FgYellow
|
||||
default:
|
||||
textStyle = theme.DefaultTextColor
|
||||
}
|
||||
|
||||
return coloredString(textStyle, content, selected, included)
|
||||
}
|
||||
|
||||
func coloredString(textStyle style.TextStyle, str string, selected bool, included bool) string {
|
||||
if selected {
|
||||
textStyle = textStyle.MergeStyle(theme.SelectedRangeBgColor)
|
||||
}
|
||||
|
||||
firstCharStyle := textStyle
|
||||
if included {
|
||||
firstCharStyle = firstCharStyle.MergeStyle(style.BgGreen)
|
||||
}
|
||||
|
||||
if len(str) < 2 {
|
||||
return firstCharStyle.Sprint(str)
|
||||
}
|
||||
|
||||
return firstCharStyle.Sprint(str[:1]) + textStyle.Sprint(str[1:])
|
||||
}
|
||||
|
||||
func parsePatch(patch string) ([]int, []int, []*PatchLine) {
|
||||
lines := strings.Split(patch, "\n")
|
||||
hunkStarts := []int{}
|
||||
stageableLines := []int{}
|
||||
pastFirstHunkHeader := false
|
||||
pastCommitDescription := true
|
||||
patchLines := make([]*PatchLine, len(lines))
|
||||
var lineKind PatchLineKind
|
||||
var firstChar string
|
||||
for index, line := range lines {
|
||||
firstChar = " "
|
||||
if len(line) > 0 {
|
||||
firstChar = line[:1]
|
||||
}
|
||||
if index == 0 && strings.HasPrefix(line, "commit") {
|
||||
lineKind = COMMIT_SHA
|
||||
pastCommitDescription = false
|
||||
} else if !pastCommitDescription {
|
||||
if strings.HasPrefix(line, "diff") || strings.HasPrefix(line, "---") {
|
||||
pastCommitDescription = true
|
||||
lineKind = PATCH_HEADER
|
||||
} else {
|
||||
lineKind = COMMIT_DESCRIPTION
|
||||
}
|
||||
} else if firstChar == "@" {
|
||||
pastFirstHunkHeader = true
|
||||
hunkStarts = append(hunkStarts, index)
|
||||
lineKind = HUNK_HEADER
|
||||
} else if pastFirstHunkHeader {
|
||||
switch firstChar {
|
||||
case "-":
|
||||
lineKind = DELETION
|
||||
stageableLines = append(stageableLines, index)
|
||||
case "+":
|
||||
lineKind = ADDITION
|
||||
stageableLines = append(stageableLines, index)
|
||||
case "\\":
|
||||
lineKind = NEWLINE_MESSAGE
|
||||
case " ":
|
||||
lineKind = CONTEXT
|
||||
}
|
||||
} else {
|
||||
lineKind = PATCH_HEADER
|
||||
}
|
||||
patchLines[index] = &PatchLine{Kind: lineKind, Content: line}
|
||||
}
|
||||
return hunkStarts, stageableLines, patchLines
|
||||
}
|
||||
|
||||
// Render returns the coloured string of the diff with any selected lines highlighted
|
||||
func (p *PatchParser) Render(isFocused bool, firstLineIndex int, lastLineIndex int, incLineIndices []int) string {
|
||||
contentToDisplay := slices.Some(p.PatchLines, func(line *PatchLine) bool {
|
||||
return line.Content != ""
|
||||
})
|
||||
if !contentToDisplay {
|
||||
return ""
|
||||
}
|
||||
|
||||
renderedLines := slices.MapWithIndex(p.PatchLines, func(patchLine *PatchLine, index int) string {
|
||||
selected := isFocused && index >= firstLineIndex && index <= lastLineIndex
|
||||
included := lo.Contains(incLineIndices, index)
|
||||
return patchLine.render(selected, included)
|
||||
})
|
||||
|
||||
result := strings.Join(renderedLines, "\n")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// RenderLinesPlain returns the non-coloured string of diff part from firstLineIndex to
|
||||
// lastLineIndex
|
||||
func (p *PatchParser) RenderLinesPlain(firstLineIndex, lastLineIndex int) string {
|
||||
return renderLinesPlain(p.PatchLines[firstLineIndex : lastLineIndex+1])
|
||||
}
|
||||
|
||||
func renderLinesPlain(lines []*PatchLine) string {
|
||||
renderedLines := slices.Map(lines, func(line *PatchLine) string {
|
||||
return line.Content + "\n"
|
||||
})
|
||||
|
||||
return strings.Join(renderedLines, "")
|
||||
}
|
||||
|
||||
// GetNextStageableLineIndex takes a line index and returns the line index of the next stageable line
|
||||
// note this will actually include the current index if it is stageable
|
||||
func (p *PatchParser) GetNextStageableLineIndex(currentIndex int) int {
|
||||
for _, lineIndex := range p.StageableLines {
|
||||
if lineIndex >= currentIndex {
|
||||
return lineIndex
|
||||
}
|
||||
}
|
||||
return p.StageableLines[len(p.StageableLines)-1]
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -69,6 +67,20 @@ index e48a11c..b2ab81b 100644
|
||||
...
|
||||
`
|
||||
|
||||
const twoChangesInOneHunk = `diff --git a/filename b/filename
|
||||
index 9320895..6d79956 100644
|
||||
--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-grape
|
||||
+kiwi
|
||||
orange
|
||||
-pear
|
||||
+banana
|
||||
lemon
|
||||
`
|
||||
|
||||
const newFile = `diff --git a/newfile b/newfile
|
||||
new file mode 100644
|
||||
index 0000000..4e680cc
|
||||
@@ -98,8 +110,7 @@ const exampleHunk = `@@ -1,5 +1,5 @@
|
||||
...
|
||||
`
|
||||
|
||||
// TestModifyPatchForRange is a function.
|
||||
func TestModifyPatchForRange(t *testing.T) {
|
||||
func TestTransform(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
filename string
|
||||
@@ -116,7 +127,6 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
filename: "filename",
|
||||
firstLineIndex: -1,
|
||||
lastLineIndex: -1,
|
||||
reverse: false,
|
||||
diffText: simpleDiff,
|
||||
expected: "",
|
||||
},
|
||||
@@ -125,7 +135,6 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
filename: "filename",
|
||||
firstLineIndex: 5,
|
||||
lastLineIndex: 5,
|
||||
reverse: false,
|
||||
diffText: simpleDiff,
|
||||
expected: "",
|
||||
},
|
||||
@@ -134,7 +143,6 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
filename: "filename",
|
||||
firstLineIndex: 0,
|
||||
lastLineIndex: 11,
|
||||
reverse: false,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -152,7 +160,6 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
filename: "filename",
|
||||
firstLineIndex: 6,
|
||||
lastLineIndex: 6,
|
||||
reverse: false,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -169,7 +176,6 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
filename: "filename",
|
||||
firstLineIndex: 7,
|
||||
lastLineIndex: 7,
|
||||
reverse: false,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -187,7 +193,6 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
filename: "filename",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: false,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -198,59 +203,6 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "whole range reversed",
|
||||
filename: "filename",
|
||||
firstLineIndex: 0,
|
||||
lastLineIndex: 11,
|
||||
reverse: true,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
+orange
|
||||
-grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "removal reversed",
|
||||
filename: "filename",
|
||||
firstLineIndex: 6,
|
||||
lastLineIndex: 6,
|
||||
reverse: true,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,6 @@
|
||||
apple
|
||||
+orange
|
||||
grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "removal reversed",
|
||||
filename: "filename",
|
||||
firstLineIndex: 7,
|
||||
lastLineIndex: 7,
|
||||
reverse: true,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,4 @@
|
||||
apple
|
||||
-grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -258,7 +210,6 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
filename: "filename",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: false,
|
||||
diffText: addNewlineToEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -272,37 +223,21 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "add newline to end of file, addition only",
|
||||
testName: "add newline to end of file, reversed",
|
||||
filename: "filename",
|
||||
firstLineIndex: 8,
|
||||
lastLineIndex: 8,
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: true,
|
||||
diffText: addNewlineToEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,5 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
+last line
|
||||
\ No newline at end of file
|
||||
last line
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "add newline to end of file, removal only",
|
||||
filename: "filename",
|
||||
firstLineIndex: 10,
|
||||
lastLineIndex: 10,
|
||||
reverse: true,
|
||||
diffText: addNewlineToEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,3 @@ grape
|
||||
@@ -60,4 +60,4 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
-last line
|
||||
\ No newline at end of file
|
||||
+last line
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -310,7 +245,24 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
filename: "filename",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: false,
|
||||
diffText: removeNewlinefromEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,4 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
-last line
|
||||
+last line
|
||||
\ No newline at end of file
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "remove newline from end of file, reversed",
|
||||
filename: "filename",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: true,
|
||||
diffText: removeNewlinefromEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -328,7 +280,6 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
filename: "filename",
|
||||
firstLineIndex: 8,
|
||||
lastLineIndex: 8,
|
||||
reverse: false,
|
||||
diffText: removeNewlinefromEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -337,6 +288,24 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
...
|
||||
...
|
||||
-last line
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "remove newline from end of file, removal only, reversed",
|
||||
filename: "filename",
|
||||
firstLineIndex: 8,
|
||||
lastLineIndex: 8,
|
||||
reverse: true,
|
||||
diffText: removeNewlinefromEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,5 +60,4 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
-last line
|
||||
last line
|
||||
\ No newline at end of file
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -344,7 +313,6 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
filename: "filename",
|
||||
firstLineIndex: 9,
|
||||
lastLineIndex: 9,
|
||||
reverse: false,
|
||||
diffText: removeNewlinefromEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -355,6 +323,23 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
last line
|
||||
+last line
|
||||
\ No newline at end of file
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "remove newline from end of file, addition only, reversed",
|
||||
filename: "filename",
|
||||
firstLineIndex: 9,
|
||||
lastLineIndex: 9,
|
||||
reverse: true,
|
||||
diffText: removeNewlinefromEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,3 +60,4 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
+last line
|
||||
\ No newline at end of file
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -362,7 +347,6 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
filename: "filename",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: false,
|
||||
diffText: twoHunks,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -389,7 +373,6 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
filename: "filename",
|
||||
firstLineIndex: 7,
|
||||
lastLineIndex: 15,
|
||||
reverse: false,
|
||||
diffText: twoHunks,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -408,32 +391,6 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "staging part of both hunks, reversed",
|
||||
filename: "filename",
|
||||
firstLineIndex: 7,
|
||||
lastLineIndex: 15,
|
||||
reverse: true,
|
||||
diffText: twoHunks,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,4 @@
|
||||
apple
|
||||
-orange
|
||||
...
|
||||
...
|
||||
...
|
||||
@@ -8,8 +7,7 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
-pear
|
||||
lemon
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -441,7 +398,6 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
filename: "newfile",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: false,
|
||||
diffText: newFile,
|
||||
expected: `--- a/newfile
|
||||
+++ b/newfile
|
||||
@@ -456,28 +412,12 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
filename: "newfile",
|
||||
firstLineIndex: 6,
|
||||
lastLineIndex: 7,
|
||||
reverse: false,
|
||||
diffText: newFile,
|
||||
expected: `--- a/newfile
|
||||
+++ b/newfile
|
||||
@@ -0,0 +1,2 @@
|
||||
+apple
|
||||
+orange
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "adding a new file, reversed",
|
||||
filename: "newfile",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: true,
|
||||
diffText: newFile,
|
||||
expected: `--- a/newfile
|
||||
+++ b/newfile
|
||||
@@ -1,3 +0,0 @@
|
||||
-apple
|
||||
-orange
|
||||
-grape
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -485,11 +425,10 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
filename: "newfile",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: false,
|
||||
diffText: addNewlineToPreviouslyEmptyFile,
|
||||
expected: `--- a/newfile
|
||||
+++ b/newfile
|
||||
@@ -0,0 +1,1 @@
|
||||
@@ -0,0 +1 @@
|
||||
+new line
|
||||
\ No newline at end of file
|
||||
`,
|
||||
@@ -499,13 +438,49 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
filename: "newfile",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: true,
|
||||
diffText: addNewlineToPreviouslyEmptyFile,
|
||||
reverse: true,
|
||||
expected: `--- a/newfile
|
||||
+++ b/newfile
|
||||
@@ -1,1 +0,0 @@
|
||||
-new line
|
||||
@@ -0,0 +1 @@
|
||||
+new line
|
||||
\ No newline at end of file
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "adding part of a hunk",
|
||||
filename: "filename",
|
||||
firstLineIndex: 6,
|
||||
lastLineIndex: 7,
|
||||
reverse: false,
|
||||
diffText: twoChangesInOneHunk,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-grape
|
||||
+kiwi
|
||||
orange
|
||||
pear
|
||||
lemon
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "adding part of a hunk, reverse",
|
||||
filename: "filename",
|
||||
firstLineIndex: 6,
|
||||
lastLineIndex: 7,
|
||||
reverse: true,
|
||||
diffText: twoChangesInOneHunk,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-grape
|
||||
+kiwi
|
||||
orange
|
||||
banana
|
||||
lemon
|
||||
`,
|
||||
},
|
||||
}
|
||||
@@ -513,37 +488,128 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
result := ModifiedPatchForRange(nil, s.filename, s.diffText, s.firstLineIndex, s.lastLineIndex, s.reverse, false)
|
||||
if !assert.Equal(t, s.expected, result) {
|
||||
fmt.Println(result)
|
||||
}
|
||||
lineIndices := ExpandRange(s.firstLineIndex, s.lastLineIndex)
|
||||
|
||||
result := Parse(s.diffText).
|
||||
Transform(TransformOpts{
|
||||
Reverse: s.reverse,
|
||||
FileNameOverride: s.filename,
|
||||
IncludedLineIndices: lineIndices,
|
||||
}).
|
||||
FormatPlain()
|
||||
|
||||
assert.Equal(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLineNumberOfLine(t *testing.T) {
|
||||
type scenario struct {
|
||||
func TestParseAndFormatPlain(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
hunk *PatchHunk
|
||||
idx int
|
||||
expected int
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
patchStr string
|
||||
}{
|
||||
{
|
||||
testName: "nothing selected",
|
||||
hunk: newHunk(strings.SplitAfter(exampleHunk, "\n"), 10),
|
||||
idx: 15,
|
||||
expected: 3,
|
||||
testName: "simpleDiff",
|
||||
patchStr: simpleDiff,
|
||||
},
|
||||
{
|
||||
testName: "addNewlineToEndOfFile",
|
||||
patchStr: addNewlineToEndOfFile,
|
||||
},
|
||||
{
|
||||
testName: "removeNewlinefromEndOfFile",
|
||||
patchStr: removeNewlinefromEndOfFile,
|
||||
},
|
||||
{
|
||||
testName: "twoHunks",
|
||||
patchStr: twoHunks,
|
||||
},
|
||||
{
|
||||
testName: "twoChangesInOneHunk",
|
||||
patchStr: twoChangesInOneHunk,
|
||||
},
|
||||
{
|
||||
testName: "newFile",
|
||||
patchStr: newFile,
|
||||
},
|
||||
{
|
||||
testName: "addNewlineToPreviouslyEmptyFile",
|
||||
patchStr: addNewlineToPreviouslyEmptyFile,
|
||||
},
|
||||
{
|
||||
testName: "exampleHunk",
|
||||
patchStr: exampleHunk,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
result := s.hunk.LineNumberOfLine(s.idx)
|
||||
if !assert.Equal(t, s.expected, result) {
|
||||
fmt.Println(result)
|
||||
// here we parse the patch, then format it, and ensure the result
|
||||
// matches the original patch. Note that unified diffs allow omitting
|
||||
// the new length in a hunk header if the value is 1, and currently we always
|
||||
// omit the new length in such cases.
|
||||
patch := Parse(s.patchStr)
|
||||
result := formatPlain(patch)
|
||||
assert.Equal(t, s.patchStr, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLineNumberOfLine(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
patchStr string
|
||||
indexes []int
|
||||
expecteds []int
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "twoHunks",
|
||||
patchStr: twoHunks,
|
||||
// this is really more of a characteristic test than anything.
|
||||
indexes: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 1000},
|
||||
expecteds: []int{1, 1, 1, 1, 1, 1, 2, 2, 3, 4, 5, 8, 8, 9, 10, 11, 12, 13, 14, 15, 15, 15, 15, 15, 15},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
for i, idx := range s.indexes {
|
||||
patch := Parse(s.patchStr)
|
||||
result := patch.LineNumberOfLine(idx)
|
||||
assert.Equal(t, s.expecteds[i], result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNextStageableLineIndex(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
patchStr string
|
||||
indexes []int
|
||||
expecteds []int
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "twoHunks",
|
||||
patchStr: twoHunks,
|
||||
indexes: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 1000},
|
||||
expecteds: []int{6, 6, 6, 6, 6, 6, 6, 7, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
for i, idx := range s.indexes {
|
||||
patch := Parse(s.patchStr)
|
||||
result := patch.GetNextChangeIdx(idx)
|
||||
assert.Equal(t, s.expecteds[i], result)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package patch
|
||||
|
||||
import "github.com/samber/lo"
|
||||
|
||||
type patchTransformer struct {
|
||||
patch *Patch
|
||||
opts TransformOpts
|
||||
}
|
||||
|
||||
type TransformOpts struct {
|
||||
// Create a patch that will applied in reverse with `git apply --reverse`.
|
||||
// This affects how unselected lines are treated when only parts of a hunk
|
||||
// are selected: usually, for unselected lines we change '-' lines to
|
||||
// context lines and remove '+' lines, but when Reverse is true we need to
|
||||
// turn '+' lines into context lines and remove '-' lines.
|
||||
Reverse bool
|
||||
|
||||
// If set, we will replace the original header with one referring to this file name.
|
||||
// For staging/unstaging lines we don't want the original header because
|
||||
// it makes git confused e.g. when dealing with deleted/added files
|
||||
// but with building and applying patches the original header gives git
|
||||
// information it needs to cleanly apply patches
|
||||
FileNameOverride string
|
||||
|
||||
// The indices of lines that should be included in the patch.
|
||||
IncludedLineIndices []int
|
||||
}
|
||||
|
||||
func transform(patch *Patch, opts TransformOpts) *Patch {
|
||||
transformer := &patchTransformer{
|
||||
patch: patch,
|
||||
opts: opts,
|
||||
}
|
||||
|
||||
return transformer.transform()
|
||||
}
|
||||
|
||||
// helper function that takes a start and end index and returns a slice of all
|
||||
// indexes inbetween (inclusive)
|
||||
func ExpandRange(start int, end int) []int {
|
||||
expanded := []int{}
|
||||
for i := start; i <= end; i++ {
|
||||
expanded = append(expanded, i)
|
||||
}
|
||||
return expanded
|
||||
}
|
||||
|
||||
func (self *patchTransformer) transform() *Patch {
|
||||
header := self.transformHeader()
|
||||
hunks := self.transformHunks()
|
||||
|
||||
return &Patch{
|
||||
header: header,
|
||||
hunks: hunks,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *patchTransformer) transformHeader() []string {
|
||||
if self.opts.FileNameOverride != "" {
|
||||
return []string{
|
||||
"--- a/" + self.opts.FileNameOverride,
|
||||
"+++ b/" + self.opts.FileNameOverride,
|
||||
}
|
||||
} else {
|
||||
return self.patch.header
|
||||
}
|
||||
}
|
||||
|
||||
func (self *patchTransformer) transformHunks() []*Hunk {
|
||||
newHunks := make([]*Hunk, 0, len(self.patch.hunks))
|
||||
|
||||
startOffset := 0
|
||||
var formattedHunk *Hunk
|
||||
for i, hunk := range self.patch.hunks {
|
||||
startOffset, formattedHunk = self.transformHunk(
|
||||
hunk,
|
||||
startOffset,
|
||||
self.patch.HunkStartIdx(i),
|
||||
)
|
||||
if formattedHunk.containsChanges() {
|
||||
newHunks = append(newHunks, formattedHunk)
|
||||
}
|
||||
}
|
||||
|
||||
return newHunks
|
||||
}
|
||||
|
||||
func (self *patchTransformer) transformHunk(hunk *Hunk, startOffset int, firstLineIdx int) (int, *Hunk) {
|
||||
newLines := self.transformHunkLines(hunk, firstLineIdx)
|
||||
newNewStart, newStartOffset := self.transformHunkHeader(newLines, hunk.oldStart, startOffset)
|
||||
|
||||
newHunk := &Hunk{
|
||||
bodyLines: newLines,
|
||||
oldStart: hunk.oldStart,
|
||||
newStart: newNewStart,
|
||||
headerContext: hunk.headerContext,
|
||||
}
|
||||
|
||||
return newStartOffset, newHunk
|
||||
}
|
||||
|
||||
func (self *patchTransformer) transformHunkLines(hunk *Hunk, firstLineIdx int) []*PatchLine {
|
||||
skippedNewlineMessageIndex := -1
|
||||
newLines := []*PatchLine{}
|
||||
|
||||
for i, line := range hunk.bodyLines {
|
||||
lineIdx := i + firstLineIdx + 1 // plus one for header line
|
||||
if line.Content == "" {
|
||||
break
|
||||
}
|
||||
isLineSelected := lo.Contains(self.opts.IncludedLineIndices, lineIdx)
|
||||
|
||||
if isLineSelected || (line.Kind == NEWLINE_MESSAGE && skippedNewlineMessageIndex != lineIdx) || line.Kind == CONTEXT {
|
||||
newLines = append(newLines, line)
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.Kind == DELETION && !self.opts.Reverse) || (line.Kind == ADDITION && self.opts.Reverse) {
|
||||
content := " " + line.Content[1:]
|
||||
newLines = append(newLines, &PatchLine{
|
||||
Kind: CONTEXT,
|
||||
Content: content,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if line.Kind == ADDITION {
|
||||
// we don't want to include the 'newline at end of file' line if it involves an addition we're not including
|
||||
skippedNewlineMessageIndex = lineIdx + 1
|
||||
}
|
||||
}
|
||||
|
||||
return newLines
|
||||
}
|
||||
|
||||
func (self *patchTransformer) transformHunkHeader(newBodyLines []*PatchLine, oldStart int, startOffset int) (int, int) {
|
||||
oldLength := nLinesWithKind(newBodyLines, []PatchLineKind{CONTEXT, DELETION})
|
||||
newLength := nLinesWithKind(newBodyLines, []PatchLineKind{CONTEXT, ADDITION})
|
||||
|
||||
var newStartOffset int
|
||||
// if the hunk went from zero to positive length, we need to increment the starting point by one
|
||||
// if the hunk went from positive to zero length, we need to decrement the starting point by one
|
||||
if oldLength == 0 {
|
||||
newStartOffset = 1
|
||||
} else if newLength == 0 {
|
||||
newStartOffset = -1
|
||||
} else {
|
||||
newStartOffset = 0
|
||||
}
|
||||
|
||||
newStart := oldStart + startOffset + newStartOffset
|
||||
|
||||
newStartOffset = startOffset + newLength - oldLength
|
||||
|
||||
return newStart, newStartOffset
|
||||
}
|
||||
@@ -301,5 +301,9 @@ func getDefaultAppState() *AppState {
|
||||
}
|
||||
|
||||
func LogPath() (string, error) {
|
||||
if os.Getenv("LAZYGIT_LOG_PATH") != "" {
|
||||
return os.Getenv("LAZYGIT_LOG_PATH"), nil
|
||||
}
|
||||
|
||||
return configFilePath("development.log")
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@ package config
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() OSConfig {
|
||||
return OSConfig{
|
||||
EditCommand: ``,
|
||||
EditCommandTemplate: "",
|
||||
OpenCommand: "open -- {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
Open: "open -- {{filename}}",
|
||||
OpenLink: "open {{link}}",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -10,21 +11,31 @@ func isWSL() bool {
|
||||
return err == nil && strings.Contains(string(data), "microsoft")
|
||||
}
|
||||
|
||||
func isContainer() bool {
|
||||
data, err := ioutil.ReadFile("/proc/1/cgroup")
|
||||
|
||||
if strings.Contains(string(data), "docker") ||
|
||||
strings.Contains(string(data), "/lxc/") ||
|
||||
[]string{string(data)}[0] != "systemd" &&
|
||||
[]string{string(data)}[0] != "init" ||
|
||||
os.Getenv("container") != "" {
|
||||
return err == nil && true
|
||||
}
|
||||
|
||||
return err == nil && false
|
||||
}
|
||||
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() OSConfig {
|
||||
if isWSL() {
|
||||
if isWSL() && !isContainer() {
|
||||
return OSConfig{
|
||||
EditCommand: ``,
|
||||
EditCommandTemplate: "",
|
||||
OpenCommand: `powershell.exe start explorer.exe {{filename}} >/dev/null`,
|
||||
OpenLinkCommand: `powershell.exe start {{link}} >/dev/null`,
|
||||
Open: `powershell.exe start explorer.exe {{filename}} >/dev/null`,
|
||||
OpenLink: `powershell.exe start {{link}} >/dev/null`,
|
||||
}
|
||||
}
|
||||
|
||||
return OSConfig{
|
||||
EditCommand: ``,
|
||||
EditCommandTemplate: "",
|
||||
OpenCommand: `xdg-open {{filename}} >/dev/null`,
|
||||
OpenLinkCommand: `xdg-open {{link}} >/dev/null`,
|
||||
Open: `xdg-open {{filename}} >/dev/null`,
|
||||
OpenLink: `xdg-open {{link}} >/dev/null`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ package config
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() OSConfig {
|
||||
return OSConfig{
|
||||
EditCommand: ``,
|
||||
EditCommandTemplate: "",
|
||||
OpenCommand: `start "" {{filename}}`,
|
||||
OpenLinkCommand: `start "" {{link}}`,
|
||||
Open: `start "" {{filename}}`,
|
||||
OpenLink: `start "" {{link}}`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package config
|
||||
|
||||
func GetEditTemplate(osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
|
||||
preset := getPreset(osConfig, guessDefaultEditor)
|
||||
template := osConfig.Edit
|
||||
if template == "" {
|
||||
template = preset.editTemplate
|
||||
}
|
||||
|
||||
return template, getEditInTerminal(osConfig, preset)
|
||||
}
|
||||
|
||||
func GetEditAtLineTemplate(osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
|
||||
preset := getPreset(osConfig, guessDefaultEditor)
|
||||
template := osConfig.EditAtLine
|
||||
if template == "" {
|
||||
template = preset.editAtLineTemplate
|
||||
}
|
||||
return template, getEditInTerminal(osConfig, preset)
|
||||
}
|
||||
|
||||
func GetEditAtLineAndWaitTemplate(osConfig *OSConfig, guessDefaultEditor func() string) string {
|
||||
preset := getPreset(osConfig, guessDefaultEditor)
|
||||
template := osConfig.EditAtLineAndWait
|
||||
if template == "" {
|
||||
template = preset.editAtLineAndWaitTemplate
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
type editPreset struct {
|
||||
editTemplate string
|
||||
editAtLineTemplate string
|
||||
editAtLineAndWaitTemplate string
|
||||
editInTerminal bool
|
||||
}
|
||||
|
||||
// IF YOU ADD A PRESET TO THIS FUNCTION YOU MUST UPDATE THE `Supported presets` SECTION OF docs/Config.md
|
||||
func getPreset(osConfig *OSConfig, guessDefaultEditor func() string) *editPreset {
|
||||
presets := map[string]*editPreset{
|
||||
"vi": standardTerminalEditorPreset("vi"),
|
||||
"vim": standardTerminalEditorPreset("vim"),
|
||||
"nvim": standardTerminalEditorPreset("nvim"),
|
||||
"emacs": standardTerminalEditorPreset("emacs"),
|
||||
"nano": standardTerminalEditorPreset("nano"),
|
||||
"kakoune": standardTerminalEditorPreset("kakoune"),
|
||||
"helix": {
|
||||
editTemplate: "hx -- {{filename}}",
|
||||
editAtLineTemplate: "hx -- {{filename}}:{{line}}",
|
||||
editAtLineAndWaitTemplate: "hx -- {{filename}}:{{line}}",
|
||||
editInTerminal: true,
|
||||
},
|
||||
"vscode": {
|
||||
editTemplate: "code --reuse-window -- {{filename}}",
|
||||
editAtLineTemplate: "code --reuse-window --goto -- {{filename}}:{{line}}",
|
||||
editAtLineAndWaitTemplate: "code --reuse-window --goto --wait -- {{filename}}:{{line}}",
|
||||
editInTerminal: false,
|
||||
},
|
||||
"sublime": {
|
||||
editTemplate: "subl -- {{filename}}",
|
||||
editAtLineTemplate: "subl -- {{filename}}:{{line}}",
|
||||
editAtLineAndWaitTemplate: "subl --wait -- {{filename}}:{{line}}",
|
||||
editInTerminal: false,
|
||||
},
|
||||
"bbedit": {
|
||||
editTemplate: "bbedit -- {{filename}}",
|
||||
editAtLineTemplate: "bbedit +{{line}} -- {{filename}}",
|
||||
editAtLineAndWaitTemplate: "bbedit +{{line}} --wait -- {{filename}}",
|
||||
editInTerminal: false,
|
||||
},
|
||||
"xcode": {
|
||||
editTemplate: "xed -- {{filename}}",
|
||||
editAtLineTemplate: "xed --line {{line}} -- {{filename}}",
|
||||
editAtLineAndWaitTemplate: "xed --line {{line}} --wait -- {{filename}}",
|
||||
editInTerminal: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Some of our presets have a different name than the editor they are using.
|
||||
editorToPreset := map[string]string{
|
||||
"kak": "kakoune",
|
||||
"hx": "helix",
|
||||
"code": "vscode",
|
||||
"subl": "sublime",
|
||||
"xed": "xcode",
|
||||
}
|
||||
|
||||
presetName := osConfig.EditPreset
|
||||
if presetName == "" {
|
||||
defaultEditor := guessDefaultEditor()
|
||||
if presets[defaultEditor] != nil {
|
||||
presetName = defaultEditor
|
||||
} else if p := editorToPreset[defaultEditor]; p != "" {
|
||||
presetName = p
|
||||
}
|
||||
}
|
||||
|
||||
if presetName == "" || presets[presetName] == nil {
|
||||
presetName = "vim"
|
||||
}
|
||||
|
||||
return presets[presetName]
|
||||
}
|
||||
|
||||
func standardTerminalEditorPreset(editor string) *editPreset {
|
||||
return &editPreset{
|
||||
editTemplate: editor + " -- {{filename}}",
|
||||
editAtLineTemplate: editor + " +{{line}} -- {{filename}}",
|
||||
editAtLineAndWaitTemplate: editor + " +{{line}} -- {{filename}}",
|
||||
editInTerminal: true,
|
||||
}
|
||||
}
|
||||
|
||||
func getEditInTerminal(osConfig *OSConfig, preset *editPreset) bool {
|
||||
if osConfig.EditInTerminal != nil {
|
||||
return *osConfig.EditInTerminal
|
||||
}
|
||||
return preset.editInTerminal
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetEditTemplate(t *testing.T) {
|
||||
trueVal := true
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
osConfig *OSConfig
|
||||
guessDefaultEditor func() string
|
||||
expectedEditTemplate string
|
||||
expectedEditAtLineTemplate string
|
||||
expectedEditAtLineAndWaitTemplate string
|
||||
expectedEditInTerminal bool
|
||||
}{
|
||||
{
|
||||
"Default template is vim",
|
||||
&OSConfig{},
|
||||
func() string { return "" },
|
||||
"vim -- {{filename}}",
|
||||
"vim +{{line}} -- {{filename}}",
|
||||
"vim +{{line}} -- {{filename}}",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Setting a preset",
|
||||
&OSConfig{
|
||||
EditPreset: "vscode",
|
||||
},
|
||||
func() string { return "" },
|
||||
"code --reuse-window -- {{filename}}",
|
||||
"code --reuse-window --goto -- {{filename}}:{{line}}",
|
||||
"code --reuse-window --goto --wait -- {{filename}}:{{line}}",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Setting a preset wins over guessed editor",
|
||||
&OSConfig{
|
||||
EditPreset: "vscode",
|
||||
},
|
||||
func() string { return "nano" },
|
||||
"code --reuse-window -- {{filename}}",
|
||||
"code --reuse-window --goto -- {{filename}}:{{line}}",
|
||||
"code --reuse-window --goto --wait -- {{filename}}:{{line}}",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Overriding a preset with explicit config (edit)",
|
||||
&OSConfig{
|
||||
EditPreset: "vscode",
|
||||
Edit: "myeditor {{filename}}",
|
||||
EditInTerminal: &trueVal,
|
||||
},
|
||||
func() string { return "" },
|
||||
"myeditor {{filename}}",
|
||||
"code --reuse-window --goto -- {{filename}}:{{line}}",
|
||||
"code --reuse-window --goto --wait -- {{filename}}:{{line}}",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Overriding a preset with explicit config (edit at line)",
|
||||
&OSConfig{
|
||||
EditPreset: "vscode",
|
||||
EditAtLine: "myeditor --line={{line}} {{filename}}",
|
||||
EditInTerminal: &trueVal,
|
||||
},
|
||||
func() string { return "" },
|
||||
"code --reuse-window -- {{filename}}",
|
||||
"myeditor --line={{line}} {{filename}}",
|
||||
"code --reuse-window --goto --wait -- {{filename}}:{{line}}",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Overriding a preset with explicit config (edit at line and wait)",
|
||||
&OSConfig{
|
||||
EditPreset: "vscode",
|
||||
EditAtLineAndWait: "myeditor --line={{line}} -w {{filename}}",
|
||||
EditInTerminal: &trueVal,
|
||||
},
|
||||
func() string { return "" },
|
||||
"code --reuse-window -- {{filename}}",
|
||||
"code --reuse-window --goto -- {{filename}}:{{line}}",
|
||||
"myeditor --line={{line}} -w {{filename}}",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Unknown preset name",
|
||||
&OSConfig{
|
||||
EditPreset: "thisPresetDoesNotExist",
|
||||
},
|
||||
func() string { return "" },
|
||||
"vim -- {{filename}}",
|
||||
"vim +{{line}} -- {{filename}}",
|
||||
"vim +{{line}} -- {{filename}}",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Guessing a preset from guessed editor",
|
||||
&OSConfig{},
|
||||
func() string { return "emacs" },
|
||||
"emacs -- {{filename}}",
|
||||
"emacs +{{line}} -- {{filename}}",
|
||||
"emacs +{{line}} -- {{filename}}",
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
template, editInTerminal := GetEditTemplate(s.osConfig, s.guessDefaultEditor)
|
||||
assert.Equal(t, s.expectedEditTemplate, template)
|
||||
assert.Equal(t, s.expectedEditInTerminal, editInTerminal)
|
||||
|
||||
template, editInTerminal = GetEditAtLineTemplate(s.osConfig, s.guessDefaultEditor)
|
||||
assert.Equal(t, s.expectedEditAtLineTemplate, template)
|
||||
assert.Equal(t, s.expectedEditInTerminal, editInTerminal)
|
||||
|
||||
template = GetEditAtLineAndWaitTemplate(s.osConfig, s.guessDefaultEditor)
|
||||
assert.Equal(t, s.expectedEditAtLineAndWaitTemplate, template)
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user